@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 CHANGED
@@ -1,595 +1,353 @@
1
- # Reactive forms binding for LitHtml
1
+ # Reactive Forms for LitHtml (Enhanced)
2
2
 
3
- #### Install
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
- #### Using it inside component
13
+ ## Installation
11
14
 
12
- ##### Important!
13
-
14
- > Define `<form>` element with `name` `<form name"my-form"></form>`
15
+ ```bash
16
+ npm i @rxdi/forms
17
+ ```
15
18
 
16
- > Put `my-form` inside @Form({ name: 'my-form' }) decorator since this will be our form selector
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 { FormGroup, Form } from '@rxdi/forms';
22
- import { BaseComponent } from '../shared/base.component';
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: 'login-component',
29
- template(this: LoginComponent) {
36
+ selector: 'user-profile',
37
+ template(this: UserProfile) {
30
38
  return html`
31
- <form name="my-form" @submit=${this.onSubmit}>
39
+ <form name="user-form" @submit=${this.onSubmit}>
40
+ <!-- Deep Binding with Dot Notation -->
32
41
  <input
33
- style="margin-bottom: 20px;"
34
- name="email"
35
- type="email"
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
- type="password"
43
- value=${this.form.value.password}
44
- name="password"
45
- placeholder="Password"
46
- required=""
49
+ name="address.city"
50
+ .value=${this.form.value.address.city}
51
+ @blur=${() => this.requestUpdate()}
47
52
  />
48
- <div>
49
- <label>
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 LoginComponent extends BaseComponent {
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
- name: 'my-form'
70
+ model: 'user', // Automatic Model Binding!
64
71
  })
65
- private form = new FormGroup({
66
- password: '',
67
- email: '',
68
- rememberMe: ''
72
+ form = new FormGroup({
73
+ firstName: '',
74
+ address: new FormGroup({
75
+ city: '',
76
+ street: '',
77
+ }),
69
78
  });
70
79
 
71
- OnInit() {
72
- this.form.valueChanges.subscribe(values => {
73
- values; // password, email, rememberMe
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
- #### Error handling and validators
92
+ Use the `model` property in the `@Form` decorator to automatically populate the form from a component property.
89
93
 
90
94
  ```typescript
91
- import { html, Component } from '@rxdi/lit-html';
92
- import { FormGroup, Form } from '@rxdi/forms';
93
- import { BaseComponent } from '../shared/base.component';
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
- export class LoginComponent extends BaseComponent {
131
- @Form({
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
- this.form.get('password'); // returns HTMLIntputElement
146
- this.form.hasError('email', 'blabla')
147
- }
102
+ The library reads `this.myData` during initialization and calls `form.patchValue(this.myData)`.
148
103
 
149
- onSubmit(event: Event) {
150
- this.form.values;
151
- }
104
+ ### Nested FormGroups & FormArray
152
105
 
153
- validateEmail(element: HTMLInputElement) {
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
- #### Group multiple inputs with single check intaraction
128
+ ### Type Safety & Autosuggestion
165
129
 
166
- > By default all inputs with same attribute `name` are binded together,
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
- @Form({
170
- strategy: 'change',
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
- ```html
179
- <label>
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
- #### Group multiple inputs with multi check intaraction
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
- > To remove binding we can set `multi: false` when defining our form
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
- strategy: 'change',
216
- name: 'my-form',
217
- multi: false
197
+ name: 'tags-form',
198
+ model: 'tags', // Triggers form.patchValue(this.tags) on INIT
218
199
  })
219
- private form = new FormGroup({
220
- condition: ''
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
- #### Native browser errors
216
+ onSubmit() {
217
+ const dirtyTags = this.form.value.tags;
218
+ console.log(dirtyTags.map((t) => t.value));
219
+ }
220
+ }
227
221
 
228
- By default this library uses native error messages provided by HTML5 form validation API
222
+ ````
229
223
 
224
+ ## API Reference
230
225
 
231
- You can create your error template as follow:
226
+ ### Validators
232
227
 
233
- ```typescript
234
- import { html } from '@rxdi/lit-html';
228
+ Validators are async functions returning `InputErrorMessage` or `void`.
235
229
 
236
- export function InputErrorTemplate(input: HTMLInputElement | AbstractInput) {
237
- // Check 'touched' to show errors only after interaction
238
- // Check 'validity.valid' silently to avoid side effects
239
- if (input && input.touched && !input.validity.valid) {
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
- > **Note:** Using `!input.validity.valid` is preferred over `!input.checkValidity()` inside templates because `checkValidity()` fires an `invalid` event which can cause recursive validation loops.
237
+ // Usage
238
+ new FormGroup({
239
+ field: ['', [CustomValidator]],
240
+ });
249
241
  ```
250
242
 
243
+ ### Error Display Information
251
244
 
252
- Usage
245
+ Use the `touched` and `validity.valid` properties for clean UI.
253
246
 
254
- ```html
255
- <form>
256
- <input
257
- name="email"
258
- type="email"
259
- value=${this.form.value.email}
260
- class="form-control"
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
- ##### Native HTML with JS
262
+ **Scenario:** A list of permissions where multiple can be selected.
273
263
 
274
264
  ```typescript
275
-
276
- import { FormGroup } from '@rxdi/forms';
277
-
278
- export function EmailValidator(element: HTMLInputElement) {
279
- const regex = /^([a-zA-Z0-9_\.\-]+)@([a-zA-Z0-9_\.\-]+)\.([a-zA-Z]{2,5})$/;
280
- if (!regex.test(element.value)) {
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
- <form name="my-form">
306
- <input
307
- name="email"
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
- #### Nested Forms (FormArray)
280
+ If the user checks "Admin" and "Viewer", `form.value.roles` will be `['admin', 'viewer']`.
323
281
 
324
- You can create nested forms using `FormArray` and `FormGroup`.
282
+ ### 2. Single Selection Checkbox (Radio Behavior with Checkboxes)
325
283
 
326
- ```typescript
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
- const form = new FormGroup({
330
- users: new FormArray([
331
- new FormGroup({
332
- name: 'User 1',
333
- email: 'user1@gmail.com'
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
- ${this.form.get('users').controls.map((group, index) => html`
343
- <div>
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
- Adding items dynamically:
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
- #### Type Safety & Decorator Checking
303
+ ### 3. Framework-Agnostic Usage (Vanilla JS)
373
304
 
374
- The `@Form` decorator now proactively checks that the decorated property is strongly typed as `FormGroup`.
305
+ You can use this library without Decorators or LitHtml, with any UI library or vanilla HTML.
375
306
 
376
307
  ```typescript
377
- export class MyComponent {
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
- Validators are also type-safe using the `ValidatorFn` type:
310
+ const form = new FormGroup({
311
+ email: '',
312
+ password: '',
313
+ });
386
314
 
387
- ```typescript
388
- import { ValidatorFn, AbstractInput, InputErrorMessage } from '@rxdi/forms';
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
- const myValidator: ValidatorFn = async (element: AbstractInput) => {
391
- // ... validation logic
392
- };
324
+ // Listen to changes
325
+ form.valueChanges.subscribe((val) => console.log(val));
393
326
  ```
394
327
 
395
- #### Complete Component Example
328
+ ### 4. Custom Error Handling Strategies
396
329
 
397
- Here is a complete, copy-pasteable example of a component using `@rxdi/forms` with best practices for validation and type safety.
330
+ By default, verification happens on `change` or `blur`. You can control this via `strategy`.
398
331
 
399
332
  ```typescript
400
- import { Component, html, LitElement } from '@rxdi/lit-html';
401
- import { Form, FormGroup, AbstractInput } from '@rxdi/forms';
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
- ### Key Takeaways for Templates
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
- import { Component, html, LitElement } from '@rxdi/lit-html';
491
- import { Form, FormGroup, FormArray, AbstractInput } from '@rxdi/forms';
492
-
493
- function ErrorTemplate(input: AbstractInput) {
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
- @Component({
501
- selector: 'team-form',
502
- template(this: TeamForm) {
503
- return html`
504
- <form @submit=${(e) => e.preventDefault()}>
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
+ ```
@@ -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>[], name?: string);
19
+ constructor(controls?: AbstractControl<T>[], nameOrOptions?: string | FormArrayOptions<T>);
17
20
  get value(): T[];
18
- getOptions(): FormOptions;
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
  }
@@ -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 = [], name = '') {
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
- this.name = name;
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;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Form = Form;
4
- const form_group_1 = require("./form.group");
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 () {
@@ -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: keyof T): AbstractControl<any> | AbstractInput;
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;
@@ -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' && value[key] !== null && (value[key]['controls'] || value[key]['push'])) {
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].constructor === String || value[0].constructor === Number || value[0].constructor === Boolean) {
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) {
@@ -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
- "name": "@rxdi/forms",
3
- "version": "0.7.213",
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.212",
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"
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
  }