• Stars
    star
    609
  • Rank 73,614 (Top 2 %)
  • Language
    TypeScript
  • 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

(Angular Reactive) Forms with Benefits ๐Ÿ˜‰


Test MIT commitizen PRs styled with prettier ngneat

(Angular Reactive) Forms with Benefits ๐Ÿ˜‰

How many times have you told yourself "I wish Angular Reactive Forms would support types", or "I really want an API to query the form reactively. It missed some methods."

Your wish is my command!
This library extends every Angular AbstractControl, and provides features that don't exist in the original one. It adds types, reactive queries, and helper methods. The most important thing is that you can start using it today! In most cases, the only thing that you need to change is the import path. So don't worry, no form refactoring required - we've got you covered;

Let's take a look at all the neat things we provide:

๐Ÿ”ฎ Features

โœ… Offers (almost) seamless FormControl, FormGroup, FormArray Replacement
โœ… Allows Typed Forms!
โœ… Provides Reactive Queries
โœ… Provides Helpful Methods
โœ… Typed and DRY ControlValueAccessor
โœ… Typed FormBuilder
โœ… Persist the form's state to local storage

๐Ÿ‘‰ npm install @ngneat/reactive-forms

Table of Contents

Control Type

FormControl/FormArray takes a generic that defines the type of the control. This type is then used to enhance every method exposed by Angular or this library.

Use it with a FormControl:

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl<string>();

// Or auto infer it based on the initial value
const control = new FormControl('');

control.valueChanges.subscribe(value => {
  // value is typed as string
});

Use it with a FormArray:

import { FormArray, FormControl } from '@ngneat/reactive-forms';

const control = new FormArray<string>([new FormControl('')]);

control.value$.subscribe(value => {
  // value is typed as string[]
});

If you use a FormGroup, it'll automatically infer the type based on the controls you supply:

import { FormGroup, FormControl } from '@ngneat/reactive-forms';

const profileForm = new FormGroup({
  firstName: new FormControl(''),
  lastName: new FormControl(''),
  address: new FormGroup({
    street: new FormControl(''),
    city: new FormControl('')
  })
});


profileForm.setValue(new Profile());

profileForm.patchValue({ firstName: 'Netanel' });

You can use the experimental ControlsOf feature if you want to force a FormGroup to implement an external type:

import { FormGroup, FormControl, ControlsOf } from '@ngneat/reactive-forms';

interface Profile {
  firstName: string;
  lastName: string;
  address: {
    street: string;
    city: string;
  };
}

const profileForm = new FormGroup<ControlsOf<Profile>>({
  firstName: new FormControl(''),
  lastName: new FormControl(''),
  address: new FormGroup({
    street: new FormControl(''),
    city: new FormControl('')
  })
});

Gotchas

  • When using array types, it'll automatically infer it as FormArray. If you need a FormControl, you must set it within your interface explicitly:
interface User {
  name: string;
  // ๐Ÿ‘‡๐Ÿป
  skills: FormControl<string[]>;
}
  • Optional fields will only work with top-level values, and will not work with FormGroup:
interface User {
  name?: string;
  foo?: string[]; 
  // ๐Ÿ‘‡๐Ÿป will not work 
  nested?: {
    id: string;
  };
}

Control Queries

value$

Observes the control's value. Unlike the behavior of the built-in valueChanges observable, it emits the current rawValue immediately (which means you'll also get the values of disabled controls).

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.value$.subscribe(value => ...);

disabled$

Observes the control's disable status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.disabled$.subscribe(isDisabled => ...);

enabled$

Observes the control's enable status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.enabled$.subscribe(isEnabled => ...);

invalid$

Observes the control's invalid status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.invalid$.subscribe(isInvalid => ...);

valid$

Observes the control's valid status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.valid$.subscribe(isValid => ...);

status$

Observes the control's status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.status$.subscribe(status => ...);

The status is typed as ControlState (valid, invalid, pending or disabled).

touch$

Observes the control's touched status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.touch$.subscribe(isTouched => ...);

This emits a value only when markAsTouched, or markAsUnTouched, has been called.

dirty$

Observes the control's dirty status.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.dirty$.subscribe(isDirty => ...);

This emits a value only when markAsDirty, or markAsPristine, has been called.

errors$

Observes the control's errors.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.errors$.subscribe(errors => ...);

select()

Selects a slice of the form's state based on the given predicate.

import { FormGroup } from '@ngneat/reactive-forms';

const group = new FormGroup({
  name: new FormControl('')
});

group.select(state => state.name).subscribe(name => ...)

Control Methods

setValue()

In addition to the built-in method functionality, it can also take an observable.

import { FormGroup } from '@ngneat/reactive-forms';

const group = new FormGroup({
  name: new FormControl('')
});

group.setValue(store.select('formValue'));

patchValue()

In addition to the built-in method functionality, it can also take an observable.

import { FormGroup } from '@ngneat/reactive-forms';

const group = new FormGroup({
  name: new FormControl('')
});

group.patchValue(store.select('formValue'));

disabledWhile()

Takes an observable that emits a boolean indicating whether to disable the control.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.disabledWhile(store.select('isDisabled'));

enabledWhile()

Takes an observable that emits a boolean indicating whether to enable the control.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.enabledWhile(store.select('isEnabled'));

markAllAsDirty()

Marks all the group's controls as dirty.

import { FormGroup } from '@ngneat/reactive-forms';

