How to Use Attribute Directives to Avoid Repetition in Angular Templates
Lately, we have started working on v3.0 of ABP Framework and our ultimate goal as the frontend team is to introduce solutions that improve developer experience. In this context, one of the things we are working on is migrating the current grid in Angular to the feature-rich ngx-datatable. Nevertheless, when we tried to implement it in the project, we have realized that the amount of code duplicated in each CRUD page was troublesome.
We have a ListService which makes it easier to work with remote pagination and sorting. It is a core feature and we are planning to keep it as UI independent as possible. Some properties of ngx-datatable fit really well, while others, sorting specifically, do not.
Nothing is wrong with ngx-datatable. It actually is an amazing work and probably one of the best grids you can use in Angular. And, to be fair, all of the bindings above are for rendering the content properly, so they are not useless after all. Still, from a developer experience perspective, this is painful. Here is how we see it:
- CRUD pages in the community version of ABP would require this code to be copied manually over and over. We should avoid this somehow.
- Although there is a nice code generator for ABP Commercial users, readability and maintenance of the generated code is an important aspect. Less is more.
Naturally, we started looking for a way to reduce the amount of code that will be necessary each time ngx-datatable is consumed.
Attribute Directives to the Rescue
The initial idea was to handle property and event bindings between the grid and the ListService instance, so we started worked on an attribute directive that works as an adapter. Later, we removed all appearance-related properties too. The following is what we came up with in the end:
Sweet, right? Thanks to two attribute directives, we now have much less code to worry about and a better focus on what really matters. The first directive, which has ngx-datatable[list]
as selector, provides a single point of communication between the DatatableComponent
and the ListService
. The second directive, ngx-datatable[default]
, eliminates the noise created by property bindings just to make ngx-datatable styles match our project. We could have built only one directive, but followed single responsibility principle and ended up creating one for appearance and another for functionality. Our intention is to grant ABP developers the flexibility to remove default appearance when they want to implement their own styles.
The Adapter Directive
I am not planning to show you the implementation details of the actual ListService. All you need to know is, it is built based on ABP backend and is UI independent. I will, however, describe what we needed to bind from it to the grid, and vice versa. First, let us take a look at the directive code:
Here is what every property and method does:
list
is for binding theListService
instance.table
is theDatatableComponent
instance, retrieved from the dependency injection system.cdRef
is theChangeDetectorRef
instance, through which we can mark the host for change detection.ngOnChanges
is setslimit
andoffset
properties of the table at first change.ngOnInit
initializes subscriptions topage
andsort
events of the grid, as well as theisLoading$
of the service.subscribeToPage
,subscribeToSort
, andsubscribeToIsLoading
are mapping observable properties at both end.subscription
is for collecting RxJS subscriptions, which are later unsubscribed from atngOnDestroy
lifecycle hook.
The key takeaway in this is the fact that an attribute directive in Angular can obtain the host instance, hook into its events, manipulate its properties, and even execute change detection manually on it when necessary. The possibilities are endless. You can adapt a component to any interface and avoid the performance penalty of the default change detection strategy if you like.
Another important aspect is the directive selector. The selector queries only ngx-datatable
elements with a list
attribute, effectively leaving the data tables without the attribute or other elements with a list property alone. You will probably want to place // tslint:disable-next-line
above the selector though because the linters are usually configured to throw an error when directive selectors do not start with the app or library prefix.
The Default Properties Directive
Creating an Angular directive for default properties is much easier compared to an adapter directive and, in consequence, probably less exciting. It is quite advantageous though. This is what it looks like:
The DatatableComponent
instance is again retrieved from Angular’s dependency injection system. Then, several properties are set within the constructor, before any property binding to that instance occurs. Thus, the defaults defined by the DatatableComponent
class are effectively overridden.
The only problematic property is class
here and that is due to this implementation of host binding in ngx-datatable. The workaround is taking class
as an input property and binding its value to host element after concatenation with ngx-datatable
class. When no class is given, a material theme is applied and some custom borders are added. Setting another class through the template is still available, so the interface is unchanged.
Finally, it would work perfectly fine if we referred to just ngx-datatable
as the selector, but we want to give the developer the opportunity to drop this directive altogether, so we did not.
Conclusion
People tend to adapt their components to UI kits and libraries, but UI may be altered in time. Core API decisions and business logic, on the other hand, do not change as frequently. I believe we should adapt UI elements to our components and services and not the other way around. Some libraries offer injectable adapters for this purpose, which is nice because they can be employed in order to adjust the behavior. Most, though, do not. As you see, adapter attribute directives prove handy in such cases.
Placing the same properties repeatedly in templates is also troubling. I have always thought this as a weak point of HTML. Now we have frontend frameworks and we keep doing the same. Angular provides us tools to avoid this and an attribute directive for default properties makes perfect sense in my opinion. Check your own project. You will surely find a good use for them.
I have created a StackBlitz playground with a dummy backend and a simplified list service. You can see both directives in action.
Thanks for reading. Have a nice day.
This article was originally published on Volosoft Blog.