Jak wykryć, kiedy wartość @Input () zmienia się w Angular?

469

Mam element nadrzędny ( CategoryComponent ), element podrzędny ( videoListComponent ) i ApiService.

Mam większość z tego działa dobrze, tj. Każdy komponent może uzyskać dostęp do Json API i uzyskać odpowiednie dane za pomocą obserwowalnych.

Obecnie komponent listy wideo po prostu pobiera wszystkie filmy, chciałbym to przefiltrować tylko do filmów w określonej kategorii. Osiągnąłem to, przekazując kategorię ID do dziecka @Input().

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

Działa to i gdy zmienia się nadrzędna kategoria CategoryComponent, wówczas wartość przechodzi przez kategorię @Input()id, ale muszę to wykryć w VideoListComponent i ponownie zażądać tablicy wideo za pośrednictwem APIService (z nowym categoryId).

W AngularJS zrobiłbym a $watchna zmiennej. Jaki jest najlepszy sposób, aby sobie z tym poradzić?

Jon Catmull
źródło

Odpowiedzi:

718

W rzeczywistości istnieją dwa sposoby wykrywania i reagowania, gdy dane wejściowe zmieniają się w elemencie potomnym w angular2 +:

  1. Możesz użyć metody cyklu życia ngOnChanges (), jak również wspomniano w starszych odpowiedziach:
    @Input() categoryId: string;

    ngOnChanges(changes: SimpleChanges) {

        this.doSomething(changes.categoryId.currentValue);
        // You can also use categoryId.previousValue and 
        // categoryId.firstChange for comparing old and new values

    }

Łącza do dokumentacji: ngOnChanges, SimpleChanges, SimpleChange
Demo Przykład: Spójrz na tego plunkera

  1. Alternatywnie można również użyć setera właściwości wprowadzania w następujący sposób:
    private _categoryId: string;

    @Input() set categoryId(value: string) {

       this._categoryId = value;
       this.doSomething(this._categoryId);

    }

    get categoryId(): string {

        return this._categoryId;

    }

Link do dokumentacji: Look tutaj .

Przykład demonstracyjny: spójrz na tego plunkera .

JAKIEGO PODEJŚCIA POWINIENEŚ WYKORZYSTAĆ?

Jeśli twój komponent ma kilka danych wejściowych, to jeśli użyjesz ngOnChanges (), otrzymasz wszystkie zmiany dla wszystkich danych naraz w ngOnChanges (). Korzystając z tego podejścia, możesz także porównać bieżące i poprzednie wartości zmienionego wejścia i odpowiednio podjąć działania.

Jeśli jednak chcesz coś zrobić, gdy zmienia się tylko jedno pojedyncze wejście (i nie przejmujesz się pozostałymi danymi wejściowymi), łatwiejsze może być ustawienie właściwości wprowadzania. Jednak to podejście nie zapewnia wbudowanego sposobu porównywania poprzednich i bieżących wartości zmienionych danych wejściowych (co można łatwo zrobić za pomocą metody cyklu życia ngOnChanges).

EDYCJA 2017-07-25: WYKRYWANIE ZMIAN KĄTOWYCH MOŻE NIE POŻAROWAĆ W NIEKTÓRYCH OKOLICZNOŚCIACH

Zwykle wykrywanie zmian zarówno dla metody ustawiającej, jak i ngOnChanges będzie uruchamiane za każdym razem, gdy składnik nadrzędny zmienia dane, które przekazuje do dziecka, pod warunkiem, że dane są pierwotnym typem danych JS (ciąg, liczba, wartość logiczna) . Jednak w poniższych scenariuszach nie będzie działać i musisz podjąć dodatkowe działania, aby działało.

  1. Jeśli używasz zagnieżdżonego obiektu lub tablicy (zamiast pierwotnego typu danych JS) do przekazywania danych z rodzica na dziecko, wykrywanie zmian (przy użyciu settera lub ngchanges) może się nie uruchomić, jak wspomniano również w odpowiedzi użytkownika: muetzerich. Rozwiązania znajdziesz tutaj .

  2. Jeśli mutujesz dane poza kontekstem kątowym (tj. Zewnętrznie), wówczas kątowy nie będzie wiedział o zmianach. Być może będziesz musiał użyć ChangeDetectorRef lub NgZone w swoim komponencie, aby uświadomić kątowe zmiany zewnętrzne i tym samym uruchomić wykrywanie zmian. Zobacz to .

