• Stars
    star
    517
  • Rank 82,385 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 4 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐Ÿฆ„ The Foundation for Proper Form Management in Angular

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.
โœ… Built-in dirty functionality.


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.

Buy Me A Coffee

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

  • valueChanges() - Observe the control's value
const value$ = formsManager.valueChanges('onboarding');
const nameValue$ = formsManager.valueChanges<string>('onboarding', 'name');
  • validityChanges() - Whether the control is valid
const valid$ = formsManager.validityChanges('onboarding');
const nameValid$ = formsManager.validityChanges('onboarding', 'name');
  • dirtyChanges() - Whether the control is dirty
const dirty$ = formsManager.dirtyChanges('onboarding');
const nameDirty$ = formsManager.dirtyChanges('onboarding', 'name');
  • disableChanges() - Whether the control is disabled
const disabled$ = formsManager.disableChanges('onboarding');
const nameDisabled$ = formsManager.disableChanges('onboarding', 'name');
  • errorsChanges() - Observe the control's errors
const errors$ = formsManager.errorsChanges<Errors>('onboarding');
const nameErrors$ = formsManager.errorsChanges<Errors>('onboarding', 'name');
  • controlChanges() - Observe the control's state
const control$ = formsManager.controlChanges('onboarding');
const nameControl$ = formsManager.controlChanges<string>('onboarding', 'name');
  • getControl() - Get the control's state
const control = formsManager.getControl('onboarding');
const nameControl = formsManager.getControl<string>('onboarding', 'name');

controlChanges 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,
   untouched: boolean,
}
  • hasControl() - Whether the control exists
