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.
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.
Related
Here is an example to reproduce: https://codesandbox.io/s/crazy-kirch-f7fso8?file=/src/App.js
To reproduce:
right click and inspect the elements.
In your inspector (assuming you have this capability), right click on the div with the wrapper id and break on subtree modifications.
Click toggle button
Click "resume script execution" arrow to jump through each subtree modification.
Notice how initially, neither imported component renders, then they pop back in on a subsequent render.
Walking through the example in a bit more detail:
I am conditionally rendering 3 types of thing depending on a single variable using the ternary operator.
String - this seems to update immediately
Element - this seems to update just after the string does
Imported component - both components disappear, then one comes back (after element and string).
Does anyone know what may be causing imported components to briefly disappear? This is causing a flash of content that I'd like to avoid.
That is because A and B are different components, so on each toggle, you need to unmount the rendered one and mount the other. This means removing the node from the DOM and then inserting the other one.
The other cases are treated as the same element with just a text content change. Not mounting/unmounting happening here.
About the flashing, i cannot reproduce on FF or Chrome. Only if i have the break-on-modifications enabled i see it.
One of the things I'm still hung up on with Angular is understanding all of the lifecycle hooks and when to use what when.
I often need to stick a little plain'ol JS into a component to deal with some DOM issue that, alas, I can't handle via Angular (usually because I'm working within a component that needs to access some elements from a parent component's rendered DOM that we have no access to the Angular code for...I do realize this isn't the 'proper' Angular way but...).
An example right now is a few pages I'm working on that use a component need that needs to hide a DOM element on the page that isn't a part of this component. I need to use JS for this (a whole other story why CSS isn't the solution for this one).
But I only want to do this once the DOM is fully rendered.
Sometimes this seems to work when inserted into ngAfterViewInit -- but sometimes not. It seems that there's no guarantee the full DOM is ready using that lifecycle.
Moving that logic into ngAfterViewChecked does work. However, the issue with ngAfterViewChecked is that it's getting called dozens of times on some pages--and the first few times it's called, the DOM isn't even ready. Not the end of the world, but there's no reason for me to be attempting to grab the same DOM object 40 times per page render. I somewhat remedy this by adding a boolean flag to tell this bit of JS to stop running once it finds the DOM elements but that's hacky.
So my question is: What is the proper (if there is one) way to handle JS manipulation of the DOM after the DOM is fully rendered in the context of an Angular component? Is it to use one of the angular lifecycle events? Something else? Or this whole idea of manipulating DOM objets outside of the component I'm working in just anathema to the 'Angular way' so just isn't something accommodated within Angular?
I was working on a component that has a button which toggles a boolean. This boolean is supposed to determine if a child component in the HTML need to re-render or not, since I want the ngOnInit function in the child to be re-run.
The situation is described in the app component here: https://codesandbox.io/s/angular-qxtm8
The app.component is the parent and second.component is the child.
I have tried three different solutions. They are onTestClickOne, onTestClickTwo, and onTestClickThree in app.component.ts. onTestClickOne and onTestClickTwo successfully re-triggers the ngOnInit in the child component. We can see the console log in it is printed on the console whenever I click the corresponding buttons. However, onTestClickThree didn't work.
I'm not 100% sure why onTestClickThree didn't work, and onTestClickTwo did.
My guesses are the following:
onTestClickTwo works because the change detection in Angular is run after the event handler has been executed. So, it will detect the boolean has been set to true. After that, the event loop will get the callback of the setTimeout and put it into the stack. Angular will execute change detection after finishing the callback.
onTestClickThree didn't work because, by the time Angular runs change detection, the boolean is already true. Angular doesn't know that it has been changed.
Let's tackle the main issue there, which is your design : why would you re-render the component to trigger ngOnInit again ?
Sure, in the case of your example, that's no big deal. But what happens for a fully coded component, making http calls, having children and all ? That will cause some severe performance issues.
Instead of re-rendering the component, you should use a function to do that.
If the event (that is initially supposed to re-render the component) comes from the child, then use an #Output. If it comes from the parent, use a #ViewChild reference.
As you can see it works well, without any detection issue.
I have a parent view with a nested view in the middle.
On a state change, the nested view seems to stick for a second or two before loading the next state. It's as though the nested view is lagging behind or something.
For example, after logging in, the login form is still visible for a second or two in the middle of the page after the state change. The parent view changes instantly, but that nested view just seems to stick.
I've been pretty careful about items on the watch list, and use one-time binding wherever possible.
But I really don't think it has to do with that, because this happens even early on in the application (from login to the main page), and other than this issue, application performance is fine.
I've googled a lot about this, but haven't turned up anything useful.
Any ideas on what to check or how to debug this?
You say it only happens the first time you transition after loading the app. So it could be you are injecting a service into the child view that you are using the first time in your app. This service is taking some time to instanciante. Servises are singletons, so this lag is only visible the first time.
Look at the answer in this thread for a possible solution, somebody had the exact some problem:
How to instantiate a service dynamically?.
Another solution might me to inject that service into the parent view as well, so you get the lag while loading the app not on first transition.
After digging into the code a bit I see that invalidate() on a control will increase a counter which seems to mark the control as invalidated.
This seems to lead to a rerender.
So if you have a control that you want rerendered, is it better practice to use invalidate() or rerender()?
How does a rerender actually get triggered? (other than by explicitly invoking it of course)
Both are marked as "protected", meaning you should not call any of them unless you are really deep into developing custom controls.
A control gets invalidated when (for example) a property is changed. In that case you usually want to re-render the control, that's why setters call invalidate by default.
When you overwrite your invalidate method in your custom control, you can analyze the source of the invalidation and then decide whether you really want to rerender etc. See for example the unified.Shell which decides what to do on invalidate based on the source:
https://sapui5.netweaver.ondemand.com/sdk/resources/sap/ui/unified/Shell-dbg.js line 1539ff
Again: Protected means that it should not be called from the outside (and that it should not be necessary, except for debugging).
Is it better practice to use invalidate() or rerender()?
As an application developer
Neither invalidate() nor rerender() should be used as they're not public APIs.
As a control developer
Usually, the framework already manages rerendering of the controls automatically if one of the control settings (properties, aggregations, or associations) changes. But if it's still required to trigger the rerendering explicitly, the API invalidate() should be favored over rerender() because:
rerender()
renders the control synchronously (blocking the main/UI thread)
works only when the control has been rendered before (no initial rendering possible)
does not combine multiple state changes into a single rerendering
causes additional layout trashing
has become deprecated since 1.70.
invalidate(), on the other hand, just adds the control to a collection of to-be-rendered controls so that it can be rendered together with them in one process asynchronously (currently in the next browser task using setTimeout(fn, 0)).
I encountered such thing in my work recently. I am fixing an animation issue but the bad thing is I have to start the animation after rendering. If that is all I can handle it, but the worse thing is the control rendering twice. When the first rendering happen, the animation start to do, but the second rendering comes immediately and the DOM was restructed, then the animation is terminated and the effect is like no animation executed. After investigate the code, I found there is a place invoke render() method which will execute render immediately. After change it to invoke invalidated() method, it looks the animation works well. It looks multiple invalidate() method invocation only cause executing render once.