0

See the following Stackblitz: https://stackblitz.com/edit/angular-psqzbo?file=src%2Fapp%2Fhello.component.ts

Notice that there are two bindings to the width member: one is in the template, and the other is a host binding. The host binding is commented out right now. Notice that no ExpressionChanged error is being thrown-- this is because this.cdr.detectChanges() is being called after we update width in ngAfterViewInit.

Now uncomment the host binding. Observe that an ExpressionChanged error is thrown. Why? What makes these bindings different? Is this a bug?

EDIT: This is not a dupe of the linked question. I know why detectChanges is needed here, my question is why it is not working on the host binding. Please re-read.

sir_thursday
  • 5,270
  • 12
  • 64
  • 118
  • Possible duplicate of [Updating boolean in AfterViewInit causes "Expression has changed after it was checked"](https://stackoverflow.com/questions/45012141/updating-boolean-in-afterviewinit-causes-expression-has-changed-after-it-was-ch) – Vlad274 Apr 05 '19 at 01:44
  • No, please, it's not a dupe. Please re-read the question ugh I was worried it would be closed as a dupe... – sir_thursday Apr 05 '19 at 02:33
  • The accepted answer to that question (moving the code to `ngOnInit`) fixes the issue in your example, additionally the explanations present on the question provide a good walkthough of the issue. Does that solution not work for your situation? – Vlad274 Apr 05 '19 at 02:51
  • No, that does not work for my situation, as the offsetWidth is only available in ngAfterViewInit (the view has been composed then). The view has not been composed in ngOnInit, or ngAfterContentInit. – sir_thursday Apr 05 '19 at 02:53
  • Taking a step back though, the accepted answer to that question (moving the code to `ngOnInit`) does not "solve" this question. This question, again, is asking what makes the template binding different from the host binding. That is the crux of the question. – sir_thursday Apr 05 '19 at 02:54
  • 1
    I have retracted my duplicate vote (as you are correct), and will be writing an answer shortly – Vlad274 Apr 05 '19 at 02:55

1 Answers1

1

This error occurs for you because you are making a change that invalidates the previously rendered component view.

Quote from this documentation

Angular's unidirectional data flow rule forbids updates to the view after it has been composed. Both of these hooks fire after the component's view has been composed.

What this means for your situation:

When Angular first renders/composes the view of this component (prior ngAfterViewInit) the width of the element is set to an initial value. This code changes the width of an element, which causes a change to the view.

I think this example using background color makes this more obvious. In the first pass rendering the view, the color is red (and you can briefly see this on the screen). In your situation, the width binding is undefined on the first pass through.

Then, the ngAfterViewInit causes a change that makes the previously created view invalid, changing the width or the color, triggering the error. I translate this error as Angular saying "I did a bunch of work and made a view that was perfect, then you did something that made that work worthless. You shouldn't do that, because it interrupts some performance optimizations/assumptions I have".

This can be fixed by ensuring the component change happens after the ngAfterViewInit method has finished running, by using setTimeout. Fixed example Or by ensuring the component change happens before the view is rendered by moving it to ngOnInit.


You may notice that I do not have detectChanges in the examples I created. This is intentional, as that is a red herring and actually has no relation to the problem (though you correctly state this is necessary for the template binding to work). When the host binding is commented out in your example, there is no expression problem because this.width has no impact on the rendered view of the component.

There is no issue when the binding is in the template because this is causing changes to the content of the component - not the component's own view.


Speculation / Things I don't fully understand:

I believe this behavior boils down to changes to the Shadow DOM vs Light DOM (I'm making guesses based on info from this SO question). At the time ngAfterViewInit is running, I believe there is already an empty tag <my-component></my-component> in the Light DOM (IE actually on the page). Changing the child content does not cause an issue, because at this point it doesn't actually exist on the page and is part of the Shadow DOM. However, changing a host binding triggers a change to the real elements on the page (part of the Light DOM) - hence the error.

Vlad274
  • 6,514
  • 2
  • 32
  • 44
  • @sir-thursday this is all a very long way of saying "because the Angular life-cycle processes them in different steps". It's late for me, but I will try to respond tomorrow if you have a question about what I just wrote. – Vlad274 Apr 05 '19 at 03:45