Typed Reactive Form. The missing piece of Angular.
MIT License
The missing piece of Angular.
npm install @gaplo917/angular-typed-forms
# OR
yarn add @gaplo917/angular-typed-forms
This library uses a ControlType
instead of ValueType
as the FormGroup
and FormArray
type constraints and
uses infer
(relatively new Typescript 2.8 feature) to extract the ValueType
from the ControlType
.
Thus, this implementation cannot be created earlier than Angular v6.1.
Features | Status |
---|---|
Strict Type Check | ✅ |
No Performance Degrade | ✅ |
Zero Force Type Cast Guarantee on .ts/.html | ✅ |
Advance implementation to handle Complex Form Architecture(fullSync & partialSync API) | ✅ |
100% Compatible to Reactive Forms | ✅ |
Enjoy slowly/partial migrate without learning a new library | ✅ |
import { TypedFormGroup, TypedFormControl, TypedFormBuilder, SimpleFormBuilder } from '@gaplo917/angular-typed-forms'
interface Foo {
first: TypedFormControl<string | null>
last: TypedFormControl<string | null>
}
// original Reactive Form modules
@Component({
selector: 'app-demo',
templateUrl: './demo.component.html',
styleUrls: ['./demo.component.css'],
})
export class DemoComponent implements OnInit {
form: TypedFormGroup<Foo>
constructor(private fb: TypedFormBuilder) {
this.form = fb.group({
first: fb.control(null),
last: fb.control(null),
})
}
}
// Simple Reactive Form
@Component({
selector: 'app-simple',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.css'],
})
export class SimpleComponent implements OnInit {
form: SimpleForm<Foo>
constructor(private fb: SimpleFormBuilder) {
this.form = fb.form({
first: fb.control(null),
last: fb.control(null),
})
}
}
new TypedFormControl<string | null>(null)
// or using from TypedFormBuilder
fb.control<string | null>(null)
This will convert the string input to number before calling setValue
. Any non-number return will return null
.
Enjoy getting a number type from the UI input.
new TypedNumberFormControl<number | null>(null)
// or using from TypedFormBuilder
fb.number<number | null>(null)
import { TypedFormGroup, TypedFormControl, TypedFormBuilder } from '@gaplo917/angular-typed-forms'
interface Foo {
first: TypedFormControl<string | null>
last: TypedFormControl<string | null>
}
const fb = new TypedFormBuilder()
const form: TypedFormGroup<Foo> = fb.group({
first: fb.control(null),
last: fb.control(null),
})
console.log(form.value) // {first: null, last: null}
form.setValue({ first: 'Nancy', last: 'Drew' })
console.log(form.value) // {first: 'Nancy', last: 'Drew'}
import { TypedFormArray, TypedFormControl, TypedFormBuilder } from '@gaplo917/angular-typed-forms'
const fb = new TypedFormBuilder()
const arr: TypedFormArray<TypedFormControl<string | null>> = fb.array({
constructArrayItem: () => fb.control<string | null>(null),
size: 2,
})
console.log(arr.value) // [null, null]
arr.setValue(['Nancy', 'Drew'])
console.log(arr.value) // ['Nancy', 'Drew']
import { TypedFormArray, TypedFormGroup, TypedFormControl, TypedFormBuilder } from '@gaplo917/angular-typed-forms'
interface Foo {
first: TypedFormControl<string | null>
last: TypedFormControl<string | null>
}
const fb = new TypedFormBuilder()
const arr: TypedFormArray<TypedFormGroup<Foo>> = fb.array([{
fb.group<Foo>({
first: fb.control(null),
last: fb.control(null),
}),
fb.group<Foo>({
first: fb.control(null),
last: fb.control(null),
})
}])
console.log(arr.value) // [{ first: null, last: null }, { first: null, last: null }]
arr.setValue([
{ first: 'Nancy', last: 'A' },
{ first: 'Drew', last: 'B' },
])
console.log(arr.value) // [{ first: 'Nancy', last: 'A' }, { first: 'Drew, last: 'B' }]
This is an EXTRA implementation based on Reactive Form Modules for a common scenario.
fullSunc
& partialSync
API that helps to sync remote API data to the formHighly recommend creating a dedicated class
to represent a complex form.
import { SimpleTable, SimpleFormBuilder, TypedFormControl, TypedNumberFormControl } from '@gaplo917/angular-typed-forms'
interface UserTableType {
id: TypedFormControl<string | null>
username: TypedFormControl<string | null>
birth: TypedFormControl<Date | null>
isStudent: TypedFormControl<boolean>
age?: TypedNumberFormControl<number | null>
// nested form
addresses: SimpleTable<{
address1: TypedFormControl<string | null>
address2: TypedFormControl<string | null>
address3: TypedFormControl<string | null>
}>
}
/**
* SimpleTable is equivalent to TypedFormArray<TypedFormGroup<UserTableType>> but with more pre-defined API
*/
export class UserTable extends SimpleTable<UserTableType> {
constructor(private fb: SimpleFormBuilder) {
super({
constructRow: (index: number) =>
fb.form({
id: fb.control(String('ID-' + index)),
username: fb.control(null),
birth: fb.control(null),
isStudent: fb.control<boolean>(false),
addresses: fb.table({
constructRow: () =>
fb.form({
address1: fb.control(null),
address2: fb.control(null),
address3: fb.control(null),
}),
size: 1,
}),
}),
size: 2,
})
}
}
fullSunc
& partialSync
APIFully-typed and synchronize the children form control with the value recursively.
Before setting the value of the FormArray
, it tries to add/remove necessary Control
according to the value.
fullSync
use setValue
internally
partialSync
use patchValue
internally
import { TypedFormGroup, TypedFormControl, SimpleFormBuilder } from '@gaplo917/angular-typed-forms'
interface Bar {
something: TypedFormControl<string | null>
}
interface Foo {
first: TypedFormControl<string | null>
last: TypedFormControl<string | null>
bar: TypedFormGroup<Bar>
}
const fb = new SimpleFormBuilder()
const form = fb.formArray<Foo>([])
console.log(form.value) // []
// full strict type check
form.fullSync([{ first: 'Nancy', last: 'Drew', bar: { something: 'happen' } }]) // OK
form.fullSync([{ first: 'Nancy', last: 'Drew', bar: {} }]) // Not compile, missing `something`
form.fullSync([{ first: 'Nancy', last: 'Drew', bar: { something: 'happen' }, unknownKey: 'not suppose here' }]) // Not compile, redundant `unknownKey`
console.log(form.value) // {first: 'Nancy', last: 'Drew', bar: { something: 'happen' }}
// partial type check
form.partialSync([{ first: 'Nancy2', last: 'Drew2' }]) // OK
form.partialSync([{ first: 'Nancy', last: 'Drew', unknownKey: 'not suppose here' }]) // Not compile, redundant `unknownKey`
console.log(form.value) // {first: 'Nancy2', last: 'Drew2', bar: { something: 'happen' }}, `bar` remain unchanged
import { SimpleTable, SimpleFormBuilder, TypedFormControl, TypedNumberFormControl } from '@gaplo917/angular-typed-forms'
interface UserTableType {
id: TypedFormControl<string | null>
username: TypedFormControl<string | null>
birth: TypedFormControl<Date | null>
isStudent: TypedFormControl<boolean>
age?: TypedNumberFormControl<number | null>
// nested form
addresses: SimpleTable<{
address1: TypedFormControl<string | null>
address2: TypedFormControl<string | null>
address3: TypedFormControl<string | null>
}>
}
/**
* SimpleTable is equivalent to TypedFormArray<TypedFormGroup<UserTableType>> but with more pre-defined API
*/
export class UserTable extends SimpleTable<UserTableType> {
constructor(private fb: SimpleFormBuilder) {
super({
constructRow: (index: number) =>
fb.form({
id: fb.control(String('ID-' + index)),
username: fb.control(null),
birth: fb.control(null),
isStudent: fb.control<boolean>(false),
addresses: fb.table({
constructRow: () =>
fb.form({
address1: fb.control(null),
address2: fb.control(null),
address3: fb.control(null),
}),
size: 1,
}),
}),
size: 2,
})
}
}
@Component({
selector: 'app-demo',
templateUrl: './demo.component.html',
styleUrls: ['./demo.component.css'],
})
export class DemoComponent {
userTable: UserTable
constructor(private fb: SimpleFormBuilder) {
this.userTable = new UserTable(fb)
}
fullSync() {
// fullSync requires strict type match of the value you input
// this method will use the values to sync the controls
// internally match the numbers of control before use `FormArray.setValue`
this.userTable.fullSync([
{
id: '42e6f1fe-93bd-4993-ab1b-ebaec9ee8d10',
username: 'Gary',
birth: new Date('2000-01-01'),
isStudent: false,
// because `age` is optional in the definition
// age: 1,
addresses: [
{
address1: 'HK',
address2: 'Earth',
address3: '',
},
],
},
])
}
partialSync() {
// partialSync only require Partial<T>, but it will also sync them number of controls
// this method will use the values to sync the controls
// internally match the numbers of control before use `FormArray.patchValue`
this.userTable.partialSync([
{
id: '42e6f1fe-93bd-4993-ab1b-ebaec9ee8d10',
username: 'Gary',
},
])
}
reset() {
this.userTable.reset()
}
}
You can use npm link
to develop this library locally without pushing every change npm registry.
dist/angular-typed-forms
and run npm link
.npm link @gaplo917/angular-typed-forms
ng build --watch
in the root of this library (optional)