FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 1

For the last 4 or 5 days, I've been trying to wrap my head around the use of ngModel in Angular 2 Beta 1. And, just to be clear, I still haven't figured it out. It's incredibly frustrating, which I let show a bit on Twitter yesterday. Probably not the right way to express myself; but, I was at my whit's end.

I think the root cause of my frustration is that I don't actually have any handle whatsoever on how change detection works in Angular 2. In AngularJS 1.x, change detection was a rather straightforward mental model - there was a digest and the digest kept running until values stabilized. With Angular 2, there are many more rules, none of which are yet apparent to me.

My goal for this post was to try and create a custom component which could then be consumed by the ngModel directive. Like I said above, this still doesn't work (though when you run it, it might appear correct thanks to the hackiest of setTimeout() hacks). At this point, I'm just posting it to see if anyone has any feedback; or, can at least tell me where I am being a huge bonehead about the whole change detection process in Angular 2.

To start with, I needed to make a custom Angular 2 component that represents some form of state. In this case, I made a "toggle" component that takes a value and renders "truthy" or "falsey" text depending on the input. When the user clicks on this component, it then needs to emit an output event for the change. Ultimately, I created a YesNoToggle component with the following inputs and outputs:

inputs: [ "value", "yes", "no" ]

outputs: [ "valueChange" ]

The "yes" and "no" inputs define the truthy and falsey text to render, respectively; the "value" input is the value used to selected the appropriate text; and, the "valueChange" output is the event emitted when the user intends to change the value. The important thing to see here is that there is no ngModel - this is just the custom component and the application that consumes it:

// I provide a toggle component that renders "Yes" text or "No" text based

// on the state of its input value. When the component is activated, it will

// emit a "valueChange" event with what the value of the input WOULD HAVE BEEN

// if the value were mutated internally.

define(

"YesNoToggle",

function registerYesNoToggle() {

// Configure the YesNoToggle component definition.

var YesNoToggleComponent = ng.core

.Component({

selector: "yes-no-toggle",

inputs: [ "value", "yes", "no" ],

outputs: [ "valueChangeEvents: valueChange" ],

host: {

"(click)": "toggle()",

"[class.for-yes]": "value",

"[class.for-no]": "! value"

},

template:

`

<span *ngIf="value">{{ yes }}</span>

<span *ngIf="! value">{{ no }}</span>

`

})

.Class({

constructor: YesNoToggleController

})

;

return( YesNoToggleComponent );

// I control the YesNoToggle component.

function YesNoToggleController() {

var vm = this;

// I am the event stream for the valueChange output.

vm.valueChangeEvents = new ng.core.EventEmitter();

// Expose the public methods.

vm.toggle = toggle;

// ---

// PUBLIC METHODS.

// ---

// I emit the value change event when the user clicks on the host.

function toggle() {

// Notice that we are emitting the value of the input as it would

// have been had we implemented the mutation. However, since we

// don't own the value, we can't mutate it - we can only announce

// that it maybe should be mutated.

vm.valueChangeEvents.emit( ! vm.value );

}

}

}

);

</script>

</body>

</html>

As you can see, the App component provides the "value" input to the YesNoToggle component and then binds to the "valueChange" output:

This works great, but it doesn't give us all of the nice tooling provided by ngModel, ngControl, and ngForm (though to be honest, I don't know too much about those yet). As such, the next step would be to enable this custom component to be ngModel consumable. This isn't as easy as adding [(ngModel)] to the template. And, we don't want to alter our existing component, since the existing component shouldn't have to know anything about ngModel. As such, we have to create an additional directive that glues the ngModel directive to the YesNoToggle component and provides a means to convey data back and forth.

In this case, I've created another directive that selects on:

yes-no-toggle[ngModel]

Notice that this selects only for YesNoToggle components that are also using ngModel (since the core YesNoToggle component works just fine without it).

Now, ngModel doesn't know anything about our target component. It doesn't know which value to set and which events to listen for. As such, this secondary directive has to translate ngModel values into YesNoToggle input values; and, it has to translate YesNoToggle output events into ngModel change events.

And, this is where things start to fall apart for me. In the following code, you will notice that I am wrapping my input-translation in a setTimeout(). If I don't do this, I would get the following AngularJS error:

Expression 'value ...' has changed after it was checked.

There is something (well, many things) that I don't understand about data checking and why this breaks. With the use of setTimeout(), the error is avoided; but, don't let that fool you into thinking this is a "working solution." The use of setTimeout() is merely a bandage on top of the gaping, pustulous wound that is my mental model of change detection.

// property to use. As such, we have to bridge the gap between ngModel

// and the input property of the YesNoToggle component.

function writeValue( newValue ) {

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

setTimeout(

function avoidExpressionChangedAfterItHasBeenCheckedException() {

yesNoToggle.value = !! newValue; // Cast to boolean.

}

);

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //

// CAUTION: If we don't use the setTimeout() method here, we get

// the following Angular error:

// --

// Expression 'value ...' has changed after it was checked.

// --

// I do not understand this, but Google shows me that this is a

// common problem. Hopefully one day, when I actually understand

// how change detection works in Angular 2, I won't need this.

// --

// NOTE: Enabling PROD mode is NOT A FIX (see note at top).

}

}

}

);

</script>

</body>

</html>

