View Source on Github

Highlights

  1. Use the [ngModel] attribute for one-way binding.
  2. Use the (ngModelChange) attribute to update the state.
  3. Access each field validation errors through ngModel.
  4. Access the form validation state through ngForm.

// app/simple-form/user.ts

export interface User {
  id: number;
  name: string;
  birthdate: Date;
  favoriteColor?: string;
}
<!-- app/simple-form/user-form-ngxs/user-form-ngxs.component.html -->

<h1>Simple Form (NGXS)</h1>

<mat-card>
  <form (ngSubmit)="submit()" #userForm="ngForm">
    <mat-form-field>
      <mat-label>Name</mat-label>
      <input
        matInput
        required
        name="name"
        [ngModel]="(user$ | async)?.name"
        (ngModelChange)="setName($event)"
        #name="ngModel"
      />
      <mat-error *ngIf="name.invalid">You must enter a value</mat-error>
    </mat-form-field>
    <mat-form-field>
      <mat-label>Birth date</mat-label>
      <input
        matInput
        required
        name="birthdate"
        [matDatepicker]="picker"
        [ngModel]="(user$ | async)?.birthdate"
        (ngModelChange)="setBirthdate($event)"
        #birthdate="ngModel"
      />
      <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
      <mat-datepicker #picker></mat-datepicker>
      <mat-error *ngIf="birthdate.invalid">You must enter a value</mat-error>
    </mat-form-field>
    <mat-form-field>
      <mat-label>Favorite color</mat-label>
      <mat-select
        name="favoriteColor"
        [ngModel]="(user$ | async)?.favoriteColor"
        (ngModelChange)="setFavoriteColor($event)"
      >
        <mat-option *ngFor="let color of colors" [value]="color">{{
          color
        }}</mat-option>
      </mat-select>
    </mat-form-field>
    <div class="form-buttons">
      <button
        mat-raised-button
        type="submit"
        color="primary"
        [disabled]="(loading$ | async) || !userForm.form.valid"
      >
        Submit
      </button>
    </div>
  </form>
</mat-card>

<h2>User (JSON)</h2>

<pre
  >{{ user$ | async | json }}
</pre>
// app/simple-form/user-form-ngxs/user-form-ngxs.component.ts

import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { User } from 'src/app/simple-form/user';
import { UserFormNgxs } from 'src/app/simple-form/user-form-ngxs/user-form-ngxs.actions';
import { UserFormNgxsSelectors } from 'src/app/simple-form/user-form-ngxs/user-form-ngxs.selectors';

@Component({
  selector: 'app-user-form-ngxs',
  templateUrl: './user-form-ngxs.component.html',
})
export class UserFormNgxsComponent {
  readonly colors = ['Red', 'Green', 'Blue'];

  @Select(UserFormNgxsSelectors.user)
  user$!: Observable<User>;

  @Select(UserFormNgxsSelectors.loading)
  loading$!: Observable<boolean>;

  constructor(
    private readonly store: Store,
    private readonly snackBar: MatSnackBar
  ) {}

  setName(name: string) {
    this.store.dispatch(new UserFormNgxs.SetName(name));
  }

  setBirthdate(birthdate: Date) {
    this.store.dispatch(new UserFormNgxs.SetBirthdate(birthdate));
  }

  setFavoriteColor(favoriteColor: string) {
    this.store.dispatch(new UserFormNgxs.SetFavoriteColor(favoriteColor));
  }

  submit(): void {
    this.store
      .dispatch(new UserFormNgxs.Submit())
      .pipe(withLatestFrom(this.user$))
      .subscribe(([, { id }]) => {
        this.snackBar.open(`User saved with ID #${id}`, 'Close');
      });
  }
}
// app/simple-form/user.service.ts

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { User } from 'src/app/simple-form/user';

export interface UserSaveResponse {
  id: number;
}

@Injectable({
  providedIn: 'root',
})
export class UserService {
  save(user: User): Observable<UserSaveResponse> {
    console.log('Save:', user);
    return of({
      id: 1,
    }).pipe(delay(1000));
  }
}
// app/simple-form/user-form-ngxs/user-form-ngxs.state.ts

import { Injectable } from '@angular/core';
import { Action, State, StateContext } from '@ngxs/store';
import { finalize, tap } from 'rxjs/operators';
import { User } from 'src/app/simple-form/user';
import { UserFormNgxs } from 'src/app/simple-form/user-form-ngxs/user-form-ngxs.actions';
import { UserService } from 'src/app/simple-form/user.service';

export interface UserFormNgxsStateModel {
  user: User;
  loading: boolean;
}

@State<UserFormNgxsStateModel>({
  name: 'userFormNgxs',
  defaults: {
    user: {
      id: 0,
      name: '',
      birthdate: new Date(),
      favoriteColor: '',
    },
    loading: false,
  },
})
@Injectable()
export class UserFormNgxsState {
  constructor(private readonly userService: UserService) {}

  @Action(UserFormNgxs.SetName)
  setName(
    context: StateContext<UserFormNgxsStateModel>,
    { name }: UserFormNgxs.SetName
  ) {
    const state = context.getState();
    context.patchState({
      user: { ...state.user, name },
    });
  }

  @Action(UserFormNgxs.SetBirthdate)
  setBirthdate(
    context: StateContext<UserFormNgxsStateModel>,
    { birthdate }: UserFormNgxs.SetBirthdate
  ) {
    const state = context.getState();
    context.patchState({
      user: { ...state.user, birthdate },
    });
  }

  @Action(UserFormNgxs.SetFavoriteColor)
  setFavoriteColor(
    context: StateContext<UserFormNgxsStateModel>,
    { favoriteColor }: UserFormNgxs.SetFavoriteColor
  ) {
    const state = context.getState();
    context.patchState({
      user: { ...state.user, favoriteColor },
    });
  }

  @Action(UserFormNgxs.Submit)
  submit(context: StateContext<UserFormNgxsStateModel>) {
    const state = context.getState();
    context.patchState({
      loading: true,
    });
    return this.userService.save(state.user).pipe(
      tap(({ id }) =>
        context.patchState({
          user: { ...state.user, id },
        })
      ),
      finalize(() =>
        context.patchState({
          loading: false,
        })
      )
    );
  }
}
// app/simple-form/user-form-ngxs/user-form-ngxs.actions.ts

export namespace UserFormNgxs {
  export class SetName {
    static readonly type = '[UserFormNgxs] SetName';
    constructor(readonly name: string) {}
  }

  export class SetBirthdate {
    static readonly type = '[UserFormNgxs] SetBirthdate';
    constructor(readonly birthdate: Date) {}
  }

  export class SetFavoriteColor {
    static readonly type = '[UserFormNgxs] SetFavoriteColor';
    constructor(readonly favoriteColor: string) {}
  }

  export class Submit {
    static readonly type = '[UserFormNgxs] Submit';
  }
}
// app/simple-form/user-form-ngxs/user-form-ngxs.selectors.ts

import { Selector } from '@ngxs/store';
import { User } from 'src/app/simple-form/user';
import {
  UserFormNgxsState,
  UserFormNgxsStateModel,
} from 'src/app/simple-form/user-form-ngxs/user-form-ngxs.state';

export class UserFormNgxsSelectors {
  @Selector([UserFormNgxsState])
  static user({ user }: UserFormNgxsStateModel): User {
    return user;
  }

  @Selector([UserFormNgxsState])
  static loading({ loading }: UserFormNgxsStateModel): boolean {
    return loading;
  }
}