如何在 Angular 中使用可观察对象管理组件状态

How to manage component status with observables in Angular

上下文

在一个Angular项目中,我做了一个组件来封装提交请求页面的逻辑(代码如下)。

此页面有两个定义特定表单的子组件(firstFormsecondForm)和一个提交按钮。

每个子组件都有一个针对禁用状态

的输入
@Input() set disabled(value: boolean | null) {
    if (value === null) {
      value = false;
    }
    this._disabled = value;
    this._onDisabledChange();
  }

以及当表单 有效 或无效时发出的输出:

@Output() onValidChange: EventEmitter<boolean> = new EventEmitter();

我想要启用以下表格,之前的表格必须有效。 这些将是启用的依赖项:

问题

当完成第一种形式和第二种形式(启用提交)时,我删除了一种第一种形式的必需输入(因此第一种形式无效),会发生这种情况:

我希望立即更新提交禁用的样式。

注:

我正在使用 changeDetection: ChangeDetectionStrategy.OnPush。作为测试,我使用了 Default 策略,控制台显示错误 ExpressionChangedAfterItHasBeenCheckedError.

代码

由于这是一个复杂的问题,我简化了代码,只留下相关部分。

组件有另一个 EventEmitter 来发出表单值,但这里我想关注有效状态。

Html模板

<div>
  <first-form (onValidChange)="firstFormValid = $event"></first-form>

  <second-form
    [disabled]="!(isSecondStepEnabled$ | async)"
    (onValidChange)="secondFormValid = $event"
  ></second-form>

  <div [ngClass]="{ disabled: !(isSubmitStepEnabled$ | async) }">
    <ul>
      <li>
        <button
          id="submit"
          [disabled]="!(isSubmitStepEnabled$ | async)"
          (click)="submit()"
        >
          Submit
        </button>
      </li>
    </ul>
  </div>
</div>

Angular 组件

export class PageComponent {

  private _isFirstFormValid$: BehaviorSubject<boolean> = new BehaviorSubject<
    boolean
  >(false);

  private _isSecondFormValid$: BehaviorSubject<boolean> = new BehaviorSubject<
    boolean
  >(false);

  set firstFormValid(valid: boolean) {
    this._isFirstFormValid$.next(valid);
  }

  set secondFormValid(valid: boolean) {
    this._isSecondFormValid$.next(valid);
  }

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

  isFirstStepCompleted$: Observable<
    boolean
  > = this._isFirstFormValid$.asObservable();

  isSecondStepEnabled$: Observable<
    boolean
  > = this._isFirstFormValid$.asObservable();

  isSecondStepCompleted$: Observable<boolean> = combineLatest([
    this.isSecondStepEnabled$,
    this.isSecondFormValid$
  ]).pipe(map(this._areAllTrue()));

  isSubmitStepEnabled$: Observable<boolean> = combineLatest([
    this.isFirstStepCompleted$,
    this.isSecondStepCompleted$
  ]).pipe(map(this._areAllTrue()));

  private _areAllTrue(): (value: boolean[]) => boolean {
    return (values: boolean[]) => {
      return values.every(valid => valid === true);
    };
  }

  submit() {
      // call external service
  }
}

版本

Angular CLI: 9.0.4
Node: 13.6.0
OS: darwin x64
Angular: 9.0.4
rxjs: 6.5.4
typescript: 3.7.5
webpack: 4.41.6

解决方案

我找到的唯一解决方案是在 Observable isSubmitStepEnabled$.

上使用 delay(0)
  isSubmitStepEnabled$: Observable<boolean> = combineLatest([
    this.isFirstStepCompleted$,
    this.isSecondStepCompleted$
  ]).pipe(delay(0), map(this._areAllTrue()));

我认为这不是一个好的解决方案,也许我有结构问题。

谢谢大家!

这是我找到的解决方案(不使用 delay 函数):

只需将 asObservable(当我想从 behaviorSubject 获取流时)替换为 .pipe(shareReplay({ refCount: true, bufferSize: ONCE }))

两者的区别是:

  • asObservable 每次调用(由“侦听器”)时都会创建一个新订阅。
  • 使用 shareReplay,就好像您有一个订阅,它的数据会广播给您所有的“听众”

使用 asObservable,我的代码没有 运行 更改检测,直到我在输入外部单击。