const control = new FormGroup();
control.markAllAsDirty();

hasErrorAndTouched()

A syntactic sugar method to be used in the template:

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('', Validators.required);
<span *ngIf="control.hasErrorAndTouched('required')"></span>

hasErrorAndDirty()

A syntactic sugar method to be used in the template:

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('', Validators.required);
<span *ngIf="control.hasErrorAndDirty('required')"></span>

setEnable()

Sets whether the control is enabled.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.setEnable();
control.setEnable(false);

setDisable()

Sets whether the control is disabled.

import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.setDisable();
control.setDisable(false);

get()

A method with typed parameters which obtains a reference to a specific control.

import { FormGroup } from '@ngneat/reactive-forms';

const group = new FormGroup({
  name: new FormControl(''),
  address: new FormGroup({
    street: new FormControl(''),
    city: new FormControl('')
  })
});

const name = group.get('name'); // FormControl<string>
const city = group.get(['address', 'city']); // FormControl<string>

// Don't use it like this
group.get('address.city') // AbstractControl

mergeErrors()

Merge validation errors. Unlike setErrors(), this will not overwrite errors already held by the control.

import { FormGroup } from '@ngneat/reactive-forms';

const group = new FormGroup(...);
group.mergeErrors({ customError: true });

removeError()

Remove an error by key from the control.

import { FormGroup } from '@ngneat/reactive-forms';

const group = new FormGroup(...);
group.removeError('customError');

FormArray methods

remove()

Remove a control from an array based on its value

import { FormArray } from '@ngneat/reactive-forms';

const array = new FormArray<string>(...);
// Remove empty strings
array.remove('')

removeIf()

Remove a control from an array based on a predicate

import { FormArray } from '@ngneat/reactive-forms';

const array = new FormArray(...);
// Only keep addresses in NYC
array.removeIf((control) => control.get('address').get('city').value !== 'New York')

Control Operators

Each valueChanges or values$ takes an operator diff(), which emits only changed parts of form:

import { FormGroup, FormControl, diff } from '@ngneat/reactive-forms';

const control = new FormGroup({
  name: new FormControl(''),
  phone: new FormGroup({
    num: new FormControl(''),
    prefix: new FormControl('')
  }),
  skills: new FormArray<string>([])
});

control.value$
  .pipe(diff())
  .subscribe(value => {
    // value is emitted only if it has been changed, and only the changed parts.
  });

ControlValueAccessor

The library exposes a typed version of ControlValueAccessor, which already implements registerOnChange and registerOnTouched under the hood:

import { ControlValueAccessor } from '@ngneat/reactive-forms';

@Component({
  selector: 'my-checkbox',
  host: { '(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()' },
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: MyCheckboxComponent,
      multi: true
    }
  ]
})
export class MyCheckboxComponent extends ControlValueAccessor<boolean> {
  writeValue(value: boolean) {

  }

  // `this.onChange`, and `this.onTouched` are already here!
}

Note that you can also use it as interface.

Form Builder

We also introduce a typed version of FormBuilder which returns a typed FormGroup, FormControl and FormArray with all our sweet additions:

import { FormBuilder } from '@ngneat/reactive-forms';

constructor(
  private fb: FormBuilder
) {}

const group = this.fb.group({ name: 'ngneat', id: 1 });

group.get('name') // FormControl<string>

Due to the complexity of the builder API, we are currently couldn't create a "good" implementation of ControlsOf for the builder.

Persist Form

Automatically persist the AbstractControl's value to the given storage:

import { persistControl } from '@ngneat/reactive-forms';

const group = new FormGroup(...);
const unsubscribe = persistControl(group, 'profile').subscribe();

The persistControl function will also set the FromGroup value to the latest state available in the storage before subscribing to value changes.

PersistOptions

Change the target storage or debounceTime value by providing options as a second argument in the persist function call.

Option Description Default
debounceTime Update delay in ms between value changes 250
manager A manager implementing the PersistManager interface LocalStorageManager
arrControlFactory Factory functions for FormArray
persistDisabledControls Defines whether values of disabled controls should be persisted false

By default the library provides LocalStorageManager and SessionStorageManager. It's possible to store the form value into a custom storage. Just implement the PersistManager interface, and use it when calling the persistControl function.

export class StateStoreManager<T> implements PersistManager<T> {
  setValue(key: string, data: T) {
     ...
  }

  getValue(key: string) {
    ...
  }
}

export class FormComponent implements OnInit {
  group = new FormGroup();

  ngOnInit() {
    persist(this.group, 'profile', { manager: new StateStoreManager() }).subscribe();
  }
}
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.

const group = new FormGroup({
  skills: new FormArray<string>([])
});

persist(group, 'profile', {
  arrControlFactory: {
     skills: value => new FormControl(value)
  }
});

Because the form is strongly typed, you can only configure factories for properties that are of type Array. The library makes it also possible to correctly infer the type of value for the factory function.

ESLint Rule

We provide a special lint rule that forbids the imports of any token we expose, such as the following: AbstractControl, AsyncValidatorFn, ControlValueAccessor, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, from @angular/forms.

Check out the documentation.

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,068
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,733
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
677
star
9

tailwind

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

query

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

forms-manager

๐Ÿฆ„ The Foundation for Proper Form Management in Angular
TypeScript
517
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
118
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
63
star
34

avvvatars

Beautifully crafted unique avatar placeholder for your next angular project.
TypeScript
46
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