@wiltech-labs/ngx-forms 0.1.0 → 1.0.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 +119 -65
- package/fesm2022/wiltech-labs-ngx-forms.mjs +121 -84
- package/fesm2022/wiltech-labs-ngx-forms.mjs.map +1 -1
- package/package.json +1 -1
- package/types/wiltech-labs-ngx-forms.d.ts +53 -13
package/README.md
CHANGED
|
@@ -21,7 +21,9 @@ npm install @wiltech-labs/ngx-forms
|
|
|
21
21
|
Peer dependencies: `@angular/core`, `@angular/common`, `@angular/forms`, `@angular/material` (all `^22`).
|
|
22
22
|
`temporal-polyfill` is installed with the package.
|
|
23
23
|
|
|
24
|
-
The fields are Angular Material components
|
|
24
|
+
The fields are Angular Material components and take the app's own M3 theme (`mat.theme()`) — in an
|
|
25
|
+
app that follows the house conventions there's nothing to add. An app without a theme needs one, and
|
|
26
|
+
the Roboto font, e.g.:
|
|
25
27
|
|
|
26
28
|
```css
|
|
27
29
|
/* styles.css */
|
|
@@ -31,7 +33,10 @@ The fields are Angular Material components, so the app needs a Material theme an
|
|
|
31
33
|
```html
|
|
32
34
|
<!-- index.html -->
|
|
33
35
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
|
34
|
-
<link
|
|
36
|
+
<link
|
|
37
|
+
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
|
38
|
+
rel="stylesheet"
|
|
39
|
+
/>
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
## Usage with DynamicForm
|
|
@@ -42,7 +47,8 @@ The fields are Angular Material components, so the app needs a Material theme an
|
|
|
42
47
|
import { formConfig } from '@wiltech-labs/ngx-forms';
|
|
43
48
|
import type { BaseSchema, SchemaConfig } from '@wiltech-labs/ngx-forms';
|
|
44
49
|
|
|
45
|
-
export interface FlightSchema extends BaseSchema {
|
|
50
|
+
export interface FlightSchema extends BaseSchema {
|
|
51
|
+
// BaseSchema adds `id` and `schemaType`
|
|
46
52
|
schemaType: 'flight';
|
|
47
53
|
from: string;
|
|
48
54
|
to: string;
|
|
@@ -57,12 +63,17 @@ export const flightFormConfig: SchemaConfig<FlightSchema> = formConfig<FlightSch
|
|
|
57
63
|
.instantDateTime('departure', 'Departure', {
|
|
58
64
|
required: true,
|
|
59
65
|
hint: 'Local time at the departure airport',
|
|
60
|
-
dateTimeConfig: { timeZone: 'Europe/London', min: '2026-01-01T00:00:00Z' }
|
|
66
|
+
dateTimeConfig: { timeZone: 'Europe/London', min: '2026-01-01T00:00:00Z' },
|
|
61
67
|
})
|
|
62
|
-
.select(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
.select(
|
|
69
|
+
'cabin',
|
|
70
|
+
'Cabin',
|
|
71
|
+
[
|
|
72
|
+
{ label: 'Economy', value: 'economy' },
|
|
73
|
+
{ label: 'Business', value: 'business' },
|
|
74
|
+
],
|
|
75
|
+
{ required: true },
|
|
76
|
+
)
|
|
66
77
|
.checkbox('delayed', 'Delayed')
|
|
67
78
|
.build({ from: '', to: '', departure: '', cabin: 'economy', delayed: false });
|
|
68
79
|
```
|
|
@@ -83,18 +94,21 @@ import type { FlightSchema } from './flight-form.config';
|
|
|
83
94
|
selector: 'app-flight-form',
|
|
84
95
|
imports: [DynamicForm],
|
|
85
96
|
template: `
|
|
86
|
-
<
|
|
97
|
+
<ngx-dynamic-form
|
|
87
98
|
[metaInfo]="config.fields"
|
|
88
99
|
[dynamicForm]="flightForm"
|
|
89
100
|
(onFormSubmit)="save()"
|
|
90
101
|
(onFormClear)="clear()"
|
|
91
102
|
/>
|
|
92
|
-
|
|
103
|
+
`,
|
|
93
104
|
})
|
|
94
105
|
export class FlightFormComponent {
|
|
95
106
|
protected readonly config = flightFormConfig;
|
|
96
107
|
protected readonly flight = signal(flightFormConfig.initialValue);
|
|
97
|
-
protected readonly flightForm = form(
|
|
108
|
+
protected readonly flightForm = form(
|
|
109
|
+
this.flight,
|
|
110
|
+
toSchema<FlightSchema>(flightFormConfig.fields),
|
|
111
|
+
);
|
|
98
112
|
|
|
99
113
|
protected save(): void {
|
|
100
114
|
console.log('Saved', this.flight());
|
|
@@ -107,6 +121,10 @@ export class FlightFormComponent {
|
|
|
107
121
|
}
|
|
108
122
|
```
|
|
109
123
|
|
|
124
|
+
The actions sit right-aligned: a text **Clear** and a filled **Save**. Name them after what they do
|
|
125
|
+
with `clearLabel`/`submitLabel` — e.g. `clearLabel="Cancel"` when that button leaves the form (it
|
|
126
|
+
still emits `onFormClear`).
|
|
127
|
+
|
|
110
128
|
`toSchema()` turns the field definitions into Signals Forms validation rules. Save is only enabled
|
|
111
129
|
while the form is valid, and `onFormSubmit` only fires for a valid form.
|
|
112
130
|
|
|
@@ -120,7 +138,11 @@ object; it doesn't need `id` or `schemaType`.
|
|
|
120
138
|
import { Component, signal } from '@angular/core';
|
|
121
139
|
import { form } from '@angular/forms/signals';
|
|
122
140
|
import {
|
|
123
|
-
BusinessDateField,
|
|
141
|
+
BusinessDateField,
|
|
142
|
+
CheckboxField,
|
|
143
|
+
FormFieldType,
|
|
144
|
+
TextField,
|
|
145
|
+
toSchema,
|
|
124
146
|
} from '@wiltech-labs/ngx-forms';
|
|
125
147
|
import type { FieldDef } from '@wiltech-labs/ngx-forms';
|
|
126
148
|
|
|
@@ -134,11 +156,11 @@ interface Newsletter {
|
|
|
134
156
|
selector: 'app-newsletter',
|
|
135
157
|
imports: [TextField, BusinessDateField, CheckboxField],
|
|
136
158
|
template: `
|
|
137
|
-
<
|
|
138
|
-
<
|
|
139
|
-
<
|
|
159
|
+
<ngx-text-field [fieldDef]="fields.email" [field]="newsletterForm.email" />
|
|
160
|
+
<ngx-business-date-field [fieldDef]="fields.startDate" [field]="newsletterForm.startDate" />
|
|
161
|
+
<ngx-checkbox-field [fieldDef]="fields.agree" [field]="newsletterForm.agree" />
|
|
140
162
|
<button [disabled]="newsletterForm().invalid()" (click)="subscribe()">Subscribe</button>
|
|
141
|
-
|
|
163
|
+
`,
|
|
142
164
|
})
|
|
143
165
|
export class NewsletterComponent {
|
|
144
166
|
protected readonly fields = {
|
|
@@ -148,13 +170,21 @@ export class NewsletterComponent {
|
|
|
148
170
|
type: FormFieldType.BUSINESS_DATE,
|
|
149
171
|
label: 'Start from',
|
|
150
172
|
required: true,
|
|
151
|
-
dateTimeConfig: { min: '2026-01-01', locale: 'en-GB' }
|
|
173
|
+
dateTimeConfig: { min: '2026-01-01', locale: 'en-GB' },
|
|
174
|
+
},
|
|
175
|
+
agree: {
|
|
176
|
+
name: 'agree',
|
|
177
|
+
type: FormFieldType.CHECKBOX,
|
|
178
|
+
label: 'I agree to receive emails',
|
|
179
|
+
required: true,
|
|
152
180
|
},
|
|
153
|
-
agree: { name: 'agree', type: FormFieldType.CHECKBOX, label: 'I agree to receive emails', required: true }
|
|
154
181
|
} satisfies Record<keyof Newsletter, FieldDef>;
|
|
155
182
|
|
|
156
183
|
protected readonly newsletter = signal<Newsletter>({ email: '', startDate: '', agree: false });
|
|
157
|
-
protected readonly newsletterForm = form(
|
|
184
|
+
protected readonly newsletterForm = form(
|
|
185
|
+
this.newsletter,
|
|
186
|
+
toSchema<Newsletter>(Object.values(this.fields)),
|
|
187
|
+
);
|
|
158
188
|
|
|
159
189
|
protected subscribe(): void {
|
|
160
190
|
console.log(this.newsletter());
|
|
@@ -163,26 +193,27 @@ export class NewsletterComponent {
|
|
|
163
193
|
```
|
|
164
194
|
|
|
165
195
|
Each component renders its own label, hint, required marker and errors, and fills the width of its
|
|
166
|
-
container
|
|
196
|
+
container. `DynamicForm` adds no padding or background of its own — put it in the app's surface
|
|
197
|
+
(e.g. a tonal panel); cap a single field with `maxWidth`.
|
|
167
198
|
|
|
168
199
|
## Field types
|
|
169
200
|
|
|
170
|
-
| `FormFieldType`
|
|
171
|
-
|
|
172
|
-
| `TEXT`
|
|
173
|
-
| `EMAIL`
|
|
174
|
-
| `PASSWORD`
|
|
175
|
-
| `SEARCH`
|
|
176
|
-
| `NUMBER`
|
|
177
|
-
| `TEXTAREA`
|
|
178
|
-
| `CODE`
|
|
179
|
-
| `CHECKBOX`
|
|
180
|
-
| `RADIO`
|
|
181
|
-
| `SELECT`
|
|
182
|
-
| `RANGE`
|
|
183
|
-
| `BUSINESS_DATE`
|
|
184
|
-
| `BUSINESS_TIME`
|
|
185
|
-
| `INSTANT_DATE_TIME` | `instantDateTime()`
|
|
201
|
+
| `FormFieldType` | Builder method | Component | Value | Material control |
|
|
202
|
+
| ------------------- | ------------------------------ | ---------------------- | ---------------------------- | ------------------------------------------------------- |
|
|
203
|
+
| `TEXT` | `text()` | `TextField` | `string` | `matInput` |
|
|
204
|
+
| `EMAIL` | `email()` | `TextField` | `string` | `matInput` (`type="email"`), plus a valid-address check |
|
|
205
|
+
| `PASSWORD` | `password()` | `TextField` | `string` | `matInput` (masked) |
|
|
206
|
+
| `SEARCH` | `search()` | `TextField` | `string` | `matInput` |
|
|
207
|
+
| `NUMBER` | `number()` | `TextField` | `number` | `matInput` |
|
|
208
|
+
| `TEXTAREA` | `textarea()` | `TextareaField` | `string` | `matInput` textarea, auto-growing |
|
|
209
|
+
| `CODE` | `code()` | `TextareaField` | `string` | Monospaced textarea, Tab indents |
|
|
210
|
+
| `CHECKBOX` | `checkbox()` | `CheckboxField` | `boolean` | `mat-checkbox` |
|
|
211
|
+
| `RADIO` | `radio(name, label, choices)` | `RadioField` | option value | `mat-radio-group` |
|
|
212
|
+
| `SELECT` | `select(name, label, choices)` | `SelectField` | option value | `mat-select` |
|
|
213
|
+
| `RANGE` | `range()` | `SliderField` | `number` | `mat-slider` (`min`/`max`/`step`, default 0–100) |
|
|
214
|
+
| `BUSINESS_DATE` | `businessDate()` | `BusinessDateField` | `'YYYY-MM-DD'` | Datepicker |
|
|
215
|
+
| `BUSINESS_TIME` | `businessTime()` | `BusinessTimeField` | `'HH:mm'` | Timepicker |
|
|
216
|
+
| `INSTANT_DATE_TIME` | `instantDateTime()` | `InstantDateTimeField` | UTC `'YYYY-MM-DDThh:mm:ssZ'` | Datepicker + timepicker |
|
|
186
217
|
|
|
187
218
|
Use `hidden(name)` on the builder for a field that belongs to the model and its validation but is never shown.
|
|
188
219
|
|
|
@@ -195,36 +226,59 @@ Use `hidden(name)` on the builder for a field that belongs to the model and its
|
|
|
195
226
|
All three display and parse dates in `dateTimeConfig.locale` (default `'en-GB'`, so `31/12/2026`),
|
|
196
227
|
never the browser's locale — typing `31/12/2026` works for any day-first locale.
|
|
197
228
|
|
|
229
|
+
### App-wide default locale
|
|
230
|
+
|
|
231
|
+
`dateTimeConfig.locale` set on a field always wins. When it's unset, the fallback comes from
|
|
232
|
+
`NGX_FORMS_LOCALE` (an `InjectionToken<() => string>`), not a hardcoded constant — provide it once
|
|
233
|
+
in `app.config.ts` to change every field's default locale together, e.g. wired to
|
|
234
|
+
[`@wiltech-labs/ngx-translations`](../translations):
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { inject } from '@angular/core';
|
|
238
|
+
import { NGX_FORMS_LOCALE } from '@wiltech-labs/ngx-forms';
|
|
239
|
+
import { TranslationsService } from '@wiltech-labs/ngx-translations';
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
provide: NGX_FORMS_LOCALE,
|
|
243
|
+
useFactory: () => { const translations = inject(TranslationsService); return () => translations.locale(); }
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Unlike `ngx-dates`' `NGX_DATES_LOCALE` (read-only display text), this locale also decides which
|
|
248
|
+
_typed_ day/month order the picker's input accepts — changing it changes user input behaviour, not
|
|
249
|
+
just wording, so it isn't defaulted to the browser's own language the way `ngx-dates` is. Leave it
|
|
250
|
+
unset and every field keeps today's default (`'en-GB'`).
|
|
251
|
+
|
|
198
252
|
## Field definition
|
|
199
253
|
|
|
200
254
|
```typescript
|
|
201
255
|
interface FieldDef {
|
|
202
|
-
name: string;
|
|
256
|
+
name: string; // Property name in the model
|
|
203
257
|
type: FormFieldType;
|
|
204
258
|
label: string;
|
|
205
259
|
required?: boolean;
|
|
206
|
-
minLength?: number;
|
|
207
|
-
maxLength?: number;
|
|
208
|
-
pattern?: RegExp | string;
|
|
209
|
-
patternMessage?: string;
|
|
210
|
-
min?: number;
|
|
211
|
-
max?: number;
|
|
212
|
-
step?: number;
|
|
213
|
-
options?: FieldOption[];
|
|
260
|
+
minLength?: number; // Text fields
|
|
261
|
+
maxLength?: number; // Text fields
|
|
262
|
+
pattern?: RegExp | string; // Text fields: a RegExp is used as-is; a string must match the whole value
|
|
263
|
+
patternMessage?: string; // Error when `pattern` doesn't match. Default: "<label> is not in the expected format"
|
|
264
|
+
min?: number; // Number and range fields
|
|
265
|
+
max?: number; // Number and range fields
|
|
266
|
+
step?: number; // Range fields
|
|
267
|
+
options?: FieldOption[]; // Radio and select: [{ label, value }]
|
|
214
268
|
orientation?: 'horizontal' | 'vertical'; // Radio: a wrapping row (default) or one option per line
|
|
215
|
-
dateTimeConfig?: DateTimeConfig;
|
|
216
|
-
hidden?: boolean;
|
|
217
|
-
disabled?: boolean;
|
|
218
|
-
hint?: string;
|
|
219
|
-
maxWidth?: string;
|
|
269
|
+
dateTimeConfig?: DateTimeConfig; // Business date/time and instant date-time
|
|
270
|
+
hidden?: boolean; // Part of the model, never rendered
|
|
271
|
+
disabled?: boolean; // Rendered but not editable
|
|
272
|
+
hint?: string; // Help text under the field
|
|
273
|
+
maxWidth?: string; // DynamicForm only: caps this field's width, Eg '400px'
|
|
220
274
|
}
|
|
221
275
|
|
|
222
276
|
interface DateTimeConfig {
|
|
223
|
-
locale?: string;
|
|
224
|
-
min?: string;
|
|
277
|
+
locale?: string; // Display/typing format, Eg 'en-GB', 'en-US', 'el-CY'. Default 'en-GB'.
|
|
278
|
+
min?: string; // In the field's own value format: '2026-12-25', '09:00' or '2026-01-01T00:00:00Z'
|
|
225
279
|
max?: string;
|
|
226
|
-
timeZone?: string;
|
|
227
|
-
disambiguation?: 'earlier' | 'later';
|
|
280
|
+
timeZone?: string; // Instant only: IANA zone, Eg 'Europe/London'. Default: the user's timezone.
|
|
281
|
+
disambiguation?: 'earlier' | 'later'; // Instant only: which occurrence when a time happens twice
|
|
228
282
|
}
|
|
229
283
|
```
|
|
230
284
|
|
|
@@ -232,15 +286,15 @@ interface DateTimeConfig {
|
|
|
232
286
|
|
|
233
287
|
`toSchema()` turns each `FieldDef` into Signals Forms rules, with messages built from the label:
|
|
234
288
|
|
|
235
|
-
| Config
|
|
236
|
-
|
|
237
|
-
| `required: true`
|
|
238
|
-
| `minLength` / `maxLength`
|
|
239
|
-
| `min` / `max`
|
|
240
|
-
| type `EMAIL`
|
|
241
|
-
| `pattern` (+ `patternMessage`) | `pattern()`
|
|
242
|
-
| `dateTimeConfig.min` / `max`
|
|
243
|
-
| `disabled: true`
|
|
289
|
+
| Config | Rule | Message |
|
|
290
|
+
| ------------------------------ | ----------------------------- | ------------------------------------------------------------------ |
|
|
291
|
+
| `required: true` | `required()` | "Email is required" |
|
|
292
|
+
| `minLength` / `maxLength` | `minLength()` / `maxLength()` | "Username must be at least 3 characters" |
|
|
293
|
+
| `min` / `max` | `min()` / `max()` | "Age must be at least 18" |
|
|
294
|
+
| type `EMAIL` | `email()` | "Email must be a valid email address" |
|
|
295
|
+
| `pattern` (+ `patternMessage`) | `pattern()` | your `patternMessage`, or "Username is not in the expected format" |
|
|
296
|
+
| `dateTimeConfig.min` / `max` | custom | "Closed on cannot be after 2026-12-31" |
|
|
297
|
+
| `disabled: true` | `disabled()` | — (field shown but not editable) |
|
|
244
298
|
|
|
245
299
|
There's no separate `requiredTrue`: `required` treats `false` as empty, so `required: true` on a
|
|
246
300
|
checkbox means it must be ticked (Eg "Accept terms").
|
|
@@ -249,8 +303,8 @@ checkbox means it must be ticked (Eg "Accept terms").
|
|
|
249
303
|
formConfig<SignUp>('signUp')
|
|
250
304
|
.text('username', 'Username', {
|
|
251
305
|
required: true,
|
|
252
|
-
pattern: '[a-z0-9.]+',
|
|
253
|
-
patternMessage: 'Use lowercase letters, digits and dots only'
|
|
306
|
+
pattern: '[a-z0-9.]+', // whole value must match
|
|
307
|
+
patternMessage: 'Use lowercase letters, digits and dots only',
|
|
254
308
|
})
|
|
255
309
|
.email('email', 'Email', { required: true })
|
|
256
310
|
.checkbox('terms', 'I accept the terms', { required: true })
|