@wiltech-labs/ngx-forms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +301 -0
- package/fesm2022/wiltech-labs-ngx-forms.mjs +2211 -0
- package/fesm2022/wiltech-labs-ngx-forms.mjs.map +1 -0
- package/package.json +33 -0
- package/types/wiltech-labs-ngx-forms.d.ts +390 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# @wiltech-labs/ngx-forms
|
|
2
|
+
|
|
3
|
+
Configuration-driven Angular forms built on the **Signals Forms API** (`@angular/forms/signals`)
|
|
4
|
+
and **Angular Material**. Describe your fields once, then either render the whole form with
|
|
5
|
+
`DynamicForm` or place individual field components in your own layout.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **`DynamicForm`** — renders a full form (fields + Save/Clear) from a list of field definitions
|
|
10
|
+
- **Field components** — every field type is a standalone Material component you can use on its own
|
|
11
|
+
- **`formConfig<T>()` builder** — fluent, type-checked way to write the config; field names are checked against your model
|
|
12
|
+
- **Validation from config** — `required`, `minLength`/`maxLength`, `min`/`max`, date/time ranges and `disabled`, shown inline under each field once touched
|
|
13
|
+
- **Date and time done properly** — timezone-aware instants and timezone-free business dates/times, displayed in a chosen locale rather than the browser's
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @wiltech-labs/ngx-forms
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Peer dependencies: `@angular/core`, `@angular/common`, `@angular/forms`, `@angular/material` (all `^22`).
|
|
22
|
+
`temporal-polyfill` is installed with the package.
|
|
23
|
+
|
|
24
|
+
The fields are Angular Material components, so the app needs a Material theme and the Roboto font:
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
/* styles.css */
|
|
28
|
+
@import '@angular/material/prebuilt-themes/azure-blue.css';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<!-- index.html -->
|
|
33
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
|
34
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage with DynamicForm
|
|
38
|
+
|
|
39
|
+
### 1. Describe the model and its fields
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { formConfig } from '@wiltech-labs/ngx-forms';
|
|
43
|
+
import type { BaseSchema, SchemaConfig } from '@wiltech-labs/ngx-forms';
|
|
44
|
+
|
|
45
|
+
export interface FlightSchema extends BaseSchema { // BaseSchema adds `id` and `schemaType`
|
|
46
|
+
schemaType: 'flight';
|
|
47
|
+
from: string;
|
|
48
|
+
to: string;
|
|
49
|
+
departure: string;
|
|
50
|
+
cabin: string;
|
|
51
|
+
delayed: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const flightFormConfig: SchemaConfig<FlightSchema> = formConfig<FlightSchema>('flight')
|
|
55
|
+
.text('from', 'Departure City', { required: true, minLength: 3, maxLength: 20 })
|
|
56
|
+
.text('to', 'Destination City', { required: true, minLength: 3, maxLength: 20 })
|
|
57
|
+
.instantDateTime('departure', 'Departure', {
|
|
58
|
+
required: true,
|
|
59
|
+
hint: 'Local time at the departure airport',
|
|
60
|
+
dateTimeConfig: { timeZone: 'Europe/London', min: '2026-01-01T00:00:00Z' }
|
|
61
|
+
})
|
|
62
|
+
.select('cabin', 'Cabin', [
|
|
63
|
+
{ label: 'Economy', value: 'economy' },
|
|
64
|
+
{ label: 'Business', value: 'business' }
|
|
65
|
+
], { required: true })
|
|
66
|
+
.checkbox('delayed', 'Delayed')
|
|
67
|
+
.build({ from: '', to: '', departure: '', cabin: 'economy', delayed: false });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`build()` returns `{ schemaType, fields, initialValue }`, with `id` and `schemaType` filled in on the
|
|
71
|
+
initial value. A misspelled or renamed field name is a compile error.
|
|
72
|
+
|
|
73
|
+
### 2. Render it
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Component, signal } from '@angular/core';
|
|
77
|
+
import { form } from '@angular/forms/signals';
|
|
78
|
+
import { DynamicForm, toSchema } from '@wiltech-labs/ngx-forms';
|
|
79
|
+
import { flightFormConfig } from './flight-form.config';
|
|
80
|
+
import type { FlightSchema } from './flight-form.config';
|
|
81
|
+
|
|
82
|
+
@Component({
|
|
83
|
+
selector: 'app-flight-form',
|
|
84
|
+
imports: [DynamicForm],
|
|
85
|
+
template: `
|
|
86
|
+
<app-dynamic-form
|
|
87
|
+
[metaInfo]="config.fields"
|
|
88
|
+
[dynamicForm]="flightForm"
|
|
89
|
+
(onFormSubmit)="save()"
|
|
90
|
+
(onFormClear)="clear()"
|
|
91
|
+
/>
|
|
92
|
+
`
|
|
93
|
+
})
|
|
94
|
+
export class FlightFormComponent {
|
|
95
|
+
protected readonly config = flightFormConfig;
|
|
96
|
+
protected readonly flight = signal(flightFormConfig.initialValue);
|
|
97
|
+
protected readonly flightForm = form(this.flight, toSchema<FlightSchema>(flightFormConfig.fields));
|
|
98
|
+
|
|
99
|
+
protected save(): void {
|
|
100
|
+
console.log('Saved', this.flight());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected clear(): void {
|
|
104
|
+
this.flight.set(flightFormConfig.initialValue);
|
|
105
|
+
this.flightForm().reset();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`toSchema()` turns the field definitions into Signals Forms validation rules. Save is only enabled
|
|
111
|
+
while the form is valid, and `onFormSubmit` only fires for a valid form.
|
|
112
|
+
|
|
113
|
+
## Usage with individual fields
|
|
114
|
+
|
|
115
|
+
Every field component takes the same two inputs — a `[fieldDef]` and the `[field]` from your
|
|
116
|
+
Signals Form — so you can use them without `DynamicForm`, in any layout. The model can be any plain
|
|
117
|
+
object; it doesn't need `id` or `schemaType`.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { Component, signal } from '@angular/core';
|
|
121
|
+
import { form } from '@angular/forms/signals';
|
|
122
|
+
import {
|
|
123
|
+
BusinessDateField, CheckboxField, FormFieldType, TextField, toSchema
|
|
124
|
+
} from '@wiltech-labs/ngx-forms';
|
|
125
|
+
import type { FieldDef } from '@wiltech-labs/ngx-forms';
|
|
126
|
+
|
|
127
|
+
interface Newsletter {
|
|
128
|
+
email: string;
|
|
129
|
+
startDate: string;
|
|
130
|
+
agree: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Component({
|
|
134
|
+
selector: 'app-newsletter',
|
|
135
|
+
imports: [TextField, BusinessDateField, CheckboxField],
|
|
136
|
+
template: `
|
|
137
|
+
<app-text-field [fieldDef]="fields.email" [field]="newsletterForm.email" />
|
|
138
|
+
<app-business-date-field [fieldDef]="fields.startDate" [field]="newsletterForm.startDate" />
|
|
139
|
+
<app-checkbox-field [fieldDef]="fields.agree" [field]="newsletterForm.agree" />
|
|
140
|
+
<button [disabled]="newsletterForm().invalid()" (click)="subscribe()">Subscribe</button>
|
|
141
|
+
`
|
|
142
|
+
})
|
|
143
|
+
export class NewsletterComponent {
|
|
144
|
+
protected readonly fields = {
|
|
145
|
+
email: { name: 'email', type: FormFieldType.TEXT, label: 'Email', required: true },
|
|
146
|
+
startDate: {
|
|
147
|
+
name: 'startDate',
|
|
148
|
+
type: FormFieldType.BUSINESS_DATE,
|
|
149
|
+
label: 'Start from',
|
|
150
|
+
required: true,
|
|
151
|
+
dateTimeConfig: { min: '2026-01-01', locale: 'en-GB' }
|
|
152
|
+
},
|
|
153
|
+
agree: { name: 'agree', type: FormFieldType.CHECKBOX, label: 'I agree to receive emails', required: true }
|
|
154
|
+
} satisfies Record<keyof Newsletter, FieldDef>;
|
|
155
|
+
|
|
156
|
+
protected readonly newsletter = signal<Newsletter>({ email: '', startDate: '', agree: false });
|
|
157
|
+
protected readonly newsletterForm = form(this.newsletter, toSchema<Newsletter>(Object.values(this.fields)));
|
|
158
|
+
|
|
159
|
+
protected subscribe(): void {
|
|
160
|
+
console.log(this.newsletter());
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Each component renders its own label, hint, required marker and errors, and fills the width of its
|
|
166
|
+
container (`DynamicForm` caps each field at 400px, overridable per field with `maxWidth`).
|
|
167
|
+
|
|
168
|
+
## Field types
|
|
169
|
+
|
|
170
|
+
| `FormFieldType` | Builder method | Component | Value | Material control |
|
|
171
|
+
|---|---|---|---|---|
|
|
172
|
+
| `TEXT` | `text()` | `TextField` | `string` | `matInput` |
|
|
173
|
+
| `EMAIL` | `email()` | `TextField` | `string` | `matInput` (`type="email"`), plus a valid-address check |
|
|
174
|
+
| `PASSWORD` | `password()` | `TextField` | `string` | `matInput` (masked) |
|
|
175
|
+
| `SEARCH` | `search()` | `TextField` | `string` | `matInput` |
|
|
176
|
+
| `NUMBER` | `number()` | `TextField` | `number` | `matInput` |
|
|
177
|
+
| `TEXTAREA` | `textarea()` | `TextareaField` | `string` | `matInput` textarea, auto-growing |
|
|
178
|
+
| `CODE` | `code()` | `TextareaField` | `string` | Monospaced textarea, Tab indents |
|
|
179
|
+
| `CHECKBOX` | `checkbox()` | `CheckboxField` | `boolean` | `mat-checkbox` |
|
|
180
|
+
| `RADIO` | `radio(name, label, choices)` | `RadioField` | option value | `mat-radio-group` |
|
|
181
|
+
| `SELECT` | `select(name, label, choices)` | `SelectField` | option value | `mat-select` |
|
|
182
|
+
| `RANGE` | `range()` | `SliderField` | `number` | `mat-slider` (`min`/`max`/`step`, default 0–100) |
|
|
183
|
+
| `BUSINESS_DATE` | `businessDate()` | `BusinessDateField` | `'YYYY-MM-DD'` | Datepicker |
|
|
184
|
+
| `BUSINESS_TIME` | `businessTime()` | `BusinessTimeField` | `'HH:mm'` | Timepicker |
|
|
185
|
+
| `INSTANT_DATE_TIME` | `instantDateTime()` | `InstantDateTimeField` | UTC `'YYYY-MM-DDThh:mm:ssZ'` | Datepicker + timepicker |
|
|
186
|
+
|
|
187
|
+
Use `hidden(name)` on the builder for a field that belongs to the model and its validation but is never shown.
|
|
188
|
+
|
|
189
|
+
### Choosing a date/time field
|
|
190
|
+
|
|
191
|
+
- **`BUSINESS_DATE`** — a calendar date with no timezone: birthdays, holidays, due dates. Christmas Day is the 25th wherever it's read.
|
|
192
|
+
- **`BUSINESS_TIME`** — a time of day with no date or timezone: opening hours, daily schedules. "Opens at 09:00" means 09:00 in whichever shop you walk into.
|
|
193
|
+
- **`INSTANT_DATE_TIME`** — an exact moment, stored in UTC and edited as the wall-clock time of `dateTimeConfig.timeZone`: meetings, deadlines, flights. When a time falls in a daylight-saving gap or overlap, the field explains how it was resolved.
|
|
194
|
+
|
|
195
|
+
All three display and parse dates in `dateTimeConfig.locale` (default `'en-GB'`, so `31/12/2026`),
|
|
196
|
+
never the browser's locale — typing `31/12/2026` works for any day-first locale.
|
|
197
|
+
|
|
198
|
+
## Field definition
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface FieldDef {
|
|
202
|
+
name: string; // Property name in the model
|
|
203
|
+
type: FormFieldType;
|
|
204
|
+
label: string;
|
|
205
|
+
required?: boolean;
|
|
206
|
+
minLength?: number; // Text fields
|
|
207
|
+
maxLength?: number; // Text fields
|
|
208
|
+
pattern?: RegExp | string; // Text fields: a RegExp is used as-is; a string must match the whole value
|
|
209
|
+
patternMessage?: string; // Error when `pattern` doesn't match. Default: "<label> is not in the expected format"
|
|
210
|
+
min?: number; // Number and range fields
|
|
211
|
+
max?: number; // Number and range fields
|
|
212
|
+
step?: number; // Range fields
|
|
213
|
+
options?: FieldOption[]; // Radio and select: [{ label, value }]
|
|
214
|
+
orientation?: 'horizontal' | 'vertical'; // Radio: a wrapping row (default) or one option per line
|
|
215
|
+
dateTimeConfig?: DateTimeConfig; // Business date/time and instant date-time
|
|
216
|
+
hidden?: boolean; // Part of the model, never rendered
|
|
217
|
+
disabled?: boolean; // Rendered but not editable
|
|
218
|
+
hint?: string; // Help text under the field
|
|
219
|
+
maxWidth?: string; // DynamicForm only: overrides the 400px cap, Eg '900px' or '100%'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface DateTimeConfig {
|
|
223
|
+
locale?: string; // Display/typing format, Eg 'en-GB', 'en-US', 'el-CY'. Default 'en-GB'.
|
|
224
|
+
min?: string; // In the field's own value format: '2026-12-25', '09:00' or '2026-01-01T00:00:00Z'
|
|
225
|
+
max?: string;
|
|
226
|
+
timeZone?: string; // Instant only: IANA zone, Eg 'Europe/London'. Default: the user's timezone.
|
|
227
|
+
disambiguation?: 'earlier' | 'later'; // Instant only: which occurrence when a time happens twice
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Validation
|
|
232
|
+
|
|
233
|
+
`toSchema()` turns each `FieldDef` into Signals Forms rules, with messages built from the label:
|
|
234
|
+
|
|
235
|
+
| Config | Rule | Message |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| `required: true` | `required()` | "Email is required" |
|
|
238
|
+
| `minLength` / `maxLength` | `minLength()` / `maxLength()` | "Username must be at least 3 characters" |
|
|
239
|
+
| `min` / `max` | `min()` / `max()` | "Age must be at least 18" |
|
|
240
|
+
| type `EMAIL` | `email()` | "Email must be a valid email address" |
|
|
241
|
+
| `pattern` (+ `patternMessage`) | `pattern()` | your `patternMessage`, or "Username is not in the expected format" |
|
|
242
|
+
| `dateTimeConfig.min` / `max` | custom | "Closed on cannot be after 2026-12-31" |
|
|
243
|
+
| `disabled: true` | `disabled()` | — (field shown but not editable) |
|
|
244
|
+
|
|
245
|
+
There's no separate `requiredTrue`: `required` treats `false` as empty, so `required: true` on a
|
|
246
|
+
checkbox means it must be ticked (Eg "Accept terms").
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
formConfig<SignUp>('signUp')
|
|
250
|
+
.text('username', 'Username', {
|
|
251
|
+
required: true,
|
|
252
|
+
pattern: '[a-z0-9.]+', // whole value must match
|
|
253
|
+
patternMessage: 'Use lowercase letters, digits and dots only'
|
|
254
|
+
})
|
|
255
|
+
.email('email', 'Email', { required: true })
|
|
256
|
+
.checkbox('terms', 'I accept the terms', { required: true })
|
|
257
|
+
.build({ username: '', email: '', terms: false });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A string `pattern` is wrapped as `^(?:…)$`, like HTML's `pattern` attribute, and can be stored in
|
|
261
|
+
JSON config. Pass a `RegExp` instead when you want a partial match or flags.
|
|
262
|
+
|
|
263
|
+
## Other exports
|
|
264
|
+
|
|
265
|
+
- `toSchema(fields)` — field definitions to Signals Forms rules
|
|
266
|
+
- `createEmptyEntity(schemaType, values)` / `defineSchema(config)` — build a `SchemaConfig` by hand instead of with the builder
|
|
267
|
+
- `ZonedDateTimeService` — converts between UTC instants and picker `Date`s in a given timezone, resolving daylight-saving gaps and overlaps
|
|
268
|
+
|
|
269
|
+
## Layout
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
src/
|
|
273
|
+
├── public-api.ts # barrel — the entire public surface; nothing outside this is exported
|
|
274
|
+
└── lib/dynamic-form/
|
|
275
|
+
├── components/ # DynamicForm + one Material component per field type
|
|
276
|
+
├── builders/ # formConfig<T>() / FormConfigBuilder
|
|
277
|
+
├── adapters/ # Locale-aware DateAdapter for the pickers
|
|
278
|
+
├── services/ # ZonedDateTimeService
|
|
279
|
+
├── shared/ # Hint/error line for controls without a mat-form-field
|
|
280
|
+
├── interfaces/ # FieldDef, FieldOption, BaseSchema, SchemaConfig
|
|
281
|
+
├── constants/ # FormFieldType, DateTimeConfig
|
|
282
|
+
├── styles/ # Shared SCSS mixins
|
|
283
|
+
└── utils/ # toSchema, date/time parsing and validation
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Status
|
|
287
|
+
|
|
288
|
+
Not yet published to npm — under development. The `apps/showcase` app demonstrates every field type.
|
|
289
|
+
|
|
290
|
+
## Publishing
|
|
291
|
+
|
|
292
|
+
To publish this package to npm:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
cd packages/forms
|
|
296
|
+
npm run build
|
|
297
|
+
cd dist
|
|
298
|
+
npm publish
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Ensure `version` in `package.json` is updated before publishing per semver conventions.
|