Skip to content

Instantly share code, notes, and snippets.

@abbazabacto
Last active March 20, 2022 13:17
Show Gist options
  • Save abbazabacto/0f290e1ef72e682a9a1309c0cfa81bf6 to your computer and use it in GitHub Desktop.
Save abbazabacto/0f290e1ef72e682a9a1309c0cfa81bf6 to your computer and use it in GitHub Desktop.
Typed Form Builder
// extended upon: https://ruanbeukes.net/Angular-Typesafe-Reactive-Forms/
import { Injectable } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup, ValidatorFn, AsyncValidatorFn, FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
type PropertyFn<T, R> = (val: T) => R;
interface AbstractControlTyped<T> extends AbstractControl {
readonly value: T;
readonly valueChanges: Observable<T>;
setValue(value: T, options?: Object): T;
patchValue(value: T, options?: Object): T;
reset(value?: T, options?: Object): void;
get(path: Array<string | number> | string): AbstractControl | null;
get<R>(path: Array<string | number> | string): AbstractControlTyped<R> | null;
getSafe<R>(propertyFn: PropertyFn<T, R>): AbstractControlTyped<R> | null;
}
interface FormGroupTyped<T> extends FormGroup {
readonly value: T;
readonly valueChanges: Observable<T>;
setValue(value: T, options?: Object): T;
patchValue(value: T, options?: Object): T;
reset(value?: T, options?: Object): void;
get(path: Array<string | number> | string): AbstractControl | null;
get<T>(path: Array<string | number> | string): AbstractControlTyped<T> | null;
getSafe<R>(propertyFn: PropertyFn<T, R>): AbstractControlTyped<R> | null;
registerControl(name: string, control: AbstractControl): AbstractControl;
registerControl<T>(name: string, control: AbstractControl): AbstractControlTyped<T>;
registerControlSafe<R>(propertyFn: PropertyFn<T, R>, control: AbstractControl): AbstractControlTyped<R>;
addControlSafe<R>(propertyFn: PropertyFn<T, R>, control: AbstractControl): void;
removeControlSafe<R>(propertyFn: PropertyFn<T, R>): void;
setControlSafe<R>(propertyFn: PropertyFn<T, R>, control: AbstractControl): void;
}
export type ControlsConfigTyped<T> = {
[P in keyof T]: FormControl | FormGroup | FormArray | undefined[] | {
0?: T[P];
1?: ValidatorFn | ValidatorFn[];
2?: AsyncValidatorFn | AsyncValidatorFn[];
};
};
@Injectable()
export class FormBuilderTyped extends FormBuilder {
private static getPropertyName(propertyFunction: Function): string {
const properties = propertyFunction.toString()
.match(/(?![. ])([a-z0-9_]+)(?=[};.])/gi)
.splice(1);
return properties.join('.');
}
private static getSafe<T, R>(group: AbstractControl, propertyFn: PropertyFn<T, R>): AbstractControlTyped<R> {
const getStr = FormBuilderTyped.getPropertyName(propertyFn);
return this.mapControl(group.get(getStr) as AbstractControlTyped<R>);
}
private static registerControlSafe<T, R>(group: FormGroup, propertyFn: PropertyFn<T, R>, control: AbstractControl): AbstractControlTyped<R> {
const getStr = FormBuilderTyped.getPropertyName(propertyFn);
return this.mapControl(group.registerControl(getStr, control) as AbstractControlTyped<R>);
}
private static addControlSafe<T, R>(group: FormGroup, propertyFn: PropertyFn<T, R>, control: AbstractControl) {
const getStr = FormBuilderTyped.getPropertyName(propertyFn);
group.addControl(getStr, control);
}
private static removeControlSafe<T, R>(group: FormGroup, propertyFn: PropertyFn<T, R>) {
const getStr = FormBuilderTyped.getPropertyName(propertyFn);
group.removeControl(getStr);
}
private static setControlSafe<T, R>(group: FormGroup, propertyFn: PropertyFn<T, R>, control: AbstractControl) {
const getStr = FormBuilderTyped.getPropertyName(propertyFn);
group.setControl(getStr, control);
}
private static mapFormGroup<T>(group: FormGroupTyped<T>): FormGroupTyped<T> {
group.registerControlSafe = function<T, R>(propertyFn: PropertyFn<T, R>, control: AbstractControl) {
return FormBuilderTyped.registerControlSafe(group, propertyFn, control);
};
group.addControlSafe = function<T, R>(propertyFn: PropertyFn<T, R>, control: AbstractControl) {
FormBuilderTyped.addControlSafe(group, propertyFn, control);
};
group.removeControlSafe = function<T, R>(propertyFn: PropertyFn<T, R>) {
FormBuilderTyped.removeControlSafe(group, propertyFn);
};
group.setControlSafe = function<T, R>(propertyFn: PropertyFn<T, R>, control: AbstractControl) {
return FormBuilderTyped.setControlSafe(group, propertyFn, control);
};
return group;
}
private static mapControl<T>(control: FormGroupTyped<T>): FormGroupTyped<T>;
private static mapControl<T>(control: AbstractControlTyped<T>): AbstractControlTyped<T>;
private static mapControl<T>(control: AbstractControlTyped<T> | FormGroupTyped<T>): AbstractControlTyped<T> {
control.getSafe = function<T, R>(propertyFn: PropertyFn<T, R>) {
return FormBuilderTyped.getSafe(control, propertyFn);
};
if (control instanceof FormGroup) {
this.mapFormGroup(control);
}
return control;
}
group(controlsConfig: ControlsConfigTyped<any>, extra?: { [key: string]: any; } | null): FormGroup;
group<T>(controlsConfig: ControlsConfigTyped<T>, extra?: { [key: string]: any; } | null): FormGroupTyped<T>;
group<T>(controlsConfig: ControlsConfigTyped<T>, extra?: { [key: string]: any; } | null): FormGroupTyped<T> {
let group = super.group(controlsConfig, extra) as FormGroupTyped<T>;
if (group) {
return FormBuilderTyped.mapControl(group);
}
}
}
@florestankorp
Copy link

Could you add an example of how to use this?

@abbazabacto
Copy link
Author

@florestankorp my recommendation would be to wait for official support for strongly typed forms, it's finally coming: angular/angular#44513

If you do feel like trying it out, it's a strongly typed version of Formbuilder.group which creates a FormGroup with strongly typed variants of FormControl and AbstractControl. You have to add add the types yourself, they aren't inferred.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment