Bonus – preventing multiple submits

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.

Creating the submit task attribute

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.

Using the submit task attribute

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.

Creating the submit button

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.

Using the submit button

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.137.76