web-dev-qa-db-fra.com

Comment re-déclencher tous les tuyaux sur toutes les arborescences de composants dans Angular 2

J'ai un pur tube TranslatePipe qui traduit des phrases en utilisant LocaleService qui a locale$: Observable<string> paramètres régionaux en cours. J'ai également ChangeDetectionStrategy.OnPush activé pour tous mes composants, y compris AppComponent. Maintenant, comment puis-je recharger toute l'application quand quelqu'un change de langue? (émet une nouvelle valeur dans locale$ observable).

Actuellement, j'utilise location.reload() après que l'utilisateur ait changé de langue. Et c'est embêtant, car toute la page est rechargée. Comment puis-je faire cette manière angulaire avec la stratégie de détection pure pipe et OnPush ?

12
EwanCoder

Merci à Günter Zöchbauer répondre (voir les commentaires), je l’ai fait fonctionner.

Comme je le comprends, le détecteur de changement angulaire fonctionne comme suit:

cd.detectChanges(); // Detects changes but doesn't update view.
cd.markForCheck();  // Marks view for check but doesn't detect changes.

Vous devez donc utiliser les deux pour reconstruire rapidement l’arborescence complète des composants.

1. Modifications du modèle

Afin de recharger toute l'application, nous devons masquer et afficher l'arborescence de tous les composants. Par conséquent, nous devons tout insérer dans app.component.html dans ng-container:

<ng-container *ngIf="!reloading">
  <header></header>
  <main>
    <router-outlet></router-outlet>
  </main>
  <footer></footer>
</ng-container>

ng-container est meilleur que div car il ne restitue aucun élément.

Pour le support async, nous pouvons faire quelque chose comme ceci:

<ng-container *ngIf="!(reloading$ | async)"> ... </ng-container>

reloading: boolean et reloading$: Observable<boolean> indiquent ici que le composant est en cours de rechargement.

Dans le composant j'ai LocaleService qui a language$ observable. Je vais écouter l'événement linguistique modifié et effectuer l'action de rechargement de l'application.

2. Exemple de synchronisation

export class AppComponent implements OnInit {
    reloading: boolean;

    constructor(
        private cd: ChangeDetectorRef,
        private locale: LocaleService) {

        this.reloading = false;
    }

    ngOnInit() {
        this.locale.language$.subscribe(_ => {
            this.reloading = true;
            this.cd.detectChanges();
            this.reloading = false;
            this.cd.detectChanges();
            this.cd.markForCheck();
        });
    }
}

3. Exemple Aync

export class AppComponent implements OnInit {
    reloading: BehaviorSubject<boolean>;

    get reloading$(): Observable<boolean> {
        return this.reloading.asObservable();
    }

    constructor(
        private cd: ChangeDetectorRef, // We still have to use it.
        private locale: LocaleService) {

        this.reloading = new BehaviorSubject<boolean>(false);
    }

    ngOnInit() {
        this.locale.language$.subscribe(_ => {
            this.reloading.next(true);
            this.cd.detectChanges();
            this.reloading.next(false);
            this.cd.detectChanges();
        });
    }
}

Nous n'avons pas à cd.markForChanges() maintenant, mais nous devons encore dire au détecteur de détecter les changements.

4. routeur

Le routeur ne fonctionne pas comme prévu. Lorsque vous rechargez une application de cette manière, le contenu de router-outlet deviendra empty. Je n'ai pas encore résolu ce problème et il peut être pénible de suivre le même itinéraire car cela signifie que toute modification apportée par l'utilisateur dans les formulaires, par exemple, sera modifiée et perdue.

5. OnInit

Vous devez utiliser le hook OnInit. Si vous essayez d'appeler cd.detectChanges () à l'intérieur du constructeur, vous obtiendrez une erreur, car angular ne générera pas encore de composant, mais vous essaierez de détecter les modifications.

Maintenant, vous pouvez penser que je suis abonné à un autre service du constructeur et que mon abonnement ne sera déclenché qu'après l'initialisation complète du composant. Mais le problème est que vous ne savez pas comment fonctionne le service! Si, par exemple, il émet simplement une valeur Observable.of('en') - vous obtiendrez une erreur, car une fois que vous vous êtes abonné - le premier élément est émis immédiatement alors que le composant n'est toujours pas initialisé.

