Share ControlValueAccessor & Provider Creation with Abstract ControlValueAccessor for Custom Components in Angular
This article is part of the two article series on how to make your custom controls form enabled and ngModel enabled. Reading the first part is not mandatory but recommended. Click here to read the first part.
In the last article on this series we created CustomInput and CustomRange controls and made them form enabled. Here is our starting point:
The CustomInput component looks like this:
and the CustomRange component looks like:
Here is a link to the final working Plnkr from the last article.
If you noticed in the Plnkr link above, there is code repetition between CustomInput and CustomRange. Both component write there own providers extending the NG_VALUE_ACCESSOR token in the exact same way. In this way, if we have 10 components, we will end up writing the exact same code 10 times. Can we go DRY ? Can we do better ?
In this article, we will explore a naive approach of sharing common code. How about a abstract factory class which implements ControlValueAccessor and then our custom control classes extending that class. That solves half the problem about not repeating ControlValueAccesor implementation. What about provider? Can we yank the provider implementation to a factory like place where other components can request a instance of provider with different types? This way, our provider creation logic will also be shared and not repeated across components.
Let's look at our basic AbstractValueAccessor :
Nothing fancy (not really). We wrote a simple abstract class AbstractValueAccessor which implements ControlValueAccessor. _value is a private class member which acts as the model for any control extending this abstract class. We also implemented getters and setters for the internal _value state/model. Notice that type of _value is set to any. (I know some folks won't like 'any') but this is on purpose. The model value of a input text box will be a 'string' and the model for a range input can be a 'number'. Depending on the type of component, model can differ from being a simple generic type to any complex object. This is the reason why we choose to give 'any' type to _value. (Offcourse we can do better, later on that) Everything else stays the same. For the component extending our AbstractValueAccessor , Angular will automagically call the registerOnChange(fn) function and pass the internal update method as argument. We save reference to that method as we did in both CustomInput & CustomRange component. I discussed this internal update method in detail in my last article. If you are interested in learning about it, jump directly to this link.
One extra thing that I would like to add to abstract-value-accessor.ts is a provider factory, a simple function which can return a common provider for NG_VALUE_ACCESSOR token.
The next step is to extend our AbstractValueAccessor class in our custom control classes.
Let's look at the two cleaner custom components now. Custom Input Component looks like:
and here is the Custom Range Component:
Both components look a lot cleaner & simpler extending the AbstractValueAccessor. Also, checkout the providers: [ MakeProvider(CustomRange) ] property on @Component decorator. making use of the MakeProvider factory function which returns the required custom provider.
Here is a working Plnkr after all these changes:
There is one last thing that we would like to improve on. In the AbstractValueAccessor, remember the pretty _value state variable and its not-very-pretty any type. What can we do about it? The idea is every custom component extending the AbstractValueAccessor will have a different data type for its model/state. You can stay with the last solution (show in Plnkr above ^) and there is absolutely no problem for most of the csaes. However, we can make use of generics in TypeScript to make this more powerful.
Since this article isn't about Generics in TypeScript, I would refrain from going deep on that. You can learn about generics here on the main TS website or in TypeScript Deep Dive eBook from Basarat Ali Syed
Our improved version of AbstractValueAccessor using TS generics looks like this:
Notice that we have replace 'any' type with a generic type T. AbstractValueAccessor is now a generic class. Custom components while extending this class need to provide a type to get typed instance of this abstract class. The internal model/state of the component which is represented by the _value variable also makes use of the same type. The value getters/setters and also the writeValue method makes use of the same type T. This is a very simple implementation of a base value accessor. In more complex cases, you can replace all your model type with type type T anywhere in your base/abstract value accessor.
In our custom components, the only change that we will have to make is to specify a type while extending AbstractValueAccessor. Here is how our updated custom controls look like:
The above snippets represent that CustomInput component's model is of type 'string' and CustomRange component's model is of type 'number'. You can think of passing complex type in place of these simple generic types to make this example more powerful.
And here is a final Plnkr for your reference:
In this article, we learned how to go DRY on implementing custom control value accessors in Angular. This basic approach works well for most of the use cases. One thing that I would recommend & that might happen in your real world scenarios is you might end up having multiple abstract versions of control value accessors. Why? Because you may want to control similar control value accessors together. For example, a basic control value accessor might work well for components in which the model consists of a single value (a string, a number etc) but for all custom components where the model is not a single value but a list of values, like checkbox group or special selector components, you may want to separate our a special abstract class for those type of components. Don't go too deep on class inheritance hierarchies. Thanks for reading!
Feel free to leave a comment, question, suggestion and corrections. Until next time, Happy learning!