4

Lets imagine an app where

  • all Components (including app.component) are onPush.

how can I call a function forceAppWideChangeDetection() in for eg app.component that will ensure run changeDetection in EACH AND EVERY component of the app.

I imagine I have to tree-walk the internal tree of components and call markForCheck on every component.

How can this be done?

Please note

  • my app is still on viewEngine. In case that this might me relevant.

  • the reason im asking this is: when the language is changed in the UI from lang-a to lang-b. Almost each and every component needs a changeDetection in order to update the displayed texts-language. Currently we have an own @Input() for that, that goes through the whole component tree and forces the components to rerender as "an input property has changed". I personally don't like this approach and was looking for a simpler solution. So as you can see this expensive method forceAppWideChangeDetection() has only to run when the language changes.

Andre Elrico
  • 10,956
  • 6
  • 50
  • 69
  • What happens when you call `this.changeDetectorRef.markForCheck();` in the `AppComponent`? AFAIK, it should run the change detection for the whole app. Are you sure that your changes are there, when you mark the component for check? – s.alem Jun 22 '21 at 13:20
  • 1
    It will not run the ChangeDetection for the all app components, only for components included to the `AppComponent` Template. I have been following this question and have created the sandbox https://stackblitz.com/edit/angular-rqfdgc?file=src%2Fapp%2Fapp.component.ts you can check how the `markForCheck` works here. @s.alem – Roman A. Jun 22 '21 at 13:36
  • markForCheck will only mark itself and all parents dirty. – Andre Elrico Jun 22 '21 at 13:44
  • @AndreElrico may I ask why do you want to run the check for the whole app? Wouldn't it be better to react to changes locally? – s.alem Jun 22 '21 at 13:59
  • hey @s.alem I have updated my `please note` section and answered your question there. – Andre Elrico Jun 22 '21 at 14:25
  • I would suggest you to remove the `OnPush` and rather use `ngZone` along with `appRef` to limit the changes. Once a component is set to `OnPush` strategy there is no way to updating itself other from within. – Jacopo Sciampi Jun 23 '21 at 14:25
  • Can't you just refresh the page? – Robin Dijkhof Jun 25 '21 at 14:07
  • you can read about change detection in detail here https://jagjeets.medium.com/using-angular-change-detection-effectively-f69d6a493c35 – jagjeet Sep 24 '21 at 17:02

4 Answers4

2

Unless there is an out of box solution that I am not aware of, you can use a service and a base class:

@Injectable({ providedIn: 'root' })
export class ChangeDetectionTriggerService {
  readonly trigger$ = new Subject<void>();
}

Then base components:

@Directive()
export class BaseComponent implements OnDestroy {

  readonly onDestroy$ = new Subject<void>();

  ngOnDestroy(): void {
    this.onDestroy$.next();
  }

}
@Directive() // https://angular.io/guide/migration-undecorated-classes
export class BaseChangeDetectionComponent implements OnInit extends BaseComponent {

  constructor(private changeDetectorRef: ChangeDetectorRef,
              private changeDetectionTriggerService: ChangeDetectionTriggerService) {
    super();
  }

  ngOnInit(): void {
    this.changeDetectionSub = changeDetectionTriggerService.trigger$
      .pipe(takeUntil(this.onDestroy$))
      .subscribe(() => this.changeDetectorRef.markForCheck());
  }

}

Usage in target component:

@Component(/** ... **/)
export class MyComponent extends BaseChangeDetectionComponent {

  constructor(private changeDetectorRef: ChangeDetectorRef,
              private changeDetectionTriggerService: ChangeDetectionTriggerService) {
    super(this.changeDetectorRef, this.changeDetectionTriggerService);
  }

}

Then use it in anywhere by simply emitting in the subject:

changeDetectionTriggerService.trigger$.next();

This would probably work. But keep it as a last resort. Hopefully there is a more simple and elegant solution.

s.alem
  • 12,579
  • 9
  • 44
  • 72
  • I had some similar idea, very good use of inheritage. The problem I had with this one is I have to extend every component. Then the current solution seems easier with the @Input and less boilerplate. – Andre Elrico Jun 23 '21 at 07:18
  • I am not so sure, because this approach abstracts this functionality and allows you to write less code in your components. Also you can enable, disable, delay etc in anyway you like in a central place. Another advantage would be adding more functionality to the base class if you want in the future. But I agree that it might be an overkill if you have only handful of components. – s.alem Jun 23 '21 at 07:36
