At the moment, our contact management application doesn't handle form submits very well. Indeed, in the contact-creation
, contact-edition
, and contact-photo
components, if the Save button is clicked once, then clicked again before the underlying Fetch call completes and the router navigates away from the form, multiple calls to the backend will be performed in parallel. Sometimes, it doesn't matter. However, it can also be a problem in many scenarios.
To fix this, we will create a custom attribute named submit-task
, which will replace the submit
handler of the form
elements. It will be bound using the call
command to a method, which is expected to return a Promise
. When the form
is submitted, the attribute will turn a flag on, and when the returned Promise
completes, it will turn it back off. This flag will indicate if the form is currently waiting for a submit task to complete:
src/resources/attributes/submit-task.js
import {inject, DOM} from 'aurelia-framework'; @inject(DOM.Element) export class SubmitTaskCustomAttribute { constructor(element) { this.element = element; this.onSubmit = this.trySubmit.bind(this); } attached() { this.element.addEventListener('submit', this.onSubmit); this.element.isSubmitTaskExecuting = false; } trySubmit(e) { e.preventDefault(); if (this.task) { return; } this.element.isSubmitTaskExecuting = true; this.task = Promise.resolve(this.value()).then( () => this.completeTask(), () => this.completeTask()); } completeTask() { this.task = null; this.element.isSubmitTaskExecuting = false; } detached() { this.element.removeEventListener('submit', this.onSubmit); } }
Here, we first use the naming convention to identify the class as a custom attribute. We also declare a dependency on the DOM element the attribute is on, which we inject in the constructor.
Here, when our custom attribute is attached
to the document, we add a listener on the element's submit
event, which will call the trySubmit
method when triggered. Additionally, a new isSubmitTaskExecuting
property is created on the element and initialized to false
.
When the element publishes a submit
event, we start by making sure that no submit task
is currently running. If one already is, we simply return. If none is, the element's isSubmitTaskExecuting
property is set to true
, and the function bound to the custom attribute's value
is called. The result is guaranteed to be a Promise
, and a callback is attached to this Promise
so isSubmitTaskExecuting
is set back to false
when the Promise
completes, no matter whether it succeeds or fails.
Lastly, when the attribute is detached
from the document, we simply remove the submit
event listener.
Now we can go into the various components with a form
element and replace the submit
event handler with the new submit-task
attribute, bound using the call
command to the save
method:
src/contact-creation.html
<template>
<!-- Omitted snippet... -->
<form class="form-horizontal" validation-renderer="bootstrap-form" submit-task.call="save()">
<!-- Omitted snippet... -->
</form>
<!-- Omitted snippet... -->
</template>
Of course, for this to work, we need to modify the save
method so it returns the Promise
tracking the Fetch call:
src/contact-creation.js
//Omitted snippet...
save() {
//Omitted snippet...
return this.contactGateway.create(this.contact)
.then(() => this.router.navigateToRoute('contacts'));
}
//Omitted snippet...
I'll leave it as an exercise to the reader to also apply those changes to the contact-edition
and contact-photo
components.
At this point, if you run the application, you shouldn't be able to trigger multiple submits when one is already in progress.
Another thing that would be great is to display a visual indicator to the user that a submit task is in progress. Now that we have a custom attribute that creates and manages the appropriate flag, let's create a submit-button
custom element that will display a spinner animated icon when its form is running a submission:
src/resources/elements/submit-button.html
<template bindable="disabled"> <button type="submit" ref="button" disabled.bind="disabled" class="btn btn-success"> <span hide.bind="button.form.isSubmitTaskExecuting"> <slot name="icon"> <i class="fa fa-check-circle-o" aria-hidden="true"></i> </slot> </span> <i class="fa fa-spinner fa-spin" aria-hidden="true" show.bind="button.form.isSubmitTaskExecuting"></i> <slot>Submit</slot> </button> </template>
Here, we first declare a disabled
bindable property on the template element. This means that this element will be made of this template only; it won't have a view-model.
Next, we declare a button
element, with a submit type
. We also use the ref
attribute to assign a reference of this button to the button
property on the binding context, and we bind the button's disabled
attribute to the disabled
bindable property.
Inside the button, we add a span
which will be hidden when the isSubmitTaskExecuting
property of the button's form
element is true
. Inside this span
, we define an icon
slot, whose default content is a check icon.
We also add a spinner icon inside the button, which will be displayed only when the isSubmitTaskExecuting
property of the button's form
element is true
.
Lastly, we define a default slot, which contains the Submit
text as its default content.
This custom element will simply show a check icon when no submit is in progress, and will replace this check icon with a spinner during any submit task. It will then toggle back to the check icon when the submit task completes.
Additionally, the icon
slot will allow instances to override the default check icon, and the unnamed slot will allow instances to override the Submit
label.
Now we can go into the various components with a form
element and replace the Save button with the new submit-button
element:
src/contact-creation.html
<template>
<!-- Omitted snippet... -->
<submit-button>Save</submit-button>
<!-- Omitted snippet... -->
</template>
Here, we simply define a submit-button
element, and project the Save
text on the default slot, which overrides its default label.
I'll leave it as an exercise to the reader to also apply those changes to the contact-edition
and contact-photo
components.
At this point, if you run the application, you should see the check icon of the various Save buttons replaced by a spinner when a submit task is in progress.
18.119.137.76