@rxdi/forms 0.7.213 → 0.7.214

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,290 @@
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
+
41
+ <!-- Deep Binding with Dot Notation -->
32
42
  <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
43
+ name="firstName"
44
+ .value=${this.form.value.firstName}
45
+ @blur=${() => this.requestUpdate()}
40
46
  />
47
+
48
+ <!-- Nested Group Binding -->
41
49
  <input
42
- type="password"
43
- value=${this.form.value.password}
44
- name="password"
45
- placeholder="Password"
46
- required=""
50
+ name="address.city"
51
+ .value=${this.form.value.address.city}
52
+ @blur=${() => this.requestUpdate()}
47
53
  />
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>
54
+
55
+ <button type="submit">Save</button>
56
56
  </form>
57
57
  `;
58
58
  }
59
59
  })
60
- export class LoginComponent extends BaseComponent {
60
+ export class UserProfile extends LitElement {
61
+
62
+ // Model to bind
63
+ @property({ type: Object })
64
+ user: UserParams = {
65
+ firstName: 'John',
66
+ address: { city: 'New York', street: '5th Ave' }
67
+ };
68
+
61
69
  @Form({
70
+ name: 'user-form',
62
71
  strategy: 'change',
63
- name: 'my-form'
72
+ model: 'user' // Automatic Model Binding!
64
73
  })
65
- private form = new FormGroup({
66
- password: '',
67
- email: '',
68
- rememberMe: ''
74
+ form = new FormGroup({
75
+ firstName: '',
76
+ address: new FormGroup({
77
+ city: '',
78
+ street: ''
79
+ })
69
80
  });
70
81
 
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;
82
+ onSubmit(e: Event) {
83
+ e.preventDefault();
84
+ console.log(this.form.value);
85
+ // Output: { firstName: 'John', address: { city: 'New York', street: '5th Ave' } }
81
86
  }
82
87
  }
83
-
84
88
  ```
85
89
 
90
+ ## New Features
86
91
 
87
-
88
- #### Error handling and validators
92
+ ### Automatic Model Binding
93
+ Use the `model` property in the `@Form` decorator to automatically populate the form from a component property.
89
94
 
90
95
  ```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
- }
96
+ @Form({
97
+ name: 'my-form',
98
+ model: 'myData' // matches this.myData
129
99
  })
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');
144
-
145
- this.form.get('password'); // returns HTMLIntputElement
146
- this.form.hasError('email', 'blabla')
147
- }
148
-
149
- onSubmit(event: Event) {
150
- this.form.values;
151
- }
152
-
153
- validateEmail(element: HTMLInputElement) {
154
- if (element.value === 'restrictedEmail@gmail.com') {
155
- return { key: 'blabla', message: 'Please specify different email'};
156
- }
157
- }
158
- }
159
-
100
+ form = new FormGroup({ ... });
160
101
  ```
102
+ The library reads `this.myData` during initialization and calls `form.patchValue(this.myData)`.
161
103
 
162
-
163
-
164
- #### Group multiple inputs with single check intaraction
165
-
166
- > By default all inputs with same attribute `name` are binded together,
104
+ ### Nested FormGroups & FormArray
105
+ You can nest `FormGroup`s arbitrarily deep.
167
106
 
168
107
  ```typescript
169
- @Form({
170
- strategy: 'change',
171
- name: 'my-form'
172
- })
173
- private form = new FormGroup({
174
- condition: ''
175
- });
108
+ form = new FormGroup({
109
+ meta: new FormGroup({
110
+ id: 1,
111
+ flags: new FormGroup({
112
+ isActive: true,
113
+ isAdmin: false
114
+ })
115
+ }),
116
+ tags: new FormArray([
117
+ new FormGroup({ label: 'red' })
118
+ ])
119
+ });
176
120
  ```
177
121
 
122
+ **Template Binding:**
123
+ Use dot notation for nested controls:
178
124
  ```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>
125
+ <input name="meta.flags.isActive" type="checkbox" />
205
126
  ```
206
127
 
