@twobirds/microcomponents 0.1.21 → 0.2.0
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/LICENSE +21 -0
- package/README.md +91 -92
- package/dist/MC.min.js +1 -345
- package/dist/autoload.min.js +1 -0
- package/dist/core/MC.d.ts +401 -0
- package/dist/core/MC.d.ts.map +1 -0
- package/dist/core/MC.js +471 -0
- package/dist/core/autoload.d.ts +54 -0
- package/dist/core/autoload.d.ts.map +1 -0
- package/dist/core/autoload.js +158 -0
- package/dist/core/elements.d.ts +232 -0
- package/dist/core/elements.d.ts.map +1 -0
- package/dist/core/elements.js +123 -0
- package/dist/core/events.d.ts +231 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +228 -0
- package/dist/core/mcGuards.d.ts +49 -0
- package/dist/core/mcGuards.d.ts.map +1 -0
- package/dist/core/mcGuards.js +19 -0
- package/dist/elements.min.js +1 -1
- package/dist/events.min.js +1 -0
- package/dist/formValues.min.js +1 -1
- package/dist/mcGuards.min.js +1 -0
- package/dist/microcomponents.d.ts +8 -5
- package/dist/microcomponents.d.ts.map +1 -1
- package/dist/microcomponents.js +8 -5
- package/dist/microcomponents.min.js +1 -1
- package/dist/observables.min.js +1 -1
- package/dist/state/formValues.d.ts +70 -0
- package/dist/state/formValues.d.ts.map +1 -0
- package/dist/state/formValues.js +334 -0
- package/dist/state/observables.d.ts +276 -0
- package/dist/state/observables.d.ts.map +1 -0
- package/dist/state/observables.js +382 -0
- package/dist/state/placeholders.d.ts +62 -0
- package/dist/state/placeholders.d.ts.map +1 -0
- package/dist/state/placeholders.js +181 -0
- package/dist/types/mcTypes.d.ts +201 -0
- package/dist/types/mcTypes.d.ts.map +1 -0
- package/dist/types/mcTypes.js +9 -0
- package/dist/utils/helpers.d.ts +195 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +340 -0
- package/package.json +96 -62
- package/dist/MC.d.ts +0 -31
- package/dist/MC.d.ts.map +0 -1
- package/dist/MC.js +0 -249
- package/dist/MC.ts +0 -613
- package/dist/MicroComponents.svg +0 -15
- package/dist/elements.d.ts +0 -5
- package/dist/elements.d.ts.map +0 -1
- package/dist/elements.js +0 -49
- package/dist/elements.ts +0 -114
- package/dist/events.d.ts +0 -11
- package/dist/events.d.ts.map +0 -1
- package/dist/events.js +0 -55
- package/dist/events.ts +0 -158
- package/dist/formValues.d.ts +0 -3
- package/dist/formValues.d.ts.map +0 -1
- package/dist/formValues.js +0 -156
- package/dist/formValues.ts +0 -371
- package/dist/helpers.d.ts +0 -15
- package/dist/helpers.d.ts.map +0 -1
- package/dist/helpers.js +0 -113
- package/dist/helpers.ts +0 -219
- package/dist/main.mjs +0 -1
- package/dist/microcomponents.ts +0 -6
- package/dist/observables.d.ts +0 -11
- package/dist/observables.d.ts.map +0 -1
- package/dist/observables.js +0 -228
- package/dist/observables.ts +0 -449
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Form Values
|
|
3
|
+
*
|
|
4
|
+
* This module exports a single function {@link formValues} that traverses a DOM element
|
|
5
|
+
* (usually an HTMLFormElement), collecting all input fields and creating a two-way bound
|
|
6
|
+
* observable inside the target object:
|
|
7
|
+
* - When input values change, the observable object updates accordingly.
|
|
8
|
+
* - When the observable property values change, the input field values update in the DOM.
|
|
9
|
+
*
|
|
10
|
+
* **Prerequisites:**
|
|
11
|
+
* - Input fields must have a `name` attribute.
|
|
12
|
+
* - Names must be unique within the form.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { DC } from '@twobirds/microcomponents';
|
|
17
|
+
* import { formValues } from '@twobirds/microcomponents/formValues';
|
|
18
|
+
*
|
|
19
|
+
* class MyMC extends DC {
|
|
20
|
+
* constructor(target: HTMLElement) {
|
|
21
|
+
* super(target);
|
|
22
|
+
* this.target.innerHTML = '<form><input name="textinput" value="" /></form>';
|
|
23
|
+
*
|
|
24
|
+
* // link the form property to the DOM input fields
|
|
25
|
+
* formValues(this, 'form', this.target);
|
|
26
|
+
*
|
|
27
|
+
* // changing the form property updates the DOM automatically
|
|
28
|
+
* this.form.textinput = 'Hello world!';
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Attaches a form-values observable to a target object property, allowing two-way binding
|
|
35
|
+
* between the DOM form controls and the object's state.
|
|
36
|
+
*
|
|
37
|
+
* @param targetObject - The object to which the form values will be attached as an observable.
|
|
38
|
+
* @param propertyName - The name of the property on the target object that will hold the form data.
|
|
39
|
+
* @param dom - The DOM element or shadow root (typically a form) whose inputs should be observed and bound.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```html
|
|
43
|
+
* <form id="myForm">
|
|
44
|
+
* <input type="text" name="username" value="">
|
|
45
|
+
* <select name="role"><option value="admin">Admin</option><option value="user">User</option></select>
|
|
46
|
+
* <textarea name="bio"></textarea>
|
|
47
|
+
* <input type="checkbox" name="active">
|
|
48
|
+
* <input type="radio" name="gender" value="male"> Male
|
|
49
|
+
* <input type="radio" name="gender" value="female"> Female
|
|
50
|
+
* </form>
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { formValues } from '@twobirds/microcomponents/formValues';
|
|
55
|
+
*
|
|
56
|
+
* const state = {};
|
|
57
|
+
* formValues(state, 'formData', document.getElementById('myForm'));
|
|
58
|
+
*
|
|
59
|
+
* // Read values from the form
|
|
60
|
+
* console.log(state.formData.username); // string
|
|
61
|
+
* console.log(state.formData.role); // "admin" or "user"
|
|
62
|
+
* console.log(state.formData.active); // boolean
|
|
63
|
+
*
|
|
64
|
+
* // Write values to the form (updates DOM automatically)
|
|
65
|
+
* state.formData.username = 'john_doe';
|
|
66
|
+
* state.formData.gender = 'male';
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function formValues<T extends object>(targetObject: T, propertyName: keyof T & string, dom: HTMLElement | ShadowRoot): void;
|
|
70
|
+
//# sourceMappingURL=formValues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formValues.d.ts","sourceRoot":"","sources":["../../src/state/formValues.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA4SH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAC1C,YAAY,EAAE,CAAC,EACf,YAAY,EAAE,MAAM,CAAC,GAAG,MAAM,EAC9B,GAAG,EAAE,WAAW,GAAG,UAAU,QAuB7B"}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Form Values
|
|
3
|
+
*
|
|
4
|
+
* This module exports a single function {@link formValues} that traverses a DOM element
|
|
5
|
+
* (usually an HTMLFormElement), collecting all input fields and creating a two-way bound
|
|
6
|
+
* observable inside the target object:
|
|
7
|
+
* - When input values change, the observable object updates accordingly.
|
|
8
|
+
* - When the observable property values change, the input field values update in the DOM.
|
|
9
|
+
*
|
|
10
|
+
* **Prerequisites:**
|
|
11
|
+
* - Input fields must have a `name` attribute.
|
|
12
|
+
* - Names must be unique within the form.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { DC } from '@twobirds/microcomponents';
|
|
17
|
+
* import { formValues } from '@twobirds/microcomponents/formValues';
|
|
18
|
+
*
|
|
19
|
+
* class MyMC extends DC {
|
|
20
|
+
* constructor(target: HTMLElement) {
|
|
21
|
+
* super(target);
|
|
22
|
+
* this.target.innerHTML = '<form><input name="textinput" value="" /></form>';
|
|
23
|
+
*
|
|
24
|
+
* // link the form property to the DOM input fields
|
|
25
|
+
* formValues(this, 'form', this.target);
|
|
26
|
+
*
|
|
27
|
+
* // changing the form property updates the DOM automatically
|
|
28
|
+
* this.form.textinput = 'Hello world!';
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { DC } from '../core/MC.js';
|
|
34
|
+
import { copyGettersSetters, isBrowser } from '../utils/helpers.js';
|
|
35
|
+
import { observe } from './observables.js';
|
|
36
|
+
/**
|
|
37
|
+
Extracts the lowercase tag name of an element (e.g., 'input', 'select', 'textarea').
|
|
38
|
+
@param inputElement - The HTML element to extract the tag name from.
|
|
39
|
+
@returns The lowercase tag name string.
|
|
40
|
+
*/
|
|
41
|
+
const getInputName = (inputElement) => {
|
|
42
|
+
return inputElement.tagName.toLowerCase();
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
Abstract base class for form input value handlers.
|
|
46
|
+
Defines the interface that all handler classes must implement.
|
|
47
|
+
*/
|
|
48
|
+
class InputBase {
|
|
49
|
+
/**
|
|
50
|
+
Creates a new InputBase instance.
|
|
51
|
+
@param e - The HTML element being handled (input, select, or textarea).
|
|
52
|
+
*/
|
|
53
|
+
constructor(e) { }
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
Handler class specifically for radio button groups.
|
|
57
|
+
Provides synchronized access to the selected value in a group of related radio buttons.
|
|
58
|
+
*/
|
|
59
|
+
class RadioHandler extends InputBase {
|
|
60
|
+
#e;
|
|
61
|
+
/**
|
|
62
|
+
Creates a new RadioHandler instance.
|
|
63
|
+
@param e - The first radio button element in the group (used to determine name and form scope).
|
|
64
|
+
*/
|
|
65
|
+
constructor(e) {
|
|
66
|
+
super(e);
|
|
67
|
+
this.#e = e;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
Gets the currently selected value of the radio button group using FormData API.
|
|
71
|
+
@returns The string value of the selected radio button, or null if none is selected.
|
|
72
|
+
*/
|
|
73
|
+
get val() {
|
|
74
|
+
if (!this.#e.form)
|
|
75
|
+
return null;
|
|
76
|
+
const ret = new FormData(this.#e.form).get(this.#e.name);
|
|
77
|
+
return ret ?? null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
Sets the selected radio button by value.
|
|
81
|
+
Finds and checks the first radio input matching the specified value within the same group.
|
|
82
|
+
@param value - The value to match against radio inputs in this group.
|
|
83
|
+
*/
|
|
84
|
+
set val(value) {
|
|
85
|
+
/* v8 ignore next */
|
|
86
|
+
const arr = [
|
|
87
|
+
...(this.#e.form?.querySelectorAll(`input[type="radio"][name="${this.#e.name}"]`) ?? []),
|
|
88
|
+
];
|
|
89
|
+
arr.filter((e) => e.getAttribute('value') === value).shift().checked =
|
|
90
|
+
true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handler class for checkbhox inputs.
|
|
95
|
+
* Provides synchronized get/set access to the checked state of a single checkbox.
|
|
96
|
+
*/
|
|
97
|
+
class CheckboxHandler extends InputBase {
|
|
98
|
+
#e;
|
|
99
|
+
/**
|
|
100
|
+
Creates a new CheckboxHandler instance.
|
|
101
|
+
@param e - The checkbox element to handle.
|
|
102
|
+
*/
|
|
103
|
+
constructor(e) {
|
|
104
|
+
super(e);
|
|
105
|
+
this.#e = e;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
Gets the checked state of the checkbox.
|
|
109
|
+
@returns True if the checkbox is checked, false otherwise.
|
|
110
|
+
*/
|
|
111
|
+
get val() {
|
|
112
|
+
return this.#e.checked;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
Sets the checked state of the checkbox.
|
|
116
|
+
@param value - The boolean value to assign as the new checked state.
|
|
117
|
+
*/
|
|
118
|
+
set val(value) {
|
|
119
|
+
this.#e.checked = value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
Handler class for standard single-line input fields (text, password, email, etc.).
|
|
124
|
+
Provides synchronized get/set access to the value of an input field.
|
|
125
|
+
*/
|
|
126
|
+
class InputHandler extends InputBase {
|
|
127
|
+
#e;
|
|
128
|
+
/**
|
|
129
|
+
* Creates a new InputHandler instance.
|
|
130
|
+
* @param e - The HTML input element to handle.
|
|
131
|
+
*/
|
|
132
|
+
constructor(e) {
|
|
133
|
+
super(e);
|
|
134
|
+
this.#e = e;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Gets the current value of the input field.
|
|
138
|
+
* @returns The string content of the input element.
|
|
139
|
+
*/
|
|
140
|
+
get val() {
|
|
141
|
+
return this.#e.value;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Sets the value of the input field.
|
|
145
|
+
* @param value - The new string value for the input element.
|
|
146
|
+
*/
|
|
147
|
+
set val(value) {
|
|
148
|
+
this.#e.value = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
Handler class for select dropdown elements, supporting both single and multiple selection modes.
|
|
153
|
+
Provides synchronized get/set access to selected option values.
|
|
154
|
+
*/
|
|
155
|
+
class SelectHandler extends InputBase {
|
|
156
|
+
#e;
|
|
157
|
+
#multi = false;
|
|
158
|
+
/**
|
|
159
|
+
* Creates a new SelectHandler instance.
|
|
160
|
+
* @param e - The select element to handle.
|
|
161
|
+
*/
|
|
162
|
+
constructor(e) {
|
|
163
|
+
super(e);
|
|
164
|
+
this.#e = e;
|
|
165
|
+
this.#multi = e.multiple;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Gets the currently selected option value(s).
|
|
169
|
+
* For single selection, returns a string. For multiple selection, returns an json.stringify()'ied array of strings.
|
|
170
|
+
* @returns A string or JSON.stringify array of strings representing selected values.
|
|
171
|
+
*/
|
|
172
|
+
get val() {
|
|
173
|
+
const ret = [];
|
|
174
|
+
// get all options
|
|
175
|
+
[...this.#e.querySelectorAll('option')]
|
|
176
|
+
.filter((o) => o.selected)
|
|
177
|
+
.forEach((o) => {
|
|
178
|
+
ret.push(o.value);
|
|
179
|
+
});
|
|
180
|
+
return this.#multi ? ret.join(',') : ret[0];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Sets the selected option(s) by value.
|
|
184
|
+
* @param value - A string or array of strings representing values to select.
|
|
185
|
+
*/
|
|
186
|
+
set val(value) {
|
|
187
|
+
value = typeof value === 'string' ? [value] : value;
|
|
188
|
+
// set all options /* v8 ignore next */
|
|
189
|
+
[...this.#e.querySelectorAll('option')].forEach((o) => {
|
|
190
|
+
if (value.indexOf(o.value) > -1) {
|
|
191
|
+
o.selected = true;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
o.selected = false;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
Handler class for textarea elements.
|
|
201
|
+
Provides synchronized get/set access to the multi-line text content of a textarea.
|
|
202
|
+
*/
|
|
203
|
+
class TextAreaHandler extends InputBase {
|
|
204
|
+
#e;
|
|
205
|
+
/**
|
|
206
|
+
* Creates a new TextAreaHandler instance.
|
|
207
|
+
* @param e - The textarea element to handle.
|
|
208
|
+
*/
|
|
209
|
+
constructor(e) {
|
|
210
|
+
super(e);
|
|
211
|
+
this.#e = e;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Gets the current value of the textarea (defaults to empty string if null/undefined).
|
|
215
|
+
* @returns The string content of the textarea element.
|
|
216
|
+
*/
|
|
217
|
+
get val() {
|
|
218
|
+
return this.#e.value || '';
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Sets the value of the textarea.
|
|
222
|
+
* @param value - The new string value for the textarea element.
|
|
223
|
+
*/
|
|
224
|
+
set val(value) {
|
|
225
|
+
this.#e.value = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
Collects all form controls within a given DOM scope and creates handler objects
|
|
230
|
+
that provide synchronized get/set access to their values.
|
|
231
|
+
|
|
232
|
+
@param e - Optional root DOM element or shadow root to search within; defaults to document if omitted.
|
|
233
|
+
@returns An object where each key is the name attribute of an input, select, or textarea,
|
|
234
|
+
and each value is a proxy-like accessor object with `val` getter/setter methods.
|
|
235
|
+
*/
|
|
236
|
+
const getInputHandlers = (e) => {
|
|
237
|
+
const inputList = [...(e || document).querySelectorAll('input,select,textarea')], handlers = {};
|
|
238
|
+
inputList.forEach((input) => {
|
|
239
|
+
const name = input.getAttribute('name');
|
|
240
|
+
if (name) {
|
|
241
|
+
let handler = false;
|
|
242
|
+
switch (getInputName(input)) {
|
|
243
|
+
case 'input':
|
|
244
|
+
switch (input.type) {
|
|
245
|
+
case 'radio':
|
|
246
|
+
handler = new RadioHandler(input);
|
|
247
|
+
break;
|
|
248
|
+
case 'checkbox':
|
|
249
|
+
handler = new CheckboxHandler(input);
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
handler = new InputHandler(input);
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case 'select':
|
|
256
|
+
handler = new SelectHandler(input);
|
|
257
|
+
break;
|
|
258
|
+
case 'textarea':
|
|
259
|
+
handler = new TextAreaHandler(input);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
/* v8 ignore next */
|
|
263
|
+
if (handler) {
|
|
264
|
+
const h = handler;
|
|
265
|
+
DC.add(input, name, h);
|
|
266
|
+
Object.defineProperty(handlers, name, {
|
|
267
|
+
enumerable: true,
|
|
268
|
+
configurable: true,
|
|
269
|
+
get: () => h.val,
|
|
270
|
+
set: (value) => {
|
|
271
|
+
if (h.val !== value) {
|
|
272
|
+
h.val = value;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return handlers;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Attaches a form-values observable to a target object property, allowing two-way binding
|
|
283
|
+
* between the DOM form controls and the object's state.
|
|
284
|
+
*
|
|
285
|
+
* @param targetObject - The object to which the form values will be attached as an observable.
|
|
286
|
+
* @param propertyName - The name of the property on the target object that will hold the form data.
|
|
287
|
+
* @param dom - The DOM element or shadow root (typically a form) whose inputs should be observed and bound.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```html
|
|
291
|
+
* <form id="myForm">
|
|
292
|
+
* <input type="text" name="username" value="">
|
|
293
|
+
* <select name="role"><option value="admin">Admin</option><option value="user">User</option></select>
|
|
294
|
+
* <textarea name="bio"></textarea>
|
|
295
|
+
* <input type="checkbox" name="active">
|
|
296
|
+
* <input type="radio" name="gender" value="male"> Male
|
|
297
|
+
* <input type="radio" name="gender" value="female"> Female
|
|
298
|
+
* </form>
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* ```ts
|
|
302
|
+
* import { formValues } from '@twobirds/microcomponents/formValues';
|
|
303
|
+
*
|
|
304
|
+
* const state = {};
|
|
305
|
+
* formValues(state, 'formData', document.getElementById('myForm'));
|
|
306
|
+
*
|
|
307
|
+
* // Read values from the form
|
|
308
|
+
* console.log(state.formData.username); // string
|
|
309
|
+
* console.log(state.formData.role); // "admin" or "user"
|
|
310
|
+
* console.log(state.formData.active); // boolean
|
|
311
|
+
*
|
|
312
|
+
* // Write values to the form (updates DOM automatically)
|
|
313
|
+
* state.formData.username = 'john_doe';
|
|
314
|
+
* state.formData.gender = 'male';
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
export function formValues(targetObject, propertyName, dom) {
|
|
318
|
+
/* v8 ignore next */
|
|
319
|
+
if (!isBrowser())
|
|
320
|
+
return; // DOM does not exist on the server side
|
|
321
|
+
const handlers = getInputHandlers(dom);
|
|
322
|
+
const source = targetObject;
|
|
323
|
+
source[propertyName] = Object.assign({}, handlers);
|
|
324
|
+
observe(targetObject, propertyName);
|
|
325
|
+
// After observe(), the property getter returns the live observable wrapper.
|
|
326
|
+
const observable = source[propertyName];
|
|
327
|
+
copyGettersSetters(handlers, observable);
|
|
328
|
+
//Object.seal(source[propertyName]); // object structure may not be changed from now on
|
|
329
|
+
[...dom.querySelectorAll('input, textarea, select')].forEach((input) => {
|
|
330
|
+
input.addEventListener('input', () => {
|
|
331
|
+
observable.notify();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@module Observables
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
This module provides reactive data binding capabilities using observables,
|
|
6
|
+
enabling automatic DOM updates when underlying data changes.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The observable system allows you to make object properties "observable" so that
|
|
11
|
+
registered callbacks are automatically invoked whenever those properties change.
|
|
12
|
+
It's particularly designed for reactive templating and binding data to HTML elements.
|
|
13
|
+
It can also handle simple object properties like strings, boolean and numbers.
|
|
14
|
+
|
|
15
|
+
## How `observe()` Works
|
|
16
|
+
|
|
17
|
+
The main function is {@link observe}, which takes an object and makes its properties
|
|
18
|
+
or a specific property observable.
|
|
19
|
+
|
|
20
|
+
### Simple properties
|
|
21
|
+
|
|
22
|
+
javascript
|
|
23
|
+
```js
|
|
24
|
+
let person = { name: "John", count: 0 };
|
|
25
|
+
|
|
26
|
+
observe( person ); // all properties inside are now observables
|
|
27
|
+
|
|
28
|
+
person.name.subscribe( (name) => {
|
|
29
|
+
console.log("Name changed:", name);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
person.count.subscribe( (count) => {
|
|
33
|
+
console.log("Count changed:", count);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
person.name = "Jane"; // > Name changed: Jane
|
|
37
|
+
person.count += 10; // > Count changed: 10
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Deep Change Detection
|
|
41
|
+
|
|
42
|
+
This means that if you modify any of the properties inside an observed object -> all callbacks are triggered.
|
|
43
|
+
|
|
44
|
+
javascript
|
|
45
|
+
```js
|
|
46
|
+
let obj = { data: { name: "John", count: 0 } };
|
|
47
|
+
|
|
48
|
+
observe( obj, 'data' ); // make the data property observable
|
|
49
|
+
|
|
50
|
+
obj.data.subscribe( (data) => {
|
|
51
|
+
console.log("Data changed:", data);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
obj.data.name = "Jane"; // > { data: { name: "Jane", count: 0 } }
|
|
55
|
+
obj.data.count += 10; // > { data: { name: "Jane", count: 10 } }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Debounced Notifications
|
|
59
|
+
|
|
60
|
+
To prevent excessive updates, notifications are debounced briefly:
|
|
61
|
+
|
|
62
|
+
javascript
|
|
63
|
+
```js
|
|
64
|
+
let obj = observe({ value: 1 });
|
|
65
|
+
for (let i = 0; i < 100; i++) {
|
|
66
|
+
obj.value++; // Only triggers one callback after all changes complete
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## DOM Binding with Placeholders
|
|
71
|
+
|
|
72
|
+
The observables integrate tightly with placeholder-based templating:
|
|
73
|
+
|
|
74
|
+
- Elements with `{placeholder}` syntax are scanned for observable data.
|
|
75
|
+
- When observed data changes, matching placeholders in the DOM are updated automatically.
|
|
76
|
+
|
|
77
|
+
@example
|
|
78
|
+
|
|
79
|
+
HTML
|
|
80
|
+
```html
|
|
81
|
+
<div class="model {visibility}">
|
|
82
|
+
Hello {name}! You have {count} messages.
|
|
83
|
+
</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Javascript
|
|
87
|
+
```js
|
|
88
|
+
const div = document.querySelector('.model');
|
|
89
|
+
|
|
90
|
+
const model = observe({
|
|
91
|
+
data: { name: "Alice", count: 0 },
|
|
92
|
+
visibility: ''
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (div) {
|
|
96
|
+
model.data.bind( div );
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
model.data.count++; // Automatically updates the DOM with new count value
|
|
100
|
+
model.visibility = 'hidden'; // adds the hidden class to the DOM element
|
|
101
|
+
```
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
A callback invoked with the current observable value when notified.
|
|
105
|
+
|
|
106
|
+
@example
|
|
107
|
+
```ts
|
|
108
|
+
const cb: Callback<number> = (v) => console.log('new value:', v);
|
|
109
|
+
observable.subscribe(cb);
|
|
110
|
+
```
|
|
111
|
+
*/
|
|
112
|
+
export type Callback<T = unknown> = (value: T) => void;
|
|
113
|
+
/**
|
|
114
|
+
Interface representing an observable object that supports subscription, notification, and DOM binding.
|
|
115
|
+
|
|
116
|
+
Observable objects track registered callbacks and invoke them when their internal state changes.
|
|
117
|
+
They can also be bound to a DOM target to automatically update placeholder values in the view layer.
|
|
118
|
+
|
|
119
|
+
@example
|
|
120
|
+
```ts
|
|
121
|
+
const state = observe({ count: 0 });
|
|
122
|
+
state.count.subscribe((v) => console.log('count is now', v));
|
|
123
|
+
state.count++; // logs: count is now 1
|
|
124
|
+
```
|
|
125
|
+
*/
|
|
126
|
+
export type Observable<T = unknown> = T & {
|
|
127
|
+
subscribe(callback: Callback<T>): Observable<T>;
|
|
128
|
+
unsubscribe(callback: Callback<T>): Observable<T>;
|
|
129
|
+
notify(enable?: boolean): Observable<T>;
|
|
130
|
+
bind(target: HTMLElement | DocumentFragment | ShadowRoot): Observable<T>;
|
|
131
|
+
/** @hidden */
|
|
132
|
+
callbacks: Array<Function>;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
Every property of `T` mapped to an observable of its value.
|
|
136
|
+
|
|
137
|
+
KNOWN LIMIT: the runtime property *setter* accepts a plain value
|
|
138
|
+
(`state.count = 5` re-wraps it), but this mapped type cannot say so — mapped
|
|
139
|
+
types cannot declare divergent read/write (get/set) types, so the wrapper type
|
|
140
|
+
governs writes too and plain assignment to a primitive observable is rejected.
|
|
141
|
+
Tracked upstream as https://github.com/microsoft/TypeScript/issues/43826
|
|
142
|
+
(variant accessors for mapped types); when it lands, the write type here
|
|
143
|
+
becomes `T[K] | Observable<T[K]>` and the limit disappears.
|
|
144
|
+
|
|
145
|
+
Until then, use one of the supported write paths:
|
|
146
|
+
- the `observed` variant, a single container whose `.value` gives
|
|
147
|
+
typed reads and writes today (`state.value.count = 5`, deep
|
|
148
|
+
writes included) — the limitation never applies;
|
|
149
|
+
- `state.count++` (compiles today; TS does not re-check the `++` write-back);
|
|
150
|
+
- the schema-first `observe<S>()` overload with a hand-declared get/set
|
|
151
|
+
interface, where the divergence IS expressible (and plain `=` becomes
|
|
152
|
+
legal) — see {@link Unobserved}.
|
|
153
|
+
|
|
154
|
+
@example
|
|
155
|
+
```ts
|
|
156
|
+
type State = Observed<{ count: number; name: string }>;
|
|
157
|
+
// { count: Observable<number>, name: Observable<string> }
|
|
158
|
+
```
|
|
159
|
+
*/
|
|
160
|
+
export type Observed<T> = {
|
|
161
|
+
[K in keyof T]: Observable<T[K]>;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
Only `K` of `T` mapped to an observable; other properties keep their type.
|
|
165
|
+
Carries the same write limitation as {@link Observed} for the mapped key.
|
|
166
|
+
|
|
167
|
+
@example
|
|
168
|
+
```ts
|
|
169
|
+
type State = ObservedKey<{ count: number; label: string }, 'count'>;
|
|
170
|
+
// { count: Observable<number>, label: string }
|
|
171
|
+
```
|
|
172
|
+
*/
|
|
173
|
+
export type ObservedKey<T, K extends keyof T> = {
|
|
174
|
+
[P in keyof T]: P extends K ? Observable<T[P]> : T[P];
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
The inverse of a hand-declared observable state interface: strips the
|
|
178
|
+
`Observable` wrapper from every property, yielding the plain seed shape.
|
|
179
|
+
`Unobserved<CounterState>` → `{ count: number }`.
|
|
180
|
+
|
|
181
|
+
Extraction goes through the `subscribe` callback parameter, so it works for
|
|
182
|
+
both accessor-declared members (`get count(): Observable<number>`) and plain
|
|
183
|
+
`Observable<T>` properties; non-observable members pass through unchanged.
|
|
184
|
+
|
|
185
|
+
Used by the schema-first `observe<S>()` overload to validate the seed object
|
|
186
|
+
against the declared interface — see that overload's example.
|
|
187
|
+
|
|
188
|
+
@example
|
|
189
|
+
```ts
|
|
190
|
+
interface CounterState {
|
|
191
|
+
get count(): Observable<number>;
|
|
192
|
+
set count(v: number);
|
|
193
|
+
}
|
|
194
|
+
type Seed = Unobserved<CounterState>; // { count: number }
|
|
195
|
+
const state = observe<CounterState>({ count: 0 }); // seed validated against schema
|
|
196
|
+
```
|
|
197
|
+
*/
|
|
198
|
+
export type Unobserved<S> = {
|
|
199
|
+
[K in keyof S]: S[K] extends {
|
|
200
|
+
subscribe(callback: (value: infer V) => void): unknown;
|
|
201
|
+
} ? V : S[K];
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
Whether a value is already an observable wrapper — `subscribe` is reachable on its prototype chain.
|
|
205
|
+
Used by {@link observe} to skip re-wrapping and to detect the "not yet observed" state on reassignment.
|
|
206
|
+
|
|
207
|
+
@example
|
|
208
|
+
```ts
|
|
209
|
+
const state = observe({ count: 0 });
|
|
210
|
+
isAlreadyObserved(state.count); // true
|
|
211
|
+
isAlreadyObserved(42); // false
|
|
212
|
+
```
|
|
213
|
+
*/
|
|
214
|
+
export declare function isAlreadyObserved(value: unknown): boolean;
|
|
215
|
+
/**
|
|
216
|
+
Makes a target object's properties observable by replacing them with wrapped values.
|
|
217
|
+
|
|
218
|
+
Wraps each property of `target` that is not already observable (and meets criteria)
|
|
219
|
+
in an observable wrapper. Also sets up deep change detection for objects and
|
|
220
|
+
debounced notifications when the data changes.
|
|
221
|
+
|
|
222
|
+
There are two sides to using it — pick per call site:
|
|
223
|
+
|
|
224
|
+
**Declarative (typed class fields).** Declare the field as `Observable<T>` up
|
|
225
|
+
front, then let `observe(this, 'field')` install the live wrapper in the
|
|
226
|
+
constructor. The declaration needs the double cast once (`0 as unknown as
|
|
227
|
+
Observable<number>`): the field starts life as a plain value and only becomes
|
|
228
|
+
the wrapper when `observe()` runs. In exchange, every later use is fully typed —
|
|
229
|
+
arithmetic, `subscribe`, `bind` — with no casts anywhere else.
|
|
230
|
+
|
|
231
|
+
**Imperative (wrapping existing data).** Hand any object to `observe()` after
|
|
232
|
+
the fact — no declarations, the return type is inferred: every property comes
|
|
233
|
+
back as `Observable<PropertyType>`.
|
|
234
|
+
|
|
235
|
+
@param target - The object whose properties should be made observable.
|
|
236
|
+
@param propertyName - Optional specific property name to make one property observable instead of all properties.
|
|
237
|
+
@returns The original target object, now with observable properties.
|
|
238
|
+
|
|
239
|
+
@example Imperative usage — wrap an existing plain object:
|
|
240
|
+
```ts
|
|
241
|
+
const state = observe({ count: 0, label: 'hi' });
|
|
242
|
+
state.count.subscribe((v) => console.log('count is now', v));
|
|
243
|
+
state.count++; // triggers the callback
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
@example Declarative usage — typed class field with double cast:
|
|
247
|
+
```ts
|
|
248
|
+
class Counter {
|
|
249
|
+
clicks: Observable<number> = 0 as unknown as Observable<number>;
|
|
250
|
+
|
|
251
|
+
constructor() {
|
|
252
|
+
observe(this, 'clicks');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
bump() {
|
|
256
|
+
this.clicks++; // plain arithmetic — the declaration carries the type
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
@example Schema-first usage — divergent read/write types via hand-declared interface:
|
|
262
|
+
```ts
|
|
263
|
+
interface CounterState {
|
|
264
|
+
get count(): Observable<number>;
|
|
265
|
+
set count(v: number);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const state = observe<CounterState>({ count: 0 }); // seed validated against schema
|
|
269
|
+
state.count.subscribe((v) => console.log(v)); // fully typed
|
|
270
|
+
state.count = 5; // plain assignment — the declared setter type permits it
|
|
271
|
+
```
|
|
272
|
+
*/
|
|
273
|
+
export declare function observe<T extends object, K extends keyof T>(target: T, propertyName: K): ObservedKey<T, K>;
|
|
274
|
+
export declare function observe<T extends object>(target: T): Observed<T>;
|
|
275
|
+
export declare function observe<S extends object>(target: Unobserved<S>): S;
|
|
276
|
+
//# sourceMappingURL=observables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observables.d.ts","sourceRoot":"","sources":["../../src/state/observables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqGE;AAiBF;;;;;;;;EAQE;AACF,MAAM,MAAM,QAAQ,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAEvD;;;;;;;;;;;;EAYE;AACF,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG;IACzC,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAChD,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,GAAG,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IACzE,cAAc;IACd,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;CAC3B,CAAC;AAwNF;;;;;;;;;;;;;;;;;;;;;;;;;EAyBE;AACF,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAE/D;;;;;;;;;EASE;AACF,MAAM,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI;KAC9C,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACrD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;EAqBE;AACF,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;KAC1B,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,GAAG,OAAO,CAAA;KAAE,GACpF,CAAC,GACD,CAAC,CAAC,CAAC,CAAC;CACP,CAAC;AAKF;;;;;;;;;;EAUE;AACF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyDE;AAEF,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAC1D,MAAM,EAAE,CAAC,EACT,YAAY,EAAE,CAAC,GACb,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACrB,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AAClE,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC"}
|