const hasControl = formsManager.hasControl('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);
  • reset() - A proxy to the original reset method
formsManager.reset('onboarding', value, options);
  • markAllAsTouched() - A proxy to the original markAllAsTouched method
formsManager.markAllAsTouched('onboarding', options);
  • markAsTouched() - A proxy to the original markAsTouched method
formsManager.markAsTouched('onboarding', options);
  • markAllAsDirty() - Marks the control and all its descendant controls as dirty
formsManager.markAllAsDirty('onboarding', options);
  • markAsDirty() - A proxy to the original markAsDirty method
formsManager.markAsDirty('onboarding', options);
  • markAsPending() - A proxy to the original markAsPending method
formsManager.markAsPending('onboarding', options);
  • markAsPristine() - A proxy to the original markAsPristine method
formsManager.markAsPristine('onboarding', options);
  • markAsUntouched() - A proxy to the original markAsUntouched method
formsManager.markAsUntouched('onboarding', 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();
  • controlDestroyed() - Emits when the control is destroyed
formsManager.controlChanges('login').pipe(takeUntil(controlDestroyed('login')));

Persist to browser storage (localStorage, sessionStorage or custom storage solution)

In the upsert method, pass the persistState flag:

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

By default, the state is persisted to localStorage (Link).

For storage to sessionStorage (Link), add FORMS_MANAGER_SESSION_STORAGE_PROVIDER to the providers array in app.module.ts:

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

@NgModule({
  declarations: [AppComponent],
  imports: [ ... ],
  providers: [
    ...
    FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
    ...
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Furthermore, a custom storage provider, which must implement the Storage interface (Link) can be provided through the FORMS_MANAGER_STORAGE token:

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

class MyStorage implements Storage {
  public clear() { ... }
  public key(index: number): string | null { ... }
  public getItem(key: string): string | null { ... }
  public removeItem(key: string) { ... }
  public setItem(key: string, value: string) { ... }
}

@NgModule({
  declarations: [AppComponent],
  imports: [ ... ],
  providers: [
    ...
    {
      provide: FORMS_MANAGER_STORAGE,
      useValue: MyStorage,
    },
    ...
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

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.valueChanges<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:

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.valueChanges('onboarding').subscribe(value => {
      // value now typed as AppForms['onboarding']
    });
  }
}

Note that you can split the types across files using a definition file:

// login-form.d.ts
interface AppForms {
  login: {
    email: string;
    password: string
  }
}

// onboarding.d.ts
interface AppForms {
  onboarding: {
    ...
  }
}

Using the Dirty Functionality

The library provides built-in support for the common "Is the form dirty?" question. Dirty means that the current control's value is different from the initial value. It can be useful when we need to toggle the visibility of a "save" button or displaying a dialog when the user leaves the page.

To start using it, you should set the withInitialValue option:

@Component({
  template: `
    <button *ngIf="isDirty$ | async">Save</button>
  `,
})
export class SettingsComponent {
  isDirty$ = this.formsManager.initialValueChanged(name);

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

  ngOnInit() {
    this.formsManager.upsert(name, control, {
      withInitialValue: true,
    });
  }
}

setInitialValue(name, value) - Set the initial form's value

formsManager.setInitialValue('form', initialValue);

getInitialValue(name) - Get the initial value or undefined if not exist.

formsManager.getInitialValue('form');

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

๐Ÿ’ป ๐Ÿ“–

Mehmet Erim

๐Ÿ“–

David Speirs

๐Ÿ’ป ๐Ÿ“–

Emmanuel De Saint Steban

๐Ÿ’ป ๐Ÿ“–

Adrian Riepl

๐Ÿ’ป ๐Ÿ“–

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

More Repositories

1

falso

All the Fake Data for All Your Real Needs ๐Ÿ™‚
TypeScript
3,098
star
2

spectator

๐ŸฆŠ ๐Ÿš€ A Powerful Tool to Simplify Your Angular Tests
TypeScript
2,029
star
3

transloco

๐Ÿš€ ๐Ÿ˜ The internationalization (i18n) library for Angular
TypeScript
1,856
star
4

until-destroy

๐ŸฆŠ RxJS operator that unsubscribe from observables on destroy
TypeScript
1,712
star
5

elf

๐Ÿง™โ€โ™€๏ธ A Reactive Store with Magical Powers
TypeScript
1,527
star
6

content-loader

โšช๏ธ SVG component to create placeholder loading, like Facebook cards loading.
TypeScript
733
star
7

hot-toast

๐Ÿž Smoking hot Notifications for Angular. Lightweight, customizable and beautiful by default.
TypeScript
687
star
8

cashew

๐Ÿฟ A flexible and straightforward library that caches HTTP requests in Angular
TypeScript
671
star
9

reactive-forms

(Angular Reactive) Forms with Benefits ๐Ÿ˜‰
TypeScript
610
star
10

tailwind

๐Ÿ”ฅ A schematic that adds Tailwind CSS to Angular applications
TypeScript
608
star
11

query

๐Ÿš€ Powerful asynchronous state management, server-state utilities and data fetching for Angular Applications
TypeScript
510
star
12

error-tailor

๐Ÿฆ„ Making sure your tailor-made error solution is seamless!
TypeScript
478
star
13

helipopper

๐Ÿš A Powerful Tooltip and Popover for Angular Applications
TypeScript
392
star
14

nx-serverless

๐Ÿš€ The Ultimate Monorepo Starter for Node.js Serverless Applications
TypeScript
388
star
15

dialog

๐Ÿ‘ป A simple to use, highly customizable, and powerful modal for Angular Applications
TypeScript
371
star
16

hotkeys

๐Ÿค– A declarative library for handling hotkeys in Angular applications
TypeScript
325
star
17

edit-in-place

A flexible and unopinionated edit in place library for Angular applications
TypeScript
252
star
18

svg-icon

๐Ÿ‘ป A lightweight library that makes it easier to use SVG icons in your Angular Application
TypeScript
251
star
19

inspector

๐Ÿ•ต๏ธ An angular library that lets you inspect and change Angular component properties
TypeScript
218
star
20

dirty-check-forms

๐ŸฌDetect Unsaved Changes in Angular Forms
TypeScript
199
star
21

input-mask

๐ŸŽญ @ngneat/input-mask is an angular library that creates an input mask
TypeScript
199
star
22

lib

๐Ÿค– Lets you focus on the stuff that matters
TypeScript
180
star
23

transloco-keys-manager

๐Ÿฆ„ The Key to a Better Translation Experience
TypeScript
174
star
24

dag

๐Ÿ  An Angular service for managing directed acyclic graphs
TypeScript
153
star
25

bind-query-params

Sync URL Query Params with Angular Form Controls
TypeScript
147
star
26

from-event

๐ŸฆŠ ViewChild and FromEvent โ€” a Match Made in Angular Heaven
TypeScript
137
star
27

overview

๐Ÿค– A collection of tools to make your Angular views more modular, scalable, and maintainable
TypeScript
104
star
28

aim

Angular Inline Module Schematics
TypeScript
97
star
29

cmdk

Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk
TypeScript
91
star
30

copy-to-clipboard

โœ‚๏ธ Modern copy to clipboard. No Flash.
TypeScript
78
star
31

variabless

JS & CSS - A Match Made in Heaven ๐Ÿ’Ž
HTML
78
star
32

loadoff

๐Ÿคฏ When it comes to loaders, take a load off your mind...
TypeScript
78
star
33

effects

๐Ÿช„ A framework-agnostic RxJS effects implementation
TypeScript
59
star
34

avvvatars

Beautifully crafted unique avatar placeholder for your next angular project.
TypeScript
42
star
35

react-rxjs

๐Ÿ”Œ "Plug and play" for Observables in React Apps!
TypeScript
37
star
36

subscribe

Subscription Handling Directive
TypeScript
34
star
37

elf-ng-router-store

Bindings to connect Angular router to Elf
TypeScript
24
star
38

ng-standalone-nx

TypeScript
24
star
39

lit-file-generator

๐ŸŽ A lit generator for a component, directive, and controller.
JavaScript
19
star
40

storage

TypeScript
18
star
41

material-schematics

TypeScript
3
star
42

svg-icon-demo

TypeScript
1
star