Angular 手动设置的@Input 值在 ngOnChanges 中仍然被相同的值覆盖

Angular manually set @Input value is still overwritten with the same value in ngOnChanges

我有一个 parent 组件 employees.component.ts 和一个 child 组件 employee.component.ts。 parent 的模板分为两部分 - 20% 的宽度是 PrimeNG 树组件(其节点代表不同的员工),其余部分用于 child。 child 的模板是一个表单,您可以在其中更改 selected 员工的数据或添加新员工。当 select 从树中提取一个节点时,员工的 ID 作为输入从 parent 发送到 child。 child 组件中的输入更改触发 ngOnChanges,其中对员工数据发出 HTTP 请求。

现在,当我尝试保存新员工时,我将该输入设置为 undefined 以指示当前显示的是一个空表单,以便可以创建新员工。保存新员工后,我从响应中得到创建的员工。我将输入设置为新员工的 ID 并用其数据填写表格。

同样在创建新员工之后,我向 parent 发出一个输出以通知它,必须将一个新员工节点添加到树中以代表这个新员工,select 它因为表格中填满了员工的数据。问题是,当 parent 将节点设置为 selected 时,员工的 ID 作为输入发送到 child(目前在 undefined 之后保存过程)再次触发 ngOnChanges 并发出 HTTP 请求,但由于我已经从 POST 请求的响应中获得了数据,所以我不想发出这个新请求。为避免这种情况,我在收到响应后立即手动设置输入。但是,即使现在输入的值正确地是树中 selected 的员工的 ID,ngOnChanges 仍然会被触发。当我将 changes: SimpleChanges object 记录到控制台时,我看到输入的先前值显示为 undefined 并且其当前值是正确的 id.

为什么之前的值显示为 undefined,即使我已经在其他任何事情之前设置它(parent 的输出)?我该怎么做才能不触发 ngOnChanges?

覆盖组件内的 @Input 属性 是一种反模式,因为它会导致各种难以追踪的意大利面条式信息流通过您的应用程序。

要解决此问题,您可以添加 BehaviorSubject 类型的中间 class 成员,例如下面的(简化)示例。

TS

@Component({ .. })
export class MyComponent implements OnChanges {
  /** single source of truth of your employeeId */
  private readonly employeeId_ = new BehaviorSubject<string>("");

  /** expose observable, that can be used in html with simple async pipe for efficient change detection */
  public readonly employeeId$ = this.employeeId_.asObservable();

  /** primary input, will detect changes from upstream parent */
  @Input() public employeeId(newValue: string) {
    this.setEmployeeIdValue(newValue);
  }

  /** lifecycle hook that detects changes from upstream and propagates them internally */
  ngOnChanges(changes: SimpleChanges) {
    if (changes.employeeId) {
      this.employeeId_.next(changes.employeeId.currentValue);
    }
  }

  /** method to synchronously evaluate latest value */
  public getEmployeeIdValue(): string {
    return this.employeeId_.getValue();
  }

  /** call from your html or from this component's code to handle your business/ui logic */
  public setEmployeeIdValue(newValue: string) {
    this.employeeId_.next(newValue);
  }
}

HTML

<div>
  {{ employeeId$ | async }}
</div>

详细了解 BehaviorSubject in the RxJS docs.

您有两个问题:

  1. 为什么之前的值是undefined
  2. 如何在创建新员工后立即在 ngOnChanges 中触发呼叫

为什么之前的值是undefined

您描述的行为是正确的。

创建新员工时,没有员工 ID (undefined),然后在填写表格后调用创建新员工并在响应中收到员工 ID。

如果您向 parent 组件发送新 ID,这会将其设置为您的 child 的 @Input,然后 ngOnChanges 将被调用,您将正确将 previousValue 设置为 undefined 并将 currentValue 设置为新 ID,这就是 SimpleChange 的工作方式。

如何在创建新员工后立即在 ngOnChanges 中触发呼叫

为避免给您刚创建的员工打电话,您可以添加额外的签入 ngOnChanges。如果您从 parent 收到的 ID 与上次创建的员工的 ID 相同,则跳过加载调用:

@Component({
    selector: 'app-employee',
    templateUrl: './app-employee.component.html',
    styleUrls: ['./app-employee.component.scss']
})
export class EmployeeComponent implements OnChanges {
    @Input() 
    selectedEmployeeId: string;

    @Output()
    createdEmployee = new EventEmitter<string>();

    private createdEmployeeId: string;

    constructor(private http: HttpClient) {}

    ngOnChanges(changes: SimpleChanges): void {
        if (typeof changes.selectedEmployeeId.currentValue === 'undefined') {
            // there is no ID, cannot load employee data
            return;
        }

        if (changes.selectedEmployeeId.currentValue === this.createdEmployeeId) {
            // skip the call and reset createdEmployeeId to null
            this.createdEmployeeId = null;
        } else {
            // do a call to load selectedEmployeeId data
            this.http
                .get(...)
                .subscribe(...);
        }
    }

    createEmployee(): void {
        this.http
            .post(...)
            .subscribe(
                employeeId => {
                    this.createdEmployeeId = employeeId;
                    this.createdEmployee.next(employeeId);
                }
            )
    }
}