3

I have created an Angular 2 app which contains a component whose HTML element I am trying to position relative to that of another component. In order to compute the exact coordinates, I need to know my own element's width and height as rendered by the browser.

I am using window.getComputedStyle(this.elementRef.nativeElement) to get those properties. I noticed that, as the page is being rendered, the properties keep changing until the page rendering has finished. In order to get informed about any changes and adjust the positioning, I check the values in a ngAfterViewChecked method of my component.

However, it seems ngAfterViewChecked is not called when the rendering leads to new computed style properties, as I found that my hook is not called any more, even though the computed style properties are still changing. I assume that the Angular framework is not designed to detect such a change.

My first attempt was to implement the ngDoCheck hook, but it seems this one isn't called either after some time and before the computed style property has its final value. I assume that I do not yet fully understand when exactly this hook is intended to be called.

I finally found out that, if I implement a setTimeout function in ngAfterViewChecked, this leads to this same hook being called again later, even if I pass only a dummy function like:

setTimeout(() => {}, 500);

But frankly, I do not understand why this so. Can someone please explain to me the connection between the setTimeout and the ngAfterViewChecked hook?

And while this occurs to be a somewhat dirty work-around: What is the proper 'Angular' way to detect and handle a change in computed style attributes?


Code excerpt:

export class MyComponent implements OnInit, AfterViewChecked
{
  private cssWidth: number;
  private cssHeight: number;

  constructor(private elementRef: ElementRef, private renderer: Renderer2)
  {
  }

  ngAfterViewChecked()
  {
    console.log("ngAfterViewChecked");
    this.updateView();
  }

