Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Brian Ghidinelli
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Brian Ghidinelli@ghidinelli )

Host Bindings Are Breaking The ngModel Bridge In Angular 2 Beta 11

By Ben Nadel on

For a while now, I've been very, very sad that I cannot figure how to get ngModel to work in Angular 2 for custom input controls. This sadness has persisted from Beta 1 through to the current (at the time of this writing) Beta 11. Now, however, I've been able to narrow the source of my sadness down to host bindings. For some reason, the change-detection for host bindings works differently than the change-detection for views. And this difference causes ExpressionChangedAfterItHasBeenCheckedException errors to be thrown unless you duct-tape your workflow with some shenanigans like a setTimeout().


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To demonstrate this, I've created a super simple Toggle component that accepts a "value" input and emits a "valueChange" event. Both the Toggle's view and its host bindings depend on the "value" input in order to render completely (with the view managing the markup and the host bindings managing CSS class attribution).

On its own, the Toggle component doesn't know anything about two-way data binding or the concept of ngModel. This keeps the responsibility of the Toggle component very focused and keeps it in adherence with the one-way data flow philosophy being used by Angular 2. As such, if we want to use ngModel with this Toggle component, we have to create a "bridge" between the two. This bridge is known as a "value accessor."


 
 
 

 
 Host bindings break the ngModel bridge in Angular 2 Beta 11. 
 
 
 

I don't want to get too much into the details about how ngModel or the value accessor bridge works as that is somewhat tangential to the point of this post. Really, what I want to do is simply demonstrate that this approach works well until you add the host bindings that depend on the input. Then, the whole thing breaks. That is, until you add some shady hackery like a setTimeout() call.

