@rxdi/forms 0.7.213 → 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 +241 -483
- package/dist/form.array.d.ts +6 -2
- package/dist/form.array.js +29 -3
- package/dist/form.decorator.js +5 -1
- package/dist/form.group.d.ts +9 -8
- package/dist/form.group.js +34 -4
- package/dist/form.tokens.d.ts +15 -0
- package/package.json +25 -25
package/README.md
CHANGED
|
@@ -1,595 +1,353 @@
|
|
|
1
|
-
# Reactive
|
|
1
|
+
# Reactive Forms for LitHtml (Enhanced)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
```bash
|
|
5
|
-
npm i @rxdi/forms
|
|
6
|
-
```
|
|
3
|
+
A lightweight, strongly-typed, reactive forms library for LitHtml applications.
|
|
7
4
|
|
|
5
|
+
## Features
|
|
8
6
|
|
|
7
|
+
- **Strict Typing**: Full TypeScript support with `UnwrapValue` and `NestedKeyOf` for deep property inference.
|
|
8
|
+
- **Nested Forms**: Support for deep `FormGroup` nesting and `FormArray`.
|
|
9
|
+
- **Automatic Binding**: Bind component models directly to forms with `@Form({ model: 'myModel' })`.
|
|
10
|
+
- **Reactive**: based on `rxjs` `BehaviorSubject` for value streams.
|
|
11
|
+
- **Recursive Updates**: `patchValue` updates deep structures recursively.
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
## Installation
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm i @rxdi/forms
|
|
17
|
+
```
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
## Basic Usage
|
|
17
20
|
|
|
21
|
+
### 1. Define Model & Component
|
|
18
22
|
|
|
19
23
|
```typescript
|
|
20
|
-
import { html, Component } from '@rxdi/lit-html';
|
|
21
|
-
import {
|
|
22
|
-
|
|
24
|
+
import { html, Component, LitElement } from '@rxdi/lit-html';
|
|
25
|
+
import { Form, FormGroup } from '@rxdi/forms';
|
|
26
|
+
|
|
27
|
+
interface UserParams {
|
|
28
|
+
firstName: string;
|
|
29
|
+
address: {
|
|
30
|
+
city: string;
|
|
31
|
+
street: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
23
34
|
|
|
24
|
-
/**
|
|
25
|
-
* @customElement login-component
|
|
26
|
-
*/
|
|
27
35
|
@Component({
|
|
28
|
-
selector: '
|
|
29
|
-
template(this:
|
|
36
|
+
selector: 'user-profile',
|
|
37
|
+
template(this: UserProfile) {
|
|
30
38
|
return html`
|
|
31
|
-
<form name="
|
|
39
|
+
<form name="user-form" @submit=${this.onSubmit}>
|
|
40
|
+
<!-- Deep Binding with Dot Notation -->
|
|
32
41
|
<input
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
value=${this.form.value.email}
|
|
37
|
-
placeholder="Email address"
|
|
38
|
-
required
|
|
39
|
-
autofocus
|
|
42
|
+
name="firstName"
|
|
43
|
+
.value=${this.form.value.firstName}
|
|
44
|
+
@blur=${() => this.requestUpdate()}
|
|
40
45
|
/>
|
|
46
|
+
|
|
47
|
+
<!-- Nested Group Binding -->
|
|
41
48
|
<input
|
|
42
|
-
|
|
43
|
-
value=${this.form.value.
|
|
44
|
-
|
|
45
|
-
placeholder="Password"
|
|
46
|
-
required=""
|
|
49
|
+
name="address.city"
|
|
50
|
+
.value=${this.form.value.address.city}
|
|
51
|
+
@blur=${() => this.requestUpdate()}
|
|
47
52
|
/>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<input name="rememberMe" type="checkbox" /> Remember me
|
|
51
|
-
</label>
|
|
52
|
-
</div>
|
|
53
|
-
<button type="submit">
|
|
54
|
-
Sign in
|
|
55
|
-
</button>
|
|
53
|
+
|
|
54
|
+
<button type="submit">Save</button>
|
|
56
55
|
</form>
|
|
57
56
|
`;
|
|
58
|
-
}
|
|
57
|
+
},
|
|
59
58
|
})
|
|
60
|
-
export class
|
|
59
|
+
export class UserProfile extends LitElement {
|
|
60
|
+
// Model to bind
|
|
61
|
+
@property({ type: Object })
|
|
62
|
+
user: UserParams = {
|
|
63
|
+
firstName: 'John',
|
|
64
|
+
address: { city: 'New York', street: '5th Ave' },
|
|
65
|
+
};
|
|
66
|
+
|
|
61
67
|
@Form({
|
|
68
|
+
name: 'user-form',
|
|
62
69
|
strategy: 'change',
|
|
63
|
-
|
|
70
|
+
model: 'user', // Automatic Model Binding!
|
|
64
71
|
})
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
form = new FormGroup({
|
|
73
|
+
firstName: '',
|
|
74
|
+
address: new FormGroup({
|
|
75
|
+
city: '',
|
|
76
|
+
street: '',
|
|
77
|
+
}),
|
|
69
78
|
});
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
this.form.getValue('password');
|
|
76
|
-
this.form.setValue('email', 'blabla');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
onSubmit(event: Event) {
|
|
80
|
-
this.form.values;
|
|
80
|
+
onSubmit(e: Event) {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
console.log(this.form.value);
|
|
83
|
+
// Output: { firstName: 'John', address: { city: 'New York', street: '5th Ave' } }
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
|
-
|
|
84
86
|
```
|
|
85
87
|
|
|
88
|
+
## New Features
|
|
86
89
|
|
|
90
|
+
### Automatic Model Binding
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
Use the `model` property in the `@Form` decorator to automatically populate the form from a component property.
|
|
89
93
|
|
|
90
94
|
```typescript
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @customElement login-component
|
|
97
|
-
*/
|
|
98
|
-
@Component({
|
|
99
|
-
selector: 'login-component',
|
|
100
|
-
template(this: LoginComponent) {
|
|
101
|
-
return html`
|
|
102
|
-
<form name="my-form" @submit=${this.onSubmit}>
|
|
103
|
-
<input
|
|
104
|
-
style="margin-bottom: 20px;"
|
|
105
|
-
name="email"
|
|
106
|
-
type="email"
|
|
107
|
-
placeholder="Email address"
|
|
108
|
-
required
|
|
109
|
-
autofocus
|
|
110
|
-
/>
|
|
111
|
-
${this.form.hasError('email', 'blabla') ? html`${this.form.getError('email', 'blabla')}` : ''}
|
|
112
|
-
<input
|
|
113
|
-
type="password"
|
|
114
|
-
name="password"
|
|
115
|
-
placeholder="Password"
|
|
116
|
-
required=""
|
|
117
|
-
/>
|
|
118
|
-
<div>
|
|
119
|
-
<label>
|
|
120
|
-
<input name="rememberMe" type="checkbox" /> Remember me
|
|
121
|
-
</label>
|
|
122
|
-
</div>
|
|
123
|
-
<button type="submit">
|
|
124
|
-
Sign in
|
|
125
|
-
</button>
|
|
126
|
-
</form>
|
|
127
|
-
`;
|
|
128
|
-
}
|
|
95
|
+
@Form({
|
|
96
|
+
name: 'my-form',
|
|
97
|
+
model: 'myData' // matches this.myData
|
|
129
98
|
})
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
strategy: 'change',
|
|
133
|
-
name: 'my-form'
|
|
134
|
-
})
|
|
135
|
-
private form = new FormGroup({
|
|
136
|
-
password: '',
|
|
137
|
-
email: ['', [this.validateEmail]],
|
|
138
|
-
rememberMe: ''
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
OnUpdate() {
|
|
142
|
-
this.form.getValue('password');
|
|
143
|
-
this.form.setValue('email', 'blabla');
|
|
99
|
+
form = new FormGroup({ ... });
|
|
100
|
+
```
|
|
144
101
|
|
|
145
|
-
|
|
146
|
-
this.form.hasError('email', 'blabla')
|
|
147
|
-
}
|
|
102
|
+
The library reads `this.myData` during initialization and calls `form.patchValue(this.myData)`.
|
|
148
103
|
|
|
149
|
-
|
|
150
|
-
this.form.values;
|
|
151
|
-
}
|
|
104
|
+
### Nested FormGroups & FormArray
|
|
152
105
|
|
|
153
|
-
|
|
154
|
-
if (element.value === 'restrictedEmail@gmail.com') {
|
|
155
|
-
return { key: 'blabla', message: 'Please specify different email'};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
106
|
+
You can nest `FormGroup`s arbitrarily deep.
|
|
159
107
|
|
|
108
|
+
```typescript
|
|
109
|
+
form = new FormGroup({
|
|
110
|
+
meta: new FormGroup({
|
|
111
|
+
id: 1,
|
|
112
|
+
flags: new FormGroup({
|
|
113
|
+
isActive: true,
|
|
114
|
+
isAdmin: false,
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
tags: new FormArray([new FormGroup({ label: 'red' })]),
|
|
118
|
+
});
|
|
160
119
|
```
|
|
161
120
|
|
|
121
|
+
**Template Binding:**
|
|
122
|
+
Use dot notation for nested controls:
|
|
162
123
|
|
|
124
|
+
```html
|
|
125
|
+
<input name="meta.flags.isActive" type="checkbox" />
|
|
126
|
+
```
|
|
163
127
|
|
|
164
|
-
|
|
128
|
+
### Type Safety & Autosuggestion
|
|
165
129
|
|
|
166
|
-
|
|
130
|
+
The library now extensively uses advanced TypeScript features:
|
|
131
|
+
|
|
132
|
+
- **`form.value`**: Returns the unwrapped pure object type (e.g., `{ meta: { flags: { isActive: boolean } } }`).
|
|
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.
|
|
167
135
|
|
|
168
136
|
```typescript
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
name: 'my-form'
|
|
172
|
-
})
|
|
173
|
-
private form = new FormGroup({
|
|
174
|
-
condition: ''
|
|
175
|
-
});
|
|
176
|
-
```
|
|
137
|
+
// TypeScript knows this is valid:
|
|
138
|
+
this.form.get('meta.flags.isActive');
|
|
177
139
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<input
|
|
181
|
-
name="condition"
|
|
182
|
-
type="checkbox"
|
|
183
|
-
value='none'
|
|
184
|
-
/>
|
|
185
|
-
None
|
|
186
|
-
</label>
|
|
187
|
-
|
|
188
|
-
<label>
|
|
189
|
-
<input
|
|
190
|
-
name="condition"
|
|
191
|
-
type="checkbox"
|
|
192
|
-
value='checked'
|
|
193
|
-
/>
|
|
194
|
-
Checked
|
|
195
|
-
</label>
|
|
196
|
-
|
|
197
|
-
<label>
|
|
198
|
-
<input
|
|
199
|
-
name="condition"
|
|
200
|
-
type="checkbox"
|
|
201
|
-
value='not-checked'
|
|
202
|
-
/>
|
|
203
|
-
Not checked
|
|
204
|
-
</label>
|
|
140
|
+
// And this is invalid:
|
|
141
|
+
this.form.get('meta.flags.wrongProp'); // Error!
|
|
205
142
|
```
|
|
206
143
|
|
|
144
|
+
### Recursive PatchValue
|
|
207
145
|
|
|
208
|
-
|
|
146
|
+
Update multiple fields deeply at once:
|
|
209
147
|
|
|
148
|
+
````typescript
|
|
149
|
+
this.form.patchValue({
|
|
150
|
+
meta: {
|
|
151
|
+
flags: {
|
|
152
|
+
isActive: false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// Only updates 'isActive', leaves other fields untouched.
|
|
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.
|
|
210
160
|
|
|
211
|
-
|
|
161
|
+
#### Full Working Example
|
|
212
162
|
|
|
213
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
|
+
|
|
214
196
|
@Form({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
multi: false
|
|
197
|
+
name: 'tags-form',
|
|
198
|
+
model: 'tags', // Triggers form.patchValue(this.tags) on INIT
|
|
218
199
|
})
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
}),
|
|
221
206
|
});
|
|
222
|
-
```
|
|
223
207
|
|
|
208
|
+
addTag() {
|
|
209
|
+
this.form.get('tags').push(new FormGroup({ value: '' }));
|
|
210
|
+
}
|
|
224
211
|
|
|
212
|
+
removeTag(index: number) {
|
|
213
|
+
this.form.get('tags').removeAt(index);
|
|
214
|
+
}
|
|
225
215
|
|
|
226
|
-
|
|
216
|
+
onSubmit() {
|
|
217
|
+
const dirtyTags = this.form.value.tags;
|
|
218
|
+
console.log(dirtyTags.map((t) => t.value));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
227
221
|
|
|
228
|
-
|
|
222
|
+
````
|
|
229
223
|
|
|
224
|
+
## API Reference
|
|
230
225
|
|
|
231
|
-
|
|
226
|
+
### Validators
|
|
232
227
|
|
|
233
|
-
|
|
234
|
-
import { html } from '@rxdi/lit-html';
|
|
228
|
+
Validators are async functions returning `InputErrorMessage` or `void`.
|
|
235
229
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return html`
|
|
241
|
-
<div style="color:red; font-size: 13px;">${input.validationMessage}</div>
|
|
242
|
-
`;
|
|
230
|
+
```typescript
|
|
231
|
+
export function CustomValidator(element: AbstractInput) {
|
|
232
|
+
if (element.value === 'invalid') {
|
|
233
|
+
return { key: 'customError', message: 'Value is invalid' };
|
|
243
234
|
}
|
|
244
|
-
return '';
|
|
245
235
|
}
|
|
246
|
-
```
|
|
247
236
|
|
|
248
|
-
|
|
237
|
+
// Usage
|
|
238
|
+
new FormGroup({
|
|
239
|
+
field: ['', [CustomValidator]],
|
|
240
|
+
});
|
|
249
241
|
```
|
|
250
242
|
|
|
243
|
+
### Error Display Information
|
|
251
244
|
|
|
252
|
-
|
|
245
|
+
Use the `touched` and `validity.valid` properties for clean UI.
|
|
253
246
|
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
placeholder="Email address"
|
|
262
|
-
required
|
|
263
|
-
autofocus
|
|
264
|
-
/>
|
|
265
|
-
${InputErrorTemplate(this.form.get('email'))}
|
|
266
|
-
</form>
|
|
247
|
+
```typescript
|
|
248
|
+
function ErrorTemplate(input: AbstractInput) {
|
|
249
|
+
if (input?.touched && !input.validity.valid) {
|
|
250
|
+
return html`<div class="error">${input.validationMessage}</div>`;
|
|
251
|
+
}
|
|
252
|
+
return html``;
|
|
253
|
+
}
|
|
267
254
|
```
|
|
268
255
|
|
|
256
|
+
## Advanced Usage
|
|
269
257
|
|
|
258
|
+
### 1. Grouping Multiple Inputs (Checkbox Groups)
|
|
270
259
|
|
|
260
|
+
By default, inputs with the same `name` attribute are treated as a single value (last write wins). However, for checkboxes, you often want an array of values.
|
|
271
261
|
|
|
272
|
-
|
|
262
|
+
**Scenario:** A list of permissions where multiple can be selected.
|
|
273
263
|
|
|
274
264
|
```typescript
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
element.classList.add('is-invalid');
|
|
282
|
-
return {
|
|
283
|
-
key: 'email-validator',
|
|
284
|
-
message: 'Email is not valid'
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
element.classList.remove('is-invalid');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const form = new FormGroup({
|
|
291
|
-
email: ['', [EmailValidator]],
|
|
292
|
-
password: '',
|
|
265
|
+
@Form({
|
|
266
|
+
name: 'permissions-form',
|
|
267
|
+
multi: true // Enable multi-value binding for same-name inputs
|
|
268
|
+
})
|
|
269
|
+
form = new FormGroup({
|
|
270
|
+
roles: [] // Will be an array of values
|
|
293
271
|
});
|
|
294
|
-
|
|
295
|
-
form
|
|
296
|
-
.setParentElement(document.body)
|
|
297
|
-
.setOptions({ name: 'my-form' })
|
|
298
|
-
.prepareValues()
|
|
299
|
-
.setFormElement(form.querySelectForm(document.body))
|
|
300
|
-
.setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));
|
|
301
|
-
|
|
302
272
|
```
|
|
303
273
|
|
|
304
274
|
```html
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
type="email"
|
|
309
|
-
placeholder="Email address"
|
|
310
|
-
required
|
|
311
|
-
autofocus
|
|
312
|
-
/>
|
|
313
|
-
<input
|
|
314
|
-
name="password"
|
|
315
|
-
type="password"
|
|
316
|
-
required
|
|
317
|
-
/>
|
|
318
|
-
</form>
|
|
319
|
-
<script src="./main.ts"></script>
|
|
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>
|
|
320
278
|
```
|
|
321
279
|
|
|
322
|
-
|
|
280
|
+
If the user checks "Admin" and "Viewer", `form.value.roles` will be `['admin', 'viewer']`.
|
|
323
281
|
|
|
324
|
-
|
|
282
|
+
### 2. Single Selection Checkbox (Radio Behavior with Checkboxes)
|
|
325
283
|
|
|
326
|
-
|
|
327
|
-
import { FormArray, FormGroup } from '@rxdi/forms';
|
|
284
|
+
If you want multiple checkboxes to act like a radio button (only one valid at a time) but with uncheck capability:
|
|
328
285
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
286
|
+
```typescript
|
|
287
|
+
@Form({
|
|
288
|
+
name: 'settings-form',
|
|
289
|
+
multi: false // Default behavior
|
|
290
|
+
})
|
|
291
|
+
form = new FormGroup({
|
|
292
|
+
mode: ''
|
|
336
293
|
});
|
|
337
294
|
```
|
|
338
295
|
|
|
339
|
-
Template usage:
|
|
340
|
-
|
|
341
296
|
```html
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<input
|
|
345
|
-
name="users[${index}].name"
|
|
346
|
-
.value=${group.value.name}
|
|
347
|
-
/>
|
|
348
|
-
<input
|
|
349
|
-
name="users[${index}].email"
|
|
350
|
-
.value=${group.value.email}
|
|
351
|
-
/>
|
|
352
|
-
<button @click=${() => this.removeUser(index)}>Remove</button>
|
|
353
|
-
</div>
|
|
354
|
-
`)}
|
|
355
|
-
<button @click=${() => this.addUser()}>Add User</button>
|
|
297
|
+
<label> <input name="mode" type="checkbox" value="dark" /> Dark </label>
|
|
298
|
+
<label> <input name="mode" type="checkbox" value="light" /> Light </label>
|
|
356
299
|
```
|
|
357
300
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
addUser() {
|
|
362
|
-
(this.form.get('users') as FormArray).push(
|
|
363
|
-
new FormGroup({ name: '', email: '' })
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
removeUser(index: number) {
|
|
368
|
-
(this.form.get('users') as FormArray).removeAt(index);
|
|
369
|
-
}
|
|
370
|
-
```
|
|
301
|
+
Checking "Dark" unchecks "Light" automatically.
|
|
371
302
|
|
|
372
|
-
|
|
303
|
+
### 3. Framework-Agnostic Usage (Vanilla JS)
|
|
373
304
|
|
|
374
|
-
|
|
305
|
+
You can use this library without Decorators or LitHtml, with any UI library or vanilla HTML.
|
|
375
306
|
|
|
376
307
|
```typescript
|
|
377
|
-
|
|
378
|
-
@Form({ name: 'my-form' })
|
|
379
|
-
form: FormGroup; // Must be typed as FormGroup!
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
If you incorrectly type it (e.g., `form: string`), the library will throw a helpful error at runtime (requires `emitDecoratorMetadata: true` in `tsconfig`).
|
|
308
|
+
import { FormGroup } from '@rxdi/forms';
|
|
384
309
|
|
|
385
|
-
|
|
310
|
+
const form = new FormGroup({
|
|
311
|
+
email: '',
|
|
312
|
+
password: '',
|
|
313
|
+
});
|
|
386
314
|
|
|
387
|
-
|
|
388
|
-
|
|
315
|
+
// manually attach to DOM
|
|
316
|
+
const formElement = document.querySelector('form');
|
|
317
|
+
form
|
|
318
|
+
.setParentElement(document.body)
|
|
319
|
+
.setOptions({ name: 'my-form' })
|
|
320
|
+
.setFormElement(formElement)
|
|
321
|
+
.prepareValues()
|
|
322
|
+
.setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));
|
|
389
323
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
324
|
+
// Listen to changes
|
|
325
|
+
form.valueChanges.subscribe((val) => console.log(val));
|
|
393
326
|
```
|
|
394
327
|
|
|
395
|
-
|
|
328
|
+
### 4. Custom Error Handling Strategies
|
|
396
329
|
|
|
397
|
-
|
|
330
|
+
By default, verification happens on `change` or `blur`. You can control this via `strategy`.
|
|
398
331
|
|
|
399
332
|
```typescript
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Helper to display errors safely.
|
|
405
|
-
* Checks 'touched' (user interacted) and 'validity.valid' (silent check).
|
|
406
|
-
*/
|
|
407
|
-
function ErrorTemplate(input: AbstractInput) {
|
|
408
|
-
if (input && input.touched && !input.validity.valid) {
|
|
409
|
-
return html`<div class="error">${input.validationMessage}</div>`;
|
|
410
|
-
}
|
|
411
|
-
return html``;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
@Component({
|
|
415
|
-
selector: 'user-profile-form',
|
|
416
|
-
template(this: UserProfileForm) {
|
|
417
|
-
return html`
|
|
418
|
-
<form
|
|
419
|
-
name="user-form"
|
|
420
|
-
@submit=${(e: Event) => {
|
|
421
|
-
e.preventDefault();
|
|
422
|
-
console.log('Form Value:', this.form.value);
|
|
423
|
-
}}
|
|
424
|
-
>
|
|
425
|
-
<!-- Email Field -->
|
|
426
|
-
<div>
|
|
427
|
-
<label>Email</label>
|
|
428
|
-
<input
|
|
429
|
-
type="email"
|
|
430
|
-
name="email"
|
|
431
|
-
required
|
|
432
|
-
.value=${this.form.value.email}
|
|
433
|
-
@blur=${() => this.requestUpdate()}
|
|
434
|
-
/>
|
|
435
|
-
<!--
|
|
436
|
-
@blur triggers a re-render so 'touched' state is reflected.
|
|
437
|
-
The library handles 'touched' internally on blur, but we need
|
|
438
|
-
to request an update to show the error message immediately.
|
|
439
|
-
-->
|
|
440
|
-
${ErrorTemplate(this.form.get('email'))}
|
|
441
|
-
</div>
|
|
442
|
-
|
|
443
|
-
<!-- Password Field -->
|
|
444
|
-
<div>
|
|
445
|
-
<label>Password</label>
|
|
446
|
-
<input
|
|
447
|
-
type="password"
|
|
448
|
-
name="password"
|
|
449
|
-
required
|
|
450
|
-
minlength="6"
|
|
451
|
-
.value=${this.form.value.password}
|
|
452
|
-
@blur=${() => this.requestUpdate()}
|
|
453
|
-
/>
|
|
454
|
-
${ErrorTemplate(this.form.get('password'))}
|
|
455
|
-
</div>
|
|
456
|
-
|
|
457
|
-
<button type="submit" ?disabled=${this.form.invalid}>
|
|
458
|
-
Save Profile
|
|
459
|
-
</button>
|
|
460
|
-
</form>
|
|
461
|
-
`;
|
|
462
|
-
},
|
|
333
|
+
@Form({
|
|
334
|
+
name: 'login',
|
|
335
|
+
strategy: 'input' // Validate on every keystroke
|
|
463
336
|
})
|
|
464
|
-
export class UserProfileForm extends LitElement {
|
|
465
|
-
@Form({
|
|
466
|
-
name: 'user-form', // Must match <form name="...">
|
|
467
|
-
strategy: 'change', // Validate on change/blur
|
|
468
|
-
})
|
|
469
|
-
form = new FormGroup({
|
|
470
|
-
email: '',
|
|
471
|
-
password: '',
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
337
|
```
|
|
475
338
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
1. **Name Matching**: The `<form name="X">` attribute must match the `@Form({ name: 'X' })` name.
|
|
479
|
-
2. **Input Binding**: Use `.value=${this.form.value.fieldName}` to bind data. The library updates the model on user input automatically.
|
|
480
|
-
3. **Error Display**: Use a helper like `ErrorTemplate` that checks `.touched` and `.validity.valid`.
|
|
481
|
-
* **Wait until touched**: `if (input.touched)` prevents errors from showing on load.
|
|
482
|
-
* **Silent Check**: `!input.validity.valid` prevents side effects (looping).
|
|
483
|
-
4. **Re-rendering**: Since validation often updates on `blur`, ensure your component re-renders to show the error state (e.g., via `@blur=${() => this.requestUpdate()}` or generic event handlers).
|
|
484
|
-
|
|
485
|
-
#### Complete Nested Form Example (FormArray)
|
|
486
|
-
|
|
487
|
-
This example demonstrates managing a dynamic list of items (e.g., Team Members) using `FormArray`.
|
|
339
|
+
You can also manually check error states (e.g. for async validation):
|
|
488
340
|
|
|
489
341
|
```typescript
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (input && input.touched && !input.validity.valid) {
|
|
495
|
-
return html`<div class="error">${input.validationMessage}</div>`;
|
|
342
|
+
async validateEmail(element: HTMLInputElement) {
|
|
343
|
+
const exists = await checkServer(element.value);
|
|
344
|
+
if (exists) {
|
|
345
|
+
return { key: 'emailExists', message: 'Email already taken' };
|
|
496
346
|
}
|
|
497
|
-
return html``;
|
|
498
347
|
}
|
|
499
348
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
<!-- Main Form Field -->
|
|
507
|
-
<div>
|
|
508
|
-
<label>Team Name</label>
|
|
509
|
-
<input
|
|
510
|
-
name="teamName"
|
|
511
|
-
required
|
|
512
|
-
.value=${this.form.value.teamName}
|
|
513
|
-
@blur=${() => this.requestUpdate()}
|
|
514
|
-
/>
|
|
515
|
-
${ErrorTemplate(this.form.get('teamName'))}
|
|
516
|
-
</div>
|
|
517
|
-
|
|
518
|
-
<h3>Members</h3>
|
|
519
|
-
|
|
520
|
-
<!-- FormArray Iteration -->
|
|
521
|
-
${this.members.controls.map((control, index) => html`
|
|
522
|
-
<div class="member-row">
|
|
523
|
-
|
|
524
|
-
<!-- Nested Field: Name -->
|
|
525
|
-
<!-- Notice the name syntax: arrayName[index].fieldName -->
|
|
526
|
-
<div class="field">
|
|
527
|
-
<input
|
|
528
|
-
placeholder="Member Name"
|
|
529
|
-
name="members[${index}].name"
|
|
530
|
-
required
|
|
531
|
-
.value=${control.value.name}
|
|
532
|
-
@blur=${() => this.requestUpdate()}
|
|
533
|
-
/>
|
|
534
|
-
<!-- Access nested control using .get() on the group -->
|
|
535
|
-
${ErrorTemplate(control.get('name'))}
|
|
536
|
-
</div>
|
|
537
|
-
|
|
538
|
-
<!-- Nested Field: Role -->
|
|
539
|
-
<div class="field">
|
|
540
|
-
<input
|
|
541
|
-
placeholder="Role"
|
|
542
|
-
name="members[${index}].role"
|
|
543
|
-
required
|
|
544
|
-
.value=${control.value.role}
|
|
545
|
-
@blur=${() => this.requestUpdate()}
|
|
546
|
-
/>
|
|
547
|
-
${ErrorTemplate(control.get('role'))}
|
|
548
|
-
</div>
|
|
549
|
-
|
|
550
|
-
<button type="button" @click=${() => this.removeMember(index)}>
|
|
551
|
-
Remove
|
|
552
|
-
</button>
|
|
553
|
-
</div>
|
|
554
|
-
`)}
|
|
555
|
-
|
|
556
|
-
<button type="button" @click=${() => this.addMember()}>
|
|
557
|
-
Add Member
|
|
558
|
-
</button>
|
|
559
|
-
|
|
560
|
-
<button type="submit" ?disabled=${this.form.invalid}>
|
|
561
|
-
Submit Team
|
|
562
|
-
</button>
|
|
563
|
-
</form>
|
|
564
|
-
`;
|
|
565
|
-
},
|
|
566
|
-
})
|
|
567
|
-
export class TeamForm extends LitElement {
|
|
568
|
-
@Form({
|
|
569
|
-
name: 'team-form',
|
|
570
|
-
strategy: 'change',
|
|
571
|
-
})
|
|
572
|
-
form = new FormGroup({
|
|
573
|
-
teamName: '',
|
|
574
|
-
members: new FormArray<{ name: string; role: string }>([]),
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
// Helper to cast the control to FormArray for better typing
|
|
578
|
-
get members() {
|
|
579
|
-
return this.form.get('members') as FormArray;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
addMember() {
|
|
583
|
-
this.members.push(
|
|
584
|
-
new FormGroup({
|
|
585
|
-
name: '',
|
|
586
|
-
role: '',
|
|
587
|
-
})
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
removeMember(index: number) {
|
|
592
|
-
this.members.removeAt(index);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
```
|
|
349
|
+
// In Template
|
|
350
|
+
${this.form.hasError('email', 'emailExists')
|
|
351
|
+
? html`<div class="error">Email taken!</div>`
|
|
352
|
+
: ''}
|
|
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
|
});
|
|
@@ -106,6 +112,7 @@ class FormArray {
|
|
|
106
112
|
getParentElement() {
|
|
107
113
|
return this.parentElement;
|
|
108
114
|
}
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures
|
|
109
116
|
set value(values) {
|
|
110
117
|
if (!Array.isArray(values)) {
|
|
111
118
|
return;
|
|
@@ -117,5 +124,24 @@ class FormArray {
|
|
|
117
124
|
});
|
|
118
125
|
this.updateValue();
|
|
119
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
|
+
}
|
|
120
146
|
}
|
|
121
147
|
exports.FormArray = FormArray;
|
package/dist/form.decorator.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Form = Form;
|
|
4
|
-
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
5
|
const rxjs_1 = require("rxjs");
|
|
6
|
+
const form_group_1 = require("./form.group");
|
|
6
7
|
function Form(options = {
|
|
7
8
|
strategy: 'none',
|
|
8
9
|
}) {
|
|
@@ -18,6 +19,9 @@ function Form(options = {
|
|
|
18
19
|
throw new Error('Value provided is not an instance of FormGroup!');
|
|
19
20
|
}
|
|
20
21
|
this[name].setParentElement(this).setOptions(options).prepareValues();
|
|
22
|
+
if (options.model && this[options.model]) {
|
|
23
|
+
this[name].patchValue(this[options.model]);
|
|
24
|
+
}
|
|
21
25
|
return Connect.call(this);
|
|
22
26
|
};
|
|
23
27
|
clazz.constructor.prototype.firstUpdated = function () {
|
package/dist/form.group.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { LitElement } from '@rxdi/lit-html';
|
|
2
|
-
import { AbstractControl, AbstractInput, ErrorObject, FormInputOptions, FormOptions, 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
|
-
}> implements AbstractControl<T
|
|
5
|
+
}> implements AbstractControl<UnwrapValue<T>> {
|
|
6
6
|
validators: Map<string, ValidatorFn[]>;
|
|
7
7
|
valid: boolean;
|
|
8
8
|
invalid: boolean;
|
|
9
|
-
errors: T
|
|
9
|
+
errors: UnwrapValue<T>;
|
|
10
10
|
private controls;
|
|
11
11
|
private readonly _valueChanges;
|
|
12
12
|
private form;
|
|
@@ -22,7 +22,7 @@ export declare class FormGroup<T = FormInputOptions, E = {
|
|
|
22
22
|
getParentElement(): LitElement;
|
|
23
23
|
setOptions(options: FormOptions): this;
|
|
24
24
|
getOptions(): FormOptions;
|
|
25
|
-
get valueChanges(): import("rxjs").Observable<T
|
|
25
|
+
get valueChanges(): import("rxjs").Observable<UnwrapValue<T>>;
|
|
26
26
|
updateValueAndValidity(): Promise<(ErrorObject | {
|
|
27
27
|
message: string;
|
|
28
28
|
})[]>;
|
|
@@ -38,18 +38,19 @@ export declare class FormGroup<T = FormInputOptions, E = {
|
|
|
38
38
|
private getModelKeyName;
|
|
39
39
|
validate(element: AbstractInput): Promise<ErrorObject>;
|
|
40
40
|
private mapInputErrors;
|
|
41
|
-
get(name:
|
|
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;
|
|
45
45
|
setFormValidity(validity?: boolean): void;
|
|
46
46
|
resetErrors(): void;
|
|
47
|
-
get value(): T
|
|
48
|
-
set value(value: T);
|
|
47
|
+
get value(): UnwrapValue<T>;
|
|
48
|
+
set value(value: UnwrapValue<T>);
|
|
49
49
|
unsubscribe(): void;
|
|
50
50
|
getValue(name: keyof T): T[keyof T];
|
|
51
|
+
patchValue(value: Partial<UnwrapValue<T>>): void;
|
|
51
52
|
setValue(name: keyof T, value: unknown): void;
|
|
52
|
-
setFormValue(value: T): void;
|
|
53
|
+
setFormValue(value: UnwrapValue<T>): void;
|
|
53
54
|
setFormElement(form: HTMLFormElement): this;
|
|
54
55
|
setInputs(inputs: AbstractInput[]): void;
|
|
55
56
|
getFormElement(): HTMLFormElement;
|
package/dist/form.group.js
CHANGED
|
@@ -26,7 +26,10 @@ class FormGroup {
|
|
|
26
26
|
this._valueChanges = new rxjs_1.BehaviorSubject(value);
|
|
27
27
|
if (value) {
|
|
28
28
|
Object.keys(value).forEach((key) => {
|
|
29
|
-
if (typeof value[key] === 'object' &&
|
|
29
|
+
if (typeof value[key] === 'object' &&
|
|
30
|
+
value[key] !== null &&
|
|
31
|
+
(value[key]['controls'] || value[key]['push']) &&
|
|
32
|
+
value[key]['valueChanges']) {
|
|
30
33
|
// It's likely a FormGroup or FormArray
|
|
31
34
|
const control = value[key];
|
|
32
35
|
if (control.name === '' || control.name === undefined) {
|
|
@@ -63,7 +66,12 @@ class FormGroup {
|
|
|
63
66
|
this.validators.set(v, [...oldValidators, val]);
|
|
64
67
|
});
|
|
65
68
|
}
|
|
66
|
-
if (value[0]
|
|
69
|
+
if (value[0] === undefined || value[0] === null) {
|
|
70
|
+
this.value[v] = '';
|
|
71
|
+
}
|
|
72
|
+
else if (value[0].constructor === String ||
|
|
73
|
+
value[0].constructor === Number ||
|
|
74
|
+
value[0].constructor === Boolean) {
|
|
67
75
|
this.value[v] = value[0];
|
|
68
76
|
}
|
|
69
77
|
else {
|
|
@@ -88,8 +96,9 @@ class FormGroup {
|
|
|
88
96
|
setOptions(options) {
|
|
89
97
|
this.options = options;
|
|
90
98
|
this.controls.forEach((c) => {
|
|
91
|
-
if (c.setOptions)
|
|
92
|
-
c.setOptions(options);
|
|
99
|
+
if (c.setOptions) {
|
|
100
|
+
c.setOptions(Object.assign(Object.assign({}, options), { namespace: this.options.namespace ? `${this.options.namespace}.${c.name}` : c.name }));
|
|
101
|
+
}
|
|
93
102
|
});
|
|
94
103
|
return this;
|
|
95
104
|
}
|
|
@@ -322,6 +331,14 @@ class FormGroup {
|
|
|
322
331
|
if (this.controls.has(name)) {
|
|
323
332
|
return this.controls.get(name);
|
|
324
333
|
}
|
|
334
|
+
if (String(name).includes('.')) {
|
|
335
|
+
const names = String(name).split('.');
|
|
336
|
+
const key = names.shift();
|
|
337
|
+
const control = this.controls.get(key);
|
|
338
|
+
if (control && control.get) {
|
|
339
|
+
return control.get(names.join('.'));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
325
342
|
return this.inputs.get(name);
|
|
326
343
|
}
|
|
327
344
|
getError(inputName, errorKey) {
|
|
@@ -368,6 +385,19 @@ class FormGroup {
|
|
|
368
385
|
getValue(name) {
|
|
369
386
|
return this.value[name];
|
|
370
387
|
}
|
|
388
|
+
patchValue(value) {
|
|
389
|
+
if (!value) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
Object.keys(value).forEach((key) => {
|
|
393
|
+
if (this.controls.has(key) && this.controls.get(key)['patchValue']) {
|
|
394
|
+
this.controls.get(key)['patchValue'](value[key]);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this.setValue(key, value[key]);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
371
401
|
setValue(name, value) {
|
|
372
402
|
const input = this.get(name);
|
|
373
403
|
if (!input) {
|
package/dist/form.tokens.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { LitElement } from '@rxdi/lit-html';
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
|
+
export type UnwrapValue<T> = T extends AbstractControl<infer U> ? U : T extends {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
} ? {
|
|
6
|
+
[K in keyof T]: UnwrapValue<T[K]>;
|
|
7
|
+
} : T;
|
|
8
|
+
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
|
|
9
|
+
export type NestedKeyOf<T, D extends number = 3> = [D] extends [0] ? never : T extends object ? {
|
|
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
|
+
}[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;
|
|
3
13
|
export type FormStrategies = keyof WindowEventMap;
|
|
4
14
|
export interface FormOptions {
|
|
5
15
|
/** Name of the form element */
|
|
@@ -20,6 +30,10 @@ export interface FormOptions {
|
|
|
20
30
|
* Internal property for handling nested forms.
|
|
21
31
|
*/
|
|
22
32
|
namespace?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Property name of the model to bind to the form
|
|
35
|
+
*/
|
|
36
|
+
model?: string;
|
|
23
37
|
}
|
|
24
38
|
export interface AbstractControl<T = any> {
|
|
25
39
|
setOptions(options: FormOptions): this | void;
|
|
@@ -69,3 +83,4 @@ export declare const InputValidityState: {
|
|
|
69
83
|
valueMissing: "valueMissing";
|
|
70
84
|
};
|
|
71
85
|
export type InputValidityState = keyof typeof InputValidityState;
|
|
86
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"name": "@rxdi/forms",
|
|
3
|
+
"version": "0.7.215",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"author": "Kristiyan Tachev",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/rxdi/forms"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@rxdi/lit-html": "^0.7.214",
|
|
16
|
+
"@types/node": "^25.0.3",
|
|
17
|
+
"rxjs": "^7.8.2",
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"rxjs": "^7.8.2",
|
|
22
|
+
"@rxdi/lit-html": "*"
|
|
23
|
+
},
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"typings": "./dist/index.d.ts"
|
|
27
27
|
}
|