Alan CS
źródło
8
Bardzo dobra uwaga dotycząca używania seterów z wejściami, zaktualizuję to, aby była akceptowaną odpowiedzią, ponieważ jest bardziej wyczerpująca. Dzięki.
Jon Catmull
2
@trichetriche, setter (set set) zostanie wywołany za każdym razem, gdy rodzic zmieni dane wejściowe. To samo dotyczy również ngOnChanges ().
Alan CS
Najwyraźniej nie ... Spróbuję to rozgryźć, prawdopodobnie zrobiłem coś złego.
1
Właśnie natknąłem się na to pytanie, zauważyłem, że powiedziałeś, że użycie settera nie pozwoli na porównanie wartości, jednak to nieprawda, możesz wywołać swoją doSomethingmetodę i wziąć 2 argumenty nowe i stare wartości przed ustawieniem nowej wartości, w inny sposób zapisuje starą wartość przed ustawieniem i wywołaniem metody po tym.
T.Aoukar
1
@ T.Aoukar Mówię o tym, że narzędzie do wprowadzania danych nie obsługuje natywnie porównywania starych / nowych wartości, takich jak ngOnChanges (). Zawsze możesz użyć hacków, aby zapisać starą wartość (jak wspomniałeś), aby porównać ją z nową wartością.
Alan CS
105

Użyj ngOnChanges()metody cyklu życia w swoim komponencie.

ngOnChanges jest wywoływany zaraz po sprawdzeniu właściwości związanych z danymi, a przed sprawdzeniem elementów podrzędnych widoku i treści, jeśli przynajmniej jedna z nich uległa zmianie.

Oto Dokumenty .

muetzerich
źródło
6
To działa, gdy zmienna jest ustawiona na nową wartość, np MyComponent.myArray = [], ale jeśli wartość wejściowa jest zmieniony przykład MyComponent.myArray.push(newValue), ngOnChanges()nie jest wyzwalany. Wszelkie pomysły na temat uchwycenia tej zmiany?
Levi Fuller,
4
ngOnChanges()nie jest wywoływany, gdy zmieni się zagnieżdżony obiekt. Może znajdziesz rozwiązanie tutaj
muetzerich,
33

Otrzymywałem błędy w konsoli, a także w kompilatorze i IDE, gdy użyłem tego SimpleChangestypu w sygnaturze funkcji. Aby uniknąć błędów, anyzamiast tego użyj słowa kluczowego w podpisie.

ngOnChanges(changes: any) {
    console.log(changes.myInput.currentValue);
}

EDYTOWAĆ:

Jak wskazał Jon poniżej, możesz używać SimpleChangespodpisu, używając notacji nawiasowej zamiast notacji kropkowej.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Darcy
źródło
1
Czy zaimportowałeś SimpleChangesinterfejs u góry pliku?
Jon Catmull
@Jon - Tak. Problemem nie była sama sygnatura, ale dostęp do parametrów przypisanych w czasie wykonywania do obiektu SimpleChanges. Na przykład w moim komponencie powiązałem moją klasę użytkownika z danymi wejściowymi (tj <my-user-details [user]="selectedUser"></my-user-details>.). Do tej właściwości można uzyskać dostęp z klasy SimpleChanges poprzez wywołanie changes.user.currentValue. Powiadomienie userjest powiązane w czasie wykonywania i nie jest częścią obiektu SimpleChanges
Darcy,
1
Próbowałeś tego jednak? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Jon Catmull,
@Jon - Przełączenie na notację w nawiasie załatwia sprawę. Zaktualizowałem swoją odpowiedź, aby uwzględnić ją jako alternatywę.
Darcy,
1
import {SimpleChanges} z '@ angular / core'; Jeśli jest w dokumentacji kątowej, a nie rodzimym typem maszynopisu, to prawdopodobnie pochodzi z modułu kątowego, najprawdopodobniej „kątowego / rdzenia”
BentOnCoding
13

Najbezpieczniejszym zakład jest pójść z udostępnionego serwisu zamiast z @Inputparametru. Ponadto @Inputparametr nie wykrywa zmian w złożonym typie zagnieżdżonego obiektu.

Prosta przykładowa usługa jest następująca:

Service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SyncService {

    private thread_id = new Subject<number>();
    thread_id$ = this.thread_id.asObservable();

    set_thread_id(thread_id: number) {
        this.thread_id.next(thread_id);
    }

}

Component.ts

export class ConsumerComponent implements OnInit {

