@rxdi/forms 0.7.212 → 0.7.213

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
@@ -233,16 +233,21 @@ You can create your error template as follow:
233
233
  ```typescript
234
234
  import { html } from '@rxdi/lit-html';
235
235
 
236
- export function InputErrorTemplate(input: HTMLInputElement) {
237
- if (input && !input.checkValidity()) {
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) {
238
240
  return html`
239
- <div>${input.validationMessage}</div>
241
+ <div style="color:red; font-size: 13px;">${input.validationMessage}</div>
240
242
  `;
241
243
  }
242
244
  return '';
243
245
  }
244
246
  ```
245
247
 
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.
249
+ ```
250
+
246
251
 
247
252
  Usage
248
253
 
@@ -312,4 +317,279 @@ form
312
317
  />
313
318
  </form>
314
319
  <script src="./main.ts"></script>
320
+ ```
321
+
322
+ #### Nested Forms (FormArray)
323
+
324
+ You can create nested forms using `FormArray` and `FormGroup`.
325
+
326
+ ```typescript
327
+ import { FormArray, FormGroup } from '@rxdi/forms';
328
+
329
+ const form = new FormGroup({
330
+ users: new FormArray([
331
+ new FormGroup({
332
+ name: 'User 1',
333
+ email: 'user1@gmail.com'
334
+ })
335
+ ])
336
+ });
337
+ ```
338
+
339
+ Template usage:
340
+
341
+ ```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
+ }
370
+ ```
371
+
372
+ #### Type Safety & Decorator Checking
373
+
374
+ The `@Form` decorator now proactively checks that the decorated property is strongly typed as `FormGroup`.
375
+
376
+ ```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`).
384
+
385
+ Validators are also type-safe using the `ValidatorFn` type:
386
+
387
+ ```typescript
388
+ import { ValidatorFn, AbstractInput, InputErrorMessage } from '@rxdi/forms';
389
+
390
+ const myValidator: ValidatorFn = async (element: AbstractInput) => {
391
+ // ... validation logic
392
+ };
393
+ ```
394
+
395
+ #### Complete Component Example
396
+
397
+ Here is a complete, copy-pasteable example of a component using `@rxdi/forms` with best practices for validation and type safety.
398
+
399
+ ```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
+ },
463
+ })
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
+ ```
475
+
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`.
488
+
489
+ ```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>`;
496
+ }
497
+ return html``;
498
+ }
499
+
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
+ }
315
595
  ```