Ma LocaleService a exactement le même problème: le sujet derrière observable est BehaviorSubject. BehaviorSubject est un sujet rxjs qui émet la valeur par défaut immédiatement après votre inscription. Donc, une fois que vous avez écrit this.locale.language$.subscribe(...) - abonnement est immédiatement déclenché au moins une fois, et alors seulement, vous attendez le changement de langue.

6
EwanCoder

Les tuyaux purs ne sont déclenchés que lorsque la valeur d’entrée change.

Vous pouvez ajouter une valeur de paramètre supplémentaire artificielle que vous modifiez.

@Pipe({name: 'translate'})
export class TranslatePipe {
  transform(value:any, trigger:number) {
    ...
  }
}

et ensuite l'utiliser comme

<div>{{label | translate:dummyCounter}}</div>

Chaque fois que dummyCounter est mis à jour, le tube est exécuté.

Vous pouvez également passer les paramètres régionaux en tant que paramètre supplémentaire au lieu du compteur . Je ne pense pas que l’utilisation de |async pour un paramètre de canal unique fonctionnera, par conséquent, cela risque d’être un peu fastidieux (il faudrait lui attribuer un champ pour pouvoir être utilisé). comme paramètre de tuyau)

18
Günter Zöchbauer

SOLUTION DE MEILLEURES PERFORMANCES:

J'ai trouvé une solution pour cela. Je déteste appeler ça une solution, mais ça marche. 

J'avais le même problème avec et orderBy pipe. J'ai essayé toutes les solutions ici, mais l'impact sur les performances était terrible.

J'ai simplement ajouté un argument supplémentaire à ma pipe

let i of someArray | groupBy:'someField':updated" 
<!--updated is updated after performing some function-->

puis chaque fois que j'effectue une mise à jour du tableau, je simplement

updateArray(){
    //this can be a service call or add, update or delete item in the array
      .then.....put this is in the callback:

    this.updated = new Date(); //this will update the pipe forcing it to re-render.
}

Cela force mon tube orderBy à refaire une transformation. Et la performance est bien meilleure.

6
Judson Terrell

Il suffit de définir la propriété pure à false

@Pipe({
  name: 'callback',
  pure: false
})
2
lohiarahul

Vous pouvez également créer votre propre tuyau non purifié pour suivre les modifications externes. Vérifiez les sources de/ Pipe Async natif pour avoir l’idée principale.

Tout ce dont vous avez besoin est d'appeler ChangeDetectorRef.markForCheck(); à l'intérieur de votre tube unpure à chaque fois que votre observable renvoie une nouvelle chaîne de paramètres régionaux. Ma solution:

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements OnDestroy, PipeTransform {

  private subscription: Subscription;
  private lastInput: string;
  private lastOutput: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) {
    this.subscription = this.globalizationService.currentLocale // <- Observable of your current locale
      .subscribe(() => {
        this.lastOutput = this.globalizationService.translateSync(this.lastInput); // sync translate function, will return string
        this.changeDetectorRef.markForCheck();
      });
  }

  ngOnDestroy(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
    this.subscription = void 0;
    this.lastInput = void 0;
    this.lastOutput = void 0;
  }

  transform(id: string): string { // this function will be called VERY VERY often for unpure pipe. Be careful.
    if (this.lastInput !== id) {
      this.lastOutput = this.globalizationService.translateSync(id);
    }
    this.lastInput = id;
    return this.lastOutput;
  }
}

Ou vous pouvez même incorporer AsyncPipe dans votre pipe (ce n'est pas une bonne solution, juste pour exemple):

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements OnDestroy, PipeTransform {

  private asyncPipe: AsyncPipe;
  private observable: Observable<string>;
  private lastValue: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) {
    this.asyncPipe = new AsyncPipe(changeDetectorRef);
  }

  ngOnDestroy(): void {
    this.asyncPipe.ngOnDestroy();
    this.lastValue = void 0;
    if (this.observable) {
      this.observable.unsubscribe();
    }
    this.observable = void 0;
    this.asyncPipe = void 0;
  }

  transform(id: string): string {
    if (this.lastValue !== id || !this.observable) {
      this.observable = this.globalizationService.translateObservable(id); // this function returns Observable
    }
    this.lastValue = id;

    return this.asyncPipe.transform(this.observable);
  }

}
0
Harry Burns