@rxdi/forms 0.7.212 → 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,315 +1,290 @@
1
- # Reactive forms binding for LitHtml
2
-
3
- #### Install
4
- ```bash
5
- npm i @rxdi/forms
6
- ```
1
+ # Reactive Forms for LitHtml (Enhanced)
7
2
 
3
+ A lightweight, strongly-typed, reactive forms library for LitHtml applications.
8
4
 
5
+ ## Features
9
6
 
10
- #### Using it inside component
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.
11
12
 
12
- ##### Important!
13
+ ## Installation
13
14
 
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
  }
88
+ ```
83
89
 
90
+ ## New Features
91
+
92
+ ### Automatic Model Binding
93
+ Use the `model` property in the `@Form` decorator to automatically populate the form from a component property.
94
+
95
+ ```typescript
96
+ @Form({
97
+ name: 'my-form',
98
+ model: 'myData' // matches this.myData
99
+ })
100
+ form = new FormGroup({ ... });
84
101
  ```
102
+ The library reads `this.myData` during initialization and calls `form.patchValue(this.myData)`.
85
103
 
104
+ ### Nested FormGroups & FormArray
105
+ You can nest `FormGroup`s arbitrarily deep.
86
106
 
107
+ ```typescript
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
+ });
120
+ ```
87
121
 
88
- #### Error handling and validators
122
+ **Template Binding:**
123
+ Use dot notation for nested controls:
124
+ ```html
125
+ <input name="meta.flags.isActive" type="checkbox" />
126
+ ```
89
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!
90
132
  ```typescript
91
- import { html, Component } from '@rxdi/lit-html';
92
- import { FormGroup, Form } from '@rxdi/forms';
93
- import { BaseComponent } from '../shared/base.component';
133
+ // TypeScript knows this is valid:
134
+ this.form.get('meta.flags.isActive');
94
135
 
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
- }
129
- })
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
- });
136
+ // And this is invalid:
137
+ this.form.get('meta.flags.wrongProp'); // Error!
138
+ ```
140
139
 
141
- OnUpdate() {
142
- this.form.getValue('password');
143
- this.form.setValue('email', 'blabla');
140
+ ### Recursive PatchValue
141
+ Update multiple fields deeply at once:
144
142
 
145
- this.form.get('password'); // returns HTMLIntputElement
146
- this.form.hasError('email', 'blabla')
143
+ ```typescript
144
+ this.form.patchValue({
145
+ meta: {
146
+ flags: {
147
+ isActive: false
148
+ }
147
149
  }
150
+ });
151
+ // Only updates 'isActive', leaves other fields untouched.
152
+ ```
148
153
 
149
- onSubmit(event: Event) {
150
- this.form.values;
151
- }
154
+ ## API Reference
152
155
 
153
- validateEmail(element: HTMLInputElement) {
154
- if (element.value === 'restrictedEmail@gmail.com') {
155
- return { key: 'blabla', message: 'Please specify different email'};
156
- }
156
+ ### Validators
157
+ Validators are async functions returning `InputErrorMessage` or `void`.
158
+
159
+ ```typescript
160
+ export function CustomValidator(element: AbstractInput) {
161
+ if (element.value === 'invalid') {
162
+ return { key: 'customError', message: 'Value is invalid' };
157
163
  }
158
164
  }
159
165
 
166
+ // Usage
167
+ new FormGroup({
168
+ field: ['', [CustomValidator]]
169
+ })
170
+ ```
171
+
172
+ ### Error Display Information
173
+ Use the `touched` and `validity.valid` properties for clean UI.
174
+
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
+ }
160
182
  ```
161
183
 
184
+ ## Advanced Usage
162
185
 
186
+ ### 1. Grouping Multiple Inputs (Checkbox Groups)
163
187
 
164
- #### Group multiple inputs with single check intaraction
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.
165
189
 
166
- > By default all inputs with same attribute `name` are binded together,
190
+ **Scenario:** A list of permissions where multiple can be selected.
167
191
 
168
192
  ```typescript
169
- @Form({
170
- strategy: 'change',
171
- name: 'my-form'
172
- })
173
- private form = new FormGroup({
174
- condition: ''
175
- });
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
199
+ });
176
200
  ```
177
201
 
178
202
  ```html
179
203
  <label>
180
- <input
181
- name="condition"
182
- type="checkbox"
183
- value='none'
184
- />
185
- None
204
+ <input name="roles" type="checkbox" value="admin" /> Admin
186
205
  </label>
187
-
188
206
  <label>
189
- <input
190
- name="condition"
191
- type="checkbox"
192
- value='checked'
193
- />
194
- Checked
207
+ <input name="roles" type="checkbox" value="editor" /> Editor
195
208
  </label>
196
-
197
209
  <label>
198
- <input
199
- name="condition"
200
- type="checkbox"
201
- value='not-checked'
202
- />
203
- Not checked
210
+ <input name="roles" type="checkbox" value="viewer" /> Viewer
204
211
  </label>
205
212
  ```
