Angular Forms Manager

Installation

yarn add @datorama/akita-ng-forms-manager

The AkitaNgFormsManager 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.

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:

import { AkitaNgFormsManager } from '@datorama/akita-ng-forms-manager';
export interface FormsState {
onboarding: {
name: string;
age: number;
city: string;
};
}
@Component({
template: `
<form [formGroup]="onboardingForm">
<input formControlName="name">
<input formControlName="age">
<input formControlName="city">
</form>
`
})
export class OnboardingComponent {
constructor(
private formsManager: AkitaNgFormsManager<FormsState>,
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();
}
}

As you can see, we’re still working with the existing API in order to create a form in Angular. We’re injecting the AkitaNgFormsManager and calling the upsert method, giving it the form name and the formGroup (this will work also with a single formControl or formArray).

From that point on, AkitaNgFormsManager will track the form value changes and update the store accordingly.

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

API

formsManager.selectForm(formName).subscribe(form => {});
formsManager.getForm(formName);
formsManager.hasForm(formName);
formsManager.selectControl(path).subscribe(control => {});
/** If it's a single control omit the path */
formsManager.selectControl().subscribe(control => {});
formsManager.getControl(path);
formsManager.selectErrors(formName, path?).subscribe(errors => {});
formsManager.selectValue(formName, path?).subscribe(value => {});
formsManager.selectDisabled(formName, path?).subscribe(disabled => {});
formsManager.selectDirty(formName, path?).subscribe(dirty => {});
formsManager.selectValid(formName, path?).subscribe(valid => {});
formsManager.upsert(formName, abstractContorl, config: {
debounceTime: number;
emitEvent: boolean;
arrControlFactory: ArrayControlFactory | HashMap<ArrayControlFactory>
});
formsManager.remove(formName);
formsManager.unsubscribe();

Helpers

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
)

For Example:

import { untilDestroyed } from 'ngx-take-until-destroy';
export class HomeComponent{
ngOnInit() {
this.form = new FormGroup({
price: new FormControl(null, Validators.min(10))
});
/*
* Check the `minPrice` value in settings form
* and update the validators
*/
this.formsManager
.selectValue<number>('settings', 'minPrice')
.pipe(untilDestroyed(this))
.subscribe(minPrice => {
setValidators(this.form.get('price'),
Validators.min(minPrice)
);
});
}
}

FormArray

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

import { AkitaNgFormsManager } from '@datorama/akita-ng-forms-manager';
export class HomeComponent {
skills: FormArray;
config: FormGroup;
constructor(
private formsManager: AkitaNgFormsManager<FormsState>
) {}
ngOnInit() {
const createControl = value => new FormControl(value);
this.skills = new FormArray([createControl('JS')]);
/** Or inside form group */
this.config = new FormGroup({
skills: new FormArray([createControl('JS')])
})
this.formsManager.upsert('skills', this.skills, { arrControlFactory: createControl }
).upsert('config', this.config, {
arrControlFactory: { skills: createControl }
})
}
ngOnDestroy() {
this.formsManager.unsubscribe();
}
}

Persist the Form Instance

You can persist the form instance, and use it in other components:

this.formsManager.upsert('onboarding', this.onboardingForm, { persistForm: true });

Now you can use it:

constructor(
private formsManager: AkitaNgFormsManager<FormsState>
) {}
ngOnInit() {
this.formsManager.getNgForm('onboarding');
this.formsManager.selectNgForm('onboarding').subscribe();
}
ngOnDestroy() {
this.formsManager.remove();
}

Check out the source code and the playground.