@rxdi/forms 0.7.214 → 0.7.215

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