The following code is "working" code because I have both the host bindings and the setTimeout() call commented-out. To see this breaking, take a look at the video (where I uncomment those parts and run the page).

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Host Bindings Are Breaking The ngModel Bridge In Angular 2 Beta 11
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • Host Bindings Are Breaking The ngModel Bridge In Angular 2 Beta 11
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/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",
  • directives: [ require( "TOGGLE_DIRECTIVES" ) ],
  • template:
  • `
  • <toggle [(ngModel)]="toggleIsOn"></toggle>
  •  
  • <p>
  • Toggle value: {{ ( toggleIsOn ? "Yes" : "No" ) }}
  • </p>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine whether or not the toggle is currently on.
  • // --
  • // NOTE: We need this start as TRUE so that when the Toggle
  • // component is mounted, it's internal value starts as "undefined"
  • // (which is a falsey) and then moves to "true" (which is obviously
  • // a truthy). That will cause the Toggle's host bindings to poop
  • // the bed.
  • vm.toggleIsOn = true;
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide all of the directives need to work the Toggle, including the
  • // ngModel variations.
  • define(
  • "TOGGLE_DIRECTIVES",
  • function registerToggleDirectives() {
  •  
  • return([
  • require( "Toggle" ),
  • require( "ToggleForNgModel" )
  • ]);
  •  
  • }
  • );
  •  
  •  
  • // I provide a Yes / No toggle component.
  • // --
  • // NOTE: Notice that the core Toggle component DOESN'T KNOW ANYTHING ABOUT
  • // NGMODEL or the concept of "value accessors." That's a good thing - it goes
  • // beyond the scope of responsibility for this component.
  • define(
  • "Toggle",
  • function registerToggle() {
  •  
  • // Configure the Toggle component definition.
  • ng.core
  • .Component({
  • selector: "toggle",
  • inputs: [ "value" ],
  • outputs: [ "valueChange" ],
  • host: {
  • "(click)": "handleClick()"
  •  
  • // If you omit these host bindings, then everything here
  • // works as one would expect. However, if you include these
  • // host bindings AND the value input's first value is a
  • // truthy, Angular 2 will throw the:
  • // --
  • // ExpressionChangedAfterItHasBeenCheckedException
  • // --
  • // ... error. To see that in action, uncomment the following
  • // host bindings. You only need one of these bindings to
  • // trigger the error.
  • // --
  • // , "[class.toggled-on]": "value"
  • // , "[class.toggled-off]": "! value"
  • },
  • template:
  • `
  • {{ ( value ? "Yes" : "No" ) }}
  • `
  • })
  • .Class({
  • constructor: ToggleController
  • })
  • ;
  •  
  • return( ToggleController );
  •  
  •  
  • // I control the Toggle component.
  • function ToggleController() {
  •  
  • var vm = this;
  •  
  • // I am the event stream for the valueChange output.
  • vm.valueChange = new ng.core.EventEmitter();
  •  
  • // Expose the public methods.
  • vm.handleClick = handleClick;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the internal click event on the component.
  • function handleClick() {
  •  
  • // When the user clicks on the toggle, we can't change the value
  • // directly - that would mess up the one-way data flow. Instead,
  • // we have to emit the value change event and let the calling
  • // context decide if it wants to respond by updating the inputs.
  • vm.valueChange.next( ! vm.value );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // I provide an ngModel-enabled bridge for the Toggle component.
  • define(
  • "ToggleForNgModel",
  • function registerToggleForNgModel() {
  •  
  • // Configure the ToggleForNgModel directive definition.
  • ng.core
  • .Directive({
  • // Notice that we are only matching on instances of the Toggle
  • // component that also include the ngModel directive.
  • selector: "toggle[ngModel]",
  • host: {
  • "(valueChange)": "handleValueChange( $event )"
  • },
  •  
  • // When ngModel is being used, we need to create a bridge between
  • // the ngModel directive and the target component. That bridge
  • // has to implement the "value accessor" interface. In this case,
  • // we're telling Angular to use THIS DIRECTIVE INSTANCE as that
  • // value accessor provider. This means that the following
  • // controller needs to provide the value accessor methods:
  • // --
  • // * registerOnChange
  • // * registerOnTouched
  • // * writeValue
  • // --
  • // NOTE: You don't need the forwardRef() here because we are
  • // using ES5 instead of TypeScript. Woot!
  • providers: [
  • ng.core.provide(
  • ng.common.NG_VALUE_ACCESSOR,
  • {
  • useExisting: ToggleForNgModelController,
  • multi: true
  • }
  • )
  • ]
  • })
  • .Class({
  • constructor: ToggleForNgModelController
  • })
  • ;
  •  
  • ToggleForNgModelController.parameters = [
  • new ng.core.Inject( require( "Toggle" ) )
  • ];
  •  
  • return( ToggleForNgModelController );
  •  
  •  
  • // I control the ToggleForNgModel directive.
  • // --
  • // NOTE: Since this controller is performing double-duty as both the
  • // directive controller AND the valueAccessor (for ngModel), it is also
  • // implementing the value accessor interface.
  • function ToggleForNgModelController( toggle ) {
  •  
  • var vm = this;
  •  
  • // Eventually, ngModel will register its own change hander. Until
  • // then, let's start with a no-op to keep the consumption uniform
  • // in the following code.
  • var onChange = function noop() {};
  •  
  • // Expose the public methods.
  • vm.handleValueChange = handleValueChange;
  • vm.registerOnChange = registerOnChange; // Value accessor interface.
  • vm.registerOnTouched = registerOnTouched; // Value accessor interface.
  • vm.writeValue = writeValue; // Value accessor interface.
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the valueChange event coming out of the Toggle component.
  • // Since ngModel doesn't know about this event, we have to bridge the
  • // gap between the Toggle component and the ngModel directive.
  • function handleValueChange( newValue ) {
  •  
  • // When we invoke the onChange() value accessor method, ngModel
  • // already assumes that the DOM (Document Object Model) is in the
  • // correct state. As such, we have ensure that the Toggle
  • // component reflects the change that it just emitted.
  • // --
  • // NOTE: At this point, we are disregarding the one-way data flow
  • // paradigm. But, that's the WHOLE POINT OF NG-MODEL.
  • toggle.value = newValue;
  •  
  • // Tell ngModel so that it can synchronize its own internal model.
  • onChange( newValue );
  •  
  • }
  •  
  •  
  • // I register the onChange handler provided by ngModel.
  • function registerOnChange( newOnChange ) {
  •  
  • onChange = newOnChange;
  •  
  • }
  •  
  •  
  • // I register the onTouched handler provided by ngModel.
  • function registerOnTouched() {
  •  
  • // console.log( "registerOnTouched" );
  •  
  • }
  •  
  •  
  • // I implement the value input invoked by ngModel. When ngModel
  • // wants to update the value of the target component, it doesn't
  • // know what property to use (or how to transform that value to
  • // something meaningful for the target component). As such, we have
  • // to bridge the gap between ngModel and the input property of the
  • // Toggle component.
  • function writeValue( newValue ) {
  •  
  • // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
  • // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
  • // --
  • // If the Toggle component has [host] bindings that depend on the
  • // value input, then you need to add this setTimeout(). However,
  • // if the Toggle component does NOT USE [host] bindings, then you
  • // can safely omit this setTimeout() ... even if the Toggle view
  • // depends on the value input. THIS HAS GOT TO BE A BUG.
  • // --
  • //setTimeout(
  • // function avoidExpressionChangedAfterItHasBeenCheckedException() {
  •  
  • toggle.value = !! newValue; // Cast to boolean.
  •  
  • // }
  • //);
  • // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
  • // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
  •  
  • // CAUTION: Setting the value in this way will not trigger the
  • // ngOnChanges() life-cycle method on the Toggle component.
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

If you don't want to watch the video, here's what happens when I uncomment these two lines of code:

  • // , "[class.toggled-on]": "value"
  • // , "[class.toggled-off]": "! value"

... and then load the page:


 
 
 

 
 Host bindings, that depend on input values, break the ngModel bridge in Angular 2 Beta 11. 
 
 
 

As you can see, Angular 2 throws an ExpressionChangedAfterItHasBeenCheckedException error. This goes away if you add the setTimeout() in the value accessor's writeValue() method. But, that is clearly not a good thing.

The part of this that really confuses me is that both the component template and its host bindings depend on the "view" input. Yet, the view is able to re-render properly when ngModel tells it to write a new value. For some reason, only the host binding has trouble with this change. And, it's this divergent behavior that makes me think this is a bug. If this were a feature, it would also break for the view template.




Reader Comments

Hi Ben -

I converted your sample to TypeScript and then re-implemented in a different direction in which I forget about `[(ngModel)]` and create my own 2-way binding property, `[(toggle)]`.

No need for the custom ValueAccessorDirective in the middle which seems to play a role in your host update challenges.

It's a simpler way to go and all you lose, AFAIK, is the NgForm-family of controls and validators.

I've rolled my own two-way binding property a few times now and been happy with it. YMMV

http://plnkr.co/edit/g60juxpdgIjLOZwLG6Fw?p=preview

Reply to this Comment

@Ward,

I think I'm starting to lean that way as well. I love the idea of all of the robust form integration that you get with ngModel. But, the honest truth is that I've never really used any of it historically. I think it holds a lot of value; but, not using it has never held me back.

And, while all of the ngModel is a tremendous hurdle, the nice thing about it is that it can be added after the fact as something external to a target component. So, you can definitely design a component to just have value/valueChange or toggle/togggleChange interactions, another developer can always come along, create another directive with:

selector: "toggle[ngModel]"

... and bridge the gap when / if they need to.

The most interesting question that all of this has brought to mind is - what "contract" is assumed when a component exposes a public property as an "Input" property. When a component exposes an Input, that component can then make assumption about things like ngOnChanges() and OnPush change detection. Which means that if ngModel gets thrown into the mix, the value accessor bridge has to bridge those contractual gaps ... which I think very few people will think about.

Reply to this Comment

@All,

Oh sweet chickens, it looks like this was fixed on or by Angular 2 Beta 17!!

Life's a garden, dig it!

Reply to this Comment

@All,

Sorry, I think I may have jumped the gun with that last statement - Angular 17 seems to have fixed the host bindings but broken something with OnPush change detection. Sadness.

Unless I'm totally messing something up.

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.