Làm cách nào để phát hiện khi giá trị @Input () thay đổi trong Angular?


471

Tôi có một thành phần cha mẹ ( CategoryComponent ), một thành phần con ( videoListComponent ) và ApiService.

Tôi có hầu hết các hoạt động tốt này, tức là mỗi thành phần có thể truy cập vào api json và nhận dữ liệu liên quan của nó thông qua các đài quan sát.

Hiện tại thành phần danh sách video chỉ nhận được tất cả các video, tôi muốn lọc phần này thành các video trong một danh mục cụ thể, tôi đã đạt được điều này bằng cách chuyển thể loạiId cho trẻ thông qua @Input().

CategoryComponent.html

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

Điều này hoạt động và khi danh mục CategoryComponent gốc thay đổi thì giá trị categoryId được truyền qua @Input()nhưng sau đó tôi cần phát hiện điều này trong VideoListComponent và yêu cầu lại mảng video thông qua APIService (với thể loại mớiId).

Trong AngularJS tôi đã thực hiện một $watchbiến. cách tốt nhất để xử lý này là gì?




Câu trả lời:


720

Trên thực tế, có hai cách để phát hiện và hành động khi một đầu vào thay đổi trong thành phần con trong angular2 +:

  1. Bạn có thể sử dụng phương pháp vòng đời ngOnChanges () như được đề cập trong các câu trả lời cũ hơn:
    @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

    }

Liên kết tài liệu: ngOnChanges, SimpleChanges, SimpleChange
Demo Ví dụ: Nhìn vào plunker này

  1. Thay phiên, bạn cũng có thể sử dụng trình thiết lập thuộc tính đầu vào như sau:
    private _categoryId: string;

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

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

    }

    get categoryId(): string {

        return this._categoryId;

    }

Liên kết tài liệu: Nhìn vào đây .

Ví dụ demo: Nhìn vào plunker này .

TIẾP CẬN NÀO BẠN NÊN SỬ DỤNG?

Nếu thành phần của bạn có một vài đầu vào, thì, nếu bạn sử dụng ngOnChanges (), bạn sẽ nhận được tất cả các thay đổi cho tất cả các đầu vào cùng một lúc trong ngOnChanges (). Sử dụng phương pháp này, bạn cũng có thể so sánh các giá trị hiện tại và trước đây của đầu vào đã thay đổi và thực hiện các hành động tương ứng.

Tuy nhiên, nếu bạn muốn làm gì đó khi chỉ một đầu vào cụ thể thay đổi (và bạn không quan tâm đến các đầu vào khác), thì có thể đơn giản hơn khi sử dụng trình thiết lập thuộc tính đầu vào. Tuy nhiên, cách tiếp cận này không cung cấp cách xây dựng để so sánh các giá trị trước đây và hiện tại của đầu vào đã thay đổi (mà bạn có thể thực hiện dễ dàng với phương pháp vòng đời ngOnChanges).

EDIT 2017-07-25: PHÁT HIỆN THAY ĐỔI ANGULAR CÓ THỂ VẪN CHƯA TỪNG HIỂU MỘT SỐ CIRCUMSTANCES

Thông thường, phát hiện thay đổi cho cả setter và ngOnChanges sẽ kích hoạt bất cứ khi nào thành phần cha mẹ thay đổi dữ liệu mà nó truyền cho con, với điều kiện dữ liệu là kiểu dữ liệu nguyên thủy của JS (chuỗi, số, boolean) . Tuy nhiên, trong các trường hợp sau, nó sẽ không kích hoạt và bạn phải thực hiện thêm các hành động để làm cho nó hoạt động.

  1. Nếu bạn đang sử dụng một đối tượng hoặc mảng lồng nhau (thay vì kiểu dữ liệu nguyên thủy JS) để truyền dữ liệu từ Parent sang Child, phát hiện thay đổi (sử dụng setter hoặc ngchanges) có thể không được kích hoạt, như người dùng đã đề cập trong câu trả lời: muetzerich. Đối với các giải pháp xem ở đây .

  2. Nếu bạn đang thay đổi dữ liệu bên ngoài bối cảnh góc (tức là bên ngoài), thì góc sẽ không biết về những thay đổi. Bạn có thể phải sử dụng ChangeDetectorRef hoặc NgZone trong thành phần của mình để nhận biết các thay đổi bên ngoài và do đó kích hoạt phát hiện thay đổi. Tham khảo điều này .