@@ -0,0 +1,31 @@
1
+ import { LitElement } from '@rxdi/lit-html';
2
+ import { BehaviorSubject } from 'rxjs';
3
+ import { AbstractControl, FormOptions } from './form.tokens';
4
+ export declare class FormArray<T = any> implements AbstractControl<T[]> {
5
+ controls: AbstractControl<T>[];
6
+ readonly valueChanges: BehaviorSubject<T[]>;
7
+ private readonly _valueChanges;
8
+ parentElement: LitElement;
9
+ name: string;
10
+ valid: boolean;
11
+ invalid: boolean;
12
+ errors: {};
13
+ private form;
14
+ private options;
15
+ private subscriptions;
16
+ constructor(controls?: AbstractControl<T>[], name?: string);
17
+ get value(): T[];
18
+ getOptions(): FormOptions;
19
+ setOptions(options: FormOptions): void;
20
+ push(control: AbstractControl<T>): Promise<void>;
21
+ removeAt(index: number): void;
22
+ private subscribeToControl;
23
+ unsubscribe(): void;
24
+ updateValue(): void;
25
+ requestUpdate(): void;
26
+ setParentElement(parent: LitElement): void;
27
+ setFormElement(form: HTMLFormElement): void;
28
+ init(): void;
29
+ getParentElement(): LitElement;
30
+ set value(values: T[]);
31
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FormArray = void 0;
13
+ const rxjs_1 = require("rxjs");
14
+ class FormArray {
15
+ constructor(controls = [], name = '') {
16
+ this.controls = [];
17
+ this.valid = true;
18
+ this.invalid = false;
19
+ this.errors = {};
20
+ this.options = {};
21
+ this.subscriptions = new Map();
22
+ this.controls = controls;
23
+ this.name = name;
24
+ this._valueChanges = new rxjs_1.BehaviorSubject(this.value);
25
+ this.valueChanges = this._valueChanges;
26
+ this.controls.forEach((c) => this.subscribeToControl(c));
27
+ }
28
+ get value() {
29
+ return this.controls.map((c) => c.value);
30
+ }
31
+ getOptions() {
32
+ return this.options;
33
+ }
34
+ setOptions(options) {
35
+ this.options = options;
36
+ this.controls.forEach((c, index) => {
37
+ c.setOptions(Object.assign(Object.assign({}, options), { namespace: `${this.name}[${index}]` }));
38
+ });
39
+ }
40
+ push(control) {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ this.controls.push(control);
43
+ this.subscribeToControl(control);
44
+ const index = this.controls.length - 1;
45
+ control.setOptions(Object.assign(Object.assign(Object.assign({}, this.options), control.getOptions()), { namespace: `${this.name}[${index}]` }));
46
+ this.updateValue();
47
+ this.requestUpdate();
48
+ if (this.parentElement) {
49
+ yield this.parentElement.updateComplete;
50
+ control.setParentElement(this.parentElement);
51
+ if (this.form) {
52
+ control.setFormElement(this.form);
53
+ control.init();
54
+ }
55
+ else if (control.getFormElement()) {
56
+ control.init();
57
+ }
58
+ else if (this.controls[0] && this.controls[0].getFormElement()) {
59
+ control.setFormElement(this.controls[0].getFormElement());
60
+ control.init();
61
+ }
62
+ }
63
+ });
64
+ }
65
+ removeAt(index) {
66
+ const control = this.controls[index];
67
+ if (this.subscriptions.has(control)) {
68
+ this.subscriptions.get(control).unsubscribe();
69
+ this.subscriptions.delete(control);
70
+ }
71
+ this.controls.splice(index, 1);
72
+ this.updateValue();
73
+ this.requestUpdate();
74
+ }
75
+ subscribeToControl(control) {
76
+ if (control.valueChanges) {
77
+ this.subscriptions.set(control, control.valueChanges.subscribe(() => {
78
+ this.updateValue();
79
+ }));
80
+ }
81
+ }
82
+ unsubscribe() {
83
+ this.subscriptions.forEach((sub) => sub.unsubscribe());
84
+ this.subscriptions.clear();
85
+ this.controls.forEach((c) => c.unsubscribe && c.unsubscribe());
86
+ }
87
+ updateValue() {
88
+ this._valueChanges.next(this.value);
89
+ }
90
+ requestUpdate() {
91
+ if (this.parentElement) {
92
+ this.parentElement.requestUpdate();
93
+ }
94
+ }
95
+ setParentElement(parent) {
96
+ this.parentElement = parent;
97
+ this.controls.forEach((c) => c.setParentElement(parent));
98
+ }
99
+ setFormElement(form) {
100
+ this.form = form;
101
+ this.controls.forEach((c) => c.setFormElement(form));
102
+ }
103
+ init() {
104
+ this.controls.forEach((c) => c.init());
105
+ }
106
+ getParentElement() {
107
+ return this.parentElement;
108
+ }
109
+ set value(values) {
110
+ if (!Array.isArray(values)) {
111
+ return;
112
+ }
113
+ values.forEach((v, i) => {
114
+ if (this.controls[i]) {
115
+ this.controls[i].value = v;
116
+ }
117
+ });
118
+ this.updateValue();
119
+ }
120
+ }
121
+ exports.FormArray = FormArray;
@@ -1,2 +1,2 @@
1
1
  import { FormOptions } from './form.tokens';
2
- export declare function Form(options?: FormOptions): (clazz: Object, name: string | number | symbol) => void;
2
+ export declare function Form(options?: FormOptions): (clazz: any, name: string | number | symbol) => void;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Form = Form;
4
4
  const form_group_1 = require("./form.group");
