{"id":46109574,"url":"https://github.com/irvrodflo/ngx-entity-form","last_synced_at":"2026-03-13T17:00:46.571Z","repository":{"id":340007762,"uuid":"1163757301","full_name":"irvrodflo/ngx-entity-form","owner":"irvrodflo","description":"Typed reactive forms for Angular — strongly-typed form builder with automatic validation, error messages, and file handling. Zero boilerplate.","archived":false,"fork":false,"pushed_at":"2026-02-22T19:22:52.000Z","size":46,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-22T23:45:03.976Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/irvrodflo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-22T05:03:36.000Z","updated_at":"2026-02-22T19:22:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/irvrodflo/ngx-entity-form","commit_stats":null,"previous_names":["irvrodflo/ngx-entity-form"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/irvrodflo/ngx-entity-form","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-entity-form","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-entity-form/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-entity-form/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-entity-form/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/irvrodflo","download_url":"https://codeload.github.com/irvrodflo/ngx-entity-form/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irvrodflo%2Fngx-entity-form/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30471114,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T11:00:43.441Z","status":"ssl_error","status_checked_at":"2026-03-13T11:00:23.173Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-01T22:00:55.624Z","updated_at":"2026-03-13T17:00:46.558Z","avatar_url":"https://github.com/irvrodflo.png","language":"TypeScript","funding_links":[],"categories":["Third Party Components"],"sub_categories":["Forms"],"readme":"# ngx-entity-forms\n\nStrongly-typed reactive forms for Angular. Define your entity interface — the library maps it to a fully-typed `FormGroup` with autocompletion, validation, and error messages out of the box.\n\n---\n\n## Requirements\n\n- Angular 17+\n\n---\n\n## Installation\n\n```bash\nnpm install @irv-labs/ngx-entity-forms\n```\n\n---\n\n## Quick Start\n\n```typescript\nimport { entity, entityForm } from '@irv-labs/ngx-entity-forms';\n\nexport interface ProductForm {\n  name: string;\n  description: string | null;\n  price: number;\n  active: boolean;\n  thumbnail: File | null;\n}\n\nprotected form = entityForm\u003cProductForm\u003e({\n  name:        entity.required(''),\n  description: entity.optional\u003cstring\u003e(null),\n  price:       entity.required(0),\n  active:      entity.required(false),\n  thumbnail:   entity.file(),\n});\n\n// TypeScript knows the exact type of every control\n// form.controls.name        → FormControl\u003cstring\u003e\n// form.controls.description → FormControl\u003cstring | null\u003e\n// form.controls.thumbnail   → FormControl\u003cFile | null\u003e\n```\n\n---\n\n## API\n\n### `entity.required(initialValue, config?)`\n\nCreates a non-nullable `FormControl` with `Validators.required` applied automatically.\n\nThe initial value can be any valid value for the field type — including `null` when you want the field to start empty. The type of the control is always inferred from the generic parameter, not the initial value.\n\n```typescript\nentity.required(''); // starts empty\nentity.required(0); // 0 is a valid initial value\nentity.required(false); // false is a valid initial value\nentity.required\u003cstring\u003e(null); // starts null, control type is FormControl\u003cstring\u003e\nentity.required\u003cnumber\u003e(null); // starts null, control type is FormControl\u003cnumber\u003e\n```\n\n\u003e When passing `null`, always annotate the generic explicitly — TypeScript cannot infer the type from `null` alone.\n\nThe second argument is flexible — pick whatever fits:\n\n```typescript\nentity.required('', Validators.minLength(3))                         // single validator\nentity.required('', [Validators.minLength(3), myValidator])          // array\nentity.required('', { validators: [...], disabled: true, updateOn: 'blur' }) // full options\n```\n\n---\n\n### `entity.optional\u003cT\u003e(initialValue, config?)`\n\nCreates a nullable `FormControl\u003cT | null\u003e`.\n\n```typescript\nentity.optional\u003cstring\u003e(null)\nentity.optional\u003cnumber\u003e(null, Validators.max(100))\nentity.optional\u003cstring\u003e(null, [Validators.maxLength(500), myValidator])\nentity.optional\u003cstring\u003e(null, { validators: [...], disabled: true })\n```\n\n---\n\n### `entity.file(config?)`\n\nCreates a `FormControl\u003cFile | null\u003e`. The value is the native `File` object — no wrappers, no library types leaking into your entity.\n\n```typescript\nentity.file();\nentity.file({ validators: [mimeTypeValidator, maxFileSizeValidator] });\n```\n\n---\n\n### `entityForm\u003cT\u003e(controls, options?)`\n\nCreates a fully-typed `FormGroup` from your entity. Supports single or multiple cross-field validators at the form level.\n\n```typescript\n// Basic\nconst form = entityForm\u003cProductForm\u003e({ ... });\n\n// With cross-field validator\nconst form = entityForm\u003cProductForm\u003e(\n  { ... },\n  { validators: passwordMatchValidator },\n);\n\n// Multiple cross-field validators\nconst form = entityForm\u003cProductForm\u003e(\n  { ... },\n  { validators: [passwordMatchValidator, priceRangeValidator] },\n);\n```\n\n---\n\n## File Handling\n\nDeclare the field as `File | null` in your entity — no library types needed.\n\n```typescript\nexport interface ProductForm {\n  thumbnail: File | null;\n}\n```\n\nUse `patchFileControl` and `clearFileControl` to connect the native input:\n\n```typescript\nimport { patchFileControl, clearFileControl } from '@irv-labs/ngx-entity-forms';\n\n@Component({\n  template: `\n    \u003cinput #fileInput type=\"file\" (change)=\"onFileChange($event)\" /\u003e\n    \u003cbutton type=\"button\" (click)=\"removeFile()\"\u003eRemove\u003c/button\u003e\n    @if (form.controls.thumbnail.value; as file) {\n      \u003cspan\u003e{{ file.name }} — {{ (file.size / 1024).toFixed(1) }}KB\u003c/span\u003e\n    }\n  `,\n})\nexport class MyComponent {\n  @ViewChild('fileInput') fileInput!: ElementRef\u003cHTMLInputElement\u003e;\n\n  protected form = entityForm\u003cProductForm\u003e({\n    thumbnail: entity.file({ validators: [mimeTypeValidator] }),\n  });\n\n  onFileChange(event: Event): void {\n    patchFileControl(event, this.form.controls.thumbnail);\n  }\n\n  removeFile(): void {\n    // Resets the native input so the browser forgets the previous selection\n    clearFileControl(this.form.controls.thumbnail, this.fileInput.nativeElement);\n  }\n\n  onSubmit(): void {\n    const { thumbnail } = this.form.getRawValue();\n    const formData = new FormData();\n    if (thumbnail) formData.append('thumbnail', thumbnail); // native File, ready to upload\n  }\n}\n```\n\n---\n\n## Global Validators\n\nRegister validators once in `app.config.ts`. They are applied automatically to every control — no need to repeat them per field.\n\n```typescript\n// app.config.ts\nimport { provideDefaultValidators } from '@irv-labs/ngx-entity-forms';\n\nprovideDefaultValidators({\n  all: [Validators.maxLength(255)], // every control\n  required: [trimValidator], // required controls only\n  optional: [], // optional controls only\n});\n```\n\nPer-control validators are always **additive** — they stack on top of the global ones.\n\n---\n\n## Error Messages\n\nBuilt-in Angular validators (`required`, `minlength`, `maxlength`, `min`, `max`, `email`, `pattern`) are resolved automatically. **English is the default locale.**\n\n### `provideErrorMessages` — custom messages\n\nMerges your custom messages on top of the English built-ins. Only define what you need.\n\n```typescript\n// app.config.ts\nimport { provideErrorMessages } from '@irv-labs/ngx-entity-forms';\n\nprovideErrorMessages({\n  // Custom validator messages\n  whitespace: 'Cannot contain only whitespace',\n  passwordMismatch: 'Passwords do not match',\n  slugTaken: (err) =\u003e `The slug \"${err.value}\" is already taken`,\n  mimeType: (err) =\u003e `Invalid format. Allowed: ${err.allowed.join(', ')}`,\n  maxFileSize: (err) =\u003e `File too large. Max size: ${err.maxMb}MB`,\n\n  // Override a built-in if needed\n  required: 'This field cannot be empty',\n});\n```\n\nMessage values can be a plain `string` or a function that receives the Angular error object:\n\n```typescript\n// Plain string\nwhitespace: 'Cannot contain only whitespace';\n\n// Function with error data\nminPrice: (err) =\u003e `Min price is ${err.min}`;\n```\n\n### `provideErrorMessagesLocale` — switch locale\n\nSwitches all built-in messages to a supported locale (`'en'` | `'es'`). Optionally extend with your custom messages on top.\n\n```typescript\nimport { provideErrorMessagesLocale } from '@irv-labs/ngx-entity-forms';\n\n// Spanish built-ins only\nprovideErrorMessagesLocale('es');\n\n// Spanish + custom messages\nprovideErrorMessagesLocale('es', {\n  whitespace: 'No puede contener solo espacios',\n  passwordMismatch: 'Las contraseñas no coinciden',\n  slugTaken: (err) =\u003e `El slug \"${err.value}\" ya está en uso`,\n\n  // Override a Spanish built-in\n  required: 'Campo requerido',\n});\n```\n\n\u003e Use either `provideErrorMessages` or `provideErrorMessagesLocale` — not both. If you need a locale other than English with custom messages, always use `provideErrorMessagesLocale`.\n\n### `fieldErrors` pipe\n\nReturns `FieldError[]` — only when the control is `touched` or `dirty`. Each item has a stable `key` and a resolved `message`.\n\n```typescript\nimports: [FieldErrorsPipe];\n```\n\n```html\n\u003c!-- Global messages --\u003e\n@for (error of form.controls.name | fieldErrors; track error.key) {\n\u003csmall class=\"error\"\u003e{{ error.message }}\u003c/small\u003e\n}\n\n\u003c!-- Local override — takes priority over global messages for this field only --\u003e\n@for ( error of form.controls.name | fieldErrors: { required: 'Product name is required' }; track\nerror.key ) {\n\u003csmall class=\"error\"\u003e{{ error.message }}\u003c/small\u003e\n}\n\n\u003c!-- Cross-field errors on the FormGroup --\u003e\n@for (error of form | fieldErrors; track error.key) {\n\u003cp class=\"error\"\u003e{{ error.message }}\u003c/p\u003e\n}\n```\n\nAlways use `track error.key` — the error key is stable and avoids Angular's `NG0956` warning.\n\n---\n\n## Async Validators\n\nPass them through the options object along with `updateOn: 'blur'` to avoid hammering the server on every keystroke.\n\n```typescript\nentity.required('', {\n  validators: [Validators.minLength(3), Validators.pattern(/^[a-z0-9-]+$/)],\n  asyncValidators: [slugAvailableValidator],\n  updateOn: 'blur',\n});\n```\n\nAlways check `form.pending` before submitting — async validators may still be running:\n\n```typescript\nonSubmit(): void {\n  this.form.markAllAsTouched();\n  if (this.form.pending) return;  // async validators still running\n  if (this.form.invalid) return;\n\n  const value = this.form.getRawValue();\n}\n```\n\n---\n\n## Dynamic Fields\n\nStart a field as disabled and toggle it based on business logic:\n\n```typescript\nprotected form = entityForm\u003cMyEntity\u003e({\n  featured:     entity.optional\u003cboolean\u003e(null),\n  discountCode: entity.optional\u003cstring\u003e(null, { disabled: true }), // starts disabled\n});\n\nonFeaturedChange(): void {\n  if (this.form.controls.featured.value) {\n    this.form.controls.discountCode.enable();\n  } else {\n    this.form.controls.discountCode.disable();\n    this.form.controls.discountCode.setValue(null);\n  }\n}\n```\n\n`getRawValue()` includes disabled fields. `value` does not.\n\n---\n\n## Considerations\n\n### Optional fields require `string | null` — not `name?`\n\nAngular's `FormControl` does not support `undefined`. Use explicit null unions instead of optional properties.\n\n```typescript\n// will not work\nexport interface ProductForm {\n  description?: string;\n}\n\n// correct\nexport interface ProductForm {\n  description: string | null;\n}\n```\n\nIf your domain entity uses `?`, create a dedicated form interface:\n\n```typescript\n// Domain entity — keep as is\nexport interface Product {\n  name: string;\n  description?: string;\n}\n\n// Form interface — explicit nulls\nexport interface ProductForm {\n  name: string;\n  description: string | null;\n}\n\nprotected form = entityForm\u003cProductForm\u003e({ ... });\n```\n\n### `0`, `false`, and `null` are valid initial values\n\n```typescript\nentity.required(0); // stock starting at zero\nentity.required(false); // checkbox starting unchecked\nentity.required\u003cstring\u003e(null); // field starting empty — annotate the generic explicitly\n```\n\n### Always use `getRawValue()` on submit\n\n`form.value` omits disabled fields. `getRawValue()` includes them.\n\n```typescript\nconst value = this.form.getRawValue(); // includes disabled fields\n```\n\n### The `fieldErrors` pipe is `pure: false`\n\nIt re-evaluates on every change detection cycle to react to control state changes (`touched`, `dirty`, `errors`). This is intentional — a pure pipe would miss mutations on the same `FormControl` reference. For large forms, pair it with `OnPush` change detection on your component.\n\n---\n\n## Full Example\n\n```typescript\nimport { Component, ElementRef, ViewChild } from '@angular/core';\nimport { ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';\nimport {\n  entity,\n  entityForm,\n  patchFileControl,\n  clearFileControl,\n  FieldErrorsPipe,\n} from '@irv-labs/ngx-entity-forms';\n\nexport interface ProductForm {\n  name: string;\n  description: string | null;\n  price: number;\n  active: boolean;\n  thumbnail: File | null;\n}\n\nfunction noNegativePrice(control: AbstractControl): ValidationErrors | null {\n  return (control.value as number) \u003c 0 ? { negativePrice: true } : null;\n}\n\n@Component({\n  standalone: true,\n  imports: [ReactiveFormsModule, FieldErrorsPipe],\n  template: `\n    \u003cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\"\u003e\n      \u003cinput formControlName=\"name\" placeholder=\"Product name\" /\u003e\n      @for (e of form.controls.name | fieldErrors; track e.key) {\n        \u003csmall\u003e{{ e.message }}\u003c/small\u003e\n      }\n\n      \u003cinput type=\"number\" formControlName=\"price\" /\u003e\n      @for (e of form.controls.price | fieldErrors; track e.key) {\n        \u003csmall\u003e{{ e.message }}\u003c/small\u003e\n      }\n\n      \u003cinput #fileInput type=\"file\" (change)=\"onFileChange($event)\" /\u003e\n      @if (form.controls.thumbnail.value; as file) {\n        \u003cspan\u003e{{ file.name }}\u003c/span\u003e\n      }\n\n      \u003cbutton type=\"submit\"\u003eSave\u003c/button\u003e\n    \u003c/form\u003e\n  `,\n})\nexport class ProductFormComponent {\n  @ViewChild('fileInput') fileInput!: ElementRef\u003cHTMLInputElement\u003e;\n\n  protected form = entityForm\u003cProductForm\u003e({\n    name: entity.required('', Validators.minLength(3)),\n    description: entity.optional\u003cstring\u003e(null),\n    price: entity.required(0, [Validators.min(0), noNegativePrice]),\n    active: entity.required(false),\n    thumbnail: entity.file(),\n  });\n\n  onFileChange(event: Event): void {\n    patchFileControl(event, this.form.controls.thumbnail);\n  }\n\n  onSubmit(): void {\n    this.form.markAllAsTouched();\n    if (this.form.pending || this.form.invalid) return;\n\n    const value = this.form.getRawValue();\n    // value.name      → string\n    // value.price     → number\n    // value.thumbnail → File | null\n  }\n}\n```\n\n---\n\n## Public API\n\n| Export                       | Description                                                                          |\n| ---------------------------- | ------------------------------------------------------------------------------------ |\n| `entity`                     | Builder object — `entity.required`, `entity.optional`, `entity.file`                 |\n| `entityForm`                 | Creates a typed `FormGroup\u003cEntityFields\u003cT\u003e\u003e`                                         |\n| `patchFileControl`           | Updates a file control from a native input `change` event                            |\n| `clearFileControl`           | Clears a file control and resets the native input element                            |\n| `FieldErrorsPipe`            | Pipe that resolves control errors to `FieldError[]`                                  |\n| `provideDefaultValidators`   | Registers global validators in `app.config`                                          |\n| `provideErrorMessages`       | Registers custom error messages in `app.config` (English base)                       |\n| `provideErrorMessagesLocale` | Switches built-in messages to a locale (`'en'` \\| `'es'`) + optional custom messages |\n| `EntityForm\u003cT\u003e`              | Type alias for `FormGroup\u003cEntityFields\u003cT\u003e\u003e`                                          |\n| `EntityFields\u003cT\u003e`            | Maps entity fields to typed `FormControl`                                            |\n| `ControlConfig`              | Second argument type for `entity.required` / `entity.optional`                       |\n| `FieldError`                 | `{ key: string; message: string }` — returned by `fieldErrors` pipe                  |\n| `ErrorMessages`              | Error messages map type                                                              |\n| `SupportedLocale`            | `'en' \\| 'es'`                                                                       |\n| `DefaultValidatorsConfig`    | Config type for `provideDefaultValidators`                                           |\n\n---\n\n## Path Alias (optional)\n\nIf you prefer a shorter import, configure a path alias in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@entity-forms\": [\"./node_modules/@irv-labs/ngx-entity-forms\"]\n    }\n  }\n}\n```\n\nThen import from the alias instead:\n\n```typescript\n// Before\nimport { entity, entityForm, FieldErrorsPipe } from '@irv-labs/ngx-entity-forms';\n\n// After\nimport { entity, entityForm, FieldErrorsPipe } from '@entity-forms';\n```\n\n\u003e This is purely a local convenience — it does not affect the published package or your teammates unless they add the same alias to their `tsconfig.json`.\n\n---\n\n## Philosophy\n\nAngular's reactive forms are powerful but verbose. Typed forms (introduced in Angular 14) improved the situation, but the boilerplate of creating controls, wiring validators, and displaying errors still adds up fast across a real project.\n\n`ngx-entity-forms` takes the position that your form should follow your entity — not the other way around. You define the shape of your data once, and the library derives the form structure from it. TypeScript does the rest.\n\n- **Entity-first** — your interface is the source of truth, the form follows it\n- **Zero guessing** — full autocompletion on `form.controls.X` with the correct type\n- **Flat API** — one builder object, three methods, one function to create the group\n- **Additive** — global validators and error messages layer on top without touching your controls\n- **No lock-in** — built entirely on Angular's own `FormControl` and `FormGroup`, no custom abstractions underneath\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firvrodflo%2Fngx-entity-form","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Firvrodflo%2Fngx-entity-form","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firvrodflo%2Fngx-entity-form/lists"}