  constructor(
    public sync: SyncService
  ) {
     this.sync.thread_id$.subscribe(thread_id => {
          **Process Value Updates Here**
      }
    }

  selectChat(thread_id: number) {  <--- How to update values
    this.sync.set_thread_id(thread_id);
  }
}

Możesz użyć podobnej implementacji w innych komponentach, a wszystkie twoje komponenty będą miały takie same wspólne wartości.

Sanket Berde
źródło
1
Dziękuję Sanket. Jest to bardzo dobry punkt do zrobienia i, w stosownych przypadkach, lepszy sposób na zrobienie tego. Pozostawię zaakceptowaną odpowiedź w jej obecnym kształcie, ponieważ odpowiada ona na moje konkretne pytania dotyczące wykrywania zmian @Input.
Jon Catmull,
1
Parametr @Input nie wykrywa zmian w złożonym typie zagnieżdżonego obiektu, ale jeśli sam obiekt się zmieni, uruchomi się, prawda? np. mam parametr wejściowy „rodzic”, więc jeśli zmienię rodzic jako całość, wykrycie zmiany zostanie uruchomione, ale jeśli zmienię rodzic.nazwa, to nie?
yogibimbi
Powinieneś podać powód, dla którego twoje rozwiązanie jest bezpieczniejsze
Joshua Kemmerer
1
@InputParametr @JoshuaKemmerer nie wykrywa zmian w złożonym typie zagnieżdżonego obiektu, ale robi to w prostych obiektach natywnych.
Sanket Berde
6

Chcę tylko dodać, że istnieje inny hak cyklu życia, DoCheckktóry jest przydatny, jeśli @Inputwartość nie jest wartością pierwotną.

Mam tablicę jako taką, Inputwięc nie wywołuje to OnChangeszdarzenia, gdy zawartość się zmienia (ponieważ sprawdzanie, czy robi to Angular, jest „proste” i nie głębokie, więc tablica nadal jest tablicą, mimo że zawartość tablicy się zmieniła).

Następnie wdrażam niestandardowy kod sprawdzający, aby zdecydować, czy chcę zaktualizować mój widok za pomocą zmienionej macierzy.

Stevo
źródło
6
  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

spróbuj użyć tej metody. Mam nadzieję że to pomoże

Akshay Rajput
źródło
4

Możesz także mieć obserwowalny, który uruchamia zmiany w komponencie nadrzędnym (CategoryComponent) i robić to, co chcesz zrobić w subskrypcji w komponencie podrzędnym. (videoListComponent)

service.ts 
public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

-----------------
CategoryComponent.ts
public onCategoryChange(): void {
  service.categoryChange$.next();
}

-----------------
videoListComponent.ts
public ngOnInit(): void {
  service.categoryChange$.subscribe(() => {
   // do your logic
});
}
Josf
źródło
2

Tutaj ngOnChanges uruchamia się zawsze, gdy zmienia się twoja właściwość wejściowa:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}
Chirag
źródło
1

To rozwiązanie wykorzystuje klasę proxy i oferuje następujące zalety:

  • Pozwala konsumentowi wykorzystać moc RXJS
  • Bardziej kompaktowy niż inne dotychczas proponowane rozwiązania
  • Bardziej bezpieczny niż używaniengOnChanges()

Przykładowe użycie:

@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');

Funkcja użyteczności:

export function observeProperty<T, K extends keyof T>(target: T, key: K) {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get(): T[K] { return subject.getValue(); },
    set(newValue: T[K]): void {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
}
Stephen Paul
źródło
0

Jeśli nie chcesz używać metody ngOnChange implement og onChange (), możesz również subskrybować zmiany określonego elementu według zdarzenia valueChanges , ETC.

 myForm= new FormGroup({
first: new FormControl()   

});

this.myForm.valueChanges.subscribe(formValue => {
  this.changeDetector.markForCheck();
});

napisał markForCheck () z powodu użycia w tym oświadczeniu:

  changeDetection: ChangeDetectionStrategy.OnPush
użytkownik1012506
źródło
3
Jest to całkowicie odmienne od pytania i chociaż pomocni ludzie powinni być świadomi, że Twój kod dotyczy innego rodzaju zmian. Pytanie zadane na temat zmiany danych pochodzących Z POZOSTAŁEGO elementu, a następnie poinformowania go o tym i zmiany w danych. Twoja odpowiedź mówi o wykrywaniu zmian w samym komponencie, na przykład na danych wejściowych w formularzu, które są LOKALNE dla komponentu.
rmcsharry
0

Możesz także przekazać EventEmitter jako dane wejściowe. Nie jestem pewien, czy jest to najlepsza praktyka ...

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

I w VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Cédric Schweizer
źródło
Podaj przydatne linki, fragmenty kodu, aby wesprzeć swoje rozwiązanie.
mishsx,
Dodałem przykład.
Cédric Schweizer