  public updateView()
  {
    const sizeX = parseFloat(window.getComputedStyle(this.elementRef.nativeElement).width) || 0;
    const sizeY = parseFloat(window.getComputedStyle(this.elementRef.nativeElement.height) || 0;

    if (sizeX === this.cssWidth && sizeY === this.cssHeight) {
      // no change
      return;
    }

    console.log("Change detected!");

    // TODO Does not work without this dummy timeout function (else no more changes detected) - why so??
    setTimeout(() => {}, 500);

    [.. doing the positioning here ..]
 }
not2savvy
  • 2,902
  • 3
  • 22
  • 37

3 Answers3

2

See the top diagram here to view the lifecycle diagram. If your hook in doCheck doesn't work, the AfterViewChecked won't work either since doCheck initiates AfterViewChecked.

Every time an async process completes, another round of doCheck occurs. That would call AfterViewChecked, and that would call your updateView function. SetTimout() is an async process. I suspect the code above would loop infinitely as is.

I believe what you want is to have the parent component of what you have shown here use ngAfterViewChecked() and have that call updateView(). That topic is explored here.

The Angular way to make two components work together is to have a parent component that can communicate changes between the child components, since the parent component is the context that can see events occurring in the children.

The solution I would personally use would be an EventEmitter, in the component not shown, in the ngOnInit() function. It would inform the parent that it is initialized. Then, in the parent component, have a function call that occurs on the emitted event that tells the child component you have here to updateView.

Community
  • 1
  • 1
Michael
  • 368
  • 2
  • 8
  • Thanks, I do now understand that any async process will cause calls to `ngDoCheck`and `ngAfterViewChecked`, with `setTimeout` being one possibility, and regardless of what function `setTimeout` calls (even if none as in my case). BTW, it is not an infinite loop, as I do not issue `setTimeout` any more when a change hasn't been detected any more. *However*, the rest of your answer seems to be based on a misunderstanding, as I am not trying to communicate between parent and child components. I am trying to get notification of changes in the computed style of my own component only. – not2savvy Apr 27 '17 at 13:56
  • Are you trying to change the location/size of the component you have shown here based on another component? – Michael Apr 27 '17 at 14:00
  • Yes, that's right, but the other component *does not* change, as it gives an immutable attribute value right from the beginning, so that is not the problem I'm trying to solve. It's the changes in the computed style of my *own* component that I need to detect. The problem would be the same even if I used a constant instead of the other components attribute value. That's why I did not include that part in the example code. – not2savvy Apr 27 '17 at 14:05
  • I think I explained poorly, my apologies. You want the shown component to react to another component. You have a way to check what values it needs to resize to, but are trying to find a way to let it know it needs to resize. To do that, you need something that is aware of the state of both the shown component and the component it reacts to. The solution to that is to have a component that can send information between the two. This is a situation with a parent and 2 children. In this case, the parent would tell the shown component to updateView() when the other component initializes. – Michael Apr 27 '17 at 14:13
  • I hate to be so insisting, but I'm afraid I think we're still not talking about the same question. I am *not* 'trying to find a way to let it know it needs to resize' as you assume. I know how to let my component know how it needs to resize, but I need a way to let Angular and/or the DOM *me* know that this calculated style has changed, or *at least* give *me* chance to find it out on my own (by calling my hook). As you can see in the example code, I can check for a change, but my function is not being called unless I use the `setTimeout` trick. – not2savvy Apr 27 '17 at 14:20
  • I've upvoted this answer because it gives a short explanation for my first question regarding the connection between the `setTimeout` and the `ngAfterViewChecked` hook. – not2savvy Apr 27 '17 at 14:26
  • It's ok, I'm failing to understand what you're looking for. I thought you were having trouble getting the updateView function to run at the appropriate times. What do you mean by 'let Angular let me know that this calculated style has changed'? I don't understand where you want the change notification to happen, or where you want that notification to end up. – Michael Apr 27 '17 at 14:48
  • The problem, as I understand now, seems to be that Angular does not detect a change when the computed style properties of an element change. Hence, such a change does not trigger Angular to call the`ngAfterViewChecked` hook, and consequently, my `updateView` function is not be invoked from `ngAfterViewChecked`. I now think that Angular does not detect the change in the computed style properties, because they are *computed* which means they are merely a result of the `getComputedStyle` function and probably do not exist for themselves, or do not get updated without a call to that function. – not2savvy Apr 27 '17 at 16:11
  • I wouldn't be surprised if that type of change did not trigger Angular's doChange. Going the Angular way, some Angular component must be aware of the computed change. From that component, you have to figure out how to notify the shown component. I need to know what is changing the style your computing, whether it's JQuery or Angular or something else. – Michael Apr 27 '17 at 16:27
  • Neither of these, it is just the browser recomputing the element's width and height several times while rendering the page. No elements are changed from within my code during that time. It would even help if I could trigger a function when the browser has finished rendering the page content. – not2savvy Apr 27 '17 at 16:34
  • Am I correct in thinking that one more `doChange()` would meet your needs (and that's why `setTimeout()` worked)? If that's the case, maybe try calling `ngAfterViewInit()` instead of `ngAfterViewChecked()`. If that doesn't work, you can try logging the `ngAfterViewInit()` in the app.component and see if that logs when you want it to, then work from there. Other folks around [here](http://stackoverflow.com/questions/38) have the same issue and have resorted to the setTimeout request. – Michael Apr 27 '17 at 17:31
  • According to https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html, I assumed that `ngAfterViewChecked`will be called at the same occasions as `ngAfterViewInit`, only just even a bit later. I guess I'll have to stick to the setTimeout solution. BTW, the link in your last comment does not work. – not2savvy Apr 27 '17 at 19:15
  • I've verified that the `ngAfterViewInit` hook doesn't make a difference for my purpose compared to `ngAfterViewChecked`. If you don't mind, I would summarize all findings in an own answer. – not2savvy Apr 28 '17 at 09:08
1

In vanilla JavaScript you could use:

MutationObserver

and trigger you own function when a DOM change is made.

MutationObserver provides developers with a way to react to changes in a DOM. It is designed as a replacement for Mutation Events defined in the DOM3 Events specification.

GibboK
  • 71,848
  • 143
  • 435
  • 658
  • Sorry, I am aware this is not a complete answer to your problem, but I hope it can be at least partially useful :) – GibboK Apr 27 '17 at 13:23
  • Thanks anyway, but I have tried `observe(this.elementRef.nativeElement, { attributes: true })` which gives me multiple callbacks with oldValue=null, newValue=undefined, and nothing more. – not2savvy Apr 27 '17 at 13:47
  • I actually doubt that a DOM change would be detected. I assume that the DOM does not change, but only the getComputedStyle function will calculate a different result. ?! – not2savvy Apr 27 '17 at 14:11
  • 1
    While this is an interesting option, after some research, I came to the conclusion that MutationObserver does not currently support observing computed style attributes. See also question [Can I use a MutationObserver to listen for changes on computed styles?](http://stackoverflow.com/questions/32746473/can-i-use-a-mutationobserver-to-listen-for-changes-on-computed-styles) – not2savvy Apr 27 '17 at 16:28
  • yes it does not listen to computed style changes, as its part of CSS OM. what are work around to get the computed style changes? – Talk is Cheap Show me Code Jan 07 '20 at 06:17
0

Why isn't the ngAfterViewChecked called when the rendering leads to new computed style properties?

The Angular framework does not detect changes in the computed style of DOM elements, at least not when they are caused by the rendering engine of the browser. Consequently, the ngAfterViewChecked hook will not be called in such cases.

Why does it work when using setTimeout?

As explained in Michael Palmer's answer, the setTimeout causes Angular to become aware of an asynchronous process that might cause a change. Therefore, the ngAfterViewChecked hook is called after the timeout function has been performed. Angular does not try to check what exactly is done in the setTimeout function and whether or not this is something that actually affects the view. Obviously, this would be quite hard ti implement and probably not worth the effort. Instead, Angular calls the ngAfterViewChecked hook just in case something has happened in the asynchronous process that affects the view.

What is the Angular way to listen to computed style changes?

There seems to be no other way than using setTimeout in order to watch changes in the computed style of DOM elements.

not2savvy
  • 2,902
  • 3
  • 22
  • 37