So, that's what I've got. Like I said, this might "look" like it works; but, it doesn't actually work. In fact, if you try it for yourself, you'll see a noticeable lag between the two components in the second demo as the ngModel-enabled component has to perform updates in a subsequent tick of the event loop.

If anyone can tell me where or why this is failing, I would be hugely appreciative.

No luck :( It prevents the error. But, the "host bindings" don't synchronize properly. Meaning, the border is "green" when the value is "false" and vice-versa (border is "red" when the value is "true").

Thanks to ZoneJS, In two different occasions Angular2's ChangeDetector wakes up and does two different kinds of book-keeping.

1) On each and every DOM event, ChangeDetector traverses the whole component-tree in a depth-first manner. For each node, it checks the component's template to see if any property bindings in the template (the ones enclosed with []) are changed. If so, it flags the node and all it's ancesstors up to the root node as dirty.

2) At the end of a "VM Turn" right before browser paint, Change detector examines the dirty-checked component-tree and given the current (on-screen) DOM, it figures the new DOM and let the browser render and paint.

The above is based on the "Default" strategy. To optimized the process application can get involved in the traversal of the whole tree and do smart things by using immutable objects or Observables, etc. This strategy is called "OnPush". That is conceptually easy to understand too.

Very interesting. I'm still not sure I could explain things back to you without some more trial and error. But, clearly I am missing something about the rules of what you can and can't do during change-detection (hence the Expression error I'm getting). So much to learn!!!

Ben - Greate example.I'm trying to do exactly the same in TS/ES2015 - 2 way bind a custom component to ngModel - I will wait for @Esfand's porting to TS/ES2015 - lets all hope we figure it out soon - it's driving me mad too!

@EddyEmbarrassingly, @Ben on Twitter let me know my knowledge of Angular2 is antiquated. So, I'm out of commission until I figure out what all those various options in ChangeDetectionStrategy enum are. Last time I checked there was only one option, 'OnChange', there. Now there are many with some new terminology such as 'Hydrated' I'd never heard of before in the context of Angular2.

I hope you will forgive me for this. But, we would really love to see you give Aurelia a spin :) When I was working with the Angular 2 team, I saw a rise in complexity that alarmed me and I felt it wasn't going to be the simple programming model I had hoped to help build. Aurelia is far from perfect, naturally, but maybe it's time to take a look and see if it would work better for you.

I totally understand that. However, I would be willing to bet one of the following is probably true:

* The job requests are for Angular 1* The job requests for Angular 2 are from sources that don't know much about it or haven't tried it out.

In either of those cases, as a technologist, you have an opportunity to help the business make a better technology decision. I would encourage you to think about what is pragmatic for the business. What will development effort, cost, maintenance, etc. be? If you help the company choose the best tech for their business needs (no matter what that is) then their success is also your success.

In our industry popularity alone should not be how we advise companies.

In a Slack chat room I happened to chat with Rob Wormald a member of the Angular2 team and a very helpful guy. According to him, none of the enum items in 'ChageDetectionStrategy' enum is relevant for users/programmers except the 'OnChange' item. The rest is only for internal usage.

The 'OnChage' item is only applicable when the component state is immutable. It signals to ChangeDetector this fact. Therefore it can act smarter. If the state is not immutable there is no need to set the 'OnChange' strategy item in the @component decorator.

First of all, I am flattered that you stopped by! Thanks so much. I know that you are not on the Angular team anymore; but, I have heard nothing but great things about what you've contributed to the new Router. I haven't dug into it yet; but, the idea of lazy loading and all the component-relative linking looks really exciting!

At the very least, I'll take a look at the intro video on the site. I've actually found digging somewhat into other frameworks, like ReactJS, very helpful in fleshing out a more well-rounded mental model.

So far, Angular 2 is definitely quite a bit more complex than AngularJS 1.x, despite the smaller number of directives. But, at the same time, I think I can see why individual features were done in a particular way. Sure, some stuff really confuses me (ala this post); but, I do concede I'm only a few weeks into my learning.

The most jarring thing, mentally, has been that I basically need throw away *almost everything* that I learned in AngularJS 1.x.

Sorry, AlmondJS is just a smaller version of RequireJS, though I think it's all AMD-compliant... maybe? I get a little lost with all the different module loading libraries. I believe that AlmondJS was build as a way to implement RequireJS in a non-build environment (or where the scripts had already been "built" and you don't need all the horse-power provided by RequireJS).

Ahh, thank you so much for the clarity. I thought I was going bananas! I was like, what the HECK, all these do the same exact thing :D That makes more sense now. That will make my exploration a bit more focused now.

I'd recommend posting questions on StackOverflow with working examples on Plunker to get more exposure and allowing people to try out their ideas.

My idea: have you tried using ChangeDetectorRef's detectChanges() method instead of the setTimeout approach? I'm not sure it'd work, but it has in some other cases -- it manually marks a component as needing updating where otherwise it had not.

For what it's worth, I spent a lot of time trying to figure out where the error is coming from (not from the Angular source code, but rather from my consumption of it). It looks like the issue is caused by the host bindings:

host: {...,"[class.for-yes]": "value","[class.for-no]": "! value"}

If you remove references to "value" in the host bindings, you no longer need to use the setTimeout() trick to avoid the error. What I don't understand is why the dynamic value works in the template, but NOT in the host bindings. It's the same element, presumably under the same change detection mechanism.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.