Angular - Custom Form Elements
The majority of the products I worked on was/is B2B and they consisted of many (MANY) web forms. You know how this goes.
Who is our best friend for situations like this? ctrl+c & ctrl+v :) Yeah I know. I've been there.
Someone might say that this "best friend" helps us in many different cases. Here a bell rings. What do you do when you have to re-use the same code again and again? The answer is Abstraction.
You know how to abstract your code in JS, in TS in C# and in many other languages. What about HTML though? Continue reading to find this out :)
What to build
Let's illustrate what we'd like to build
It doesn't seem hard. Let's see the code before any abstraction
The above HTML has 4 form controls along with their error messages. Let's improve it, minimize the code and make it more fun :) and easier to maintain
Create Your Custom Form Control
First things first.
1. Create a component
2. Tell Angular that this component should work with Template Driven and Reactive Forms
We registered the providers and implemented a new interface.
The NG_VALUE_ACCESSOR used to provide a ControlValueAccessor for form controls.
The interface ControlValueAccessor is used by Angular to sync with formControl and ngModel, and provides the following methods:
3. Let's add some HTML in our newly created component
Our goal is to remove as much repeated code from HTML as it could be. As such, we are gonna remove all the code that is wrapped in the <div class="form-group">. Hence our custom form element component will have the wrapper div and its children
The variables that we used in the template are just @Input() fields and a public one :)
4. Let's give it a try and use the component in our form.
At this point we have just a form control with no binding in our form model. Hold on your horses
We commented out the old code and we replaced it with the new component selector
5. Binding with form model
In order to bind your form control with the component, or in other words to make it work with formControlName or ngModel you have to utilize the writeValue method.
According to Angular's documentation
The writeValue method "Writes a new value to the element."
This is the fun part of our code. We are going to use a form field that is not immediately connected with our initial form that we've created in our form component. Wait, what? Let's see the code :)
Create a field and bind it in your HTML.
The diagram below depicts the data flow
** A friend of mine asked the following: "when the writeValue is been triggered?"
I won't go with the component life-cycle approach, but simply by saying that when the formControl has a value, firstName:['foo'], this value ("foo" in this example) will pass in the custom component and trigger the writeValue method.
Can I use now the formControlName? Ohh Yeah!
Up to that point we managed to pass the value from the main component to the custom form control. It's one way data binding. We now have to create two way data binding as we want whatever the user writes in our custom input control to go back in our formGroup -> formControl.
I promise that it won't last long and soon you will have your first custom control :)
6. Two way data binding
In order to implement a two way data binding you have to utilize the registerOnChange method
According to Angular's documentation
The registerOnChange "Registers a callback function that is called when the control's value changes in the UI."
Up to this point we created our custom Form Element and works as a charm.
I hear your whisper.... "what about the validation messages?" If you want to implement the validation messages continue reading, otherwise you can see the complete code in this repo https://github.com/profanis/angular-custom-form-elements/tree/custom-form-no-validation
7. Validation messages
The best approach to accomplish this is to have embedded validations using the NG_VALIDATORS Provider. (We won't cover this here)
If however you want to make it work with the bare minimum changes in your code, you can pass in the custom form control the FormControl as @Input()
You can display the error messages iterating the result of getErrors() method
You can find all the code in this repo https://github.com/profanis/angular-custom-form-elements
Thanks for spending your time reading my article. I hope you enjoyed it! :)
I am an Angular GDE, content creator who loves sharing knowledge