1

I have an observable that fetches information from the backend which I then use to fill values in a form like this:

<div class="column">
  <div class="row">
      <mat-select
        name="selectField"
        [options]="someOptions"
        [value]="(store.observable$ | async).selectFieldValue"
        [required]="true"
      >
      </mat-select>
      <mat-input
        name="inputField"
        [required]="true"
        [value]="(store.observable$ | async).inputfieldValue"
      >
      </mat-input>
      <mat-input
        name="otherInputField"
        [required]="true"
        [value]="(store.observable$ | async).otherInputFieldValue"
      >
      </mat-input>
      ...
  </div>
</div>

The observable comes from a selector in my component's ComponentStore:

export class MyComponentStore extends ComponentStore<MyStoreClass> {
  constructor(
    private service: MyHttpService,
  ) {
    super({ ... });
  }

  readonly observable$ = this.select((state) => state.storeClass);

  readonly updateObservable = this.updater(
    (state, payload: SomeClass) => {
      return {
        ...state,
        observable: payload
      };
    }
  );

  getObservableInfo = this.effect((req$: Observable<void>) => {
    return req$.pipe(
      switchMap((_) => this.service.getInfo()),
      map((payload) => this.updateObservable(payload.data))
    );
  })
}

I then call this.store.getObservableInfo() in my component's ngOnInit. I was getting an ExpressionChangedAfterItHasBeenCheckedError error if I tried to do this:

<div class="column" *ngIf="(store.observable$ | async); let observable">
  <div class="row">
      <mat-select
        name="selectField"
        [options]="someOptions"
        [value]="observable.selectFieldValue"
        [required]="true"
      >
      ...

So I dismissed that idea. But I'm bothered by having to write (store.observable$ | async) in each of my controls' values. Is there a way to declare that as a variable somewhere and simplify it like you can do with *ngIf without using *ngIf (to avoid the error)?

Thank you

Heathcliff
  • 3,048
  • 4
  • 25
  • 44
  • My first thought would be that if `ngIf` is causing that error, that there may be something more wrong somewhere else. If you are using NgRx, then your data should be immutable, which should mean that you should not get an "expression changed" error. Is there any way to build a *simple* stackblitz that demonstrates the issue? (With NgRx I'd almost assume the answer to that may be no. LOL) – DeborahK Aug 12 '21 at 19:25
  • @DeborahK give me some time and I'll see if I can set something up. – Heathcliff Aug 12 '21 at 19:26
  • I'm looking at this: ` return { ...state, observable: payload };` If payload is an object, I assume this should be making a copy of it, not assigning a reference? – DeborahK Aug 12 '21 at 19:27
  • (And you might want to remove "simpler" from your post title. I think that may be why your question is getting voted down and marked to close. This may be construed to mean you want an opinion and opinion questions are a "no-no" here.) :-( – DeborahK Aug 12 '21 at 19:30
  • @DeborahK I'll make those editions. Thank you. – Heathcliff Aug 12 '21 at 19:42

2 Answers2

3

Try changing this to make a copy of the payload instead of a direct reference:

  readonly updateObservable = this.updater(
    (state, payload: SomeClass) => {
      return {
        ...state,
        observable: {...payload}
      };
    }
  );

That may resolve your expressionChangedAfterCheck error. If not, there is more info on that error and correction ideas here: https://angular.io/errors/NG0100

If you can resolve that error, then you can use an ngIf like in the first (now deleted) answer:

<div class="column">
  <div class="row" *ngIf="store.observable$ | async as storeObservable">
      <mat-select
        name="selectField"
        [options]="someOptions"
        [value]="storeObservable.selectFieldValue"
        [required]="true"
      >
      </mat-select>
      <mat-input
        name="inputField"
        [required]="true"
        [value]="storeObservable.inputfieldValue"
      >
      </mat-input>
      <mat-input
        name="otherInputField"
        [required]="true"
        [value]="storeObservable.otherInputFieldValue"
      >
      </mat-input>
      ...
  </div>
</div>
DeborahK
  • 57,520
  • 12
  • 104
  • 129
0

Looking at the suggestions in this question, you can probably go with a combination of ng-template and ng-container, something like this:

<ng-container
  *ngTemplateOutlet="myControls; context: { data: observable$ | async }"
></ng-container>

<ng-template #myControls let-data="data">
  <div class="column">
    <div class="row">
      <mat-select
        name="selectField"
        [options]="someOptions"
        [value]="data.selectFieldValue"
        [required]="true"
      >
      </mat-select>
      <mat-input
        name="inputField"
        [required]="true"
        [value]="data.inputfieldValue"
      >
      </mat-input>
      <mat-input
        name="otherInputField"
        [required]="true"
        [value]="data.otherInputFieldValue"
      >
      </mat-input>
      ...
    </div>
  </div>
</ng-template>

Octavian Mărculescu
  • 4,312
  • 1
  • 16
  • 29