8
Điểm rất tốt liên quan đến việc sử dụng setters với đầu vào, tôi sẽ cập nhật đây là câu trả lời được chấp nhận vì nó toàn diện hơn. Cảm ơn.
Jon Catmull

2
@trichetriche, setter (phương thức set) sẽ được gọi bất cứ khi nào cha mẹ thay đổi đầu vào. Ngoài ra, điều tương tự cũng đúng với ngOnChanges ().
Alan CS

Rõ ràng là không ... Tôi sẽ cố gắng để tìm ra điều này, có lẽ tôi đã làm gì đó sai.

1
Tôi chỉ vấp phải câu hỏi này, lưu ý rằng bạn đã nói sử dụng setter sẽ không cho phép so sánh các giá trị, tuy nhiên điều đó không đúng, bạn có thể gọi doSomethingphương thức của mình và lấy 2 đối số giá trị mới và cũ trước khi thực sự đặt giá trị mới, theo cách khác sẽ lưu trữ giá trị cũ trước khi cài đặt và gọi phương thức sau đó.
T.Aoukar

1
@ T.Aoukar Điều tôi đang nói là setter đầu vào không hỗ trợ so sánh các giá trị cũ / mới như ngOnChanges (). Bạn luôn có thể sử dụng hack để lưu trữ giá trị cũ (như bạn đã đề cập) để so sánh nó với giá trị mới.
Alan CS

105

Sử dụng ngOnChanges()phương pháp vòng đời trong thành phần của bạn.

ngOnChanges được gọi ngay sau khi các thuộc tính ràng buộc dữ liệu đã được kiểm tra và trước khi xem và nội dung trẻ em được kiểm tra nếu ít nhất một trong số chúng đã thay đổi.

Đây là Tài liệu .


6
Điều này hoạt động khi một biến được đặt thành một giá trị mới MyComponent.myArray = [], vd , nhưng nếu giá trị đầu vào bị thay đổi MyComponent.myArray.push(newValue), vd , ngOnChanges()không được kích hoạt. Bất kỳ ý tưởng về làm thế nào để nắm bắt sự thay đổi này?
Levi Fuller

4
ngOnChanges()không được gọi khi một đối tượng lồng nhau đã thay đổi. Có lẽ bạn tìm thấy một giải pháp ở đây
muetzerich

33

Tôi đã gặp lỗi trong giao diện điều khiển cũng như trình biên dịch và IDE khi sử dụng SimpleChangeskiểu trong chữ ký hàm. Để ngăn ngừa lỗi, anythay vào đó hãy sử dụng từ khóa trong chữ ký.

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

BIÊN TẬP:

Như Jon chỉ ra bên dưới, bạn có thể sử dụng SimpleChangeschữ ký khi sử dụng ký hiệu dấu ngoặc thay vì ký hiệu dấu chấm.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}

1
Bạn đã nhập SimpleChangesgiao diện ở đầu tệp của bạn?
Jon Catmull

@Jon - Vâng. Vấn đề không phải là chữ ký, nhưng truy cập các tham số được gán trong thời gian chạy đến đối tượng SimpleChanges. Ví dụ, trong thành phần của tôi, tôi đã ràng buộc lớp Người dùng của mình với đầu vào (nghĩa là <my-user-details [user]="selectedUser"></my-user-details>). Thuộc tính này được truy cập từ lớp SimpleChanges bằng cách gọi changes.user.currentValue. Thông báo userbị ràng buộc trong thời gian chạy và không phải là một phần của đối tượng SimpleChanges
Darcy

1
Bạn đã thử điều này mặc dù? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Jon Catmull

@Jon - Chuyển sang ký hiệu khung thực hiện thủ thuật. Tôi đã cập nhật câu trả lời của mình để đưa nó vào thay thế.
Darcy

1
nhập {SimpleChanges} từ '@ angular / core'; Nếu đó là trong các tài liệu góc, và không phải là kiểu bản thảo gốc, thì có lẽ đó là từ một mô-đun góc, rất có thể là 'góc / lõi'
BentOnCoding

13

Việc đặt cược an toàn nhất là đi với một chia sẻ dịch vụ thay vì của một @Inputtham số. Cũng thế,@Input tham số không phát hiện các thay đổi trong loại đối tượng lồng nhau phức tạp.

