Как определить, когда значение @Input () изменяется в Angular?

471

У меня есть родительский компонент ( CategoryComponent ), дочерний компонент ( videoListComponent ) и ApiService.

У меня большая часть этой работы работает нормально, т. Е. Каждый компонент может получить доступ к json api и получить соответствующие данные через наблюдаемые объекты.

В настоящее время компонент списка видео только получает все видео, я хотел бы отфильтровать это только для видео в определенной категории, я достиг этого путем передачи categoryId ребенку через @Input().

CategoryComponent.html

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

Это работает, и когда родительская категория CategoryComponent изменяется, тогда значение categoryId передается через, @Input()но затем мне нужно обнаружить это в VideoListComponent и повторно запросить массив видео через APIService (с новым categoryId).

В AngularJS я бы сделал для $watchпеременной. Каков наилучший способ справиться с этим?

Джон Кэтмалл
источник
смотреть изменения ввода: - angulartutorial.net/2017/12/watch-input-changes-angular-4.html
Prashobh
для изменения массива: stackoverflow.com/questions/42962394/…
dasfdsa

Ответы:

720

На самом деле, существует два способа обнаружения и обработки при изменении входа дочернего компонента в angular2 +:

  1. Вы можете использовать метод жизненного цикла ngOnChanges (), как также упоминалось в предыдущих ответах:
    @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

    }

Ссылки на документацию: ngOnChanges, SimpleChanges, SimpleChange
Demo Пример: посмотрите на этот плункер

  1. В качестве альтернативы, вы также можете использовать средство ввода свойства следующим образом:
    private _categoryId: string;

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

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

    }

    get categoryId(): string {

        return this._categoryId;

    }

Ссылка на документацию: смотрите здесь .

Пример демо: Посмотрите на этот плункер .

Какой подход вы должны использовать?

Если ваш компонент имеет несколько входов, то, если вы используете ngOnChanges (), вы получите все изменения сразу для всех входов в ngOnChanges (). Используя этот подход, вы также можете сравнить текущие и предыдущие значения входных данных, которые изменились, и принять соответствующие меры.

Однако, если вы хотите что-то сделать, когда изменяется только один отдельный вход (и вас не волнуют другие входные данные), тогда может быть проще использовать установщик свойства ввода. Однако этот подход не предоставляет встроенного способа сравнения предыдущих и текущих значений измененного ввода (что можно легко сделать с помощью метода жизненного цикла ngOnChanges).

РЕДАКТИРОВАТЬ 2017-07-25: ОБНАРУЖЕНИЕ ИЗМЕНЕНИЯ УГЛОВОГО ИЗМЕНЕНИЯ МОЖЕТ ЕЩЕ НЕ ПОЖАРИТЬСЯ ОТ НЕКОТОРЫХ ОБСТОЯТЕЛЬСТВ

Обычно обнаружение изменений для setter и ngOnChanges срабатывает всякий раз, когда родительский компонент изменяет данные, которые он передает дочернему, при условии, что данные являются примитивным типом данных JS (строка, число, логическое значение) . Однако в следующих сценариях он не сработает, и вам придется предпринять дополнительные действия, чтобы заставить его работать.

  1. Если вы используете вложенный объект или массив (вместо примитивного типа данных JS) для передачи данных от Parent к Child, обнаружение изменений (с использованием либо setter, либо ngchanges) может не сработать, что также упоминается в ответе пользователя: muetzerich. Решения смотрите здесь .

  2. Если вы изменяете данные вне углового контекста (то есть извне), то angular не будет знать об изменениях. Возможно, вам придется использовать ChangeDetectorRef или NgZone в вашем компоненте, чтобы постоянно информировать о внешних изменениях и тем самым запускать обнаружение изменений. Обратитесь к этому .

Alan CS
источник
8
Очень хороший момент относительно использования сеттеров со входами, я обновлю это, чтобы быть принятым ответом, поскольку это более полно. Спасибо.
Джон
2
@trichetriche, метод set (метод set) будет вызываться всякий раз, когда родитель изменяет ввод. Также то же самое верно и для ngOnChanges ().
Алан CS
Ну, очевидно, нет ... Я постараюсь выяснить это, вероятно, я что-то не так сделал.
1
Я просто наткнулся на этот вопрос, отметив, что вы сказали, что использование установщика не позволит сравнивать значения, однако это не так, вы можете вызвать свой doSomethingметод и взять 2 аргумента новых и старых значений перед установкой нового значения другим способом. будет сохранять старое значение перед установкой и вызывать метод после этого.
Т.Аукар
1
@ T.Aoukar То, что я говорю, это то, что входной установщик не поддерживает сравнение старых / новых значений, как ngOnChanges (). Вы всегда можете использовать хаки для хранения старого значения (как вы упомянули), чтобы сравнить его с новым значением.
Алан CS
105

Используйте ngOnChanges()метод жизненного цикла в вашем компоненте.

ngOnChanges вызывается сразу после проверки привязанных к данным свойств и перед проверкой дочерних элементов просмотра и содержимого, если хотя бы одно из них изменилось.

Вот документы .