206
213
 
214
+ If the user checks "Admin" and "Viewer", `form.value.roles` will be `['admin', 'viewer']`.
207
215
 
208
- #### Group multiple inputs with multi check intaraction
209
-
216
+ ### 2. Single Selection Checkbox (Radio Behavior with Checkboxes)
210
217
 
211
- > To remove binding we can set `multi: false` when defining our form
218
+ If you want multiple checkboxes to act like a radio button (only one valid at a time) but with uncheck capability:
212
219
 
213
220
  ```typescript
214
- @Form({
215
- strategy: 'change',
216
- name: 'my-form',
217
- multi: false
218
- })
219
- private form = new FormGroup({
220
- condition: ''
221
- });
222
- ```
223
-
224
-
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:
232
-
233
- ```typescript
234
- import { html } from '@rxdi/lit-html';
235
-
236
- export function InputErrorTemplate(input: HTMLInputElement) {
237
- if (input && !input.checkValidity()) {
238
- return html`
239
- <div>${input.validationMessage}</div>
240
- `;
241
- }
242
- return '';
243
- }
221
+ @Form({
222
+ name: 'settings-form',
223
+ multi: false // Default behavior
224
+ })
225
+ form = new FormGroup({
226
+ mode: ''
227
+ });
244
228
  ```
245
229
 
246
-
247
- Usage
248
-
249
230
  ```html
250
- <form>
251
- <input
252
- name="email"
253
- type="email"
254
- value=${this.form.value.email}
255
- class="form-control"
256
- placeholder="Email address"
257
- required
258
- autofocus
259
- />
260
- ${InputErrorTemplate(this.form.get('email'))}
261
- </form>
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>
262
237
  ```
238
+ Checking "Dark" unchecks "Light" automatically.
263
239
 
240
+ ### 3. Framework-Agnostic Usage (Vanilla JS)
264
241
 
265
-
266
-
267
- ##### Native HTML with JS
242
+ You can use this library without Decorators or LitHtml, with any UI library or vanilla HTML.
268
243
 
269
244
  ```typescript
270
-
271
245
  import { FormGroup } from '@rxdi/forms';
272
246
 
273
- export function EmailValidator(element: HTMLInputElement) {
274
- const regex = /^([a-zA-Z0-9_\.\-]+)@([a-zA-Z0-9_\.\-]+)\.([a-zA-Z]{2,5})$/;
275
- if (!regex.test(element.value)) {
276
- element.classList.add('is-invalid');
277
- return {
278
- key: 'email-validator',
279
- message: 'Email is not valid'
280
- };
281
- }
282
- element.classList.remove('is-invalid');
283
- }
284
-
285
247
  const form = new FormGroup({
286
- email: ['', [EmailValidator]],
287
- password: '',
248
+ email: '',
249
+ password: ''
288
250
  });
289
251
 
252
+ // manually attach to DOM
253
+ const formElement = document.querySelector('form');
290
254
  form
291
255
  .setParentElement(document.body)
292
256
  .setOptions({ name: 'my-form' })
257
+ .setFormElement(formElement)
293
258
  .prepareValues()
294
- .setFormElement(form.querySelectForm(document.body))
295
259
  .setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));
296
260
 
261
+ // Listen to changes
262
+ form.valueChanges.subscribe(val => console.log(val));
297
263
  ```
298
264
 
299
- ```html
300
- <form name="my-form">
301
- <input
302
- name="email"
303
- type="email"
304
- placeholder="Email address"
305
- required
306
- autofocus
307
- />
308
- <input
309
- name="password"
310
- type="password"
311
- required
312
- />
313
- </form>
314
- <script src="./main.ts"></script>
265
+ ### 4. Custom Error Handling Strategies
266
+
267
+ By default, verification happens on `change` or `blur`. You can control this via `strategy`.
268
+
269
+ ```typescript
270
+ @Form({
271
+ name: 'login',
272
+ strategy: 'input' // Validate on every keystroke
273
+ })
274
+ ```
275
+
276
+ You can also manually check error states (e.g. for async validation):
277
+
278
+ ```typescript
279
+ async validateEmail(element: HTMLInputElement) {
280
+ const exists = await checkServer(element.value);
281
+ if (exists) {
282
+ return { key: 'emailExists', message: 'Email already taken' };
283
+ }
284
+ }
285
+
286
+ // In Template
287
+ ${this.form.hasError('email', 'emailExists')
288
+ ? html`<div class="error">Email taken!</div>`
289
+ : ''}
315
290
  ```
@@ -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,122 @@
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
+ // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures
110
+ set value(values) {
111
+ if (!Array.isArray(values)) {
112
+ return;
113
+ }
114
+ values.forEach((v, i) => {
115
+ if (this.controls[i]) {
116
+ this.controls[i].value = v;
117
+ }
118
+ });
119
+ this.updateValue();
120
+ }
121
+ }
122
+ 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;