@rxdi/forms 0.7.214 → 0.7.215
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 +103 -40
- package/dist/form.array.d.ts +6 -2
- package/dist/form.array.js +28 -3
- package/dist/form.group.d.ts +2 -2
- package/dist/form.tokens.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ interface UserParams {
|
|
|
29
29
|
address: {
|
|
30
30
|
city: string;
|
|
31
31
|
street: string;
|
|
32
|
-
}
|
|
32
|
+
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
@Component({
|
|
@@ -37,14 +37,13 @@ interface UserParams {
|
|
|
37
37
|
template(this: UserProfile) {
|
|
38
38
|
return html`
|
|
39
39
|
<form name="user-form" @submit=${this.onSubmit}>
|
|
40
|
-
|
|
41
40
|
<!-- Deep Binding with Dot Notation -->
|
|
42
41
|
<input
|
|
43
42
|
name="firstName"
|
|
44
43
|
.value=${this.form.value.firstName}
|
|
45
44
|
@blur=${() => this.requestUpdate()}
|
|
46
45
|
/>
|
|
47
|
-
|
|
46
|
+
|
|
48
47
|
<!-- Nested Group Binding -->
|
|
49
48
|
<input
|
|
50
49
|
name="address.city"
|
|
@@ -55,33 +54,32 @@ interface UserParams {
|
|
|
55
54
|
<button type="submit">Save</button>
|
|
56
55
|
</form>
|
|
57
56
|
`;
|
|
58
|
-
}
|
|
57
|
+
},
|
|
59
58
|
})
|
|
60
59
|
export class UserProfile extends LitElement {
|
|
61
|
-
|
|
62
60
|
// Model to bind
|
|
63
61
|
@property({ type: Object })
|
|
64
62
|
user: UserParams = {
|
|
65
63
|
firstName: 'John',
|
|
66
|
-
address: { city: 'New York', street: '5th Ave' }
|
|
64
|
+
address: { city: 'New York', street: '5th Ave' },
|
|
67
65
|
};
|
|
68
66
|
|
|
69
67
|
@Form({
|
|
70
68
|
name: 'user-form',
|
|
71
69
|
strategy: 'change',
|
|
72
|
-
model: 'user' // Automatic Model Binding!
|
|
70
|
+
model: 'user', // Automatic Model Binding!
|
|
73
71
|
})
|
|
74
72
|
form = new FormGroup({
|
|
75
73
|
firstName: '',
|
|
76
74
|
address: new FormGroup({
|
|
77
75
|
city: '',
|
|
78
|
-
street: ''
|
|
79
|
-
})
|
|
76
|
+
street: '',
|
|
77
|
+
}),
|
|
80
78
|
});
|
|
81
79
|
|
|
82
80
|
onSubmit(e: Event) {
|
|
83
81
|
e.preventDefault();
|
|
84
|
-
console.log(this.form.value);
|
|
82
|
+
console.log(this.form.value);
|
|
85
83
|
// Output: { firstName: 'John', address: { city: 'New York', street: '5th Ave' } }
|
|
86
84
|
}
|
|
87
85
|
}
|
|
@@ -90,6 +88,7 @@ export class UserProfile extends LitElement {
|
|
|
90
88
|
## New Features
|
|
91
89
|
|
|
92
90
|
### Automatic Model Binding
|
|
91
|
+
|
|
93
92
|
Use the `model` property in the `@Form` decorator to automatically populate the form from a component property.
|
|
94
93
|
|
|
95
94
|
```typescript
|
|
@@ -99,9 +98,11 @@ Use the `model` property in the `@Form` decorator to automatically populate the
|
|
|
99
98
|
})
|
|
100
99
|
form = new FormGroup({ ... });
|
|
101
100
|
```
|
|
101
|
+
|
|
102
102
|
The library reads `this.myData` during initialization and calls `form.patchValue(this.myData)`.
|
|
103
103
|
|
|
104
104
|
### Nested FormGroups & FormArray
|
|
105
|
+
|
|
105
106
|
You can nest `FormGroup`s arbitrarily deep.
|
|
106
107
|
|
|
107
108
|
```typescript
|
|
@@ -110,25 +111,28 @@ form = new FormGroup({
|
|
|
110
111
|
id: 1,
|
|
111
112
|
flags: new FormGroup({
|
|
112
113
|
isActive: true,
|
|
113
|
-
isAdmin: false
|
|
114
|
-
})
|
|
114
|
+
isAdmin: false,
|
|
115
|
+
}),
|
|
115
116
|
}),
|
|
116
|
-
tags: new FormArray([
|
|
117
|
-
new FormGroup({ label: 'red' })
|
|
118
|
-
])
|
|
117
|
+
tags: new FormArray([new FormGroup({ label: 'red' })]),
|
|
119
118
|
});
|
|
120
119
|
```
|
|
121
120
|
|
|
122
121
|
**Template Binding:**
|
|
123
122
|
Use dot notation for nested controls:
|
|
123
|
+
|
|
124
124
|
```html
|
|
125
125
|
<input name="meta.flags.isActive" type="checkbox" />
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
### Type Safety & Autosuggestion
|
|
129
|
+
|
|
129
130
|
The library now extensively uses advanced TypeScript features:
|
|
131
|
+
|
|
130
132
|
- **`form.value`**: Returns the unwrapped pure object type (e.g., `{ meta: { flags: { isActive: boolean } } }`).
|
|
131
|
-
- **`form.get('path.to.prop')`**: Provides autocomplete for deep paths!
|
|
133
|
+
- **`form.get('path.to.prop')`**: Provides autocomplete for deep paths and infers return types!
|
|
134
|
+
- `form.get('key')` returns exact control type (e.g. `FormArray`) without casting.
|
|
135
|
+
|
|
132
136
|
```typescript
|
|
133
137
|
// TypeScript knows this is valid:
|
|
134
138
|
this.form.get('meta.flags.isActive');
|
|
@@ -138,9 +142,10 @@ this.form.get('meta.flags.wrongProp'); // Error!
|
|
|
138
142
|
```
|
|
139
143
|
|
|
140
144
|
### Recursive PatchValue
|
|
145
|
+
|
|
141
146
|
Update multiple fields deeply at once:
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
````typescript
|
|
144
149
|
this.form.patchValue({
|
|
145
150
|
meta: {
|
|
146
151
|
flags: {
|
|
@@ -149,11 +154,77 @@ this.form.patchValue({
|
|
|
149
154
|
}
|
|
150
155
|
});
|
|
151
156
|
// Only updates 'isActive', leaves other fields untouched.
|
|
152
|
-
|
|
157
|
+
### Dynamic Array Inputs (FormArray)
|
|
158
|
+
|
|
159
|
+
For lists of primitive values, use `FormArray` with an `itemFactory` and automatic model binding. This removes the need for manual population.
|
|
160
|
+
|
|
161
|
+
#### Full Working Example
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { Component, html, LitElement, property } from '@rxdi/lit-html';
|
|
165
|
+
import { Form, FormGroup, FormArray } from '@rxdi/forms';
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@Component({
|
|
169
|
+
selector: 'tags-component',
|
|
170
|
+
template(this: TagsComponent) {
|
|
171
|
+
return html`
|
|
172
|
+
<form @submit=${(e) => e.preventDefault()}>
|
|
173
|
+
<h3>Tags</h3>
|
|
174
|
+
|
|
175
|
+
<!-- List Tags -->
|
|
176
|
+
${this.form.get('tags').controls.map(
|
|
177
|
+
(control, index) => html`
|
|
178
|
+
<div class="tag-row">
|
|
179
|
+
<input name="tags[${index}].value" .value=${control.value.value} @blur=${() => this.requestUpdate()} />
|
|
180
|
+
<button type="button" @click=${() => this.removeTag(index)}>Remove</button>
|
|
181
|
+
</div>
|
|
182
|
+
`
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
<button type="button" @click=${() => this.addTag()}>Add Tag</button>
|
|
186
|
+
<button type="button" @click=${() => this.onSubmit()}>Submit</button>
|
|
187
|
+
</form>
|
|
188
|
+
`;
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
export class TagsComponent extends LitElement {
|
|
192
|
+
// Model automatically binds to 'tags' in form
|
|
193
|
+
@property({ type: Array })
|
|
194
|
+
tags = ['news', 'tech'];
|
|
195
|
+
|
|
196
|
+
@Form({
|
|
197
|
+
name: 'tags-form',
|
|
198
|
+
model: 'tags', // Triggers form.patchValue(this.tags) on INIT
|
|
199
|
+
})
|
|
200
|
+
form = new FormGroup({
|
|
201
|
+
tags: new FormArray<{ value: string }>([], {
|
|
202
|
+
name: 'tags',
|
|
203
|
+
// Factory describes how to create new controls from model data
|
|
204
|
+
itemFactory: (value) => new FormGroup({ value: value.value || value }),
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
addTag() {
|
|
209
|
+
this.form.get('tags').push(new FormGroup({ value: '' }));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
removeTag(index: number) {
|
|
213
|
+
this.form.get('tags').removeAt(index);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
onSubmit() {
|
|
217
|
+
const dirtyTags = this.form.value.tags;
|
|
218
|
+
console.log(dirtyTags.map((t) => t.value));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
````
|
|
153
223
|
|
|
154
224
|
## API Reference
|
|
155
225
|
|
|
156
226
|
### Validators
|
|
227
|
+
|
|
157
228
|
Validators are async functions returning `InputErrorMessage` or `void`.
|
|
158
229
|
|
|
159
230
|
```typescript
|
|
@@ -165,11 +236,12 @@ export function CustomValidator(element: AbstractInput) {
|
|
|
165
236
|
|
|
166
237
|
// Usage
|
|
167
238
|
new FormGroup({
|
|
168
|
-
field: ['', [CustomValidator]]
|
|
169
|
-
})
|
|
239
|
+
field: ['', [CustomValidator]],
|
|
240
|
+
});
|
|
170
241
|
```
|
|
171
242
|
|
|
172
243
|
### Error Display Information
|
|
244
|
+
|
|
173
245
|
Use the `touched` and `validity.valid` properties for clean UI.
|
|
174
246
|
|
|
175
247
|
```typescript
|
|
@@ -200,15 +272,9 @@ form = new FormGroup({
|
|
|
200
272
|
```
|
|
201
273
|
|
|
202
274
|
```html
|
|
203
|
-
<label>
|
|
204
|
-
|
|
205
|
-
</label>
|
|
206
|
-
<label>
|
|
207
|
-
<input name="roles" type="checkbox" value="editor" /> Editor
|
|
208
|
-
</label>
|
|
209
|
-
<label>
|
|
210
|
-
<input name="roles" type="checkbox" value="viewer" /> Viewer
|
|
211
|
-
</label>
|
|
275
|
+
<label> <input name="roles" type="checkbox" value="admin" /> Admin </label>
|
|
276
|
+
<label> <input name="roles" type="checkbox" value="editor" /> Editor </label>
|
|
277
|
+
<label> <input name="roles" type="checkbox" value="viewer" /> Viewer </label>
|
|
212
278
|
```
|
|
213
279
|
|
|
214
280
|
If the user checks "Admin" and "Viewer", `form.value.roles` will be `['admin', 'viewer']`.
|
|
@@ -228,13 +294,10 @@ form = new FormGroup({
|
|
|
228
294
|
```
|
|
229
295
|
|
|
230
296
|
```html
|
|
231
|
-
<label>
|
|
232
|
-
|
|
233
|
-
</label>
|
|
234
|
-
<label>
|
|
235
|
-
<input name="mode" type="checkbox" value="light" /> Light
|
|
236
|
-
</label>
|
|
297
|
+
<label> <input name="mode" type="checkbox" value="dark" /> Dark </label>
|
|
298
|
+
<label> <input name="mode" type="checkbox" value="light" /> Light </label>
|
|
237
299
|
```
|
|
300
|
+
|
|
238
301
|
Checking "Dark" unchecks "Light" automatically.
|
|
239
302
|
|
|
240
303
|
### 3. Framework-Agnostic Usage (Vanilla JS)
|
|
@@ -246,7 +309,7 @@ import { FormGroup } from '@rxdi/forms';
|
|
|
246
309
|
|
|
247
310
|
const form = new FormGroup({
|
|
248
311
|
email: '',
|
|
249
|
-
password: ''
|
|
312
|
+
password: '',
|
|
250
313
|
});
|
|
251
314
|
|
|
252
315
|
// manually attach to DOM
|
|
@@ -259,7 +322,7 @@ form
|
|
|
259
322
|
.setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));
|
|
260
323
|
|
|
261
324
|
// Listen to changes
|
|
262
|
-
form.valueChanges.subscribe(val => console.log(val));
|
|
325
|
+
form.valueChanges.subscribe((val) => console.log(val));
|
|
263
326
|
```
|
|
264
327
|
|
|
265
328
|
### 4. Custom Error Handling Strategies
|
|
@@ -284,7 +347,7 @@ async validateEmail(element: HTMLInputElement) {
|
|
|
284
347
|
}
|
|
285
348
|
|
|
286
349
|
// In Template
|
|
287
|
-
${this.form.hasError('email', 'emailExists')
|
|
288
|
-
? html`<div class="error">Email taken!</div>`
|
|
350
|
+
${this.form.hasError('email', 'emailExists')
|
|
351
|
+
? html`<div class="error">Email taken!</div>`
|
|
289
352
|
: ''}
|
|
290
|
-
```
|
|
353
|
+
```
|
package/dist/form.array.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { LitElement } from '@rxdi/lit-html';
|
|
2
2
|
import { BehaviorSubject } from 'rxjs';
|
|
3
3
|
import { AbstractControl, FormOptions } from './form.tokens';
|
|
4
|
+
export interface FormArrayOptions<T = any> extends FormOptions {
|
|
5
|
+
itemFactory?: (value: T) => AbstractControl;
|
|
6
|
+
}
|
|
4
7
|
export declare class FormArray<T = any> implements AbstractControl<T[]> {
|
|
5
8
|
controls: AbstractControl<T>[];
|
|
6
9
|
readonly valueChanges: BehaviorSubject<T[]>;
|
|
@@ -13,9 +16,9 @@ export declare class FormArray<T = any> implements AbstractControl<T[]> {
|
|
|
13
16
|
private form;
|
|
14
17
|
private options;
|
|
15
18
|
private subscriptions;
|
|
16
|
-
constructor(controls?: AbstractControl<T>[],
|
|
19
|
+
constructor(controls?: AbstractControl<T>[], nameOrOptions?: string | FormArrayOptions<T>);
|
|
17
20
|
get value(): T[];
|
|
18
|
-
getOptions():
|
|
21
|
+
getOptions(): FormArrayOptions<T>;
|
|
19
22
|
setOptions(options: FormOptions): void;
|
|
20
23
|
push(control: AbstractControl<T>): Promise<void>;
|
|
21
24
|
removeAt(index: number): void;
|
|
@@ -28,4 +31,5 @@ export declare class FormArray<T = any> implements AbstractControl<T[]> {
|
|
|
28
31
|
init(): void;
|
|
29
32
|
getParentElement(): LitElement;
|
|
30
33
|
set value(values: T[]);
|
|
34
|
+
patchValue(values: T[]): void;
|
|
31
35
|
}
|
package/dist/form.array.js
CHANGED
|
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.FormArray = void 0;
|
|
13
13
|
const rxjs_1 = require("rxjs");
|
|
14
14
|
class FormArray {
|
|
15
|
-
constructor(controls = [],
|
|
15
|
+
constructor(controls = [], nameOrOptions = '') {
|
|
16
16
|
this.controls = [];
|
|
17
17
|
this.valid = true;
|
|
18
18
|
this.invalid = false;
|
|
@@ -20,7 +20,13 @@ class FormArray {
|
|
|
20
20
|
this.options = {};
|
|
21
21
|
this.subscriptions = new Map();
|
|
22
22
|
this.controls = controls;
|
|
23
|
-
|
|
23
|
+
if (typeof nameOrOptions === 'string') {
|
|
24
|
+
this.name = nameOrOptions;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.name = nameOrOptions.name || '';
|
|
28
|
+
this.options = nameOrOptions;
|
|
29
|
+
}
|
|
24
30
|
this._valueChanges = new rxjs_1.BehaviorSubject(this.value);
|
|
25
31
|
this.valueChanges = this._valueChanges;
|
|
26
32
|
this.controls.forEach((c) => this.subscribeToControl(c));
|
|
@@ -32,7 +38,7 @@ class FormArray {
|
|
|
32
38
|
return this.options;
|
|
33
39
|
}
|
|
34
40
|
setOptions(options) {
|
|
35
|
-
this.options = options;
|
|
41
|
+
this.options = Object.assign(Object.assign({}, this.options), options);
|
|
36
42
|
this.controls.forEach((c, index) => {
|
|
37
43
|
c.setOptions(Object.assign(Object.assign({}, options), { namespace: `${this.name}[${index}]` }));
|
|
38
44
|
});
|
|
@@ -118,5 +124,24 @@ class FormArray {
|
|
|
118
124
|
});
|
|
119
125
|
this.updateValue();
|
|
120
126
|
}
|
|
127
|
+
patchValue(values) {
|
|
128
|
+
if (!Array.isArray(values)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
values.forEach((v, i) => {
|
|
132
|
+
if (this.controls[i]) {
|
|
133
|
+
if (this.controls[i]['patchValue']) {
|
|
134
|
+
this.controls[i]['patchValue'](v);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.controls[i].value = v;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (this.options.itemFactory) {
|
|
141
|
+
this.push(this.options.itemFactory(v));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.updateValue();
|
|
145
|
+
}
|
|
121
146
|
}
|
|
122
147
|
exports.FormArray = FormArray;
|
package/dist/form.group.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LitElement } from '@rxdi/lit-html';
|
|
2
|
-
import { AbstractControl, AbstractInput, ErrorObject, FormInputOptions, FormOptions, NestedKeyOf, UnwrapValue, ValidatorFn } from './form.tokens';
|
|
2
|
+
import { AbstractControl, AbstractInput, ErrorObject, FormInputOptions, FormOptions, NestedKeyOf, UnwrapValue, ValidatorFn, DeepPropType } from './form.tokens';
|
|
3
3
|
export declare class FormGroup<T = FormInputOptions, E = {
|
|
4
4
|
[key: string]: never;
|
|
5
5
|
}> implements AbstractControl<UnwrapValue<T>> {
|
|
@@ -38,7 +38,7 @@ export declare class FormGroup<T = FormInputOptions, E = {
|
|
|
38
38
|
private getModelKeyName;
|
|
39
39
|
validate(element: AbstractInput): Promise<ErrorObject>;
|
|
40
40
|
private mapInputErrors;
|
|
41
|
-
get<K extends NestedKeyOf<T>>(name: K):
|
|
41
|
+
get<K extends NestedKeyOf<T>>(name: K): DeepPropType<T, K>;
|
|
42
42
|
getError(inputName: keyof T, errorKey: string): never;
|
|
43
43
|
hasError(inputName: keyof T, errorKey: string): boolean;
|
|
44
44
|
reset(): void;
|
package/dist/form.tokens.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
|
|
|
9
9
|
export type NestedKeyOf<T, D extends number = 3> = [D] extends [0] ? never : T extends object ? {
|
|
10
10
|
[K in keyof T & (string | number)]: T[K] extends AbstractControl<infer U> ? U extends object ? `${K}` | `${K}.${NestedKeyOf<U, Prev[D]>}` : `${K}` : T[K] extends object ? `${K}` | `${K}.${NestedKeyOf<T[K], Prev[D]>}` : `${K}`;
|
|
11
11
|
}[keyof T & (string | number)] : never;
|
|
12
|
+
export type DeepPropType<T, P extends string> = P extends keyof T ? T[P] : P extends `${infer K}.${infer R}` ? K extends keyof T ? DeepPropType<T[K], R> : any : any;
|
|
12
13
|
export type FormStrategies = keyof WindowEventMap;
|
|
13
14
|
export interface FormOptions {
|
|
14
15
|
/** Name of the form element */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rxdi/forms",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.215",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"author": "Kristiyan Tachev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build": "tsc"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@rxdi/lit-html": "^0.7.
|
|
15
|
+
"@rxdi/lit-html": "^0.7.214",
|
|
16
16
|
"@types/node": "^25.0.3",
|
|
17
17
|
"rxjs": "^7.8.2",
|
|
18
18
|
"typescript": "^5.9.3"
|