@rxdi/forms 0.7.214 → 0.7.216

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,79 @@ this.form.patchValue({
149
154
  }
150
155
  });
151
156
  // Only updates 'isActive', leaves other fields untouched.
152
- ```
157
+ ````
158
+
159
+ ### Dynamic Array Inputs (FormArray)
160
+
161
+ For lists of primitive values, use `FormArray` with an `itemFactory` and automatic model binding. This removes the need for manual population.
162
+
163
+ #### Full Working Example
164
+
165
+ ```typescript
166
+ import { Component, html, LitElement, property } from '@rxdi/lit-html';
167
+ import { Form, FormGroup, FormArray } from '@rxdi/forms';
168
+
169
+
170
+ @Component({
171
+ selector: 'tags-component',
172
+ template(this: TagsComponent) {
173
+ return html`
174
+ <form @submit=${(e) => e.preventDefault()}>
175
+ <h3>Tags</h3>
176
+
177
+ <!-- List Tags -->
178
+ ${this.form.get('tags').controls.map(
179
+ (control, index) => html`
180
+ <div class="tag-row">
181
+ <input name="tags[${index}].value" .value=${control.value.value} @blur=${() => this.requestUpdate()} />
182
+ <button type="button" @click=${() => this.removeTag(index)}>Remove</button>
183
+ </div>
184
+ `
185
+ )}
186
+
187
+ <button type="button" @click=${() => this.addTag()}>Add Tag</button>
188
+ <button type="button" @click=${() => this.onSubmit()}>Submit</button>
189
+ </form>
190
+ `;
191
+ },
192
+ })
193
+ export class TagsComponent extends LitElement {
194
+ // Model automatically binds to 'tags' in form
195
+ @property({ type: Array })
196
+ tags = ['news', 'tech'];
197
+
198
+ @Form({
199
+ name: 'tags-form',
200
+ model: 'tags', // Triggers form.patchValue(this.tags) on INIT
201
+ })
202
+ form = new FormGroup({
203
+ tags: new FormArray<{ value: string }>([], {
204
+ name: 'tags',
205
+ // Factory describes how to create new controls from model data
206
+ itemFactory: (value) => new FormGroup({ value: value.value || value }),
207
+ }),
208
+ });
209
+
210
+ addTag() {
211
+ this.form.get('tags').push(new FormGroup({ value: '' }));
212
+ }
213
+
214
+ removeTag(index: number) {
215
+ this.form.get('tags').removeAt(index);
216
+ }
217
+
218
+ onSubmit() {
219
+ const dirtyTags = this.form.value.tags;
220
+ console.log(dirtyTags.map((t) => t.value));
221
+ }
222
+ }
223
+
224
+ ````
153
225
 
154
226
  ## API Reference
155
227
 
156
228
  ### Validators
229
+
157
230
  Validators are async functions returning `InputErrorMessage` or `void`.
158
231
 
159
232
  ```typescript
@@ -165,11 +238,12 @@ export function CustomValidator(element: AbstractInput) {
165
238
 
166
239
  // Usage
167
240
  new FormGroup({
168
- field: ['', [CustomValidator]]
169
- })
241
+ field: ['', [CustomValidator]],
242
+ });
170
243
  ```
171
244
 
172
245
  ### Error Display Information
246
+
173
247
  Use the `touched` and `validity.valid` properties for clean UI.
174
248
 
175
249
  ```typescript
@@ -200,15 +274,9 @@ form = new FormGroup({
200
274
  ```
201
275
 
202
276
  ```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>
277
+ <label> <input name="roles" type="checkbox" value="admin" /> Admin </label>
278
+ <label> <input name="roles" type="checkbox" value="editor" /> Editor </label>
279
+ <label> <input name="roles" type="checkbox" value="viewer" /> Viewer </label>
212
280
  ```
213
281
 
214
282
  If the user checks "Admin" and "Viewer", `form.value.roles` will be `['admin', 'viewer']`.
