Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ken Auenson
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ken Auenson@KenAuenson )

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

By Ben Nadel on

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 don't get ngModel - ahhh, so angry, much frustartion. 
 
 
 

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.

ASIDE: Can you feel my frustration?


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

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:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 1
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 1
  • </h1>
  •  
  • <h2>
  • Without ngModel - Just The Custom Component
  • </h2>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ "AppComponent" ],
  • function run( AppComponent ) {
  •  
  • ng.platform.browser.bootstrap( AppComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "AppComponent",
  • function registerAppComponent() {
  •  
  • var YesNoToggle = require( "YesNoToggle" );
  •  
  • // Configure the App component definition.
  • var AppComponent = ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ YesNoToggle ],
  •  
  • // Notice that our YesNoToggle component takes a [value] property
  • // and emits a (valueChange) event. This allows the calling context
  • // to determine how to mutate data by responding to the events and
  • // updating the inputs as necessary (or not at all).
  • // --
  • // NOTE: By using the "_" and "_Change" pattern of inputs and
  • // outputs, it means that we could also use the [(value)] syntax
  • // if we wanted to implement two-way data binding without the
  • // benefit of all the ngModel, ngControl, and ngForm functionality.
  • template:
  • `
  • <p>
  • Can I wheez the juice?
  • </p>
  •  
  • <yes-no-toggle
  • [value]="canWheezTheJuice"
  • (valueChange)="handleValueChange( $event )"
  • yes="Yeah buddy &mdash; wheez the ju-uice!"
  • no="No &mdash; no wheezing the ju-uice!">
  • </yes-no-toggle>
  •  
  • <p>
  • Current value:
  •  
  • <strong
  • class="indicator"
  • [class.can-wheez]="canWheezTheJuice">
  • {{ canWheezTheJuice }}
  • </strong>.
  • </p>
  •  
  • <p>
  • <a (click)="toggleExternally()">Toggle input</a>
  • outside of component.
  • </p>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppComponent );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine if it's OK to "wheez the juice!".
  • // --
  • // Pop-Culture Reference: https://www.youtube.com/watch?v=nPn6sqGUM5A
  • vm.canWheezTheJuice = true;
  •  
  • // Expose the public methods.
  • vm.handleValueChange = handleValueChange;
  • vm.toggleExternally = toggleExternally;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the valueChange event emitted by the YesNoToggle component
  • // and update the inputs accordingly.
  • function handleValueChange( newValue ) {
  •  
  • vm.canWheezTheJuice = newValue;
  •  
  • }
  •  
  •  
  • // I toggle the flag externally to the YesNoToggle component in an
  • // effort to ensure that the component will synchronize with the
  • // state of its own inputs.
  • function toggleExternally() {
  •  
  • vm.canWheezTheJuice = ! vm.canWheezTheJuice;
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // 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:


 
 
 

 
 Custom component workflow in Angular 2. 
 
 
 

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.


 
 
 

 
 ngModel control flow with custom components in Angular 2. 
 
 
 

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.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 1
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 1
  • </h1>
  •  
  • <h2>
  • With ngModel - Bridging The Gap
  • </h2>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ "AppComponent" ],
  • function run( AppComponent ) {
  •  
  • // DO NOT DO THIS! There are many answers on the net that say to
  • // enable "production mode" in order to get rid of the following error:
  • // --
  • // Expression '...' has changed after it was checked.
  • // --
  • // DO NOT DO THIS! It doesn't actually work. Parts of it may look like
  • // it is working, but part of the data are desynchronizing. This becomes
  • // obvious when you have multiple toggles on the page, one of which is
  • // not using ngModel.
  • // --
  • // ng.core.enableProdMode();
  •  
  • ng.platform.browser.bootstrap( AppComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "AppComponent",
  • function registerAppComponent() {
  •  
  • // NOTE: We are including a DIFFERENT DIRECTIVE here.
  • // --
  • // Core directive: YesNoToggle.
  • // NgModel-enabled directive: YesNoToggleForNgModel.
  • var YesNoToggle = require( "YesNoToggleForNgModel" );
  •  
  • // Configure the App component definition.
  • var AppComponent = ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ YesNoToggle ],
  •  
  • // In this version, we're putting two instances of the YesNoToggle
  • // component in the view at the same time. The first one uses the
  • // two-way data binding syntax to update the value directly. The
  • // second one uses ngModel to bridge the gap between the value and
  • // the component inputs.
  • template:
  • `
  • <p>
  • Can I wheez the juice?
  • </p>
  •  
  • <!-- Uses native two-way data binding. -->
  • <yes-no-toggle
  • [(value)]="canWheezTheJuice"
  • yes="Yeah buddy &mdash; wheez the ju-uice!"
  • no="No &mdash; no wheezing the ju-uice!">
  • </yes-no-toggle>
  •  
  • <!-- Uses NG-MODEL two-way data binding. -->
  • <yes-no-toggle
  • [(ngModel)]="canWheezTheJuice"
  • yes="Yeah buddy &mdash; wheez the ju-uice!"
  • no="No &mdash; no wheezing the ju-uice!">
  • </yes-no-toggle>
  •  
  • <p>
  • Current value:
  •  
  • <strong
  • class="indicator"
  • [class.can-wheez]="canWheezTheJuice">
  • {{ canWheezTheJuice }}
  • </strong>.
  • </p>
  •  
  • <p>
  • <a (click)="toggleExternally()">Toggle input</a>
  • outside of component.
  • </p>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppComponent );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine if it's OK to "wheez the juice!".
  • // --
  • // Pop-Culture Reference: https://www.youtube.com/watch?v=nPn6sqGUM5A
  • vm.canWheezTheJuice = true;
  •  
  • // Expose the public methods.
  • vm.handleValueChange = handleValueChange;
  • vm.toggleExternally = toggleExternally;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the valueChange event emitted by the YesNoToggle component
  • // and update the inputs accordingly.
  • function handleValueChange( newValue ) {
  •  
  • vm.canWheezTheJuice = newValue;
  •  
  • }
  •  
  •  
  • // I toggle the flag externally to the YesNoToggle component in an
  • // effort to ensure that the component will synchronize with the
  • // state of its own inputs.
  • function toggleExternally() {
  •  
  • vm.canWheezTheJuice = ! vm.canWheezTheJuice;
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // 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 );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide an ngModel-enabled version of the YesNoToggle.
  • define(
  • "YesNoToggleForNgModel",
  • function registerYesNoToggleForNgModel() {
  •  
  • var YesNoToggle = require( "YesNoToggle" );
  •  
  • // When we use the ngModel directive, the ngModel controller needs to
  • // know how to access and mutate data on the target component. To do
  • // this, it needs a "Value Accessor" that implements an interface for
  • // writing values and responding to changes. It checks for this accessor
  • // instance in the multi-provider collection, "NG_VALUE_ACCESSOR". In our
  • // case, we're going to use the EXISTING INSTANCE of our
  • // "YesNoToggleForNgModelDirective" directive as the accessor. This means
  • // that our directive will actually be playing double-duty, both as the
  • // local provider of the accessor as well as the implementer of the
  • // said accessor.
  • // --
  • // NOTE: We have to use a forwardRef() since the directive isn't actually
  • // defined yet.
  • var valueAccessorProvider = ng.core.provide(
  • ng.common.NG_VALUE_ACCESSOR,
  • {
  • useExisting: ng.core.forwardRef(
  • function resolveDIToken() {
  •  
  • return( YesNoToggleForNgModelDirective );
  •  
  • }
  • ),
  • multi: true
  • }
  • );
  •  
  •  
  • // NOTE: If we wanted to side-step the use of NG_VALUE_ACCESSOR, we could
  • // have had our directive "require" the ngModel instance and then inject
  • // itself into the ngModel by way of:
  • // --
  • // ngModel.valueAccessor = this;
  • // --
  • // However, I am not sure how I feel about this. To me, that approach
  • // seems to work "by coincidence", and not by intent.
  •  
  •  
  • // Configure the YesNoToggleForNgModel directive definition. Notice that
  • // the selector here only selects on instances of the YesNoToggle
  • // element that are also using ngModel.
  • // --
  • // NOTE: This directive is also a local provider of the valueAccessor
  • // collection which is providing the value accessor for the ngModel
  • // component (which, incidentally, is also this component instance).
  • var YesNoToggleForNgModelDirective = ng.core
  • .Directive({
  • selector: "yes-no-toggle[ngModel]",
  • host: {
  • "(valueChange)": "handleValueChange( $event )"
  • },
  • providers: [ valueAccessorProvider ]
  • })
  • .Class({
  • constructor: YesNoToggleForNgModelController
  • })
  • ;
  •  
  • // Configure the constructor to require the local YesNoToggle instance.
  • // We need it in order to bridge the gap between ngModel and the state
  • // of the toggle.
  • YesNoToggleForNgModelDirective.parameters = [
  • new ng.core.Inject( YesNoToggle )
  • ];
  •  
  • // Notice that we are returning TWO directives here - the core YesNoToggle
  • // component and the ngModel-enabled directive that we just defined. This
  • // way, the calling context doesn't have to explicitly include both
  • // directives - just "this one", which will implicitly include both.
  • return( [ YesNoToggle, YesNoToggleForNgModelDirective ] );
  •  
  •  
  • // I control the YesNoToggleForNgModel directive.
  • // --
  • // NOTE: Since this controller is also acting as double-duty for the
  • // valueAccessor, it is also implementing the value accessor interface.
  • function YesNoToggleForNgModelController( yesNoToggle ) {
  •  
  • var vm = this;
  •  
  • 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 YesNoToggle
  • // component. Since ngModel doesn't know about this event, we have
  • // to bridge the gap.
  • 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 YesNoToggle
  • // reflects the change that it just emitted.
  • yesNoToggle.value = newValue;
  •  
  • // Tell ngModel.
  • 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. 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.




Reader Comments

@Fergus,

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").

Reply to this Comment

@Fergus,

I really appreciate the feedback, though - this is so frustrating :D I hope that once I have the answer it's like "Oh, d'uh!" :D

Reply to this Comment

Angular2 Change-Detection Metal Model:

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.

Reply to this Comment

@Esfand,

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!!!

Reply to this Comment

I'm so badly infected with ES2015 and TypeScript viruses that I can't see the parts. As an exercise, I'll try to port your ES5 code to TS/ES2015 to see what's happening.

Reply to this Comment

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!

Eddy

Reply to this Comment

@Eddy
Embarrassingly, @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.

Reply to this Comment

Ben,

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.

Rob

Reply to this Comment

@Rob,

Aurelia is totes awesome (love it, love it, love it) but as a jobbing programmer I am compelled to follow the herd. Sadly, I've never seen a job description requiring Aurelia, yet Angular ones flood my inbox.

Pragmatism.

Reply to this Comment

@Fergus

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.

Reply to this Comment

@Ben @Fergus @Mike

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.

Just wanted to share this here.

Reply to this Comment

@Fergus

Ooops! Sorry I meant 'OnPush'. There is no 'OnChange' enum item. My mistake.

Apparently, 'OnPush' is the only one intended to be used by us and the way to set is is via the @component decorator (obviously using TypeScript).

When set (via the decorator) that tells the framework: "the component state is immutable!".

I've never used Angular2 without TypeScript, so I'm not familiar with setting 'OnPush' without TS.

Reply to this Comment

@Rob,

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.

Reply to this Comment

@Mike,

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).

Reply to this Comment

@Esfand,

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.

Reply to this Comment

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.

Relevant example Plunker, though using TypeScript: http://plnkr.co/edit/mSnsKXUCoxPqrmr3wpHz?p=preview

Reply to this Comment

@All,

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.

Very frustrating!

Reply to this Comment

@All,

Arrrg. I think I may have jumped the gun on that last statement. It looks like one thing started working and then another thing started breaking :/

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.