Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Brien Hodges and Jessica Kennedy
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Brien Hodges and Jessica Kennedy@mistersender )

Creating An Abstract Value Accessor For NgModel In Angular 2 Beta 17

By Ben Nadel on

Over the last few months, I've be doing a lot of work with ngModel and Angular 2. To be honest, I probably won't use custom ngModel components all that much in production. But, it's been such an epic journey that I feel somewhat compelled to continue noodling on it. Implementing ngModel - which is really implementing a "value accessor" - requires a non-trivial amount of effort. But, I see a pattern in the noise. And, I think 95% of that effort can be factored out into a reusable abstract base class.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

In Angular 2, the value accessor is a service that bridges the communication gap between the ngModel directive(s) and your custom components. The responsibilities of the value accessor are multi-faceted:

  • Push external data into the component.
  • Announce data-change events emitted by the component.
  • Loop data-change events back into the component (implementing two-way data flow).
  • Format data on the way into the component (implementing optional formatters).
  • Parse data on the way out of the component (implementing optional parsers).
  • Coordinate with change-detection strategies.
  • Coordinate with the component life-cycle event handlers.

This looks like a lot of functionality. And it is. But, it's very repetitive. And, when you boil it down, all of the logic hinges on just a few unique pieces of data:

  • The component reference.
  • The name of the property that's being mutated.
  • The event being emitted.
  • The changeDetectorRef.

If we can isolate these references, the rest of the code becomes boilerplate that we can factor out and extend.

To experiment with this idea, I've created a service called AbstractValueAccessor which exposes two sets of public methods. One set of methods are consumed by ngModel (as part of the ControlValueAccessor interface; the other set of methods (which I've bolded) are intended to be consumed or overridden by your concrete class.

  • constructor( target, property, changeDetectorRef )
  • format( incomingValue ) - Optional format method you can override in your concrete class.
  • parse( outgoingValue ) - Optional parse method you can override in your concrete class.
  • onChange( newValue ) - Method you bind to your host's change event.
  • onTouched() - Method you call when your component has been touched.
  • registerOnChange( newNgModelOnChange ) - Consumed by ngModel.
  • registerOnTouched( newNgModelOnTouched) - Consumed by ngModel.
  • writeValue( newValue ) - Consumed by ngModel.

The key is the constructor() function - that's where we pass in those unique references on which the rest of the functionality hinges: the target component, the name of the property, and the changeDetectorRef. The abstract class (more or less) takes care of the rest.

To see this in action, I've created a Toggle component that accepts a [value] property and emits a (valueChange) event. On its own, this component doesn't know anything about ngModel; so, we have to build a value accessor bridge using our AbstractValueAccessor to ngModel-enable it.

While I love ES5, I must admit that, in this demo, our value accessor has so little logic in it that the ES5 "cruft" becomes frustrating. In the following code, I'm pretty sure that the TypeScript version of the ToggleForNgModelController() class would be about 1/3 smaller (or maybe even smaller than that).