muetzerich
источник
6
Это работает, когда переменная установлена ​​на новое значение, например MyComponent.myArray = [], но если входное значение изменено, например MyComponent.myArray.push(newValue), ngOnChanges()не запускается. Есть идеи о том, как запечатлеть это изменение?
Леви Фуллер
4
ngOnChanges()не вызывается, когда вложенный объект изменился. Может быть, вы найдете решение здесь
muetzerich
33

Я получал ошибки в консоли, а также в компиляторе и IDE при использовании SimpleChangesтипа в сигнатуре функции. Чтобы избежать ошибок, используйте anyключевое слово в подписи.

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

РЕДАКТИРОВАТЬ:

Как указал Джон ниже, вы можете использовать SimpleChangesсигнатуру при использовании скобочных обозначений, а не точечных.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Darcy
источник
1
Вы импортировали SimpleChangesинтерфейс вверху файла?
Джон Кэтмалл,
@Джон - Да. Проблема была не в самой подписи, а в доступе к параметрам, которые назначаются во время выполнения объекту SimpleChanges. Например, в моем компоненте я привязал свой класс User к входу (т.е. <my-user-details [user]="selectedUser"></my-user-details>). Это свойство доступно из класса SimpleChanges путем вызова changes.user.currentValue. Уведомление userпривязано во время выполнения и не является частью объекта SimpleChanges
Дарси
1
Вы пробовали это хотя? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Джон Кэтмалл
@Jon - Переключение на скобочные обозначения делает свое дело. Я обновил свой ответ, чтобы включить его в качестве альтернативы.
Дарси
1
import {SimpleChanges} из '@ angular / core'; Если это в угловых документах, а не в собственном машинописном типе, то это, вероятно, из углового модуля, скорее всего, «angular / core»
BentOnCoding
13

Самый безопасный выбор должен пойти с общей службой вместо части с @Inputпараметром. Кроме того, @Inputпараметр не обнаруживает изменения в сложном типе вложенных объектов.

Простой пример службы выглядит следующим образом:

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);
  }
}

Вы можете использовать аналогичную реализацию в других компонентах, и все ваши комментарии будут иметь одинаковые общие значения.

Санкет Берде
источник
1
Спасибо, Санкет. Это очень хорошая точка зрения, и, где это уместно, лучший способ сделать это. Я оставлю принятый ответ в том виде, в каком он есть, так как он отвечает на мои конкретные вопросы, касающиеся обнаружения изменений @Input.
Джон
1
Параметр @Input не обнаруживает изменения внутри сложного вложенного типа объекта, но если сам объект изменится, он сработает, верно? Например, у меня есть входной параметр "parent", поэтому, если я полностью изменю parent, обнаружение изменений сработает, но если я изменю parent.name, не будет?
йогибимби
Вы должны указать причину, по которой ваше решение безопаснее
Джошуа Кеммерер,
1
@InputПараметр @JoshuaKemmerer не обнаруживает изменений в сложном типе вложенных объектов, но в простых нативных объектах.
Санкет Берде
6

Я просто хочу добавить, что есть еще один хук Lifecycle, DoCheckкоторый будет полезен, если@Input значение не является примитивным значением.

У меня есть Array как, Inputтак что это не вызывает OnChangesсобытие при изменении содержимого (потому что проверка, которую выполняет Angular, является «простой» и не глубокой, поэтому Array по-прежнему является Array, даже если содержимое в Array изменилось).

Затем я реализую некоторый пользовательский код проверки, чтобы решить, хочу ли я обновить свое представление с измененным массивом.

Stevo
источник
6
  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

пожалуйста, попробуйте использовать этот метод. Надеюсь это поможет

Акшай Раджпут
источник
4

Вы также можете иметь заметку, которая запускает изменения в родительском компоненте (CategoryComponent) и делать то, что вы хотите сделать в подписке в дочернем компоненте. (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
источник
2

Здесь ngOnChanges будет срабатывать всегда, когда изменяется ваше входное свойство:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}
Чираг
источник
1

Это решение использует прокси- класс и предлагает следующие преимущества:

  • Позволяет потребителю использовать возможности RXJS
  • Более компактный, чем другие решения, предложенные до сих пор
  • Более безопасный, чем использованиеngOnChanges()

Пример использования:

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

Вспомогательная функция:

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;
}
Стивен Пол
источник
0

Если вы не хотите использовать ngOnChange, реализуйте метод og onChange (), вы также можете подписаться на изменения определенного элемента с помощью события valueChanges , ETC.

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

});

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

markForCheck (), написанный из-за использования в этом объявлении:

  changeDetection: ChangeDetectionStrategy.OnPush
user1012506
источник
3
Это полностью отличается от вопроса, и, хотя полезные люди должны знать, что ваш код относится к другому типу изменений. Заданный вопрос об изменении данных, происходящих из-за пределов компонента, а затем о том, что ваш компонент знает и реагирует на факт изменения данных. Ваш ответ говорит об обнаружении изменений внутри самого компонента в таких вещах, как входные данные в форме, которые являются ЛОКАЛЬНЫМИ для компонента.
rmcsharry
0

Вы также можете просто передать EventEmitter как Input. Не совсем уверен, что это лучшая практика, хотя ...

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>

И в VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Седрик Швайцер
источник
Пожалуйста, предоставьте полезные ссылки, фрагменты кода для поддержки вашего решения.
Mishsx
Я добавил пример.
Седрик Швайцер