1

In case you're not afraid of using private API then you can traverse all component views and mark them as dirty

ViewEngine

import { ApplicationRef, Component } from '@angular/core';

@Component({
  ...
})
export class AnyComponent {
  constructor(private appRef: ApplicationRef) {}

  runCd() {
    forceAppWideChangeDetection(this.appRef);
  }  
} 

function markParentViewsForCheck(view) {
  var currView = view;
  while (currView) {
      if (currView.def.flags & 2 /* OnPush */) { 
          currView.state |= 8 /* ChecksEnabled */;
      }
      currView = currView.viewContainerParent || currView.parent;
  }
}

function forEachEmbeddedViews(view, visitorVn: (view) => void) {
  const def = view.def;

  if (!(def.nodeFlags & 16777216 /* EmbeddedViews */)) {
    return;
  }
  for (var i = 0; i < def.nodes.length; i++) {
    var nodeDef = def.nodes[i];
    if (nodeDef.flags & 16777216 /* EmbeddedViews */) { 
      var embeddedViews = view.nodes[i].viewContainer._embeddedViews;
      for (var k = 0; k < embeddedViews.length; k++) {
        visitorVn(embeddedViews[k]);
      }
    } else if ((nodeDef.childFlags & 16777216) /* EmbeddedViews */ === 0) {
      i += nodeDef.childCount;
    }
  }
}

function forEachComponentViews(view, visitorVn: (view) => void) {
  const def = view.def;
  if (!(def.nodeFlags & 33554432 /* ComponentView */)) {
    return;
  }
  for (var i = 0; i < def.nodes.length; i++) {
    var nodeDef = def.nodes[i];
    if (nodeDef.flags & 33554432 /* ComponentView */) {
      visitorVn(view.nodes[i].componentView);
    }
    else if ((nodeDef.childFlags & 33554432 /* ComponentView */) === 0) {
        i += nodeDef.childCount;
    }
  }
}

function visitView(view) {
  markParentViewsForCheck(view);

  forEachEmbeddedViews(view, visitView);
  forEachComponentViews(view, visitView);
}

function forceAppWideChangeDetection(appRef: ApplicationRef) { 
  for (const view of (appRef as any)._views) {
    visitView(view._view);
  }
}

Stackblitz Angular 8

Ivy

In Ivy you can loop over all your rendered component's hosts and make use of __ngContext__ to mark corresponding view as dirty.

import { Component, ɵmarkDirty } from '@angular/core';

@Component({
  ...
})
export class AnyComponent {

  runCd() {
    forceAppWideChangeDetection();
  }
}

function forceAppWideChangeDetection() {
  const CONTEXT = 8;
  const PREFIX = 'app-'.toUpperCase();
  const allHosts = 
       Array.from(document.querySelectorAll<any>('*'))
            .filter(el => !!el.__ngContext__ && el.tagName.startsWith(PREFIX));

  for (const host of allHosts) {
    const elementWithinHost = host.firstElementChild;
    if (elementWithinHost && elementWithinHost.__ngContext__) {
      const component = elementWithinHost.__ngContext__[CONTEXT];
      ɵmarkDirty(component)
    }
  }
}

Ng-run Angular 11

yurzui
  • 205,937
  • 32
  • 433
  • 399
0

You can load this on every component you want to monitor or just app component. this is just a loose example. Angular performs a life cycle check that can be called with OnChanges life cycle hook - you should create an input like data 0 and data 1 on every parent component.

To do this create a component to do this so it can listen to and import it into either appComponent or into each component parent component you want to monitor. This may not be exactly what you want but it will be very close.

from angular cli

ng g c viewportListener

import { Component, OnInit, OnChanges, SimpleChanges, Input } from '@angular/core';

@Component({
  selector: 'app-viewport-listener',
  template: '<div></div>',
  styleUrls: ['./viewport-listener.component.css']
})
export class ViewportListenerComponent implements OnInit, OnChanges {
 @Input() data: any;
  constructor() { }

  ngOnInit(): void {
  }
  ngOnChanges(changes: SimpleChanges){
    forceAppWideDetction()
  }

}
function forceAppWideDetction(){}

https://angular.io/api/core/OnChanges https://www.stackchief.com/blog/ngOnChanges%20Example%20%7C%20Angular https://dev.to/nickraphael/ngonchanges-best-practice-always-use-simplechanges-always-1feg

