ngfe
v20.0.0
Published
[](https://www.npmjs.com/package/ngfe) 
Readme
ngfe | Angular Forms Engine | Template-based Signal Forms
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.
Features
- 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.
Caveats
- 3rd party lib.
- Not battle-tested enough yet.
- Sometimes too much freedom for developers.
Why template forms
- 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
Terms
- 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.
Installation
$ npm i ngfengfe@13for Angular@12 and Angular@13. RxJS@7 needed.ngfe@15no-signals version with[feControl]syntax for Angular@14+.
Usage
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, ...]feImports
A convenience constant containing all directives for quick standalone component setup:
import { feImports } from 'ngfe';
@Component({
standalone: true,
imports: [feImports],
...
})
export class MyComponent {}Binding
On the surface [(model)] works exactly like [(ngModel)].
<input [(model)]="field">Form
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>Example
<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>invalidValueStrategy
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.
asyncValidatorsStrategy
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.
Built-in value accessors
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>valueType
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">updateOn
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">File inputs
<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>Multiple select
<select [(model)]="selectedItems" multiple>
@for (let item of items) {
<option [value]="item">{{ item.name }}</option>
}
</select>Custom compare function
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;FeSelectOption
Selector: option.
Automatically connects to the parent FeSelect directive.
Debounce
Define debounce time for values from a value accessor:
<input [(model)]="field" [debounce]="400">Validation
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>
}
}Visible Errors
.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>
}
}Built-in validators
| 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>Custom validator
As a function
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>
}As a directive
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>Async validators
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();
});
};Forced errors
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.
Submit
Two directives that mark all form controls as touched and check validity on submit.
FeSubmit (on button or form)
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.
Custom Value Accessor
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.
Util
Set of functions useful for working with forms.
ensureNumber
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); // undefinedreadFiles
Read 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);
});
});@angular/forms adapter
Enables an easy transition from Angular forms to ngfe.
Install package:
$ npm i ngfe-ng-adapterImport 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.
LICENSE
MIT
TODO
- 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