128
+ ### Type Safety & Autosuggestion
129
+ The library now extensively uses advanced TypeScript features:
130
+ - **`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!
132
+ ```typescript
133
+ // TypeScript knows this is valid:
134
+ this.form.get('meta.flags.isActive');
207
135
 
208
- #### Group multiple inputs with multi check intaraction
209
-
136
+ // And this is invalid:
137
+ this.form.get('meta.flags.wrongProp'); // Error!
138
+ ```
210
139
 
211
- > To remove binding we can set `multi: false` when defining our form
140
+ ### Recursive PatchValue
141
+ Update multiple fields deeply at once:
212
142
 
213
143
  ```typescript
214
- @Form({
215
- strategy: 'change',
216
- name: 'my-form',
217
- multi: false
218
- })
219
- private form = new FormGroup({
220
- condition: ''
221
- });
144
+ this.form.patchValue({
145
+ meta: {
146
+ flags: {
147
+ isActive: false
148
+ }
149
+ }
150
+ });
151
+ // Only updates 'isActive', leaves other fields untouched.
222
152
  ```
223
153
 
154
+ ## API Reference
224
155
 
225
-
226
- #### Native browser errors
227
-
228
- By default this library uses native error messages provided by HTML5 form validation API
229
-
230
-
231
- You can create your error template as follow:
156
+ ### Validators
157
+ Validators are async functions returning `InputErrorMessage` or `void`.
232
158
 
233
159
  ```typescript
234
- import { html } from '@rxdi/lit-html';
235
-
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
- `;
160
+ export function CustomValidator(element: AbstractInput) {
161
+ if (element.value === 'invalid') {
162
+ return { key: 'customError', message: 'Value is invalid' };
243
163
  }
244
- return '';
245
164
  }
246
- ```
247
165
 
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.
166
+ // Usage
167
+ new FormGroup({
168
+ field: ['', [CustomValidator]]
169
+ })
249
170
  ```
250
171
 
172
+ ### Error Display Information
173
+ Use the `touched` and `validity.valid` properties for clean UI.
251
174
 
252
- Usage
253
-
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>
175
+ ```typescript
176
+ function ErrorTemplate(input: AbstractInput) {
177
+ if (input?.touched && !input.validity.valid) {
178
+ return html`<div class="error">${input.validationMessage}</div>`;
179
+ }
180
+ return html``;
181
+ }
267
182
  ```
268
183
 
184
+ ## Advanced Usage
269
185
 
186
+ ### 1. Grouping Multiple Inputs (Checkbox Groups)
270
187
 
188
+ 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
189
 
272
- ##### Native HTML with JS
190
+ **Scenario:** A list of permissions where multiple can be selected.
273
191
 
274
192
  ```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: '',
193
+ @Form({
194
+ name: 'permissions-form',
195
+ multi: true // Enable multi-value binding for same-name inputs
196
+ })
197
+ form = new FormGroup({
198
+ roles: [] // Will be an array of values
293
199
  });
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
200
  ```
303
201
 
304
202
  ```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>
203
+ <label>
204
+ <input name="roles" type="checkbox" value="admin" /> Admin
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>
320
212
  ```
321
213
 
322
- #### Nested Forms (FormArray)
214
+ If the user checks "Admin" and "Viewer", `form.value.roles` will be `['admin', 'viewer']`.
323
215
 
324
- You can create nested forms using `FormArray` and `FormGroup`.
216
+ ### 2. Single Selection Checkbox (Radio Behavior with Checkboxes)
325
217
 
326
- ```typescript
327
- import { FormArray, FormGroup } from '@rxdi/forms';
218
+ If you want multiple checkboxes to act like a radio button (only one valid at a time) but with uncheck capability:
328
219
 
329
- const form = new FormGroup({
330
- users: new FormArray([
331
- new FormGroup({
332
- name: 'User 1',
333
- email: 'user1@gmail.com'
334
- })
335
- ])
220
+ ```typescript
221
+ @Form({
222
+ name: 'settings-form',
223
+ multi: false // Default behavior
224
+ })
225
+ form = new FormGroup({
226
+ mode: ''
336
227
  });
337
228
  ```
338
229
 
339
- Template usage:
340
-
341
230
  ```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>
356
- ```
357
-
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
- }
231
+ <label>
232
+ <input name="mode" type="checkbox" value="dark" /> Dark
233
+ </label>
234
+ <label>
235
+ <input name="mode" type="checkbox" value="light" /> Light
236
+ </label>
370
237
  ```
238
+ Checking "Dark" unchecks "Light" automatically.
371
239
 
372
- #### Type Safety & Decorator Checking
240
+ ### 3. Framework-Agnostic Usage (Vanilla JS)
373
241
 
374
- The `@Form` decorator now proactively checks that the decorated property is strongly typed as `FormGroup`.
242
+ You can use this library without Decorators or LitHtml, with any UI library or vanilla HTML.
375
243
 
376
244
  ```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`).
245
+ import { FormGroup } from '@rxdi/forms';
384
246
 
385
- Validators are also type-safe using the `ValidatorFn` type:
247
+ const form = new FormGroup({
248
+ email: '',
249
+ password: ''
250
+ });
386
251
 
387
- ```typescript
388
- import { ValidatorFn, AbstractInput, InputErrorMessage } from '@rxdi/forms';
252
+ // manually attach to DOM
253
+ const formElement = document.querySelector('form');
254
+ form
255
+ .setParentElement(document.body)
256
+ .setOptions({ name: 'my-form' })
257
+ .setFormElement(formElement)
258
+ .prepareValues()
259
+ .setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));
389
260
 
390
- const myValidator: ValidatorFn = async (element: AbstractInput) => {
391
- // ... validation logic
392
- };
261
+ // Listen to changes
262
+ form.valueChanges.subscribe(val => console.log(val));
393
263
  ```
394
264
 
395
- #### Complete Component Example
265
+ ### 4. Custom Error Handling Strategies
396
266
 
397
- Here is a complete, copy-pasteable example of a component using `@rxdi/forms` with best practices for validation and type safety.
267
+ By default, verification happens on `change` or `blur`. You can control this via `strategy`.
398
268
 
399
269
  ```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
- },
270
+ @Form({
271
+ name: 'login',
272
+ strategy: 'input' // Validate on every keystroke
463
273
  })
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
274
  ```
475
275
 
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`.
276
+ You can also manually check error states (e.g. for async validation):
488
277
 
489
278
  ```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>`;
279
+ async validateEmail(element: HTMLInputElement) {
280
+ const exists = await checkServer(element.value);
281
+ if (exists) {
282
+ return { key: 'emailExists', message: 'Email already taken' };
496
283
  }
497
- return html``;
498
284
  }
499
285
 
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
- }
286
+ // In Template
287
+ ${this.form.hasError('email', 'emailExists')
288
+ ? html`<div class="error">Email taken!</div>`
289
+ : ''}
595
290
  ```
@@ -106,6 +106,7 @@ class FormArray {
106
106
  getParentElement() {
107
107
  return this.parentElement;
108
108
  }
109
+ // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures
109
110
  set value(values) {
110
111
  if (!Array.isArray(values)) {
111
112
  return;
@@ -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 } 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): AbstractControl | AbstractInput;
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,14 @@
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;
3
12
  export type FormStrategies = keyof WindowEventMap;
4
13
  export interface FormOptions {
5
14
  /** Name of the form element */
@@ -20,6 +29,10 @@ export interface FormOptions {
20
29
  * Internal property for handling nested forms.
21
30
  */
22
31
  namespace?: string;
32
+ /**
33
+ * Property name of the model to bind to the form
34
+ */
35
+ model?: string;
23
36
  }
24
37
  export interface AbstractControl<T = any> {
25
38
  setOptions(options: FormOptions): this | void;
@@ -69,3 +82,4 @@ export declare const InputValidityState: {
69
82
  valueMissing: "valueMissing";
70
83
  };
71
84
  export type InputValidityState = keyof typeof InputValidityState;
85
+ 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.214",
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.213",
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
  }