chris burd
  • 728
  • 3
  • 9
  • hey thank you. I would have used [DoCheck](https://angular.io/api/core/DoCheck) if I wanted to add logic to each and every component. I would like to solve this from the root level without having to add any logic to any of the children. – Andre Elrico Jun 18 '21 at 12:33
  • 1
    @AndreElrico you could also use a changeDetectorRef which listens and marks it dirty on its own. https://angular.io/api/core/ChangeDetectorRef. Angular has a lot of capabilities to do the same thing. Create the detector on the app component though. – chris burd Jun 18 '21 at 12:38
  • yes, but I want to control it from the top level. The root level changeDetectorRef does not help me much. – Andre Elrico Jun 18 '21 at 12:40
  • 1
    Umm the root level is the top level dude, its the same thing. load whatever you want to listen too or write whatever change detection hooks on the AppComponent which is the parent - there is no other higher order component, because it waterfalls through the Dom - you can do it here and it will listen for changes on your components - but you have to set a base point and since amending app component is frowned upon, loading a component there to listen to changes to the "top level" or "root" component is the way to go. – chris burd Jun 18 '21 at 12:44
0

Easy and clean way to do it

If you're looking to change the language of the entire application when changing a language without having to reload the page, I would consider taking a look at the following https://github.com/ngx-translate/core.
ngx-translate let you use pipe to translate languages, when you do change the language inside the ngx translate service, then every pipe do upload themselves and your app do change their languages, without any other fuss

<h2>{{ 'WELCOME' | translate }}</h2>
import { TranslateService } from '@ngx-translate/core'

//..,
constructor(private _translateService: TranslateService){}

setLanguage(language: string) {
this._translateService.use(language)
}

And you're done, I let you read this for further details.

Response to the question

This is my own way of doing things if I didn't had the right to use ngx-translate. I would create a "base" file, with the logic you need, we'll talking here about observable I think

let's name it base.component.ts

import { Directive, OnDestroy, OnInit } from '@angular/core'
import { Subject } from 'rxjs'
import { filter, takeUntil } from 'rxjs/operators'
import { LanguageService } from 'some/where'

@Directive()
export class BaseComponent implements OnInit, OnDestroy {
  protected _unsubscribeAll: Subject<any> = new Subject()

  constructor(public languageService: LanguageService) {}

  ngOnInit(): void {
    this.languageService.onLanguageChanged$
      .pipe(
        filter((langChange) => (langChange?.firstChange ? false : true)), // Avoid subscribing to the first change
        takeUntil(this._unsubscribeAll),
      )
      .subscribe((value) => {
        // Do the reload you need to
      })

    this._extendedInit()
  }

  protected _extendedInit() {
    // For your default ngOnInit
  }

  ngOnDestroy(): void {
    this._unsubscribeAll.next()
    this._unsubscribeAll.complete()

    this._extendedDestroy()
  }

  protected _extendedDestroy() {
    // For your default ngOnDestroy
  }
}

You then, extends every component that need to be updated on language change, (or anything else, if you need to)

import { Directive } from '@angular/core'
import { LanguageService } from 'some/where'
import { BaseComponent } from 'some/where'

@Directive()
export class RandomComponent extends BaseComponent {
  constructor(public languageService: LanguageService) {
    super(languageService)
  }

  protected _extendedInit(): void {
    // For custom ngOnInit
  }
}

If you do not need to add other import to the constructor, you may ignore it, like the following

import { Directive } from '@angular/core'

@Directive()
export class RandomComponent extends BaseComponent {
  protected _extendedInit(): void {
    // For custom ngOnInit
  }
}

And if needed, this his how you could create your service.
I did add the same logic as angular do for the ngOnChange, maybe help you avoid reloading things if you do not want to, for example for the first load

import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'

export interface LangChange {
  previousValue: any
  currentValue: any
  firstChange: boolean
}

@Injectable({
  providedIn: 'root',
})
export class LanguageService {
  onSelectedLanguageChange$: BehaviorSubject<LangChange> = new BehaviorSubject(null)

  constructor() {}

  setLanguage(language: string) {
    this.onSelectedLanguageChange$.next({
      previousValue: this.onSelectedLanguageChange$.value,
      currentValue: language,
      firstChange: !this.onSelectedLanguageChange$.value,
    })
  }
}

then, into your app.component or anywhere you wants

  constructor(private _languageService: LanguageService) {}

  changeLang(language: string) {
    this._languageService.setLanguage('de')
  }
Raphaël Balet
  • 6,334
  • 6
  • 41
  • 78