menrodriguez
menrodriguez

Reputation: 285

Async pipe not working when using an observable created from a ControlValueAccessor control's valueChanges

I have a ControlValueAccesor with one FormControl. I need to create a new observable from the control's valueChanges and use it in the template via AsyncPipe. So in CVA's writeValue I update the form control's value using setValue().

 public isOdd$ = new Observable<boolean>();
 nestedFc = new FormControl([null]);

 writeValue() {
   this.nestedFc.setValue(22);
 }
 ngOnInit() {
    this.isOdd$ = this.nestedFc.valueChanges.pipe(
      map((value) => value % 2 !== 0)
    );
 }
<span> <input [formControl]="nestedFc"/> </span>
<span *ngIf="{value: isOdd$ | async} as context"> {{context.value}}</span>

The problem is that valueChanges is triggered when writeValue is first called but the async pipe does not "see" these changes, and so the view does not show the update.

There is a couple GitHub issues around this: ng 11: patchValue, valueChanges & async pipe and https://github.com/angular/angular/issues/40826.

Based on the last one I have created a stackblitz to reproduce the problem. If writeValue uses setTimeout to patch the form control's value, valueChanges is triggered and the async pipe "sees" the change.

Is this correct? Is there really no other way but to use setTimeout?

Upvotes: 3

Views: 167

Answers (3)

nicowernli
nicowernli

Reputation: 3348

Why not just initialize isOdd$ on declaration?

export class ChildComponent
  implements ControlValueAccessor, OnInit
{

  nestedFc = new FormControl([null]);

  public isOdd$ = this.nestedFc.valueChanges.pipe(
    startWith(this.nestedFc.value), //<--start with
    tap((changes) =>
      console.log(
        `[child] valueChanges triggered on pipe, changes: ${JSON.stringify(
          changes
        )}`
      )
    ),
    map((value) => value % 2 !== 0)
  );

  constructor() {}

  writeValue(value: any) {
    console.log('[child] writeValue called');
    // Uncomment the setTimeout for this to work
    // setTimeout(() => {
    this.nestedFc.setValue(value);
    // }, 200);
  }

  ngOnInit() {
    console.log('[child] on init');

    this.nestedFc.valueChanges.subscribe((changes) => {
      console.log(
        `[child] valueChanges triggered, changes: ${JSON.stringify(changes)}`
      );
    });
  }

  registerOnChange(fn: any): void {
    // throw new Error("Method not implemented.");
  }
  registerOnTouched(fn: any): void {
    // throw new Error("Method not implemented.");
  }
  setDisabledState?(isDisabled: boolean): void {
    // throw new Error("Method not implemented.");
  }
}

Upvotes: 0

Eliseo
Eliseo

Reputation: 58019

The problem is that the writeValue it's executed before the ngOnInit. Generally you use startWith rxjs operator in the way

this.isOdd$ = this.nestedFc.valueChanges.pipe(
  startWith(this.nestedFc.value), //<--start with
  tap((changes) =>
    console.log(
      `valueChanges triggered on pipe, changes: ${JSON.stringify(changes)}`
    )
  ),
  map((value) => value % 2 !== 0)
);

Upvotes: 3

Yoann ROCHE
Yoann ROCHE

Reputation: 128

isOdd = new BehaviorSubject(false);

private _destoyed = new Subject<void>()

ngOnInit(): void {
 this.nestedFc.valueChanges
      .pipe(
        takeUntil(this_destroyed),
        map((value: any) => value % 2 !== 0)
      )
      .subscribe((odd) => {
        this.isOdd.next(odd);
      });

Front :

<span *ngIf="(isOdd | async) as value"> {{value}}</span>`

Dont forget to next into complete _destroyed on ngDestroy.

cdr.detectChanges() isn't best way to fix this.

And with this behaviorSubject impl, you can add this one (Optional but better perf) :

changeDetection: ChangeDetectionStrategy.OnPush,

Upvotes: 0

Related Questions