13

My plan is to store the values of a form in my ngrx store to allow my users to navigate around the site and back to the form if they wish. The idea would be that the values of the form would repopulate from the store using an observable.

here is how I'm doing it currently:

constructor(private store: Store<AppState>, private fb: FormBuilder) {
    this.images = images;
    this.recipe$ = store.select(recipeBuilderSelector);
    this.recipe$.subscribe(recipe => this.recipe = recipe); // console.log() => undefined
    this.recipeForm = fb.group({
      foodName: [this.recipe.name], // also tried with an OR: ( this.recipe.name || '')
      description: [this.recipe.description]
    })
  }

The store is given an initial value which I have seen passes through my selector function properly, but by the time my form is created, I don't think that value has returned. Therefore this.recipe is still undefined.

Is this the wrong approach, or can I somehow ensure that the observable is returned before creating the form?

2 Answers 2

13

Although adding another layer might seem more complicated, it is much easier to deal with observables by splitting the single component into two: a container component and a presentational component.

The container component deals only with observables and not with the presentation. The data from any observables is passed to the presentation component via @Input properties and the async pipe is used:

@Component({
  selector: "recipe-container",
  template: `<recipe-component [recipe]="recipe$ | async"></recipe-component>`
})
export class RecipeContainer {

  public recipe$: Observable<any>;

  constructor(private store: Store<AppState>) {
    this.recipe$ = store.select(recipeBuilderSelector);
  }
}

The presentational component receives simple properties and does not have to deal with observables:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "recipe-component",
  template: `...`
})
export class RecipeComponent {

  public recipeForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.recipeForm = this.formBuilder.group({
      foodName: [""],
      description: [""]
    });
  }

  @Input() set recipe(value: any) {
    this.recipeForm.patchValue({
      foodName: value.name,
      description: value.description
    });
  }
}

The notion of using container and presentational components is a general Redux concept and is explained in Presentational and Container Components.

Sign up to request clarification or add additional context in comments.

7 Comments

I could not get this way to work for me. Somehow the form was being built before the Selector was called
Yep, I see your point. I should have created the form in the constructor and only applied the changes when the @Input was changed. I've updated the answer. Whichever way you decide to go with this, I'd encourage you to look into separating things into container and presentational components, as it does make life easier.
any criticism or shusson's option 2 answer? I was able to do it without a container by using [formGroup]="recipe$ | async"
You can definitely get it working without the container/presentation separation, but I find the separation beneficial with larger apps. It's up to you to decide whether or not it's worth it in your situation. It's just something to be aware of; there are almost always multiple ways of getting something done. I use container components for all store and service interactions, leaving the presentation components to deal with simple inputs. BTW, I simplified the answer to use an @Input setter instead of OnChanges.
@cartant Could you use ChangeStrategy.onPush with @Input() on the presentational layout ?
|
7

I can think of two options...

Option 1:

Use an *ngIf on the html that displays the form something like

<form *ngIf="this.recipe">...</form>

Option 2: Use the async pipe in your template and create your model like:

component

model: Observable<FormGroup>;    
...
this.model = store.select(recipeBuilderSelector)
    .startWith(someDefaultValue)
    .map((recipe: Recipe) => {
        return fb.group({
            foodName: [recipe.name],
            description: [recipe.description]
        })
    })

template

<app-my-form [model]="(model | async)"></app-my-form>

You would have to consider how to handle updates to the store and to the current model.

3 Comments

I wasn't able to get it to work this way. startsWith() does not exist on type Observable. that function seems to exist only for strings.
I'm sorry, the method should be startWith. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
.startWith() worked on the first iteration, but when the second (dev mode) ran it was again undefined. I got it to work by removing startWith() and changing my selector to: return _.cloneDeep(state.recipebuilder) || someDefaultValue;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.