Angular self-saving dropdowns - yet another directive

Use the power of Angular directives to create a reusable self-saving dropdown directive

Angular self-saving dropdowns - yet another directive
“It is not enough for code to work” - Robert C. Martin

Angular directive is a powerful pattern provided by the framework which can be used to add additional behaviors to the elements. We will create a custom Angular directive in this article to enable self-saving(autosaving) for dropdown elements.

What is a self-saving dropdown anyways?

Consider this simple scenario in an example application where you have a dropdown to determine the sexual age group of a person. The natural and efficient implementation of this case is to use a dropdown with options. It is typical to use a form with a dropdown field and a submit button. But what if this dropdown is not part of a larger form? What if this is a single select item in the page without a save button? You might not want the user to click a button to initiate the save in some cases as it can cause friction.

We can use a self saving dropdown which syncs the state with the backend database on dropdown change events. You can find this pattern commonly in react applications with the data fetcher approach where the component is responsible for fetching its required data itself.

Great! Now, how do we communicate to the user that this change triggered a save? How do we let users know in case of an error? It is obvious that we need an elegant solution to provide visual feedback to the user of in-progress, success, and failure events. This is where you might want to implement self-saving behavior.


Let’s get to it

Take a  peek into the final results on how it works before we start.

Self saving drop-downs

That looks cool, doesn't it? Let’s now outline the plan

  1. Directive should take an input that refers to the http call being made to the backend.
  2. When the host element triggers the 'change` event, we need the listener to run.
  3. Need to show the loader before subscription
  4. Need to remove the loader, add green tick icon on successful call
  5. Need to remove the tick mark after one second
  6. On failure, need to show the error icon next to the select field

In the diagram,

Self saving dropdown, the plan

Let’s start by creating an Angular directive using the Angular CLI.

ng generate directive self-save

We will have only one input to this directive, ie

@Input('observableFn')
 observableFn!: () => Observable<any>;

ObservableFn will have the reference to the http request that has to be made to save this particular data. We will be passing this information down from the parent where this directive is getting used.

Let’s now bring in ElementRef, Renderer2, and Document

constructor(
	private elRef: ElementRef, 
	private renderer: Renderer2,
	@Inject(DOCUMENT) private document: Document
) {}

We will be using these dependencies to

  1. ElementRef - Refer to the host element
  2. Renderer2 - To Attach the error text element in case of error
  3. Document- reference to the document

We need to listen to the change event on the host element to know when the dropdown value changes

 @HostListener('change')
 onChange() {
  // Do all craziness
 }

Let’s start implementing the self-saving behavior now,

if (this.observableFn instanceof Function) {
     const element: HTMLElement = this.elRef.nativeElement;

     this.addLoader(element);
     const changeObservable: Observable<unknown> = this.observableFn();
     changeObservable.subscribe(
       _ => {
         this.handleSuccessCase(element);
       },
       _ => {
         this.handleErrorCase(element);
       }
     );
   }

We are making use of the elementRef to grab the, native host element(select element) as we need to manipulate this as we go. Next, we are subscribing to the observable that is already passed as the input by calling the input function. There are two helper methods here to which we are passing the select element . Let us now see how they are implemented.

handleSuccessCase(element: HTMLElement) {
   this.removeBackground(element);
   this.addSuccess(element);
   setTimeout(() => {
     this.removeBackground(element);
   }, 1000);
 }

 handleErrorCase(element) {
   this.removeBackground(element);
   const child = this.document.createElement('img');
   child.src = ERROR_ICON;
   const parent = this.renderer.parentNode(this.elRef.nativeElement);
   this.renderer.appendChild(parent, child);
   setTimeout(() => {
     this.renderer.removeChild(parent, child);
   }, 1000);
 }

This code follows the initial flowchart that we created. ie, on change of the dropdown value,

  1. Adds a loader to the element to indicate save in-progress
  2. Gets the http observable and subscribes

On success

  1. Removes the loader background
  2. Add success tick icon
  3. Remove the tick icon after 1 second

And on failure,

  1. Create a dummy div element to hold the error text
  2. Populated the innerText as the error message received from server
  3. Use Renderer to append this child element to the parent node so that the error message is not displayed below the dropdown

Let us now implement these helper methods that we used above,
To add a loader to the dropdown,

 addLoader(element: HTMLElement) {
   this.addBackground(
     element,
     LOADER_ICON,
     20
   );
 }

The same can be followed to add the success indicator as well,

 addSuccess(element: HTMLElement) {
   this.addBackground(
     element,
     SUCCESS_ICON,
     20
   );
 }

Now to implement the addBackground() method,

addBackground(
   element: HTMLElement,
   backgroundImg: string,
   backgroundSize: number
 ) {
   element.style.background = `#fff url("${backgroundImg}") no-repeat right 20px center`;
   element.style.backgroundSize = `${backgroundSize}px`;
 }

We can simply set the element background to none to remove background.

removeSuccess(element: HTMLElement)  {
  this.removeBackground(element);
}
removeBackground(element: HTMLElement) {
   element.style.background = ‘none’;
 }

Now to use this directive on any select element,

<select selfSave [observableFn]="post()">
 <option value="One">One</option>
 <option value="Two">Two</option>
</select>

Let us also create the post() method which returns a function that returns the http save observable.

// Done for demo purposes only. Use a service to talk to APIs
 post(): Function {
   return () => {
     return this.http.post(“https://jsonplaceholder.typicode.com/posts”, {});
   };
 }

You can play around with the stackblitz project here.

Conclusion

Directives are very handy when it comes to a repeated pattern that we need to apply to multiple elements in our application. They can be used to make reusable patterns across the application and make the developer's life easier by keeping the application cleaner!