5
- const rx_fake_1 = require("./rx-fake");
5
+ const rxjs_1 = require("rxjs");
6
6
  function Form(options = {
7
7
  strategy: 'none',
8
8
  }) {
@@ -10,9 +10,9 @@ function Form(options = {
10
10
  if (!options.name) {
11
11
  throw new Error('Missing form name');
12
12
  }
13
- const Destroy = clazz.constructor.prototype.disconnectedCallback || rx_fake_1.noop;
14
- const UpdateFirst = clazz.constructor.prototype.firstUpdated || rx_fake_1.noop;
15
- const Connect = clazz.constructor.prototype.connectedCallback || rx_fake_1.noop;
13
+ const Destroy = clazz.constructor.prototype.disconnectedCallback || rxjs_1.noop;
14
+ const UpdateFirst = clazz.constructor.prototype.firstUpdated || rxjs_1.noop;
15
+ const Connect = clazz.constructor.prototype.connectedCallback || rxjs_1.noop;
16
16
  clazz.constructor.prototype.connectedCallback = function () {
17
17
  if (!(this[name] instanceof form_group_1.FormGroup)) {
18
18
  throw new Error('Value provided is not an instance of FormGroup!');
@@ -1,18 +1,20 @@
1
- import { FormInputOptions, FormOptions, ErrorObject, AbstractInput } from './form.tokens';
2
1
  import { LitElement } from '@rxdi/lit-html';
2
+ import { AbstractControl, AbstractInput, ErrorObject, FormInputOptions, FormOptions, ValidatorFn } from './form.tokens';
3
3
  export declare class FormGroup<T = FormInputOptions, E = {
4
4
  [key: string]: never;
5
- }> {
6
- validators: Map<string, Function[]>;
5
+ }> implements AbstractControl<T> {
6
+ validators: Map<string, ValidatorFn[]>;
7
7
  valid: boolean;
8
8
  invalid: boolean;
9
9
  errors: T;
10
+ private controls;
10
11
  private readonly _valueChanges;
11
12
  private form;
12
13
  private errorMap;
13
14
  private inputs;
14
15
  private options;
15
16
  private parentElement;
17
+ private subscriptions;
16
18
  constructor(value?: T, errors?: E);
17
19
  init(): void;
18
20
  prepareValues(): this;
@@ -21,7 +23,9 @@ export declare class FormGroup<T = FormInputOptions, E = {
21
23
  setOptions(options: FormOptions): this;
22
24
  getOptions(): FormOptions;
23
25
  get valueChanges(): import("rxjs").Observable<T>;
24
- updateValueAndValidity(): Promise<ErrorObject[]>;
26
+ updateValueAndValidity(): Promise<(ErrorObject | {
27
+ message: string;
28
+ })[]>;
25
29
  private updateValueAndValidityOnEvent;
26
30
  applyValidationContext({ errors, element }: ErrorObject): boolean;
27
31
  querySelectForm(shadowRoot: HTMLElement | ShadowRoot): HTMLFormElement;
@@ -31,11 +35,12 @@ export declare class FormGroup<T = FormInputOptions, E = {
31
35
  setElementValidity(el: AbstractInput, validity?: boolean): Promise<void>;
32
36
  setElementDirty(input: AbstractInput): void;
33
37
  isInputPresentOnStage(input: AbstractInput): number;
38
+ private getModelKeyName;
34
39
  validate(element: AbstractInput): Promise<ErrorObject>;
35
40
  private mapInputErrors;
36
- get(name: keyof T): AbstractInput;
37
- getError(inputName: keyof T, errorKey: keyof E): never;
38
- hasError(inputName: keyof T, errorKey: keyof E): boolean;
41
+ get(name: keyof T): AbstractControl<any> | AbstractInput;
42
+ getError(inputName: keyof T, errorKey: string): never;
43
+ hasError(inputName: keyof T, errorKey: string): boolean;
39
44
  reset(): void;
40
45
  setFormValidity(validity?: boolean): void;
41
46
  resetErrors(): void;
@@ -43,7 +48,7 @@ export declare class FormGroup<T = FormInputOptions, E = {
43
48
  set value(value: T);
44
49
  unsubscribe(): void;
45
50
  getValue(name: keyof T): T[keyof T];
46
- setValue(name: keyof T, value: any): void;
51
+ setValue(name: keyof T, value: unknown): void;
47
52
  setFormValue(value: T): void;
48
53
  setFormElement(form: HTMLFormElement): this;
49
54
  setInputs(inputs: AbstractInput[]): void;
@@ -10,36 +10,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FormGroup = void 0;
13
+ const rxjs_1 = require("rxjs");
13
14
  const form_tokens_1 = require("./form.tokens");
14
- const rx_fake_1 = require("./rx-fake");
15
15
  class FormGroup {
16
16
  constructor(value, errors) {
17
17
  this.validators = new Map();
18
18
  this.valid = true;
19
19
  this.invalid = false;
20
20
  this.errors = {};
21
+ this.controls = new Map();
21
22
  this.errorMap = new Map();
22
23
  this.inputs = new Map();
23
24
  this.options = {};
24
- this._valueChanges = new rx_fake_1.BehaviorSubject(value);
25
+ this.subscriptions = new Map();
26
+ this._valueChanges = new rxjs_1.BehaviorSubject(value);
27
+ if (value) {
28
+ Object.keys(value).forEach((key) => {
29
+ if (typeof value[key] === 'object' && value[key] !== null && (value[key]['controls'] || value[key]['push'])) {
30
+ // It's likely a FormGroup or FormArray
31
+ const control = value[key];
32
+ if (control.name === '' || control.name === undefined) {
33
+ control.name = key;
34
+ }
35
+ this.controls.set(key, control);
36
+ if (control.valueChanges) {
37
+ this.subscriptions.set(key, control.valueChanges.subscribe(() => {
38
+ this._valueChanges.next(this.value);
39
+ }));
40
+ }
41
+ }
42
+ });
43
+ }
25
44
  }
26
45
  init() {
27
46
  this.setFormElement(this.querySelectForm(this.parentElement.shadowRoot || this.parentElement)).setInputs(this.mapEventToInputs(this.querySelectorAllInputs()));
47
+ this.controls.forEach((c) => {
48
+ if (c.init)
49
+ c.init();
50
+ });
28
51
  }
29
52
  prepareValues() {
30
53
  Object.keys(this.value).forEach((v) => {
54
+ // Skip nested controls
55
+ if (this.controls.has(v))
56
+ return;
31
57
  const value = this.value[v];
32
58
  this.errors[v] = this.errors[v] || {};
33
- if (value.constructor === Array) {
59
+ if (value && value.constructor === Array) {
34
60
  if (value[1] && value[1].constructor === Array) {
35
61
  value[1].forEach((val) => {
36
62
  const oldValidators = this.validators.get(v) || [];
37
63
  this.validators.set(v, [...oldValidators, val]);
38
64
  });
39
65
  }
40
- if (value[0].constructor === String ||
41
- value[0].constructor === Number ||
42
- value[0].constructor === Boolean) {
66
+ if (value[0].constructor === String || value[0].constructor === Number || value[0].constructor === Boolean) {
43
67
  this.value[v] = value[0];
44
68
  }
45
69
  else {
@@ -51,6 +75,11 @@ class FormGroup {
51
75
  }
52
76
  setParentElement(parent) {
53
77
  this.parentElement = parent;
78
+ this.controls.forEach((c) => {
79
+ if (c.setParentElement) {
80
+ c.setParentElement(parent);
81
+ }
82
+ });
54
83
  return this;
55
84
  }
56
85
  getParentElement() {
@@ -58,6 +87,10 @@ class FormGroup {
58
87
  }
59
88
  setOptions(options) {
60
89
  this.options = options;
90
+ this.controls.forEach((c) => {
91
+ if (c.setOptions)
92
+ c.setOptions(options);
93
+ });
61
94
  return this;
62
95
  }
63
96
  getOptions() {
@@ -66,6 +99,7 @@ class FormGroup {
66
99
  get valueChanges() {
67
100
  return this._valueChanges.asObservable();
68
101
  }
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
103
  updateValueAndValidity() {
70
104
  return __awaiter(this, void 0, void 0, function* () {
71
105
  this.resetErrors();
@@ -75,11 +109,21 @@ class FormGroup {
75
109
  this.setElementDirty(i);
76
110
  return yield this.validate(i);
77
111
  })));
112
+ for (const [key, control] of this.controls.entries()) {
113
+ if (control.updateValueAndValidity) {
114
+ yield control.updateValueAndValidity();
115
+ if (control.invalid) {
116
+ this.invalid = true;
117
+ this.valid = false;
118
+ }
119
+ }
120
+ }
78
121
  this.getParentElement().requestUpdate();
79
- return inputs.filter((e) => e.errors.length);
122
+ return inputs.filter((e) => e.errors.length) || (this.invalid ? [{ message: 'Invalid Form' }] : []);
80
123
  });
81
124
  }
82
125
  updateValueAndValidityOnEvent(method) {
126
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
83
127
  const self = this;
84
128
  return function (event) {
85
129
  return __awaiter(this, void 0, void 0, function* () {
@@ -101,7 +145,7 @@ class FormGroup {
101
145
  value = Number(value);
102
146
  }
103
147
  const inputsWithBindings = [
104
- ...(self.getFormElement().querySelectorAll(`input[name="${this.name}"]:checked`)).values(),
148
+ ...self.getFormElement().querySelectorAll(`input[name="${this.name}"]:checked`).values(),
105
149
  ];
106
150
  if (hasMultipleBindings > 1) {
107
151
  if (!self.options.multi && this.type === 'checkbox') {
@@ -117,13 +161,13 @@ class FormGroup {
117
161
  if (self.options.strict) {
118
162
  if (isValid) {
119
163
  yield self.setElementValidity(this, isValid);
120
- self.setValue(this.name, value);
164
+ self.setValue(self.getModelKeyName(this.name), value);
121
165
  }
122
166
  self.parentElement.requestUpdate();
123
167
  return method.call(self.parentElement, event);
124
168
  }
125
169
  yield self.setElementValidity(this, isValid);
126
- self.setValue(this.name, value);
170
+ self.setValue(self.getModelKeyName(this.name), value);
127
171
  self.parentElement.requestUpdate();
128
172
  return method.call(self.parentElement, event);
129
173
  });
@@ -132,18 +176,21 @@ class FormGroup {
132
176
  applyValidationContext({ errors, element }) {
133
177
  const form = this.getFormElement();
134
178
  if (errors.length) {
135
- this.invalid = form.invalid = true;
136
- this.valid = form.valid = false;
179
+ this.invalid = form['invalid'] = true;
180
+ this.valid = form['valid'] = false;
137
181
  return false;
138
182
  }
139
183
  else {
140
- this.errors[element.name] = {};
141
- this.invalid = form.invalid = false;
142
- this.valid = form.valid = true;
184
+ this.errors[this.getModelKeyName(element.name)] = {};
185
+ this.invalid = form['invalid'] = !form.checkValidity();
186
+ this.valid = form['valid'] = form.checkValidity();
143
187
  return true;
144
188
  }
145
189
  }
146
190
  querySelectForm(shadowRoot) {
191
+ if (this.options['form']) {
192
+ return this.options['form'];
193
+ }
147
194
  const form = shadowRoot.querySelector(`form[name="${this.options.name}"]`);
148
195
  if (!form) {
149
196
  throw new Error(`Form element with name "${this.options.name}" not present inside ${this.getParentElement().outerHTML} component`);
@@ -155,10 +202,9 @@ class FormGroup {
155
202
  return form;
156
203
  }
157
204
  querySelectAll(name) {
158
- return [
159
- ...this.form.querySelectorAll(name).values(),
160
- ];
205
+ return [...this.form.querySelectorAll(name).values()];
161
206
  }
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
208
  querySelectorAllInputs() {
163
209
  var _a;
164
210
  return [...((_a = this.options.customElements) !== null && _a !== void 0 ? _a : []), 'input', 'select', 'textarea']
@@ -167,11 +213,14 @@ class FormGroup {
167
213
  .filter((el) => this.isInputPresentOnStage(el))
168
214
  .filter((el) => !!el.name);
169
215
  }
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
217
  mapEventToInputs(inputs = []) {
171
218
  return inputs.map((el) => {
172
219
  const strategy = `on${this.options.strategy}`;
173
220
  if (!el[strategy]) {
174
- el[strategy] = function () { };
221
+ el[strategy] = function () {
222
+ //
223
+ };
175
224
  }
176
225
  const customAttributes = Object.keys(el.attributes)
177
226
  .map((k) => (el.attributes[k].name.startsWith('#') ? el.attributes[k] : null))
@@ -202,20 +251,39 @@ class FormGroup {
202
251
  }
203
252
  isInputPresentOnStage(input) {
204
253
  if (input.outerHTML === '<input type="submit" style="display: none;">') {
205
- return;
254
+ return 0;
206
255
  }
207
- const isInputPresent = Object.keys(this.value).filter((v) => v === input.name);
256
+ const keyIndex = this.getModelKeyName(input.name);
257
+ const isInputPresent = Object.keys(this.value).filter((v) => v === keyIndex);
208
258
  return isInputPresent.length;
209
259
  }
260
+ getModelKeyName(domName) {
261
+ if (this.options['namespace']) {
262
+ // pattern: namespace[key] or namespace.key
263
+ // Example: allowedIps[0].ip -> namespace=allowedIps[0] -> key=ip
264
+ if (domName.startsWith(this.options['namespace'])) {
265
+ const suffix = domName.replace(this.options['namespace'], '');
266
+ // Handle .key or [key]
267
+ if (suffix.startsWith('.')) {
268
+ return suffix.substring(1);
269
+ }
270
+ if (suffix.startsWith('[')) {
271
+ return suffix.substring(1, suffix.length - 1); // Not dealing with that yet
272
+ }
273
+ }
274
+ return domName;
275
+ }
276
+ return domName;
277
+ }
278
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
210
279
  validate(element) {
211
280
  return __awaiter(this, void 0, void 0, function* () {
212
281
  let errors = [];
213
282
  element.setCustomValidity('');
214
- this.setFormValidity(true);
215
283
  if (!element.checkValidity()) {
216
284
  return {
217
285
  errors: errors.concat(Object.keys(form_tokens_1.InputValidityState)
218
- .map((key) => element.validity[key] ? { key, message: element.validationMessage } : null)
286
+ .map((key) => (element.validity[key] ? { key, message: element.validationMessage } : null))
219
287
  .filter((i) => !!i)),
220
288
  element,
221
289
  };
@@ -224,32 +292,36 @@ class FormGroup {
224
292
  if (!errors.length) {
225
293
  return { errors: [], element };
226
294
  }
227
- this.setFormValidity(false);
228
295
  element.setCustomValidity(errors[0].message);
229
296
  return { element, errors };
230
297
  });
231
298
  }
299
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
300
  mapInputErrors(element) {
233
301
  return __awaiter(this, void 0, void 0, function* () {
234
- const res = yield Promise.all((this.validators.get(element.name) || []).map((v) => __awaiter(this, void 0, void 0, function* () {
235
- this.errors[element.name] = this.errors[element.name] || {};
302
+ const modelKey = this.getModelKeyName(element.name);
303
+ const res = yield Promise.all((this.validators.get(modelKey) || []).map((v) => __awaiter(this, void 0, void 0, function* () {
304
+ this.errors[modelKey] = this.errors[modelKey] || {};
236
305
  const error = yield v.bind(this.getParentElement())(element);
237
306
  // if (error) {
238
307
  // element.focus();
239
308
  // }
240
309
  if (error && error.key) {
241
- this.errors[element.name][error.key] = error.message;
310
+ this.errors[modelKey][error.key] = error.message;
242
311
  this.errorMap.set(v, error.key);
243
312
  return { key: error.key, message: error.message };
244
313
  }
245
314
  else if (this.errorMap.has(v)) {
246
- delete this.errors[element.name][this.errorMap.get(v)];
315
+ delete this.errors[modelKey][this.errorMap.get(v)];
247
316
  }
248
317
  })));
249
318
  return res.filter((i) => !!i);
250
319
  });
251
320
  }
252
321
  get(name) {
322
+ if (this.controls.has(name)) {
323
+ return this.controls.get(name);
324
+ }
253
325
  return this.inputs.get(name);
254
326
  }
255
327
  getError(inputName, errorKey) {
@@ -277,7 +349,11 @@ class FormGroup {
277
349
  this.errorMap.clear();
278
350
  }
279
351
  get value() {
280
- return this._valueChanges.getValue();
352
+ const values = this._valueChanges.getValue();
353
+ this.controls.forEach((control, key) => {
354
+ values[key] = control.value;
355
+ });
356
+ return values;
281
357
  }
282
358
  set value(value) {
283
359
  this._valueChanges.next(value);
@@ -285,6 +361,9 @@ class FormGroup {
285
361
  unsubscribe() {
286
362
  this.reset();
287
363
  this._valueChanges.unsubscribe();
364
+ this.subscriptions.forEach((s) => s.unsubscribe());
365
+ this.subscriptions.clear();
366
+ this.controls.forEach((c) => { var _a; return (_a = c.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(c); });
288
367
  }
289
368
  getValue(name) {
290
369
  return this.value[name];
@@ -292,9 +371,12 @@ class FormGroup {
292
371
  setValue(name, value) {
293
372
  const input = this.get(name);
294
373
  if (!input) {
295
- return;
374
+ // If no input, maybe just set the value in model?
375
+ // User code had return; but we might want to update model even if no input?
376
+ // return;
296
377
  }
297
- input.value = value;
378
+ if (input && input.value !== undefined)
379
+ input.value = value;
298
380
  const values = this.value;
299
381
  values[name] = value;
300
382
  this.value = values;
@@ -304,12 +386,17 @@ class FormGroup {
304
386
  }
305
387
  setFormElement(form) {
306
388
  this.form = form;
389
+ this.controls.forEach((c) => {
390
+ if (c.setFormElement)
391
+ c.setFormElement(form);
392
+ });
307
393
  return this;
308
394
  }
309
395
  setInputs(inputs) {
310
396
  this.inputs = new Map(inputs.map((e) => {
311
- e.value = this.getValue(e.name);
312
- return [e.name, e];
397
+ const key = this.getModelKeyName(e.name);
398
+ e.value = this.getValue(key);
399
+ return [key, e];
313
400
  }));
314
401
  }
315
402
  getFormElement() {
@@ -1,3 +1,5 @@
1
+ import { LitElement } from '@rxdi/lit-html';
2
+ import { Observable } from 'rxjs';
1
3
  export type FormStrategies = keyof WindowEventMap;
2
4
  export interface FormOptions {
3
5
  /** Name of the form element */
@@ -14,9 +16,30 @@ export interface FormOptions {
14
16
  * Example can be found here https://gist.github.com/Stradivario/57acf0fa19900867a7f55b0f01251d6e
15
17
  * */
16
18
  customElements?: string[];
19
+ /**
20
+ * Internal property for handling nested forms.
21
+ */
22
+ namespace?: string;
23
+ }
24
+ export interface AbstractControl<T = any> {
25
+ setOptions(options: FormOptions): this | void;
26
+ getOptions(): FormOptions;
27
+ init(): void;
28
+ setParentElement(parent: LitElement): this | void;
29
+ setFormElement(form: HTMLFormElement): this | void;
30
+ unsubscribe(): void;
31
+ valueChanges: Observable<T>;
32
+ value: T;
33
+ valid: boolean;
34
+ invalid: boolean;
35
+ updateValueAndValidity?(): Promise<any>;
36
+ name?: string;
37
+ push?(control: AbstractControl): void;
38
+ getFormElement?(): HTMLFormElement;
17
39
  }
40
+ export type ValidatorFn = (element: AbstractInput | HTMLInputElement) => Promise<InputErrorMessage | void> | InputErrorMessage | void;
18
41
  export interface FormInputOptions {
19
- [key: string]: [string, Function[]];
42
+ [key: string]: [string | number | boolean | Date, ValidatorFn[]];
20
43
  }
21
44
  export interface InputErrorMessage<T = any> {
22
45
  key: T;
@@ -18,5 +18,5 @@ exports.InputValidityState = strEnum([
18
18
  'tooShort',
19
19
  'typeMismatch',
20
20
  'valid',
21
- 'valueMissing'
21
+ 'valueMissing',
22
22
  ]);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './form.decorator';
2
2
  export * from './form.group';
3
3
  export * from './form.tokens';
4
+ export * from './form.array';
package/dist/index.js CHANGED
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./form.decorator"), exports);
18
18
  __exportStar(require("./form.group"), exports);
19
19
  __exportStar(require("./form.tokens"), exports);
20
+ __exportStar(require("./form.array"), exports);
package/package.json CHANGED
@@ -1,23 +1,27 @@
1
1
  {
2
- "name": "@rxdi/forms",
3
- "version": "0.7.212",
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.211",
16
- "@types/node": "^25.0.3",
17
- "rxjs": "^7.8.2",
18
- "typescript": "^5.9.3"
19
- },
20
- "types": "./dist/index.d.ts",
21
- "module": "./dist/index.js",
22
- "typings": "./dist/index.d.ts"
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"
23
27
  }
package/dist/rx-fake.d.ts DELETED
@@ -1,34 +0,0 @@
1
- import { BehaviorSubject as BS, Observable as O, Subscription as S } from 'rxjs';
2
- type OBS<T> = (o: $Observable<T>) => void | Function;
3
- type FN<T> = (a: T) => void;
4
- export declare class $Subscription<T> {
5
- o: Map<Function, FN<T>>;
6
- unsubscribe(): void;
7
- }
8
- export declare class $Observable<T> extends $Subscription<T> {
9
- fn: OBS<T>;
10
- init: boolean;
11
- constructor(fn?: OBS<T>);
12
- subscribe(c: FN<T>): $Subscription<T>;
13
- complete(): void;
14
- next(s: T): void;
15
- }
16
- export declare class $BehaviorSubject<T> extends $Observable<T> {
17
- v: T;
18
- constructor(v: T);
19
- private setValue;
20
- next(s: T): void;
21
- getValue(): T;
22
- asObservable(): this;
23
- }
24
- export declare function noop(): void;
25
- export declare function BehaviorSubject<T>(init: T): void;
26
- export declare function Observable<T>(fn?: OBS<T>): void;
27
- export declare function Subscription<T>(): void;
28
- export interface BehaviorSubject<T> extends BS<T> {
29
- }
30
- export interface Observable<T> extends O<T> {
31
- }
32
- export interface Subscription extends S {
33
- }
34
- export {};
package/dist/rx-fake.js DELETED
@@ -1,99 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.$BehaviorSubject = exports.$Observable = exports.$Subscription = void 0;
4
- exports.noop = noop;
5
- exports.BehaviorSubject = BehaviorSubject;
6
- exports.Observable = Observable;
7
- exports.Subscription = Subscription;
8
- class $Subscription {
9
- constructor() {
10
- this.o = new Map();
11
- }
12
- unsubscribe() {
13
- [...this.o.values()].forEach(v => this.o.delete(v));
14
- }
15
- }
16
- exports.$Subscription = $Subscription;
17
- class $Observable extends $Subscription {
18
- constructor(fn) {
19
- super();
20
- this.init = true;
21
- this.fn = fn;
22
- }
23
- subscribe(c) {
24
- this.o.set(c, c);
25
- if (typeof this.fn === 'function' && this.init) {
26
- this.fn(this);
27
- this.init = false;
28
- }
29
- return {
30
- unsubscribe: () => {
31
- this.o.delete(c);
32
- }
33
- };
34
- }
35
- complete() {
36
- this.unsubscribe();
37
- }
38
- next(s) {
39
- [...this.o.values()].forEach(f => f(s));
40
- }
41
- }
42
- exports.$Observable = $Observable;
43
- class $BehaviorSubject extends $Observable {
44
- constructor(v) {
45
- if (typeof v === 'function') {
46
- super(v);
47
- }
48
- super(null);
49
- this.setValue(v);
50
- }
51
- setValue(v) {
52
- this.v = v;
53
- }
54
- next(s) {
55
- this.setValue(s);
56
- super.next(s);
57
- }
58
- getValue() {
59
- return this.v;
60
- }
61
- asObservable() {
62
- return this;
63
- }
64
- }
65
- exports.$BehaviorSubject = $BehaviorSubject;
66
- function behaviorOrFake() {
67
- try {
68
- return require('rxjs').BehaviorSubject;
69
- }
70
- catch (e) { }
71
- return $BehaviorSubject;
72
- }
73
- function observableOrFake() {
74
- try {
75
- return require('rxjs').Observable;
76
- }
77
- catch (e) { }
78
- return $Observable;
79
- }
80
- function subscriptionOrFake() {
81
- try {
82
- return require('rxjs').Subscription;
83
- }
84
- catch (e) { }
85
- return $Subscription;
86
- }
87
- function noop() { }
88
- function BehaviorSubject(init) {
89
- const b = behaviorOrFake();
90
- return new b(init);
91
- }
92
- function Observable(fn) {
93
- const o = observableOrFake();
94
- return new o(fn);
95
- }
96
- function Subscription() {
97
- const s = subscriptionOrFake();
98
- return new s();
99
- }