Boosted template-driven Angular forms.
It is an alternative for the Angular Forms of any kind: simpler, more flexible, more powerful, no restrictions.
If your project have complex and dynamic forms this package will save you a lot of time and lines of code.
- Focused on template-driven approach.
- Signal under the hood.
- Less abstractions, ultimate control.
- More freedom for developers.
- Nothing exceptionally new for Angular people.
- Less boilerplate to write:
- Simple custom value accessors creation.
- Simple custom validators creation.
- Single interface for sync and async validators.
- No
ControlContainerproviding for sub-forms. - No required
namebinding. - Handy way to display validation errors only on touched fields.
- Function validators binding.
- Built-in debounce.
- Two-way state binding in templates (e.g
[(touched)]). - Almost all states have reactive alternative (e.g
.errors+.errors$). - Submit directive which touches all fields and checks validity.
- Stricter types in controls.
- SSR support.
- Zero deps.
- Reduced bundle size without @angular/forms (~20KB parsed size in prod mode).
- Does not conflict with the Angular
FormsModule. - Optional integration with Angular
ValidatorandValueAccessorinterfaces. - Works with Angular Material.
- 3rd party lib.
- Not battle-tested enough yet.
- Sometimes too much freedom for developers.
- Angular template is the best DSL for describing forms.
- Single source of truth for your forms - templates.
- No structure and binding duplication.
- Almost all logic written in a declarative manner.
- Less code to write.
- https://www.youtube.com/watch?v=L7rGogdfe2Q
- Form - tool for displaying and manipulating data in Browser.
- Model - variable that represents a field of data.
- Input - HTML element (or custom component) allows you to display and change some state.
- Control - a bridge between Model and Input.
- Value accessor - directive or component that connects Input to the Control.
- Validator - function to check Model or Input values to meet some conditions.
- Error - returned by Validator if value is invalid.
- Validity - represents current validation state:
pending- one or more async Validators are running,invalid- one or more Validators returned errors,valid- all Validators returned no errors.
- Touched - Input had interaction with user (was focused for built-in Value accessors).
- Dirty - Input was changed by user.
$ npm i ngfe
ngfe@13for Angular@12 and Angular@13. RxJS@7 needed.ngfe@15no-signals version with[feControl]syntax for Angular@14+.
Import the module:
import { FeModule } from 'ngfe';
...
imports: [
FeModule,
...
]All directives are standalone and can be imported separately:
imports: [FeForm, FeModel, FeSubmit, FeInput, FeSelect, FeRequiredValidator, ...]A convenience constant containing all directives for quick standalone component setup:
import { feImports } from 'ngfe';
@Component({
standalone: true,
imports: [feImports],
...
})
export class MyComponent {}On the surface [(model)] works exactly like [(ngModel)].
<input [(model)]="field">FeForm is automatically applied to <form> elements (selector: form:not([noForm]),[feForm]).
It aggregates all child FeModel controls and provides form-level state.
Use the noForm attribute to opt out on a specific <form> element:
<form noForm>
<!-- No FeForm directive here -->
</form>Use the [feForm] attribute to create a form group on a non-form element:
<div feForm>
<input [(model)]="field">
</div><form #form="form" [(disabled)]="formDisabled">
<input [(model)]="name" name="name" required>
<input [(model)]="email" name="email" email>
@if (form.invalid()) {
<p>Form has errors</p>
}
<button (validSubmit)="save()">Submit</button>
</form>Model (FeModel)
FeModel is the core control directive.
Selector: [model]:not([noModel]),[modelChange]:not([noModel]).
Use the noModel attribute to opt out:
<input [model]="value" noModel>Controls what happens when input validation fails:
'accept'(default) - update model with the input value even if invalid.'retain'- keep the previous valid value, do not update model.{value: VALUE}- update model with the provided fallback value.
Controls when async validators run:
'runAfterSyncValid'(default) - async validators only run if all sync validators pass.'runAlways'- async validators always run regardless of sync validation results.
Input (FeInput)
Selector: input[model],textarea[model].
Bridges native <input> and <textarea> elements to FeModel.
<input [(model)]="field">
<input [(model)]="field2" type="checkbox">
<input [(model)]="field3" type="radio" value="1">
<input [(model)]="field4" type="date">
<textarea [(model)]="field5"></textarea>Force a specific value type regardless of the input element type:
<!-- Force number parsing for a text input -->
<input [(model)]="amount" valueType="number">
<!-- Force Date object from a date input -->
<input [(model)]="date" type="date" valueType="Date">
<!-- Keep string value for a number input -->
<input [(model)]="code" type="number" valueType="string">Control when the model value is updated:
<!-- Default: update on every keystroke -->
<input [(model)]="field" updateOn="change">
<!-- Update only when input loses focus -->
<input [(model)]="field" updateOn="blur"><input (modelChange)="loadFiles($event)" type="file">import { readFiles } from 'ngfe';
...
loadFiles(files?: FileList) {
readFiles(files || []).subscribe(loadedFiles => {
...
});
}Select (FeSelect)
Selector: select[model]. Bridges native <select> elements to FeModel.
<select [(model)]="field">
<option value="1">ONE</option>
<option value="2">TWO</option>
</select>Any type of value available to bind to option[value]:
field: number;<select [(model)]="field">
<option [value]="1">ONE</option>
<option [value]="2">TWO</option>
</select><select [(model)]="selectedItems" multiple>
@for (let item of items) {
<option [value]="item">{{ item.name }}</option>
}
</select>Useful when option values are objects:
<select [(model)]="selected" [compareFn]="compareById">
@for (let item of items) {
<option [value]="item">{{ item.name }}</option>
}
</select>compareById = (v1: any, v2: any) => v1?.id === v2?.id;Selector: option.
Automatically connects to the parent FeSelect directive.
Define debounce time for values from a value accessor:
<input [(model)]="field" [debounce]="400">Works very similar to the default Angular validation.
<input #model="model" [(model)]="field" required>
@if (model.errors(); as errors) {
@if (errors.required) {
<span>Required</span>
}
}.visibleErrors() returns the errors object only when the control is touched:
<input #model="model" [(model)]="field" required>
@if (model.visibleErrors(); as errors) {
@if (errors.required) {
<span>Required</span>
}
}| Validator | Selector | Key Inputs | Error Key |
|---|---|---|---|
FeRequiredValidator |
[model][required] |
required: boolean (default: true) |
{required: {value}} |
FeEmailValidator |
[model][email] |
email: boolean (default: true) |
{email: {value}} |
FeEqualValidator |
[model][equal] |
equal: any, activeWhenEmpty: boolean |
{equal: {equal, value}} |
FeNotEqualValidator |
[model][notEqual] |
notEqual: any, activeWhenEmpty: boolean |
{notEqual: {notEqual, value}} |
FeIsNumberValidator |
[model][isNumber] |
isNumber: boolean (default: true) |
{isNumber: {value}} |
FeLengthValidator |
[model][minLength],[model][maxLength] |
minLength, maxLength |
{minLength: {requiredLength, actualLength, value}}, {maxLength: ...} |
FeMinmaxValidator |
[model][min],[model][max] |
min, max |
{min: {min, value, numberValue}}, {max: ...} |
FePatternValidator |
[model][pattern] |
pattern: string | RegExp |
{pattern: {pattern, value}} |
All boolean-toggle validators (required, email, isNumber) can be disabled by binding false:
<input [(model)]="field" [required]="isRequired">
<input [(model)]="field" [email]="shouldValidateEmail">The equal and notEqual validators have an activeWhenEmpty input (default: false). When false, validation is skipped if the value is empty:
<input [(model)]="field" [equal]="expectedValue" activeWhenEmpty>Use FeValidator interface to implement a validator. Return errors object FeValidationErrors or undefined if value is valid.
// Invalid if value is not empty and have value "BOOM".
notBoom: FeValidator<string> = value => {
return value !== 'BOOM'
? undefined
: {notBoom: true};
};Pass it to [validators] input:
<input #model="model" [(model)]="field" [validators]="[notBoom]">
@if (model.errors()?.notBoom) {
<span>Value should not be "BOOM"</span>
}Or, create a validator directive:
@Directive({
selector: '[model][notBoom]',
standalone: true,
})
export class NotBoomValidatorDirective {
private model = inject(FeModel<string>);
private removeFn = this.model.addValidator(value => {
return value !== 'BOOM'
? undefined
: {notBoom: true};
});
}<input [(model)]="field" notBoom>Return from a validation function Observable or Promise with FeValidatorResult:
asyncValidator: FeValidator<string> = (value, control) => {
return new Observable<FeValidatorResult>(observer => {
// Async check...
observer.next(isValid ? undefined : {asyncError: true});
observer.complete();
});
};You can programmatically set errors on a control using forcedErrors:
<input #model="model" [(model)]="field" [forcedErrors]="serverErrors()">// Set from server response
serverErrors = signal<FeValidationErrors | undefined>(undefined);
onSubmit() {
this.api.save(this.field).subscribe({
error: (err) => {
this.serverErrors.set({serverError: err.message});
}
});
}Set forcedErrors to 'pending' to force pending validity state.
Two directives that mark all form controls as touched and check validity on submit.
Selector: button[anySubmit],button[validSubmit],button[invalidSubmit]
<form>
...
<button (anySubmit)="doStuff()">Submit</button>
<button (validSubmit)="doValidStuff()">Submit</button>
<button (invalidSubmit)="doInvalidStuff()">Submit</button>
</form>Selector: form[anySubmit],form[validSubmit],form[invalidSubmit]
<form (anySubmit)="doStuff()" (validSubmit)="doValidStuff()" (invalidSubmit)="doInvalidStuff()">
...
</form>| Output | Type | Description |
|---|---|---|
anySubmit |
boolean |
Emits validity (true/false) on click. |
validSubmit |
void |
Emits on click when form is valid. |
invalidSubmit |
void |
Emits on click when form is invalid. |
Both directives call form.touchAll() before emitting, so all validation errors become visible.
You do not need to implement ValueAccessor interface.
Just inject FeModel and use its properties and methods:
@Component({
selector: 'app-custom-control',
...
})
export class AppCustomControlComponent {
private model = inject(FeModel);
onUserAction(value: any) {
this.model.input(value);
}
onFocus() {
this.model.touch();
}
}<app-custom-control [(model)]="field" />You can use any signal or subscribe to any observable of the model and define any state.
Set of functions useful for working with forms.
Convert a string value to number if possible.
Returns undefined for empty string or non-numeric values.
import { ensureNumber } from 'ngfe';
ensureNumber('42'); // 42
ensureNumber('abc'); // undefined
ensureNumber(''); // undefined
ensureNumber(undefined); // undefinedRead file data from File[] or FileList (typically from file inputs).
import { readFiles } from 'ngfe';
readFiles(fileList, 'DataURL').subscribe((loadedFiles: FeLoadedFile[]) => {
loadedFiles.forEach(f => {
console.log(f.file.name, f.data);
});
});Enables an easy transition from Angular forms to ngfe.
Install package:
$ npm i ngfe-ng-adapter
Import module:
imports: [
...
FeModule,
FeNgAdapterModule,
]After that you can use Angular ValueAccessors and Validators with [(model)].
Also, with this package, FeModel provides NgControl and allows you to use ngfe with Material components or other UI libs.
MIT
- Fix tests
- Bind state classes for control/form
- Any validator should be possible to switch off
- Programmatic validation using Component:
validate(SomeFormComponent, {formState}): Observable<FeValidationResult> - Clean up logs