Một dịch vụ ví dụ đơn giản như sau:

Dịch vụ

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

}

Thành phần

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

Bạn có thể sử dụng một triển khai tương tự trong các thành phần khác và tất cả các thành phần của bạn sẽ chia sẻ cùng các giá trị được chia sẻ.


1
Cảm ơn bạn. Đây là một điểm rất tốt để thực hiện và nơi thích hợp một cách tốt hơn để làm điều đó. Tôi sẽ để lại câu trả lời được chấp nhận vì nó trả lời các câu hỏi cụ thể của tôi về việc phát hiện các thay đổi @Input.
Jon Catmull

1
Tham số @Input không phát hiện các thay đổi bên trong một loại đối tượng lồng nhau phức tạp, nhưng nếu chính đối tượng đó thay đổi, nó sẽ kích hoạt, phải không? ví dụ: tôi có tham số đầu vào là "cha mẹ", vì vậy nếu tôi thay đổi toàn bộ cha mẹ, phát hiện thay đổi sẽ kích hoạt, nhưng nếu tôi thay đổi tên cha mẹ
Yogibimbi

Bạn nên cung cấp một lý do tại sao giải pháp của bạn an toàn hơn
Joshua Kemmerer

1
@InputTham số @JoshuaKemmerer không phát hiện các thay đổi trong loại đối tượng lồng nhau phức tạp nhưng nó thực hiện trong các đối tượng gốc đơn giản.
Sanket Berde

6

Tôi chỉ muốn thêm rằng có một móc Vòng đời khác được gọi DoChecklà hữu ích nếu@Input giá trị không phải là giá trị nguyên thủy.

Tôi có một Array Inputvì vậy điều này không kích hoạt OnChangessự kiện khi nội dung thay đổi (vì việc kiểm tra Angular thực hiện là 'đơn giản' và không sâu nên Array vẫn là một Array, mặc dù nội dung trên Array đã thay đổi).

Sau đó tôi thực hiện một số mã kiểm tra tùy chỉnh để quyết định xem tôi có muốn cập nhật chế độ xem của mình với Mảng đã thay đổi hay không.


6
  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

vui lòng thử sử dụng phương pháp này Hi vọng điêu nay co ich


4

Bạn cũng có thể, có thể quan sát được kích hoạt các thay đổi trong thành phần cha mẹ (CategoryComponent) và làm những gì bạn muốn làm trong đăng ký trong thành phần con. (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
});
}

2

Tại đây ngOnChanges sẽ kích hoạt luôn khi thuộc tính đầu vào của bạn thay đổi:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}

1

Giải pháp này sử dụng lớp proxy và cung cấp các ưu điểm sau:

  • Cho phép người tiêu dùng tận dụng sức mạnh của RXJS
  • Nhỏ gọn hơn hơn so với các giải pháp khác được đề nghị cho đến nay
  • Nhiều loại an toàn hơn so với sử dụngngOnChanges()

Ví dụ sử dụng:

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

Chức năng tiện ích:

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

Nếu bạn không muốn sử dụng phương thức ngOnChange hiện thực og onChange (), bạn cũng có thể đăng ký thay đổi một mục cụ thể theo sự kiện valueChanges , ETC.

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

});

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

markForCheck () đã viết vì sử dụng trong khai báo này:

  changeDetection: ChangeDetectionStrategy.OnPush

3
Điều này hoàn toàn khác với câu hỏi và mặc dù những người hữu ích nên biết rằng mã của bạn là về một loại thay đổi khác nhau. Câu hỏi được hỏi về việc thay đổi dữ liệu từ OUTSIDE một thành phần và sau đó để thành phần của bạn biết và trả lời thực tế dữ liệu đã thay đổi. Câu trả lời của bạn là nói về việc phát hiện các thay đổi trong chính thành phần trên những thứ như đầu vào trên một biểu mẫu LỘC cho thành phần.
rmcsharry

0

Bạn cũng có thể chỉ cần vượt qua một EventEuctor làm Đầu vào. Không hoàn toàn chắc chắn nếu đây là thực hành tốt nhất ...

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>

Và trong VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}

Vui lòng cung cấp các liên kết hữu ích, đoạn mã để hỗ trợ giải pháp của bạn.
mishsx

Tôi đã thêm một ví dụ.
Cédric Schweizer
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.