Don't get me wrong, though - I still love me some ES5.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating An Abstract Value Accessor For NgModel In Angular 2 Beta 17
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • Creating An Abstract Value Accessor For NgModel In Angular 2 Beta 17
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/17/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/17/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/17/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • ng.platform.browser.bootstrap( require( "App" ) );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  •  
  • // Notice that we are proving two Toggle directives - one that
  • // handles the Toggle functionality; and, one that implements
  • // the ngModel value accessor.
  • directives: [
  • require( "Toggle" ),
  • require( "ToggleForNgModel" )
  • ],
  • template:
  • `
  • <toggle [(ngModel)]="isOn"></toggle>
  •  
  • <p>
  • Set Toggle:
  • <a (click)="setToggle( true )">On</a>
  • <a (click)="setToggle( false )">Off</a>
  • </p>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine whether or not the toggle is on.
  • vm.isOn = false;
  •  
  • // Expose the public methods.
  • vm.setToggle = setToggle;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I set the current toggle value.
  • function setToggle( newValue ) {
  •  
  • vm.isOn = newValue;
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a toggle component that accepts a [value] input binding and emits
  • // a (valueChange) output binding event.
  • // --
  • // NOTE: The Toggle component doesn't know anything about ngModel. It only knows
  • // about the [value] and (valueChange) bindings. All ngModel functionality is
  • // provided by the sibling directive that implements a "value accessor" bridge.
  • define(
  • "Toggle",
  • function registerToggle() {
  •  
  • // Configure the Toggle component definition.
  • ng.core
  • .Component({
  • selector: "toggle",
  • inputs: [ "value", "onLabel", "offLabel" ],
  • outputs: [ "valueChange" ],
  • host: {
  • "(click)": "handleClick()",
  • "[class.for-on]": "value",
  • "[class.for-off]": "! value"
  • },
  • // NOTE: OnPush currently broken in Beta 17.
  • // --
  • // changeDetection: ng.core.ChangeDetectionStrategy.OnPush,
  • template:
  • `
  • {{ ( value ? onLabel : offLabel ) }}
  • `
  • })
  • .Class({
  • constructor: ToggleController,
  •  
  • // Define the life-cycle methods on the prototype to that they
  • // will be picked up at runtime.
  • ngOnChanges: function noop() {}
  • })
  • ;
  •  
  • return( ToggleController );
  •  
  •  
  • // I control the Toggle component.
  • function ToggleController() {
  •  
  • var vm = this;
  •  
  • // I hold the output text for the given states.
  • vm.offLabel = "Off";
  • vm.onLabel = "On";
  •  
  • // I hold the value of the toggle.
  • vm.value = false;
  •  
  • // I hold the event stream for the valueChange output binding.
  • vm.valueChange = new ng.core.EventEmitter();
  •  
  • // Expose the public methods.
  • vm.handleClick = handleClick;
  • vm.ngOnChanges = ngOnChanges;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the internal click event on the component.
  • function handleClick() {
  •  
  • // Since we are adhering to a one-way data flow, we can't change
  • // the value directly. Instead, we have to emit a change and then
  • // leave it up to the calling context as to whether or not the
  • // change should be committed back to the component.
  • vm.valueChange.next( ! vm.value );
  •  
  • }
  •  
  •  
  • // I get called when any of the input bindings change.
  • function ngOnChanges( changes ) {
  •  
  • console.log(
  • "ngOnChanges: [value] from [%s] to [%s].",
  • ( changes.value.isFirstChange() ? false : changes.value.previousValue ),
  • changes.value.currentValue
  • );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide an ngModel-enabled bridge for the Toggle component.
  • define(
  • "ToggleForNgModel",
  • function registerToggleForNgModel() {
  •  
  • var AbstractValueAccessor = require( "AbstractValueAccessor" );
  •  
  • // Configure the ToggleForNgModel directive definition.
  • ng.core
  • .Directive({
  • // As the ngModel bridge, we want to match on instances of the
  • // Toggle that are attempting to use ngModel.
  • selector: "toggle[ngModel],toggle[ngControl]",
  •  
  • // The value accessor bridge has to deal with both input and
  • // output bindings. As such, we have to listen for (valueChange)
  • // events on the target component and translate them into "change"
  • // events on ngModel. For this, we will use the onChange() event
  • // handler provided by our Abstract Value Accessor.
  • host: {
  • "(valueChange)": "onChange( $event )"
  • },
  •  
  • // Tell Angular that we're going to be using THIS DIRECTIVE as
  • // the VALUE ACCESSOR implementation.
  • providers: [
  • ng.core.provide(
  • ng.common.NG_VALUE_ACCESSOR,
  • {
  • useExisting: ToggleForNgModelController,
  • multi: true
  • }
  • )
  • ]
  • })
  • .Class({
  • constructor: ToggleForNgModelController
  • })
  • ;
  •  
  • // Have our directive extend our base value accessor.
  • ToggleForNgModelController.prototype = Object.create( AbstractValueAccessor.prototype );
  •  
  • // Configure the injectables.
  • ToggleForNgModelController.parameters = [
  • new ng.core.Inject( require( "Toggle" ) ),
  • new ng.core.Inject( ng.core.ChangeDetectorRef )
  • ];
  •  
  • return( ToggleForNgModelController );
  •  
  •  
  • // I control the ToggleForNgModel directive.
  • function ToggleForNgModelController( toggle, changeDetectorRef ) {
  •  
  • // Invoke the super constructor.
  • AbstractValueAccessor.call( this, toggle, "value", changeDetectorRef );
  •  
  • // NOTE: There's nothing else to do here - the rest of the heavy
  • // lifting is performed by the base class.
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide an abstract / base Value Accessor.
  • define(
  • "AbstractValueAccessor",
  • function registerAbstractValueAccessor() {
  •  
  • // I provide a base Value Accessor class that is intended to be extended
  • // by any class that needs to implement the ControlValueAccessor interface.
  • // While this class takes care of the ControlValueAccessor methods, it
  • // exposes several hooks for the base class to either override or invoke:
  • // --
  • // * onChange() - I handle the value change from the target component.
  • // * onTouched() - I handle the touch event from the target component.
  • // * format() - I format values going into the target component.
  • // * parse() - I parse values coming out of the target component.
  • // --
  • function AbstractValueAccessor( target, property, changeDetectorRef ) {
  •  
  • // I hold the change detector reference (if supplied) used to mark
  • // the component as dirty (for OnPush change detection).
  • this._changeCount = 0;
  • this._changeDetectorRef = ( changeDetectorRef || null );
  •  
  • // I hold the change handlers registered by native Angular directives.
  • this._ngModelOnChange = null;
  • this._ngModelOnTouched = null;
  •  
  • // I hold the name of the property on the target component that is
  • // being modified via the two-way data flow.
  • this._property = property;
  •  
  • // I hold the component for which we are implementing the ngModel-
  • // based two-way data flow.
  • this._target = target;
  •  
  • }
  •  
  • // Define the instance methods.
  • AbstractValueAccessor.prototype = {
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I format the property value going into the target component.
  • // --
  • // NOTE: This method is intended to be OVERRIDDEN by the concrete class.
  • format: function( incomingValue ) {
  •  
  • return( incomingValue );
  •  
  • },
  •  
  •  
  • // I handle the property change event emitted by the target component.
  • // --
  • // NOTE: This method is intended to be INVOKED by the concrete class.
  • onChange: function( newValue ) {
  •  
  • // The whole point of the ngModel workflow is that we are breaking
  • // the one-way flow of data. As such, we want to take the emitted
  • // value and pipe it right back into the target component.
  • this._applyChangesToTarget( this._getValue(), newValue );
  •  
  • // Tell Angular that the component value has changed.
  • if ( this._ngModelOnChange ) {
  •  
  • this._ngModelOnChange( this.parse( newValue ) );
  •  
  • }
  •  
  • },
  •  
  •  
  • // I handle the touch event emitted by the target component.
  • // --
  • // NOTE: This method is intended to be INVOKED by the concrete class.
  • onTouched: function() {
  •  
  • this._ngModelOnTouched && this._ngModelOnTouched();
  •  
  • },
  •  
  •  
  • // I parse the property value coming out of the target component.
  • // --
  • // NOTE: This method is intended to be OVERRIDDEN by the concrete class.
  • parse: function( outgoingValue ) {
  •  
  • return( outgoingValue );
  •  
  • },
  •  
  •  
  • // I register the onChange handler provided by ngModel.
  • registerOnChange: function( newNgModelOnChange ) {
  •  
  • this._ngModelOnChange = newNgModelOnChange;
  •  
  • },
  •  
  •  
  • // I register the onTouched handler provided by ngModel.
  • registerOnTouched: function( newNgModelOnTouched) {
  •  
  • this._ngModelOnTouched = newNgModelOnTouched;
  •  
  • },
  •  
  •  
  • // I write the external value to the target component.
  • writeValue: function( newValue ) {
  •  
  • this._applyChangesToTarget( this._getValue(), this.format( newValue ) );
  •  
  • },
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I apply the previous / new values to the target component and work
  • // with the change detector and component life-cycle methods to ensure
  • // that the target component is kept in sync.
  • _applyChangesToTarget: function( previousValue, newValue ) {
  •  
  • // Pipe the value right back into the target component.
  • this._setValue( newValue );
  •  
  • // If we have a change detector, flag the component as dirty.
  • if ( this._changeDetectorRef ) {
  •  
  • this._changeDetectorRef.markForCheck();
  •  
  • }
  •  
  • // If the target component exposes the ngOnChanges() life-cycle
  • // method, we have to build the changes object and pass it through.
  • if ( this._target.ngOnChanges ) {
  •  
  • this._target.ngOnChanges( this._buildChanges( previousValue, newValue ) );
  •  
  • }
  •  
  • },
  •  
  •  
  • // I build and return a collection of SimpleChange objects for the
  • // given change in the target property.
  • _buildChanges: function( previousValue, newValue ) {
  •  
  • var changes = {};
  • var change = changes[ this._property ] = new ng.core.SimpleChange( previousValue, newValue )
  •  
  • // Unfortunately, Angular uses a private class in its internal
  • // workflow to indicate which change is the first change. Since
  • // we don't have access to that class, we need to patch the
  • // SimpleChange object on the first change to adhere to the
  • // behavior that the target component is expecting.
  • if ( ! this._changeCount++ ) {
  •  
  • change.isFirstChange = this._returnTrue;
  •  
  • }
  •  
  • return( changes );
  •  
  • },
  •  
  •  
  • // I get the property value from the target component.
  • _getValue: function() {
  •  
  • return( this._target[ this._property ] );
  •  
  • },
  •  
  •  
  • // I return true. Always.
  • _returnTrue: function() {
  •  
  • return( true );
  •  
  • },
  •  
  •  
  • // I store the give value into the property of the target component.
  • _setValue: function( value ) {
  •  
  • this._target[ this._property ] = value;
  •  
  • }
  •  
  • };
  •  
  • return( AbstractValueAccessor );
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

There's a lot of code here. But, if you look at our value accessor implementation, there's really only like 2 lines of meaningful code: where it binds to the (valueChange) event and where it calls the super constructor. The rest of the code is handled by the base class.

When we run the above code and toggle the component a few times, we get the following output:


 
 
 

 
 Using an abstract value accessor in Angular 2 in order to ngModel-enable your custom components. 
 
 
 

As you can see, our Toggle component works seamlessly with ngModel despite the fact that it knows nothing about ngModel. And, our abstract base class is completely reusable with any other type of component that needs to interact with ngModel. This should make authoring custom form controls much easier in Angular 2.




Reader Comments

Hi Ben, this article really rocks! Do you think there would be a possibility for you to include a Typescript implementation? Considering that a great deal of the Angular 2 developer community are using it and many of us are still learning ES6, it would be really valuable to have these examples in TS.

Thanks for this great article!

Reply to this Comment

@Will de la,

Thanks for the kind words! I actually _just got_ TypeScript running in my demos using System.js. As such, I'll try to be writing my demos using TS going forward. That said, I'll try to re-do this one in TypeScript as well as I agree that it would benefit from the shorter syntax in places.

Thanks!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.