/forms-manager

🦄 The Foundation for Proper Form Management in Angular

Primary LanguageTypeScriptMIT LicenseMIT


The Foundation for Proper Form Management in Angular

Build Status commitizen PRs coc-badge semantic-release styled with prettier All Contributors

🔮 Features

✅ Allows Typed Forms!
✅ Auto persists the form's state upon user navigation.
✅ Provides an API to reactively querying any form, from anywhere.
✅ Persist the form's state to local storage.


NgFormsManager lets you sync Angular’s FormGroup, FormControl, and FormArray, via a unique store created for that purpose. The store will hold the controls' data like values, validity, pristine status, errors, etc.

This is powerful, as it gives you the following abilities:

  1. It will automatically save the current control value and update the form value according to the value in the store when the user navigates back to the form.
  2. It provides an API so you can query a form’s values and properties from anywhere. This can be useful for things like multi-step forms, cross-component validation and more.
  3. It can persist the form's state to local storage.

The goal in creating this was to work with the existing Angular form ecosystem, and save you the trouble of learning a new API. Let’s see how it works:

First, install the library:

Installation

npm i @ngneat/forms-manager

Then, create a component with a form:

import { NgFormsManager } from '@ngneat/forms-manager';

@Component({
  template: `
   <form [formGroup]="onboardingForm">
     <input formControlName="name">
     <input formControlName="age">
     <input formControlName="city">
   </form>
  `
})
export class OnboardingComponent {

  onboardingForm: FormGroup;

  constructor(
    private formsManager: NgFormsManager,
    private builder: FormBuilder
  ) {}

  ngOnInit() {
    this.onboardingForm = this.builder.group({
      name: [null, Validators.required],
      age:  [null, Validators.required]),
      city: [null, Validators.required]
    });

    this.formsManager.upsert('onboarding', this.onboardingForm);
  }

  ngOnDestroy() {
    this.formsManager.unsubscribe('onboarding');
  }
}

As you can see, we’re still working with the existing API in order to create a form in Angular. We’re injecting the NgFormsManager and calling the upsert method, giving it the form name and an AbstractForm. From that point on, NgFormsManager will track the form value changes, and update the store accordingly.

With this setup, you’ll have an extensive API to query the store and update the form from anywhere in your application:

API

  • selectValid() - Whether the control is valid
const isFormValid$ = formsManager.selectValid('onboarding');
const isNameValid$ = formsManager.selectValid('onboarding', 'name');
  • selectDirty() - Whether the control is dirty
const isFormDirty$ = formsManager.selectDirty('onboarding');
const isNameDirty$ = formsManager.selectDirty('onboarding', 'name');
  • selectDisabled() - Whether the control is disabled
const isFormDisabled$ = formsManager.selectDisabled('onboarding');
const isNameDisabled$ = formsManager.selectDisabled('onboarding', 'name');
  • selectValue() - Observe the control's value
const value$ = formsManager.selectValue('onboarding');
const nameValue$ = formsManager.selectValue<string>('onboarding', 'name');
  • selectErrors() - Observe the control's errors
const errors$ = formsManager.selectErrors('onboarding');
const nameErros$ = formsManager.selectErrors('onboarding', 'name');
  • selectControl() - Observe the control's state
const control$ = formsManager.selectControl('onboarding');
const nameControl$ = formsManager.selectControl('onboarding', 'name');
  • getControl() - Get the control's state
const control = formsManager.getControl('onboarding');
const nameControl = formsManager.getControl('onboarding', 'name');

selectControl and getControl will return the following state:

{
   value: any,
   rawValue: object,
   errors: object,
   valid: boolean,
   dirty: boolean,
   invalid: boolean,
   disabled: boolean,
   touched: boolean,
   pristine: boolean,
   pending: boolean,
}
  • selectForm() - Observe the form's state
const form$ = formsManager.selectForm('onboarding');
  • getForm() - Get the form's state
const form = formsManager.getForm('onboarding');
  • hasForm() - Whether the form exists
const hasForm = formsManager.hasForm('onboarding');
  • patchValue() - A proxy to the original patchValue method
formsManager.patchValue('onboarding', value, options);
  • setValue() - A proxy to the original setValue method
formsManager.setValue('onboarding', value, options);
  • unsubscribe() - Unsubscribe from the form's valueChanges observable (always call it on ngOnDestroy)
formsManager.unsubscribe('onboarding');
formsManager.unsubscribe();
  • clear() - Delete the form from the store
formsManager.clear('onboarding');
formsManager.clear();
  • destroy() - Destroy the form (Internally calls clear and unsubscribe)
formsManager.destroy('onboarding');
formsManager.destroy();

Persist to Local Storage

In the upsert method, pass the persistState flag:

formsManager.upsert(formName, abstractContorl, {
  persistState: true;
});

Validators

The library exposes two helpers method for adding cross component validation:

export function setValidators(
  control: AbstractControl,
  validator: ValidatorFn | ValidatorFn[] | null
);

export function setAsyncValidators(
  control: AbstractControl,
  validator: AsyncValidatorFn | AsyncValidatorFn[] | null
);

Here's an example of how we can use it:

export class HomeComponent{
  ngOnInit() {
    this.form = new FormGroup({
      price: new FormControl(null, Validators.min(10))
    });

    /*
    * Observe the `minPrice` value in the `settings` form
    * and update the price `control` validators
    */
    this.formsManager.selectValue<number>('settings', 'minPrice')
     .subscribe(minPrice => setValidators(this.form.get('price'), Validators.min(minPrice));
  }
}

Using FormArray Controls

When working with a FormArray, it's required to pass a factory function that defines how to create the controls inside the FormArray. For example:

import { NgFormsManager } from '@ngneat/forms-manager';

export class HomeComponent {
  skills: FormArray;
  config: FormGroup;

  constructor(private formsManager: NgFormsManager<FormsState>) {}

  ngOnInit() {
    this.skills = new FormArray([]);

    /** Or inside a FormGroup */
    this.config = new FormGroup({
      skills: new FormArray([]),
    });

    this.formsManager
      .upsert('skills', this.skills, { arrControlFactory: value => new FormControl(value) })
      .upsert('config', this.config, {
        arrControlFactory: { skills: value => new FormControl(value) },
      });
  }

  ngOnDestroy() {
    this.formsManager.unsubscribe();
  }
}

NgFormsManager Generic Type

NgFormsManager can take a generic type where you can define the forms shape. For example:

export interface AppForms = {
  onboarding: {
    name: string;
    age: number;
    city: string;
  }
}

This will make sure that the queries are typed, and you don't make any mistakes in the form name.

export class OnboardingComponent {
  constructor(private formsManager: NgFormsManager<AppForms>, private builder: FormBuilder) {}

  ngOnInit() {
    this.formsManager.selectValue('onboarding').subscribe(value => {
      // value now typed as AppForms['onboarding']
    });
  }
}

NgFormsManager Config

You can override the default config by passing the NG_FORMS_MANAGER_CONFIG provider:

import { NG_FORMS_MANAGER_CONFIG, NgFormsManagerConfig } from '@ngneat/forms-manager';

@NgModule({
  declarations: [AppComponent],
  imports: [ReactiveFormsModule],
  providers: [
    {
      provide: NG_FORMS_MANAGER_CONFIG,
      useValue: new NgFormsManagerConfig({
        debounceTime: 1000, // defaults to 300
        storage: {
          key: 'NgFormManager',
        },
      }),
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Netanel Basal

💻 📖 🤔

Colum Ferry

💻 📖

This project follows the all-contributors specification. Contributions of any kind welcome!