3

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?

2
  • The answer by @Eliseo is what I would do, simply emit the control's initial value since valueChanges will only emit the changes, not the initial value. Note, you do not need to initialize isOdd$ with an empty observable, then reassign it, you can simply initialize it to the value from your form control valueChanges StackBlitz Commented Oct 28, 2024 at 14:29
  • Thanks for the answer! I have updated the demo with your suggestion. I might be mistaken but the problem is still the same: upon loading the page, the async pipe shows "false" in the label beside the textbox, when it should show true (as the control value set by the "parent", 21, is an odd value). Then, if the user interacts with the textbox everything works as expected. Commented Oct 29, 2024 at 7:40

3 Answers 3

3

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)
);
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the answer! I have updated the demo with your suggestion. I might be mistaken but the problem is still the same: upon loading the page, the async pipe shows "false" in the label beside the textbox, when it should show true (as the control value set by the "parent", 21, is an odd value). Then, if the user interacts with the textbox everything works as expected.
@menrodriguez, If you use the ngAfterViewInit instead of ngOnInit to subscribe looks like work your forked stackblitz, apologies for not checking better my answer :(
@Eliseo thank you! Moving the observable creation to AfterViewInit raises the dreaded Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null'. Current value: 'true'.
0
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,

4 Comments

since Menrodriguez use pipe async it's not necessary unsubscribe
In my case, its two differents obs. From form value change sub in .ts and behavior async in front.
::glups:: I read so quick :(
Thanks for the answer! I have updated the demo with your suggestion, it works as expected. Would you be so kind as to explain why subscribing explicitly to the valueChanges and emiting a new value for a subject to which the async pipe subscribes works as opposed to subscribing via async pipe to an observable created from transforming the valueChanges?
0

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

1 Comment

Thanks for your response! I have updated the demo with your suggestion. I might be mistaken but the problem is still the same: upon loading the page, the async pipe shows "false" in the label beside the textbox, when it should show true (as the control value set by the "parent", 21, is an odd value). Then, if the user interacts with the textbox everything works as expected.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.