This post is based on something I learned that completely changed the way I designed Angular applications. Core UI components in the app such as buttons can change a lot, which can be a pain to maintain if you have to fix this in a lot of places. One example is using Angular Material buttons and then your UX’er wants to change the buttons to something different and now you need to find all the places in your application where the Angular Material buttons are used and fix them. Does this sound DRY to you?
The solution is to apply the adapter pattern which means to separate the abstraction and the implementation. This means that the interface can stay the same while you can change the implementation. Also, it gives the benefit that you can get to choose the interface you find useful and use that everywhere. Now, as a contrast to using “raw” Angular material buttons, you only need to change the adapter component one place to change all buttons in the application.
When should I use the adapter pattern?
You should use the adapter pattern for central UI components that may be hard to change without, eg. buttons, checkbox, and selects.
Applying the adapter pattern in an Angular app
We are going through how to apply the adapter pattern in an Angular app by wrapping the buttons into an adapter. For a complete demo of how this is done watch my demo project.
Creating the buttons adapter components
We are gonna create an adapter for buttons in our application, so they have the same interface and we can change all buttons by only change one central place.
First, clone my demo project or create a new Angular CLI project by opening the terminal and write:
ng new adapter-demo
We are gonna create a buttons adapter inside a folder called shared folder, which is where the shared components and pipes go.
We can generate a new buttons module with:
ng g m buttons
This creates a folder for buttons. We want to set up the following structure:
The ButtonsModule
is the module for all the buttons in our app. It exports a list of parent buttons and each is acting as a button adapter, wrapping the actual implementation of a button with an abstraction.
When looking at the folder structure picture, you see that we have button parent components and button child component. Button parent components are an abstract class and are the abstractions, that are actually used externally in the app and button child components are components internal to a button parent component.
The code for the button’s parent abstract class look like this:
This is the external interface to our button adapters. This interface allows us to specify a buttonType
(can be Primary or Secondary here), set the button as disabled and add style and classes to the button. We use the onClick
method and clicked event to support disabling the button.
To avoid parsing inputs and events around in the template for each child button component, the child components just need to inherit the ButtonChildComponent
abstract class:
Now, let’s say that we want to create an adapter for all the square buttons in our app. We can create such a square component as:
We see that this simply inherits ButtonParentComponent
.
The template for the SquareButtonComponent looks like this:
This switch case is for supporting three buttonTypes: primary, secondary and default.
Let’s look at how to create the primary buttonType:
The component class is simply inheriting our ButtonChildComponent
for making it get inputs and send events from the parent.
Finally, the actual implementation detail of the button is in the template:
This is styling the primary button as a Bootstrap primary button and is enabling passing of classes, style, and disabled state. When clicking the button it will call onClick and trigger the parent’s click event. This way, if we wanted to change the primary button to eg. Angular Material buttons, we only need to change this component and it will reflect everywhere.
This component can now be used like this:
Integrating an adapter UI component with forms using ControlValueAccessor
If we have UI components, that are taking input, we normally want to hook it into the form handling so we can do form validation and show error messages.
Let’s consider we were creating a date picker component based on Angular Material’s date picker. Let’s look at the internal implementation of the date picker:
Here we are just setting up an Angular material date picker and wrapping it in an MatFormField
and is using MatError
for showing error messages.
We want to support error handling as well as show an error message in case of errors. To support integrating with the host component’s form validation, we will need to implement ControlValueAccessor
:
The date picker is implementing ControlValueAccessor
by creating the methods: writeValue
, registerOnTouched
, registerOnChange
and setDisabledState
. Note here, that we are integrating the component in the form handling by providing it in the NG_VALUE_ACCESSOR
injection token.
The ControlValueAccessor
methods
Let’s go through what each method does and how each of these is implemented.
writeValue
This method is triggered when a value is set on the component either through [ngModel]
(template-driven forms) or formControl.setValue(someVal)
(reactive forms).
When this is triggered, we set the date on the date property.
registerOnTouched
This method is for setting a callback function for marking the control as touched. We save the callback method in a property and call it in the onDateChange
method.
registerOnChange
Like for registerOnTouched, but for triggering value change events. This method is for setting a callback function for triggering a value change event. We save the callback method in a property and call it in the onDateChange
method with the newly changed date.
setDisabledState
This method will be called when the component is set as disabled using [disabled]="true"
in the host component’s template. We can then pass that value to a isDisabled property in our component and use it to disable the internal date picker implementation.
Implementing the custom ErrorStateMatcher
ErrorStateMatcher
is coming from Angular material’s form field and is a way of specifying when the form control should be reflecting an error state like:
This is done by implementing an isErrorState
method.
For flexibility, we are allowing it to be overridden by a hasError
property or will otherwise reflect error state if the form control is invalid and the parent form is submitted.
Showing error messages
For the most flexibility, we are allowing the consumer to specify what error message to show as well or otherwise fall back to a sensible default using the errorMessage
property.
Using the date picker UI adapter
Now, we can use the date picker UI adapter like this:
Conclusion
In this post, we saw how the adapter pattern can provide your Angular app with good interfaces and flexibility. The benefit of this is to be able to change the implementation, without affecting the interface of the adapter component. We saw how to apply this in an Angular app that used Bootstrap as the implementation of the button adapter.
This made our application’s buttons easy to change by having a stable interface and only needing to change implementation details one central place.
We also looked at how to create a UI adapter component for UI components working with data inputs (a date picker) and how to integrate it in the form validation by implementing the ValueAccessControl
methods.
Thanks for reading. Remember to comment, share and follow me on Twitter.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.
8 thoughts on “Using the Adapter Pattern in Angular Apps for Easier Maintenance”
Pingback: Implementing a Plugin Architecture with Angular and OpenLayers – Christian Lüdemann IT
Pingback: The Three Building Blocks of Elite Angular Teams – Christian Lüdemann
Pingback: The Ten Commandments of Angular Development – Christian Lüdemann
Cool article and definitely the way to go. Your example is of a simple case though – the button. I have done the same for other form controls as well (inputs, the third-party ng-select and so on), and implement ControlValueAccessor to integrate with reactive forms. This has worked well, except in ng-select, where setting that control’s value raised an ExpressionChangedAfterItHasBeenCheckedError. Regardless of my error though, would you agree that’s the way to approach wrapping third-party inputs?
Yes, use ValueControlAccessor for integrating the ui adapter component in the form validation. I just added another section of the blog post explaining this 🙂
Pingback: Form validation with ControlValueAccessor – Christian Lüdemann
Have you looked at ngx-formly for abstracting forms ?
Thanks for an interesting articel. Though, i’ve got some questions:
1. Seems that SquareButtonComponent template with its switchcase is gonna be duplicated each time when we need to create new shpae of buttons, violationg DRY principle? I.e. we need CircleButtonComponent, then we;d have to duplicate this template with all switch cases?
2. Doesn’t injecting SquareButtonComponent into Primary and secondary buttons create tight coupling between PrimaryButtonComponent and SquareButtonComponent violation open-closed? Doens’t it prevent PrimaryButtonComponent from being able to be used with any other “any-shape”ButtonComponent? Couldn’t we just inject ChildButtonComponent, and provide it in “any-shape”ButtonComponent like this
{ provide: ChildButtonComponent, useExisting: forwardRef(() => "any-shape"ButtonComponent) }
instead?