1

I have a child component that is used twice within its parent, it uses ngOnChages to update values;

This all worked fine, but now I'm trying to manipulate the template, within the child, according to information passed it via @Input variables.

What I'm finding is; when the parent component is first loaded, the template variables are undefined, within a function call

This makes sense because this function is called within the onChanges method and does not give the template enough time to load.

I need to refactor my code, but how?

At the moment it works if I use setTimeout

So to simply:

@ViewChild('myContainer', {read: ElementRef}) myContainer: ElementRef;
@Input() myUpdatingVar

ngOnChanges(): void {
  if (this.myUpdatingVar === someValue) {
      this.myFunc(this.someValue1);
   } else {
      this.myFunc(this.someValue2);
   }
}

myFunc(param){
   do stuff....
   this.updateDom(anotherParam);
}

updateDom(param) {
// If I use setTimeout this works
this.myContainer.nativeElement.querySelector(`#someId`); // Undefined
}
Kamil Naja
  • 6,267
  • 6
  • 33
  • 47
RasMason
  • 1,968
  • 4
  • 32
  • 54
  • 2
    Why not using a setter? Like `@Input() set myUpdatingVar(val) {}`. Also it would be really helpful if you reproduced your problem on StackBlitz or similar. – Sergey May 08 '19 at 19:22
  • 1
    you cant' try to access viewchildren until the afterviewinit hook. You'll need to do a check in your onchanges hook to see if the component is initialized yet and skip if not, and also put in an initial run in the afterviewinit hook. That being said, there probably is a much more angular way to do this. – bryan60 May 08 '19 at 19:33

5 Answers5

5

Instead of using ngOnChanges, You can detect changes using a setter.

Here is what you need to do

@Input()
set myUpdatingVar(value: any) {
   if(value) {
        this.myFunc(value);
   }
}

It will not only load when initializing the view but also Whenever something is changed in the myUpdatingVar, Angular will run setter and execute your function. Also, ngOnChanges is an expensive lifecycle hook. If you don't use it carefully, It will run everytime something is changed in the component.

But if you want to use ngOnChanges, you should use changes it detects in the variable instead of using the variable itself.

Here is the example

ngOnChanges(changes: SimpleChanges): void {
  if (changes.myUpdatingVar.currentValue === someValue) {
      this.myFunc(changes.myUpdatingVar.currentValue);
   } else {
      this.myFunc(changes.myUpdatingVar.currentValue);
   }
}

You can check if it is the first change and can also get currentValue and previousValue which is useful in this case.

I hope my answer helped you somehow. Happy Coding!

Torab Shaikh
  • 456
  • 1
  • 5
  • 17
1

As others have said, onchanges runs too early for accessing viewchildren, you'll need something like this:

@ViewChild('myContainer', {read: ElementRef}) myContainer: ElementRef;
@Input() myUpdatingVar

ngOnChanges(): void {
  if (this.myContainer) {
    if (this.myUpdatingVar === someValue) {
       this.myFunc(this.someValue1);
     } else {
       this.myFunc(this.someValue2);
     }
   }
}

ngAfterViewInit() {
  this.ngOnChanges();
}

myFunc(param){
   do stuff....
   this.updateDom(anotherParam);
}

updateDom(param) {
// If I use setTimeout this works
this.myContainer.nativeElement.querySelector(`#someId`); // Undefined
}

This way you skip the logic if the view hasn't been initialized and you manually trigger a run of ngOnChanges after the view is initialized.

All this said, there's probably a far more angular way of accomplishing this goal, unless you're interacting with some poorly integrated 3rd party lib.

bryan60
  • 28,215
  • 4
  • 48
  • 65
  • I would advise against ever calling ngOnChanges manually. It’s likely unnecessary anyway as I’m sure he’s not depending on the initial onChanges as that is not a “real” change and would be bad practice. –  May 08 '19 at 19:41
  • I'd normally agree but it seems like he may need the initial run as he seems to be manually manipulating the DOM. This whole thing could likely be improved, but this is the answer to the question asked. – bryan60 May 08 '19 at 19:43
  • If simply the initially binded value is needed then ngOnInit or ngAfterViewInit will suffice. If changes are actually needed, all he has to do is add the initialization Boolean I described in my answer. –  May 08 '19 at 19:52
  • i mean it sounds like you'd be more comfortable if there was some 3rd function that contained the onchanges logic that both afterviewinit on onchanges called... which is fine stylistically, but is more or less the same. a view initialisation boolean is identical to checking if the viewchild is instantiated. Neither is better or worse than the other for any reason other than stylistic preference. The OP is free to edit my answer to fit his style prefs. – bryan60 May 08 '19 at 19:54
1

Checkout this guide: https://angular.io/guide/lifecycle-hooks

Angular lifecycle events fire in a specific order and some happen before the dom elements are loaded.

ngAfterViewInit is the hook that lets you know that the DOM(or at least the abstraction you see) is ready to be queried. This means template variables and viewchildren among other things wont work properly before this event has triggered.

EDIT: it sounds like you understand the timing. To fix this I suggest checking that the template variable is defined before using it in onChanges(now it will happen on all subsequent calls to onChanges) then do the same thing in ngAfterViewInit if you need to run it once at the beginning.

rjustin
  • 1,399
  • 8
  • 19
1

With newer angular (9.x) I solved this problem by setting static to true:

@ViewChild('myContainer', {static: true})
Spikolynn
  • 4,067
  • 2
  • 37
  • 44
0

For DOM manipulation, you should use the ngAfterViewInit lifecycle hook, to ensure the template has loaded into the DOM.

ngOnChanges fires once early, even before ngOnInit. Far too early for DOM manipulation.

You can use an isViewInitialized Boolean and set it to true in ngAfterViewInit, and then use it in an if condition in ngOnChanges.