Picture this: You just finished developing a new dropdown component incorporating all the product and designer requirements and start using it all over your application pages. In the beginning everything is great, but after awhile, you get requests to add new features like an icon/image or multiple text lines. So you add those annoying *ngIf conditions to the template and your code starts getting kind of messy.
Later on, you need to integrate with a new API DTO that’s different from your dropdown’s expected parameters. Now you have to maintain a transformation function or even make changes to the component logic itself to support different kinds of input parameters, affecting your code’s readability.
In this article, we are going to implement a dropdown component that supports dynamic templates using the ngTemplateOutlet and ngTemplateOutletContext directives that Angular provide us.
You can find a full example at this Github repository link.
Let’s get started
The following snapshots are the dropdown component’s class logic and template structure.
Let’s go over the important stuff here:
- Implementing the ControlValueAccessor interface to support Angular forms integration; the selected form control property acts as a bridge between the selected value and the host component.
- The sourceOptions property represents the input list value from the host and the displayedOptions$ property filters options after the search term changes.
- The templateRef input property is the actual option item template that will be displayed on the dropdown options container.
- The dropdown template combines two child components: the search component which emits values every time the user changes the search term and the list component which is in charge of displaying the filtered options. The purpose of this breakdown is to prevent triggering a change detection each time the user invokes a keyup event on their search. Each component uses the onPush change detection strategy, meaning it will triggers a change detection if one of its input property references changes or if an event is dispatched by it or one of its child components. Neglecting to make these components standalone will cause the change detection mechanism to start running and check all the template bindings (for each available option) each time the user presses the keyboard, while focusing the search element — even if we use debounceTime operator. By breaking down these two separated child components the list component will only trigger the change detection if the displayed list value actually changes.
- The viewChild ref for the search component is used to clear the search each time the selected value changed.
- All of the subscriptions are wrapped with the takeUntil operator to avoid memory leaks.
- Looking at the template, you will see that if the user doesn’t select any value the dropdown presents a placeholder with the text ‘Select option’. However, if the user does select a value, we use the ng-container element to display that value as context of the template provided from the host component. Using the ngTemplateOutlet we are telling the container which template we’d like to present. ngTemplateOutletContext is used to provide the relevant context to this template. We also use the same logic in the list component to display each of the list items.
- Every time the user selects an item from the list we catch this event from the list component and set the value to the selected form control.
From here, both the list and the search component’s implementations are pretty straightforward and you can find them together with the style files in this repo.
To use the dynamic dropdown we need to supply it with the required template and the list of the available options.
Using the let- property on the ng-template element we can declare the context of the template and bind its properties. The above example will produce the following template.
You can see that we are moving the responsibility of the template structure to the host component instead of the actual dropdown component, providing us with the power to create any template we want without any constraints. Using this approach to create reusable component allows us to keep the code clean and maintainable even with complex requirements.
Thanks for reading 😊