assign-gingerly 0.0.65 → 0.0.67
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/DX/IterableMixin.js +49 -0
- package/DX/IterableMixin.ts +62 -0
- package/{installForwarding.js → DX/installForwarding.js} +2 -2
- package/{installForwarding.ts → DX/installForwarding.ts} +2 -2
- package/DX/paths.js +381 -0
- package/{paths.ts → DX/paths.ts} +1 -1
- package/README.md +43 -13
- package/assignFrom.js +3 -3
- package/assignFrom.ts +6 -5
- package/assignFromAsync.js +3 -3
- package/assignFromAsync.ts +5 -5
- package/assignGingerly.js +52 -10
- package/assignGingerly.ts +53 -10
- package/assignTentatively.js +8 -0
- package/assignTentatively.ts +6 -0
- package/{builtInEmoji.ts → emojis.js} +46 -34
- package/emojis.ts +52 -0
- package/getValues.js +0 -1
- package/getValues.ts +1 -1
- package/handlers/addEventListener.js +153 -0
- package/handlers/addEventListener.ts +178 -0
- package/handlers/lazyLoad.js +3 -3
- package/handlers/lazyLoad.ts +3 -3
- package/handlers/manageTemplateList.js +47 -20
- package/handlers/manageTemplateList.ts +50 -20
- package/index.js +1 -1
- package/index.ts +1 -1
- package/inferencer/types/assign-gingerly/types.d.ts +62 -4
- package/package.json +12 -7
- package/resolveIdRef.js +1 -1
- package/resolveIdRef.ts +1 -1
- package/types/assign-gingerly/types.d.ts +39 -23
- package/builtInEmoji.js +0 -26
- package/paths.js +0 -231
- /package/{pinCorrector.js → DX/pinCorrector.js} +0 -0
- /package/{pinCorrector.ts → DX/pinCorrector.ts} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IterableMixin
|
|
3
|
+
* -------------
|
|
4
|
+
* Adds a private `TItem[]` list to the *instance* (storage has to live
|
|
5
|
+
* somewhere), but exposes all the behavior — getItems/setItems/assignTo
|
|
6
|
+
* — as STATIC methods on the resulting constructor, matching the
|
|
7
|
+
* original `static getItems(instance)` style. No instance methods are
|
|
8
|
+
* added.
|
|
9
|
+
*
|
|
10
|
+
* Because private fields are only reachable from code written inside
|
|
11
|
+
* the class body, the static methods have to be declared right next
|
|
12
|
+
* to the `#myList` field — that's what makes `instance.#myList` legal
|
|
13
|
+
* from a `static` method.
|
|
14
|
+
*
|
|
15
|
+
* Works on HTMLElement subclasses (dispatches an event if the instance
|
|
16
|
+
* supports it) and on plain classes (skips the dispatch silently).
|
|
17
|
+
*/
|
|
18
|
+
export function IterableMixin() {
|
|
19
|
+
return function (Base) {
|
|
20
|
+
class WithIterableStatics extends Base {
|
|
21
|
+
#myList = [];
|
|
22
|
+
static getItems(instance) {
|
|
23
|
+
return instance.#myList;
|
|
24
|
+
}
|
|
25
|
+
static setItems(instance, items) {
|
|
26
|
+
instance.#myList = items;
|
|
27
|
+
WithIterableStatics.#notifyChange(instance);
|
|
28
|
+
}
|
|
29
|
+
static assignTo(instance, rhs) {
|
|
30
|
+
if (Array.isArray(rhs)) {
|
|
31
|
+
WithIterableStatics.setItems(instance, rhs);
|
|
32
|
+
}
|
|
33
|
+
else if (typeof rhs === 'object' && rhs !== null) {
|
|
34
|
+
Object.assign(instance, rhs);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
static #notifyChange(instance) {
|
|
38
|
+
// Only dispatch if this instance actually supports it
|
|
39
|
+
// (i.e. it's an HTMLElement / EventTarget). Plain classes
|
|
40
|
+
// just skip this silently.
|
|
41
|
+
const maybeTarget = instance;
|
|
42
|
+
if (typeof maybeTarget.dispatchEvent === 'function') {
|
|
43
|
+
maybeTarget.dispatchEvent(new Event('items-changed'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return WithIterableStatics;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A constructor type — anything `new`-able. Required so the mixin
|
|
3
|
+
* can be applied on top of an arbitrary base class.
|
|
4
|
+
*/
|
|
5
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* IterableMixin
|
|
9
|
+
* -------------
|
|
10
|
+
* Adds a private `TItem[]` list to the *instance* (storage has to live
|
|
11
|
+
* somewhere), but exposes all the behavior — getItems/setItems/assignTo
|
|
12
|
+
* — as STATIC methods on the resulting constructor, matching the
|
|
13
|
+
* original `static getItems(instance)` style. No instance methods are
|
|
14
|
+
* added.
|
|
15
|
+
*
|
|
16
|
+
* Because private fields are only reachable from code written inside
|
|
17
|
+
* the class body, the static methods have to be declared right next
|
|
18
|
+
* to the `#myList` field — that's what makes `instance.#myList` legal
|
|
19
|
+
* from a `static` method.
|
|
20
|
+
*
|
|
21
|
+
* Works on HTMLElement subclasses (dispatches an event if the instance
|
|
22
|
+
* supports it) and on plain classes (skips the dispatch silently).
|
|
23
|
+
*/
|
|
24
|
+
export function IterableMixin<TItem>() {
|
|
25
|
+
return function <TBase extends Constructor>(Base: TBase) {
|
|
26
|
+
class WithIterableStatics extends Base {
|
|
27
|
+
#myList: TItem[] = [];
|
|
28
|
+
|
|
29
|
+
static getItems(instance: WithIterableStatics): TItem[] {
|
|
30
|
+
return instance.#myList;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static setItems(instance: WithIterableStatics, items: TItem[]): void {
|
|
34
|
+
instance.#myList = items;
|
|
35
|
+
WithIterableStatics.#notifyChange(instance);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static assignTo(
|
|
39
|
+
instance: WithIterableStatics,
|
|
40
|
+
rhs: TItem[] | Record<string, unknown>
|
|
41
|
+
): void {
|
|
42
|
+
if (Array.isArray(rhs)) {
|
|
43
|
+
WithIterableStatics.setItems(instance, rhs);
|
|
44
|
+
} else if (typeof rhs === 'object' && rhs !== null) {
|
|
45
|
+
Object.assign(instance, rhs);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static #notifyChange(instance: WithIterableStatics): void {
|
|
50
|
+
// Only dispatch if this instance actually supports it
|
|
51
|
+
// (i.e. it's an HTMLElement / EventTarget). Plain classes
|
|
52
|
+
// just skip this silently.
|
|
53
|
+
const maybeTarget = instance as unknown as { dispatchEvent?: (e: Event) => boolean };
|
|
54
|
+
if (typeof maybeTarget.dispatchEvent === 'function') {
|
|
55
|
+
maybeTarget.dispatchEvent(new Event('items-changed'));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return WithIterableStatics;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* installForwarding(ClubMember);
|
|
21
21
|
* // Now el.command delegates to el.behaviors.commandBehavior.command
|
|
22
22
|
*/
|
|
23
|
-
import { resolveValue } from '
|
|
24
|
-
import assignGingerly from '
|
|
23
|
+
import { resolveValue } from '../resolveValues.js';
|
|
24
|
+
import assignGingerly from '../assignGingerly.js';
|
|
25
25
|
/**
|
|
26
26
|
* Installs property forwarding on a class prototype based on `static propLinks`.
|
|
27
27
|
*
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* // Now el.command delegates to el.behaviors.commandBehavior.command
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { resolveValue, ResolveValuesOptions } from '
|
|
25
|
-
import assignGingerly, { IAssignGingerlyOptions } from '
|
|
24
|
+
import { resolveValue, ResolveValuesOptions } from '../resolveValues.js';
|
|
25
|
+
import assignGingerly, { IAssignGingerlyOptions } from '../assignGingerly.js';
|
|
26
26
|
|
|
27
27
|
export interface InstallForwardingOptions extends ResolveValuesOptions, IAssignGingerlyOptions {}
|
|
28
28
|
|
package/DX/paths.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths.ts — Typed path proxy and template tag for assignFrom authoring.
|
|
3
|
+
*
|
|
4
|
+
* Provides compile-time autocomplete and type safety for `?.`-prefixed path strings.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { paths, sp } from 'assign-gingerly/DX/paths.js';
|
|
8
|
+
*
|
|
9
|
+
* interface Person {
|
|
10
|
+
* firstName?: string;
|
|
11
|
+
* middleName?: string;
|
|
12
|
+
* lastName: string;
|
|
13
|
+
* address: { city: string; zip: string };
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* const $ = paths<Person>();
|
|
17
|
+
*
|
|
18
|
+
* // Use sp (split into parts) to create arrays for builtIns.join:
|
|
19
|
+
* const value = sp`${$.lastName}, ${$.firstName}`;
|
|
20
|
+
* // ['?.lastName', ', ', '?.firstName']
|
|
21
|
+
*
|
|
22
|
+
* // Use .path for raw string contexts (object keys, plain arrays):
|
|
23
|
+
* const key = $.textContent.path; // '?.textContent'
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Symbol used internally to detect path proxy objects.
|
|
27
|
+
* The sp tag function uses this to auto-extract path strings from proxies.
|
|
28
|
+
*/
|
|
29
|
+
const PATH_SYMBOL = Symbol('assign-gingerly-path');
|
|
30
|
+
/**
|
|
31
|
+
* Create a proxy for id-ref paths (#[varName]).
|
|
32
|
+
* After the initial #[varName], further property access chains with ?. from the resolved element.
|
|
33
|
+
* .path returns the #[varName] prefix (optionally with further ?. path).
|
|
34
|
+
*/
|
|
35
|
+
function createIdRefProxy(idRef, options) {
|
|
36
|
+
function handler() { }
|
|
37
|
+
return new Proxy(handler, {
|
|
38
|
+
get(_, prop) {
|
|
39
|
+
if (prop === 'path' || prop === PATH_SYMBOL) {
|
|
40
|
+
return idRef;
|
|
41
|
+
}
|
|
42
|
+
if (typeof prop === 'symbol')
|
|
43
|
+
return undefined;
|
|
44
|
+
// Chain further path segments after the id ref
|
|
45
|
+
const chained = `${idRef}?.${String(prop)}`;
|
|
46
|
+
return createIdRefProxy(chained, options);
|
|
47
|
+
},
|
|
48
|
+
apply(_, __, args) {
|
|
49
|
+
if (args.length > 0) {
|
|
50
|
+
const arg = args[0];
|
|
51
|
+
let argStr;
|
|
52
|
+
if (arg === true)
|
|
53
|
+
argStr = 'true';
|
|
54
|
+
else if (arg === false)
|
|
55
|
+
argStr = 'false';
|
|
56
|
+
else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
|
|
57
|
+
const fullPath = arg[PATH_SYMBOL];
|
|
58
|
+
argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
argStr = String(arg);
|
|
62
|
+
const chained = `${idRef}?.${argStr}`;
|
|
63
|
+
return createIdRefProxy(chained, options);
|
|
64
|
+
}
|
|
65
|
+
return createIdRefProxy(idRef, options);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a recursive proxy that records property access paths.
|
|
71
|
+
* Supports both property access and method call syntax (via apply trap on function target).
|
|
72
|
+
*
|
|
73
|
+
* When `aka` is provided, property names that match an alias *value* are output
|
|
74
|
+
* using the alias *key* instead (reverse alias).
|
|
75
|
+
*/
|
|
76
|
+
function createPathProxy(prefix, options) {
|
|
77
|
+
const aliasMap = options?.aka;
|
|
78
|
+
// Use a function as the target to enable the apply trap
|
|
79
|
+
function handler() { }
|
|
80
|
+
return new Proxy(handler, {
|
|
81
|
+
get(_, prop) {
|
|
82
|
+
if (prop === 'path' || prop === PATH_SYMBOL) {
|
|
83
|
+
return prefix.length > 0 ? `?.${prefix}` : '?.';
|
|
84
|
+
}
|
|
85
|
+
// Ignore symbol access (Symbol.iterator, Symbol.toPrimitive, etc.)
|
|
86
|
+
if (typeof prop === 'symbol')
|
|
87
|
+
return undefined;
|
|
88
|
+
let segment = String(prop);
|
|
89
|
+
// #-prefix: $['#firstName'] → '#[firstName]' (cached element ref)
|
|
90
|
+
if (segment.startsWith('#')) {
|
|
91
|
+
const varName = segment.substring(1);
|
|
92
|
+
const idRef = `#[${varName}]`;
|
|
93
|
+
// Return a proxy that starts from this id ref (can chain further with ?.)
|
|
94
|
+
return createIdRefProxy(idRef, options);
|
|
95
|
+
}
|
|
96
|
+
// Apply reverse alias: if prop matches an alias value, use the alias key
|
|
97
|
+
if (aliasMap) {
|
|
98
|
+
for (const [alias, target] of Object.entries(aliasMap)) {
|
|
99
|
+
if (target === segment) {
|
|
100
|
+
segment = alias;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const newPath = prefix ? `${prefix}?.${segment}` : segment;
|
|
106
|
+
return createPathProxy(newPath, options);
|
|
107
|
+
},
|
|
108
|
+
apply(_, __, args) {
|
|
109
|
+
// Method call syntax: $.querySelector('.username') → extends path with the argument
|
|
110
|
+
if (args.length > 0) {
|
|
111
|
+
const arg = args[0];
|
|
112
|
+
let argStr;
|
|
113
|
+
if (arg === true)
|
|
114
|
+
argStr = 'true';
|
|
115
|
+
else if (arg === false)
|
|
116
|
+
argStr = 'false';
|
|
117
|
+
else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
|
|
118
|
+
// Proxy arg — extract path without '?.' prefix
|
|
119
|
+
const fullPath = arg[PATH_SYMBOL];
|
|
120
|
+
argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
argStr = String(arg);
|
|
124
|
+
const newPath = prefix ? `${prefix}?.${argStr}` : argStr;
|
|
125
|
+
return createPathProxy(newPath, options);
|
|
126
|
+
}
|
|
127
|
+
// No args — method called with no arguments, return self
|
|
128
|
+
return createPathProxy(prefix, options);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create a typed path proxy for a given interface/type.
|
|
134
|
+
* Property accesses on the returned proxy produce `?.`-prefixed path strings.
|
|
135
|
+
* Method calls append their argument to the path.
|
|
136
|
+
*
|
|
137
|
+
* @param options - Optional aka aliases and withMethods for path generation
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* const $ = paths<Person>();
|
|
141
|
+
* $.lastName.path // '?.lastName'
|
|
142
|
+
* $.address.city.path // '?.address?.city'
|
|
143
|
+
*
|
|
144
|
+
* // With aka (reverse alias applied):
|
|
145
|
+
* const $ = paths<MyEl>({ aka: { q: 'querySelector' } });
|
|
146
|
+
* $.querySelector('.user').textContent.path // '?.q?..user?.textContent'
|
|
147
|
+
*
|
|
148
|
+
* // Inside sp template literals, .path is not needed:
|
|
149
|
+
* sp`${$.lastName}, ${$.firstName}` // ['?.lastName', ', ', '?.firstName']
|
|
150
|
+
*/
|
|
151
|
+
export function paths(options) {
|
|
152
|
+
return createPathProxy('', options);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create an assignment pair: { [lhs.path]: rhs.path }.
|
|
156
|
+
* Used to express "set this target to this source value" in a spreadable form.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* set($.clone.querySelector('.username').textContent).to($.username)
|
|
160
|
+
* // { '?.clone?.q?..username?.textContent': '?.username' }
|
|
161
|
+
*
|
|
162
|
+
* // Spread into an assign object:
|
|
163
|
+
* assign: {
|
|
164
|
+
* ...set($.textContent).to($.name),
|
|
165
|
+
* ...set($.className).to($.theme),
|
|
166
|
+
* count: 1
|
|
167
|
+
* }
|
|
168
|
+
*/
|
|
169
|
+
export function set(lhs) {
|
|
170
|
+
const lhsStr = lhs && typeof lhs === 'object' && PATH_SYMBOL in lhs
|
|
171
|
+
? lhs[PATH_SYMBOL]
|
|
172
|
+
: String(lhs);
|
|
173
|
+
return {
|
|
174
|
+
to(rhs) {
|
|
175
|
+
const rhsStr = rhs && typeof rhs === 'object' && PATH_SYMBOL in rhs
|
|
176
|
+
? rhs[PATH_SYMBOL]
|
|
177
|
+
: rhs;
|
|
178
|
+
return { [lhsStr]: rhsStr };
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Recursively walk a value and convert any path proxy objects to their
|
|
184
|
+
* `?.`-prefixed string representation.
|
|
185
|
+
*
|
|
186
|
+
* Use this to wrap entire config objects or arrays that contain proxy values,
|
|
187
|
+
* extracting all path strings in one pass.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const $ = paths<MyVM>({ aka: { q: 'querySelector' } });
|
|
191
|
+
*
|
|
192
|
+
* const config = smoothOver({
|
|
193
|
+
* assign: {
|
|
194
|
+
* incrementButton: $.clone.querySelector('.increment'),
|
|
195
|
+
* decrementButton: $.clone.querySelector('.decrement'),
|
|
196
|
+
* }
|
|
197
|
+
* });
|
|
198
|
+
* // { assign: { incrementButton: '?.clone?.q?..increment', ... } }
|
|
199
|
+
*/
|
|
200
|
+
export function smoothOver(value) {
|
|
201
|
+
if (value && typeof value === 'object' && PATH_SYMBOL in value) {
|
|
202
|
+
return value[PATH_SYMBOL];
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return value.map(smoothOver);
|
|
206
|
+
}
|
|
207
|
+
if (value && typeof value === 'object') {
|
|
208
|
+
const proto = Object.getPrototypeOf(value);
|
|
209
|
+
if (proto === Object.prototype || proto === null) {
|
|
210
|
+
const result = {};
|
|
211
|
+
for (const [k, v] of Object.entries(value)) {
|
|
212
|
+
result[k] = smoothOver(v);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Merge multiple set(...).to(...) pairs (and/or plain objects) into an `{ assign: {...} }` object.
|
|
221
|
+
* Spread the result into a merge config to avoid repeated `...` per entry.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* {
|
|
225
|
+
* ifKeyIn: ['statusClassName', 'statusMessageText'],
|
|
226
|
+
* ifAllOf: ['clone'],
|
|
227
|
+
* ...doAssign(
|
|
228
|
+
* set($.clone.querySelector('.status').className).to($.statusClassName),
|
|
229
|
+
* set($.clone.querySelector('.status-text').textContent).to($.statusMessageText),
|
|
230
|
+
* )
|
|
231
|
+
* }
|
|
232
|
+
* // Equivalent to: { ifKeyIn: [...], ifAllOf: [...], assign: { '?.clone?.q?..status?.className': '?.statusClassName', ... } }
|
|
233
|
+
*
|
|
234
|
+
* // Mix with literal values:
|
|
235
|
+
* ...doAssign(
|
|
236
|
+
* set($.clone.querySelector('.count-value').textContent).to($.count),
|
|
237
|
+
* { renderCount: 1 },
|
|
238
|
+
* )
|
|
239
|
+
*/
|
|
240
|
+
export function doAssign(...pairs) {
|
|
241
|
+
return { assign: Object.assign({}, ...pairs) };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Compile-time loop expansion: generates one entry per key from a factory function.
|
|
245
|
+
* Creates a typed proxy internally — the factory receives both the key and the proxy.
|
|
246
|
+
*
|
|
247
|
+
* @param keys - Array of property names to iterate (type-checked against T)
|
|
248
|
+
* @param factory - Function that produces a config entry for each key
|
|
249
|
+
* @param options - Optional PathsOptions (aka, withMethods) for the internal proxy
|
|
250
|
+
* @returns Array of factory results (one per key) — spread into merges array
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* import { forEachKeyIn, set, doAssign } from 'assign-gingerly/paths.js';
|
|
254
|
+
*
|
|
255
|
+
* interface Person extends HTMLElement { firstName: string; lastName: string; }
|
|
256
|
+
*
|
|
257
|
+
* const merges = [
|
|
258
|
+
* ...forEachKeyIn<Person>(['firstName', 'lastName'], (key, $) => ({
|
|
259
|
+
* ifKeyIn: [key],
|
|
260
|
+
* assignOptions: { pin: { [key]: { qry: `[name="${key}"]` } } },
|
|
261
|
+
* ...doAssign(set($['#' + key]).to($[key]))
|
|
262
|
+
* })),
|
|
263
|
+
* ];
|
|
264
|
+
*/
|
|
265
|
+
export function forEachKeyIn(keys, factory, options) {
|
|
266
|
+
const $ = paths(options);
|
|
267
|
+
return keys.map(key => factory(key, $));
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Tagged template literal that splits a template into an array of parts.
|
|
271
|
+
* Interleaves static string segments with interpolated values.
|
|
272
|
+
*
|
|
273
|
+
* Path proxy objects are auto-detected and converted to their `?.`-prefixed
|
|
274
|
+
* string representation — no `.path` call needed inside sp template literals.
|
|
275
|
+
*
|
|
276
|
+
* Arrays passed as interpolations are preserved as nested arrays (for
|
|
277
|
+
* all-or-nothing optional segments in builtIns.join).
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* const $ = paths<Person>();
|
|
281
|
+
*
|
|
282
|
+
* // Basic usage:
|
|
283
|
+
* sp`${$.lastName}, ${$.firstName}`
|
|
284
|
+
* // ['?.lastName', ', ', '?.firstName']
|
|
285
|
+
*
|
|
286
|
+
* // With optional segment (nested array, all-or-nothing in join):
|
|
287
|
+
* sp`${$.lastName}${[', ', $.middleName]}, ${$.firstName}`
|
|
288
|
+
* // ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
289
|
+
*/
|
|
290
|
+
export function sp(strings, ...values) {
|
|
291
|
+
const result = [];
|
|
292
|
+
for (let i = 0; i < strings.length; i++) {
|
|
293
|
+
if (strings[i])
|
|
294
|
+
result.push(strings[i]);
|
|
295
|
+
if (i < values.length) {
|
|
296
|
+
const v = values[i];
|
|
297
|
+
if (v && typeof v === 'object' && PATH_SYMBOL in v) {
|
|
298
|
+
// Auto-extract path from proxy object
|
|
299
|
+
result.push(v[PATH_SYMBOL]);
|
|
300
|
+
}
|
|
301
|
+
else if (Array.isArray(v)) {
|
|
302
|
+
// Nested array — recursively extract paths from proxy elements
|
|
303
|
+
result.push(v.map(el => el && typeof el === 'object' && PATH_SYMBOL in el ? el[PATH_SYMBOL] : el));
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
result.push(v);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Extract the last segment from a `?.`-prefixed path string.
|
|
314
|
+
* e.g., '?.address?.city' → 'city', '?.firstName' → 'firstName'
|
|
315
|
+
*/
|
|
316
|
+
function extractPropName(pathStr) {
|
|
317
|
+
const parts = pathStr.split('?.');
|
|
318
|
+
return parts[parts.length - 1];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Tagged template literal that produces an array of {prop, val} objects + literal strings.
|
|
322
|
+
* Designed for `builtIns.microDataJoin` — provides both the property name (for itemprop)
|
|
323
|
+
* and the path string (for resolution).
|
|
324
|
+
*
|
|
325
|
+
* Path proxy objects are auto-detected and converted to `{prop, val}` objects.
|
|
326
|
+
* Plain objects passed as interpolations are preserved as-is (allows developer overrides
|
|
327
|
+
* with custom prop, val, format, etc.).
|
|
328
|
+
* Arrays are preserved as nested arrays (for optional segments).
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* const $ = paths<Person>();
|
|
332
|
+
*
|
|
333
|
+
* // Basic usage:
|
|
334
|
+
* md`${$.firstName} ${$.lastName}`
|
|
335
|
+
* // [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'lastName', val: '?.lastName' }]
|
|
336
|
+
*
|
|
337
|
+
* // With developer override (custom prop name, format):
|
|
338
|
+
* md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
|
|
339
|
+
* // [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'birthDate', val: '?.birthDT', format: 'long' }]
|
|
340
|
+
*
|
|
341
|
+
* // With optional segment:
|
|
342
|
+
* md`${$.firstName}${[' ', $.middleName]} ${$.lastName}`
|
|
343
|
+
* // [{ prop: 'firstName', val: '?.firstName' }, [' ', { prop: 'middleName', val: '?.middleName' }], ' ', { prop: 'lastName', val: '?.lastName' }]
|
|
344
|
+
*/
|
|
345
|
+
export function md(strings, ...values) {
|
|
346
|
+
const result = [];
|
|
347
|
+
for (let i = 0; i < strings.length; i++) {
|
|
348
|
+
if (strings[i])
|
|
349
|
+
result.push(strings[i]);
|
|
350
|
+
if (i < values.length) {
|
|
351
|
+
const v = values[i];
|
|
352
|
+
if (v && typeof v === 'object' && PATH_SYMBOL in v) {
|
|
353
|
+
// Proxy object → {prop, val}
|
|
354
|
+
const pathStr = v[PATH_SYMBOL];
|
|
355
|
+
result.push({ prop: extractPropName(pathStr), val: pathStr });
|
|
356
|
+
}
|
|
357
|
+
else if (Array.isArray(v)) {
|
|
358
|
+
// Nested array — recursively convert proxy elements to {prop, val}
|
|
359
|
+
result.push(v.map(el => {
|
|
360
|
+
if (el && typeof el === 'object' && PATH_SYMBOL in el) {
|
|
361
|
+
const pathStr = el[PATH_SYMBOL];
|
|
362
|
+
return { prop: extractPropName(pathStr), val: pathStr };
|
|
363
|
+
}
|
|
364
|
+
return el;
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
else if (v && typeof v === 'object' && 'prop' in v) {
|
|
368
|
+
// Developer override object — extract val from proxy if present
|
|
369
|
+
const processed = { ...v };
|
|
370
|
+
if (processed.val && typeof processed.val === 'object' && PATH_SYMBOL in processed.val) {
|
|
371
|
+
processed.val = processed.val[PATH_SYMBOL];
|
|
372
|
+
}
|
|
373
|
+
result.push(processed);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
result.push(v);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
}
|
package/{paths.ts → DX/paths.ts}
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides compile-time autocomplete and type safety for `?.`-prefixed path strings.
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
|
-
* import { paths, sp } from 'assign-gingerly/paths.js';
|
|
7
|
+
* import { paths, sp } from 'assign-gingerly/DX/paths.js';
|
|
8
8
|
*
|
|
9
9
|
* interface Person {
|
|
10
10
|
* firstName?: string;
|
package/README.md
CHANGED
|
@@ -816,7 +816,7 @@ While we are in the business of passing values of object A into object B, we mig
|
|
|
816
816
|
|
|
817
817
|
| Operator | Name | Description | Example |
|
|
818
818
|
|----------|------|-------------|---------|
|
|
819
|
-
| ` +=` | Increment | Add to numeric
|
|
819
|
+
| ` +=` | Increment | Add to numeric, concat strings, append to arrays, or [bind events](docs/event-binding.md) | `'count +=': 5` |
|
|
820
820
|
| ` =!` | Toggle | Negate a boolean (or any value via `!`) | `'visible =!': '.'` |
|
|
821
821
|
| ` -=` | Delete | Remove properties from an object | `'?.data -=': 'key'` |
|
|
822
822
|
| ` Y=` | Merge | Recursively `assignGingerly` into a sub-object | `'style Y=': { width: '100px' }` |
|
|
@@ -857,6 +857,8 @@ The `+=` command syntax is `<path> +=` where the path uses the `?.` nested notat
|
|
|
857
857
|
| LHS type | RHS type | Result |
|
|
858
858
|
|----------|----------|--------|
|
|
859
859
|
| number | number | addition (`2 += 3` → `5`) |
|
|
860
|
+
| number | string (numeric) | parse + addition (`5 += '3'` → `8`) |
|
|
861
|
+
| number | string (non-numeric) | string concatenation (`5 += 'px'` → `'5px'`) |
|
|
860
862
|
| string | any | string concatenation (`"hello" += 3` → `"hello3"`) |
|
|
861
863
|
| array | array | array concatenation (`[1,2] += [3,4]` → `[1,2,3,4]`) |
|
|
862
864
|
| array | non-array | push single item (`[1,2] += 3` → `[1,2,3]`) |
|
|
@@ -876,6 +878,22 @@ assignGingerly(obj, {
|
|
|
876
878
|
assignGingerly(obj, { '?.tags +=': 'e' }); // ['a', 'b', 'c', 'd', 'e']
|
|
877
879
|
```
|
|
878
880
|
|
|
881
|
+
**Event binding with `+=`:**
|
|
882
|
+
|
|
883
|
+
When the LHS resolves to a DOM Element and the RHS is an object with an `on` property, `+=` attaches a declarative event listener:
|
|
884
|
+
|
|
885
|
+
```JavaScript
|
|
886
|
+
assignFrom(this.shadowRoot, {
|
|
887
|
+
'?.querySelector?.button +=': {
|
|
888
|
+
on: 'click',
|
|
889
|
+
'?.isHappy =!': '.', // toggle host property
|
|
890
|
+
fromLHS: { '?.age +=': '?.dataset.diff' } // read from button, assign to host
|
|
891
|
+
}
|
|
892
|
+
}, { from: this, withMethods: ['querySelector'] });
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
The handler is lazy-loaded on demand. For full details including assignment vectors, dedup, nudge, and custom event dispatch, see [docs/event-binding.md](docs/event-binding.md).
|
|
896
|
+
|
|
879
897
|
## Example 5 - Toggling boolean values and negating
|
|
880
898
|
|
|
881
899
|
The `=!` command allows us to toggle boolean values:
|
|
@@ -3900,7 +3918,7 @@ await assignFromAsync(container, {
|
|
|
3900
3918
|
if: '?.showPanel',
|
|
3901
3919
|
instantiate: 'globalThis://panelTemplate',
|
|
3902
3920
|
assign: {
|
|
3903
|
-
|
|
3921
|
+
toClone: {
|
|
3904
3922
|
'#[title]?.textContent': '?.panelTitle',
|
|
3905
3923
|
'#[body]?.textContent': '?.panelContent'
|
|
3906
3924
|
},
|
|
@@ -3913,7 +3931,7 @@ await assignFromAsync(container, {
|
|
|
3913
3931
|
}, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
3914
3932
|
```
|
|
3915
3933
|
|
|
3916
|
-
The `assign.
|
|
3934
|
+
The `assign.toClone` paths resolve against `options.from` (the same source that drives the `if` condition). For multi-element templates, use `assign.configs` (same zip semantics as `manageTemplateList`).
|
|
3917
3935
|
|
|
3918
3936
|
### View Transitions
|
|
3919
3937
|
|
|
@@ -4202,7 +4220,7 @@ Uses comment markers (`<!--?start name="microDataJoin"-->` / `<!--?end-->`) to t
|
|
|
4202
4220
|
The `md` tagged template literal produces the `{prop, val}` structure from proxy objects — full autocomplete and type safety:
|
|
4203
4221
|
|
|
4204
4222
|
```TypeScript
|
|
4205
|
-
import { paths, md } from 'assign-gingerly/paths.js';
|
|
4223
|
+
import { paths, md } from 'assign-gingerly/DX/paths.js';
|
|
4206
4224
|
|
|
4207
4225
|
interface Person { firstName: string; lastName: string; birthDT: Date; age: number; }
|
|
4208
4226
|
const $ = paths<Person>();
|
|
@@ -4230,7 +4248,7 @@ assignFrom(document.body, {
|
|
|
4230
4248
|
instantiate: 'globalThis://country-ranking',
|
|
4231
4249
|
},
|
|
4232
4250
|
fromEachItem: {
|
|
4233
|
-
|
|
4251
|
+
toClone: { '?.querySelector?.tr?.ish': '?.' },
|
|
4234
4252
|
withOptions: { withMethods: ['querySelector'], infer: true },
|
|
4235
4253
|
get: { key: '?.rank' }
|
|
4236
4254
|
}
|
|
@@ -4246,7 +4264,7 @@ assignFrom(document.body, {
|
|
|
4246
4264
|
|
|
4247
4265
|
1. Resolves `forEach` (iterable) and `instantiate` (template) from the `resolve` block
|
|
4248
4266
|
2. Clones the template once per item, buffering all clones into a `DocumentFragment`
|
|
4249
|
-
3. For each clone, calls `assignFrom(clone,
|
|
4267
|
+
3. For each clone, calls `assignFrom(clone, toClone, { from: item, ...withOptions })` — distributing the item's data
|
|
4250
4268
|
4. Inserts the fragment between comment markers in one DOM operation
|
|
4251
4269
|
5. On subsequent calls, reconciles by `key` — adds new items, removes missing ones, updates existing clones in place
|
|
4252
4270
|
|
|
@@ -4259,13 +4277,13 @@ The `key` field (in `fromEachItem.get`) identifies each item for stable identity
|
|
|
4259
4277
|
|
|
4260
4278
|
Without `key`, positional matching is used (item[i] → clone[i]).
|
|
4261
4279
|
|
|
4262
|
-
**Shared parent data (`
|
|
4280
|
+
**Shared parent data (`fromHost`):**
|
|
4263
4281
|
|
|
4264
4282
|
Pass data from the outer VM into each clone (e.g., aggregate totals):
|
|
4265
4283
|
|
|
4266
4284
|
```JavaScript
|
|
4267
|
-
|
|
4268
|
-
|
|
4285
|
+
fromHost: {
|
|
4286
|
+
toClone: {
|
|
4269
4287
|
'?.querySelector?.[part~="totalMedalCount"]?.textContent': '?.totalMedalCount'
|
|
4270
4288
|
},
|
|
4271
4289
|
withOptions: { withMethods: ['querySelector'] }
|
|
@@ -4327,7 +4345,7 @@ assignFrom(element, {
|
|
|
4327
4345
|
For JSON generated config files generated from TypeScript/`.mts`/`mjs` files during a build or server-side rendering, the `paths` utility provides compile-time autocomplete and type safety for `?.`-prefixed path strings. The `sp` tagged template literal ("split into parts") produces arrays suitable for `builtIns.join`. The `md` tagged template literal produces `{prop, val}` objects suitable for `builtIns.microDataJoin`.
|
|
4328
4346
|
|
|
4329
4347
|
```TypeScript
|
|
4330
|
-
import { paths, sp } from 'assign-gingerly/paths.js';
|
|
4348
|
+
import { paths, sp } from 'assign-gingerly/DX/paths.js';
|
|
4331
4349
|
|
|
4332
4350
|
interface Person {
|
|
4333
4351
|
firstName?: string;
|
|
@@ -4403,7 +4421,7 @@ const pattern = {
|
|
|
4403
4421
|
The `md` tag produces `{prop, val}` objects for `builtIns.microDataJoin`:
|
|
4404
4422
|
|
|
4405
4423
|
```TypeScript
|
|
4406
|
-
import { paths, md } from 'assign-gingerly/paths.js';
|
|
4424
|
+
import { paths, md } from 'assign-gingerly/DX/paths.js';
|
|
4407
4425
|
|
|
4408
4426
|
interface Person { firstName: string; lastName: string; birthDT: Date; age: number; }
|
|
4409
4427
|
const $ = paths<Person>();
|
|
@@ -4772,6 +4790,18 @@ console.log(app.todos.title); // 'My Todos'
|
|
|
4772
4790
|
console.log([...app.todos]); // ['Buy milk', 'Walk dog'] (list unchanged)
|
|
4773
4791
|
```
|
|
4774
4792
|
|
|
4793
|
+
This pattern seems useful and reusable enough that this package provides a DX utility mixin to add this functionality to any class definition:
|
|
4794
|
+
|
|
4795
|
+
```JS
|
|
4796
|
+
import {IterableMixin} from 'assign-gingerly/DX/IterableMixin.js';
|
|
4797
|
+
|
|
4798
|
+
class PlainList extends IterableMixin<number>()(class {}) {}
|
|
4799
|
+
|
|
4800
|
+
class MyIterableCustomElement extends IterableMixin<string>()(HTMLElement) {}
|
|
4801
|
+
|
|
4802
|
+
class Combined extends OtherMixin()(IterableMixin<string>()(HTMLElement)) {}
|
|
4803
|
+
```
|
|
4804
|
+
|
|
4775
4805
|
### Use case: Validation on assignment
|
|
4776
4806
|
|
|
4777
4807
|
```JavaScript
|
|
@@ -4816,7 +4846,7 @@ Only classes that explicitly define their own `assignTo` are affected. The check
|
|
|
4816
4846
|
`installForwarding` installs getter/setter pairs on a class prototype that delegate to nested paths on the instance. This is useful for exposing deeply nested properties at the top level of an object — particularly for custom elements that delegate behavior to compositional feature classes.
|
|
4817
4847
|
|
|
4818
4848
|
```JavaScript
|
|
4819
|
-
import { installForwarding } from 'assign-gingerly/installForwarding.js';
|
|
4849
|
+
import { installForwarding } from 'assign-gingerly/DX/installForwarding.js';
|
|
4820
4850
|
```
|
|
4821
4851
|
|
|
4822
4852
|
### Basic usage
|
|
@@ -5914,7 +5944,7 @@ assignGingerly(el, {
|
|
|
5914
5944
|
|
|
5915
5945
|
```JavaScript
|
|
5916
5946
|
import { PropertyBag, assignFeatures } from 'assign-gingerly/assignFeatures.js';
|
|
5917
|
-
import { installForwarding } from 'assign-gingerly/installForwarding.js';
|
|
5947
|
+
import { installForwarding } from 'assign-gingerly/DX/installForwarding.js';
|
|
5918
5948
|
|
|
5919
5949
|
// 1. Define a feature container by subclassing PropertyBag
|
|
5920
5950
|
class ClubMemberBehaviors extends PropertyBag {
|