@@ -228,13 +296,10 @@ form = new FormGroup({
228
296
  ```
229
297
 
230
298
  ```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>
299
+ <label> <input name="mode" type="checkbox" value="dark" /> Dark </label>
300
+ <label> <input name="mode" type="checkbox" value="light" /> Light </label>
237
301
  ```
302
+
238
303
  Checking "Dark" unchecks "Light" automatically.
239
304
 
240
305
  ### 3. Framework-Agnostic Usage (Vanilla JS)
@@ -246,7 +311,7 @@ import { FormGroup } from '@rxdi/forms';
246
311
 
247
312
  const form = new FormGroup({
248
313
  email: '',
249
- password: ''
314
+ password: '',
250
315
  });
251
316
 
252
317
  // manually attach to DOM
@@ -259,7 +324,7 @@ form
259
324
  .setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));
260
325
 
261
326
  // Listen to changes
262
- form.valueChanges.subscribe(val => console.log(val));
327
+ form.valueChanges.subscribe((val) => console.log(val));
263
328
  ```
264
329
 
265
330
  ### 4. Custom Error Handling Strategies
@@ -284,7 +349,7 @@ async validateEmail(element: HTMLInputElement) {
284
349
  }
285
350
 
286
351
  // In Template
287
- ${this.form.hasError('email', 'emailExists')
288
- ? html`<div class="error">Email taken!</div>`
352
+ ${this.form.hasError('email', 'emailExists')
353
+ ? html`<div class="error">Email taken!</div>`
289
354
  : ''}
290
- ```
355
+ ```
@@ -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
  });
@@ -82,15 +88,14 @@ class FormArray {
82
88
  unsubscribe() {
83
89
  this.subscriptions.forEach((sub) => sub.unsubscribe());
84
90
  this.subscriptions.clear();
85
- this.controls.forEach((c) => c.unsubscribe && c.unsubscribe());
91
+ this.controls.forEach((c) => { var _a; return (_a = c.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(c); });
86
92
  }
87
93
  updateValue() {
88
94
  this._valueChanges.next(this.value);
89
95
  }
90
96
  requestUpdate() {
91
- if (this.parentElement) {
92
- this.parentElement.requestUpdate();
93
- }
97
+ var _a;
98
+ (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.requestUpdate();
94
99
  }
95
100
  setParentElement(parent) {
96
101
  this.parentElement = parent;
@@ -112,8 +117,21 @@ class FormArray {
112
117
  return;
113
118
  }
114
119
  values.forEach((v, i) => {
115
- if (this.controls[i]) {
116
- this.controls[i].value = v;
120
+ this.controls[i] && (this.controls[i].value = v);
121
+ });
122
+ this.updateValue();
123
+ }
124
+ patchValue(values) {
125
+ if (!Array.isArray(values)) {
126
+ return;
127
+ }
128
+ values.forEach((v, i) => {
129
+ const control = this.controls[i];
130
+ if (control) {
131
+ control.patchValue ? control.patchValue(v) : (control.value = v);
132
+ }
133
+ else if (this.options.itemFactory) {
134
+ this.push(this.options.itemFactory(v));
117
135
  }
118
136
  });
119
137
  this.updateValue();
@@ -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, DeepPropType, ErrorObject, FormInputOptions, FormOptions, NestedKeyOf, UnwrapValue, ValidatorFn } 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;
@@ -46,10 +46,13 @@ class FormGroup {
46
46
  }
47
47
  }
48
48
  init() {
49
+ if (!this.parentElement) {
50
+ return;
51
+ }
49
52
  this.setFormElement(this.querySelectForm(this.parentElement.shadowRoot || this.parentElement)).setInputs(this.mapEventToInputs(this.querySelectorAllInputs()));
50
53
  this.controls.forEach((c) => {
51
- if (c.init)
52
- c.init();
54
+ var _a;
55
+ (_a = c.init) === null || _a === void 0 ? void 0 : _a.call(c);
53
56
  });
54
57
  }
55
58
  prepareValues() {
@@ -84,9 +87,8 @@ class FormGroup {
84
87
  setParentElement(parent) {
85
88
  this.parentElement = parent;
86
89
  this.controls.forEach((c) => {
87
- if (c.setParentElement) {
88
- c.setParentElement(parent);
89
- }
90
+ var _a;
91
+ (_a = c.setParentElement) === null || _a === void 0 ? void 0 : _a.call(c, parent);
90
92
  });
91
93
  return this;
92
94
  }
@@ -96,9 +98,8 @@ class FormGroup {
96
98
  setOptions(options) {
97
99
  this.options = options;
98
100
  this.controls.forEach((c) => {
99
- if (c.setOptions) {
100
- c.setOptions(Object.assign(Object.assign({}, options), { namespace: this.options.namespace ? `${this.options.namespace}.${c.name}` : c.name }));
101
- }
101
+ var _a;
102
+ (_a = c.setOptions) === null || _a === void 0 ? void 0 : _a.call(c, Object.assign(Object.assign({}, options), { namespace: this.options.namespace ? `${this.options.namespace}.${c.name}` : c.name }));
102
103
  });
103
104
  return this;
104
105
  }
@@ -200,6 +201,9 @@ class FormGroup {
200
201
  if (this.options['form']) {
201
202
  return this.options['form'];
202
203
  }
204
+ if (!(shadowRoot === null || shadowRoot === void 0 ? void 0 : shadowRoot.querySelector)) {
205
+ return null;
206
+ }
203
207
  const form = shadowRoot.querySelector(`form[name="${this.options.name}"]`);
204
208
  if (!form) {
205
209
  throw new Error(`Form element with name "${this.options.name}" not present inside ${this.getParentElement().outerHTML} component`);
@@ -335,7 +339,7 @@ class FormGroup {
335
339
  const names = String(name).split('.');
336
340
  const key = names.shift();
337
341
  const control = this.controls.get(key);
338
- if (control && control.get) {
342
+ if (control === null || control === void 0 ? void 0 : control.get) {
339
343
  return control.get(names.join('.'));
340
344
  }
341
345
  }
@@ -390,8 +394,9 @@ class FormGroup {
390
394
  return;
391
395
  }
392
396
  Object.keys(value).forEach((key) => {
393
- if (this.controls.has(key) && this.controls.get(key)['patchValue']) {
394
- this.controls.get(key)['patchValue'](value[key]);
397
+ const control = this.controls.get(key);
398
+ if (control === null || control === void 0 ? void 0 : control['patchValue']) {
399
+ control['patchValue'](value[key]);
395
400
  }
396
401
  else {
397
402
  this.setValue(key, value[key]);
@@ -417,8 +422,8 @@ class FormGroup {
417
422
  setFormElement(form) {
418
423
  this.form = form;
419
424
  this.controls.forEach((c) => {
420
- if (c.setFormElement)
421
- c.setFormElement(form);
425
+ var _a;
426
+ (_a = c.setFormElement) === null || _a === void 0 ? void 0 : _a.call(c, form);
422
427
  });
423
428
  return this;
424
429
  }
@@ -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.216",
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.215",
16
16
  "@types/node": "^25.0.3",
17
17
  "rxjs": "^7.8.2",
18
18
  "typescript": "^5.9.3"