assign-gingerly 0.0.53 → 0.0.55
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 +847 -5
- package/assignFrom.js +229 -9
- package/assignFrom.ts +338 -9
- package/assignGingerly.js +34 -1
- package/assignGingerly.ts +54 -1
- package/beVigilant.js +73 -0
- package/beVigilant.ts +85 -0
- package/enhanceAll.js +106 -0
- package/enhanceAll.ts +138 -0
- package/handlers/join.js +74 -0
- package/handlers/join.ts +80 -0
- package/handlers/lazyLoad.js +212 -0
- package/handlers/lazyLoad.ts +307 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +63 -0
- package/handlers/microDataJoin.js +184 -0
- package/handlers/microDataJoin.ts +270 -0
- package/inferencer/.gitmodules +3 -0
- package/inferencer/.vscode/settings.json +2 -0
- package/inferencer/InferencedPropagator.js +230 -0
- package/inferencer/InferencedPropagator.ts +269 -0
- package/inferencer/LICENSE +21 -0
- package/inferencer/README.md +524 -0
- package/inferencer/Requirements/SupportForPropagator.md +368 -0
- package/inferencer/imports.html +7 -0
- package/inferencer/inferencer.js +254 -0
- package/inferencer/inferencer.ts +292 -0
- package/inferencer/package-lock.json +129 -0
- package/inferencer/package.json +60 -0
- package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
- package/inferencer/playwright-report/index.html +90 -0
- package/inferencer/playwright.config.ts +54 -0
- package/inferencer/test-results/.last-run.json +6 -0
- package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
- package/inferencer/tests/inferencedPropagator.html +428 -0
- package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
- package/inferencer/tests/inferencer.html +355 -0
- package/inferencer/tests/inferencer.spec.ts +19 -0
- package/inferencer/tsconfig.json +19 -0
- package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
- package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
- package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
- package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
- package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
- package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
- package/inferencer/types/LICENSE +21 -0
- package/inferencer/types/NewCustomElement.md +388 -0
- package/inferencer/types/NewCustomElementFeature.md +683 -0
- package/inferencer/types/NewEnhancementInstructions.md +705 -0
- package/inferencer/types/README.md +2 -0
- package/inferencer/types/agrace/types.d.ts +11 -0
- package/inferencer/types/assign-gingerly/types.d.ts +572 -0
- package/inferencer/types/be-a-beacon/types.d.ts +17 -0
- package/inferencer/types/be-bound/types.d.ts +66 -0
- package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
- package/inferencer/types/be-calculating/types.d.ts +54 -0
- package/inferencer/types/be-clonable/types.d.ts +38 -0
- package/inferencer/types/be-committed/types.d.ts +22 -0
- package/inferencer/types/be-consoling/types.d.ts +24 -0
- package/inferencer/types/be-decked-with/types.d.ts +26 -0
- package/inferencer/types/be-delible/types.d.ts +27 -0
- package/inferencer/types/be-dispatching/types.d.ts +34 -0
- package/inferencer/types/be-evanescent/types.d.ts +20 -0
- package/inferencer/types/be-flashy/types.d.ts +21 -0
- package/inferencer/types/be-gone/types.d.ts +25 -0
- package/inferencer/types/be-observing/types.d.ts +55 -0
- package/inferencer/types/be-reflective/types.d.ts +78 -0
- package/inferencer/types/be-reformable/types.d.ts +49 -0
- package/inferencer/types/be-render-neutral/types.d.ts +32 -0
- package/inferencer/types/be-switched/types.d.ts +146 -0
- package/inferencer/types/be-typed/types.d.ts +32 -0
- package/inferencer/types/be-valued/types.d.ts +22 -0
- package/inferencer/types/data-props/types.d.ts +34 -0
- package/inferencer/types/do-inc/types.d.ts +56 -0
- package/inferencer/types/do-invoke/types.d.ts +38 -0
- package/inferencer/types/do-merge/types.d.ts +28 -0
- package/inferencer/types/do-toggle/types.d.ts +31 -0
- package/inferencer/types/face-up/types.d.ts +100 -0
- package/inferencer/types/fetch-for/types.d.ts +36 -0
- package/inferencer/types/folder-picker/types.d.ts +21 -0
- package/inferencer/types/global.d.ts +29 -0
- package/inferencer/types/id-generation/types.d.ts +26 -0
- package/inferencer/types/inferencer/types.d.ts +46 -0
- package/inferencer/types/mount-observer/types.d.ts +363 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
- package/inferencer/types/pipe-in/types.d.ts +52 -0
- package/inferencer/types/roundabout/types.d.ts +268 -0
- package/inferencer/types/soak-up/types.d.ts +40 -0
- package/inferencer/types/templ-maker/types.d.ts +43 -0
- package/inferencer/types/time-ticker/types.d.ts +62 -0
- package/inferencer/types/truth-sourcer/types.d.ts +44 -0
- package/inferencer/upSearch.js +27 -0
- package/inferencer/upSearch.ts +26 -0
- package/inferencer/withScopePerimeter.js +27 -0
- package/inferencer/withScopePerimeter.ts +33 -0
- package/inferredAssignments.js +38 -0
- package/inferredAssignments.ts +65 -0
- package/isAllowedImportPath.js +42 -0
- package/isAllowedImportPath.ts +53 -0
- package/package.json +57 -3
- package/paths.js +231 -0
- package/paths.ts +413 -0
- package/processHandlerCommands.js +188 -0
- package/processHandlerCommands.ts +217 -0
- package/resolveIdRef.js +144 -0
- package/resolveIdRef.ts +170 -0
- package/resolveValues.js +41 -2
- package/resolveValues.ts +41 -1
- package/transitionHelper.js +109 -0
- package/transitionHelper.ts +132 -0
- package/types/assign-gingerly/types.d.ts +89 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Support for Propagator
|
|
2
|
+
|
|
3
|
+
A common requirement for working with multiple DOM elements, built in and custom, is being able to subscribe to changes of a property.
|
|
4
|
+
|
|
5
|
+
<!-- IMPLEMENTATION THOUGHTS (Kiro):
|
|
6
|
+
|
|
7
|
+
The core challenge here is creating a unified event-based interface for property changes across heterogeneous element types. The PropagatorInferencer needs to act as an adapter/facade that normalizes different change detection mechanisms into a single EventTarget interface.
|
|
8
|
+
|
|
9
|
+
Key architectural insight: We're building a "virtual propagator" - an EventTarget that emits property change events by intelligently hooking into whatever change detection mechanism is available for that specific element type.
|
|
10
|
+
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
One major obstacle to doing that is there is no standard metadata that elements support in order to indicate how to do that. So we try our best to "infer" how to do this to our best ability, hence it seems that this functionality fits in a package that focusing on inferring.
|
|
14
|
+
|
|
15
|
+
There are a number of scenarios to consider:
|
|
16
|
+
|
|
17
|
+
1. Mostly built-in elements that have "source of truth" attributes that reflect the value of the property.
|
|
18
|
+
|
|
19
|
+
Examples are the aria attributes, the output's value property, the anchor tag href property.
|
|
20
|
+
|
|
21
|
+
Setting the property value updates the attributes, and vice versa.
|
|
22
|
+
|
|
23
|
+
2. Properties of built-in elements with corresponding events, but the events are only user driven. An example would be the input's value property. If the property is set programmatically, no attribute changes, nor any event fires. How can we listen for changes made regardless of how? We could theoretically override the value setter, but if memory serves, that not be reliable either (We need to do some experiments on this).
|
|
24
|
+
|
|
25
|
+
3. Other even more challenging scenarios are the iframe's src property.
|
|
26
|
+
|
|
27
|
+
4. For custom elements, because standard setters and getters are typically generated on the prototype, overriding the setter may often be a viable solution. Perhaps the only solution, since there is no currently available way to determine any metadata between properties and attributes, and we currently don't have a way to subscribe to custom css state changes.
|
|
28
|
+
|
|
29
|
+
5. For custom elements that adopt a [roundabout property](https://github.com/bahrus/roundabout), such custom elements will have a propagator property that emits events with the name of the property when that property changes. That's really what we need.
|
|
30
|
+
|
|
31
|
+
So this addendum to the Infer class should have a new method:
|
|
32
|
+
|
|
33
|
+
```TypeScript
|
|
34
|
+
export class Infer<TValue = any, TDisplay = any> {
|
|
35
|
+
async getPropagator(){
|
|
36
|
+
const {elementEnhancement} = this;
|
|
37
|
+
const {localName} = elementEnhancement;
|
|
38
|
+
if(localName.includes('-')){
|
|
39
|
+
await (enhancedElement.customElementRegistry || customElements).whenDefined(localName);
|
|
40
|
+
const {propagator} = elementEnhancement;
|
|
41
|
+
if(propagator instanceof EventTarget) return propagator;
|
|
42
|
+
}
|
|
43
|
+
//no built in propagator. Implement a virtual propagator as best we can using our
|
|
44
|
+
//best inferring abilities
|
|
45
|
+
const {PropagatorInferencer} = await import('./PropagatorInferencer.js');
|
|
46
|
+
await propagatorInference = new PropagatorInferencer(elementEnhancement);
|
|
47
|
+
return await propagatorInference.getPropagator();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Implementation Thoughts (Kiro)
|
|
56
|
+
|
|
57
|
+
### Architectural Approach
|
|
58
|
+
|
|
59
|
+
The PropagatorInferencer should create a **virtual propagator** - an EventTarget that emits property change events by intelligently detecting changes through whatever mechanism is available for each element type.
|
|
60
|
+
|
|
61
|
+
### Core Design Principles
|
|
62
|
+
|
|
63
|
+
1. **Lazy Initialization**: Don't set up watchers until `getPropagator()` is called
|
|
64
|
+
2. **Property-Specific Watching**: Each property being watched may need different strategies
|
|
65
|
+
3. **Memory Safety**: Use WeakRefs and proper cleanup to avoid memory leaks
|
|
66
|
+
4. **Fallback Chain**: Try the most reliable method first, fall back to less ideal approaches
|
|
67
|
+
|
|
68
|
+
### Strategy Pattern by Element Type
|
|
69
|
+
|
|
70
|
+
#### Strategy 1: Attribute-Reflected Properties (aria-*, href, etc.)
|
|
71
|
+
- **Detection**: Check if setting property updates corresponding attribute
|
|
72
|
+
- **Implementation**: Use MutationObserver on attributes
|
|
73
|
+
- **Pros**: Reliable, no prototype pollution
|
|
74
|
+
- **Cons**: Only works for reflected properties
|
|
75
|
+
- **Example**: `aria-valuenow`, `href`, `src` (for some elements)
|
|
76
|
+
|
|
77
|
+
#### Strategy 2: Native Event-Driven Properties (input.value, etc.)
|
|
78
|
+
- **Detection**: Check if element fires native events (input, change)
|
|
79
|
+
- **Implementation**: Listen to native events, but this only catches user-driven changes
|
|
80
|
+
- **Challenge**: Programmatic changes don't fire events
|
|
81
|
+
- **Hybrid Solution**: Combine event listening with setter interception
|
|
82
|
+
|
|
83
|
+
#### Strategy 3: Setter Interception (Custom Elements)
|
|
84
|
+
- **Detection**: Check if property descriptor exists on prototype
|
|
85
|
+
- **Implementation**: Create a proxy or override setter on the instance
|
|
86
|
+
- **Pros**: Catches all changes (user and programmatic)
|
|
87
|
+
- **Cons**: Can be fragile, may conflict with frameworks
|
|
88
|
+
- **Best for**: Custom elements with standard getters/setters
|
|
89
|
+
|
|
90
|
+
#### Strategy 4: Polling (Last Resort)
|
|
91
|
+
- **Detection**: When no other method works
|
|
92
|
+
- **Implementation**: requestAnimationFrame-based polling with dirty checking
|
|
93
|
+
- **Pros**: Always works
|
|
94
|
+
- **Cons**: Performance overhead, delayed detection
|
|
95
|
+
- **Use case**: iframe.src and other problematic properties
|
|
96
|
+
|
|
97
|
+
### Proposed Class Structure
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
export class PropagatorInferencer {
|
|
101
|
+
#element: Element;
|
|
102
|
+
#propagator: EventTarget;
|
|
103
|
+
#watchers: Map<string, WatcherStrategy>;
|
|
104
|
+
#abortController: AbortController;
|
|
105
|
+
|
|
106
|
+
constructor(element: Element) {
|
|
107
|
+
this.#element = element;
|
|
108
|
+
this.#propagator = new EventTarget();
|
|
109
|
+
this.#watchers = new Map();
|
|
110
|
+
this.#abortController = new AbortController();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getPropagator(): Promise<EventTarget> {
|
|
114
|
+
// Return the virtual propagator that will emit events
|
|
115
|
+
// when properties change
|
|
116
|
+
return this.#propagator;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async watchProperty(propName: string): Promise<void> {
|
|
120
|
+
// Determine best strategy for this property
|
|
121
|
+
// Set up the appropriate watcher
|
|
122
|
+
// Store cleanup logic in #watchers
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#selectStrategy(propName: string): WatcherStrategy {
|
|
126
|
+
// 1. Check if attribute-reflected
|
|
127
|
+
// 2. Check if has native events
|
|
128
|
+
// 3. Check if has setter on prototype
|
|
129
|
+
// 4. Fall back to polling
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
destroy(): void {
|
|
133
|
+
// Clean up all watchers
|
|
134
|
+
this.#abortController.abort();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Strategy Detection Algorithm
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
async #selectStrategy(propName: string): Promise<WatcherStrategy> {
|
|
143
|
+
const element = this.#element;
|
|
144
|
+
|
|
145
|
+
// Strategy 1: Check for attribute reflection
|
|
146
|
+
const attrName = this.#getAttributeName(propName);
|
|
147
|
+
if (attrName && this.#isAttributeReflected(propName, attrName)) {
|
|
148
|
+
return new AttributeWatcher(element, propName, attrName);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Strategy 2: Check for native events
|
|
152
|
+
const eventType = this.#getNativeEventType(propName);
|
|
153
|
+
if (eventType) {
|
|
154
|
+
// Hybrid: event + setter interception for programmatic changes
|
|
155
|
+
return new HybridEventWatcher(element, propName, eventType);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Strategy 3: Setter interception (custom elements)
|
|
159
|
+
if (element.localName.includes('-')) {
|
|
160
|
+
const descriptor = this.#getPropertyDescriptor(propName);
|
|
161
|
+
if (descriptor?.set) {
|
|
162
|
+
return new SetterInterceptor(element, propName, descriptor);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Strategy 4: Polling fallback
|
|
167
|
+
return new PollingWatcher(element, propName);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Key Implementation Challenges
|
|
172
|
+
|
|
173
|
+
1. **Setter Interception Reliability**:
|
|
174
|
+
- Need to test if we can reliably override setters on instances
|
|
175
|
+
- May need to use Proxy instead of direct override
|
|
176
|
+
- Must preserve original setter behavior
|
|
177
|
+
|
|
178
|
+
2. **Attribute Name Mapping**:
|
|
179
|
+
- `aria-valuenow` ↔ `ariaValueNow`
|
|
180
|
+
- Need bidirectional mapping logic
|
|
181
|
+
- Handle edge cases (data-*, custom attributes)
|
|
182
|
+
|
|
183
|
+
3. **Memory Leaks**:
|
|
184
|
+
- MutationObservers must be disconnected
|
|
185
|
+
- Event listeners must be removed
|
|
186
|
+
- Polling must be stopped
|
|
187
|
+
- Use AbortController for cleanup
|
|
188
|
+
|
|
189
|
+
4. **Race Conditions**:
|
|
190
|
+
- Custom element may not be defined yet
|
|
191
|
+
- Properties may change during setup
|
|
192
|
+
- Need to handle async initialization
|
|
193
|
+
|
|
194
|
+
### Testing Strategy
|
|
195
|
+
|
|
196
|
+
1. Test each strategy independently with known element types
|
|
197
|
+
2. Test fallback chain works correctly
|
|
198
|
+
3. Test memory cleanup (no leaks)
|
|
199
|
+
4. Test with custom elements that have propagator property
|
|
200
|
+
5. Test edge cases (iframe.src, programmatic input.value changes)
|
|
201
|
+
|
|
202
|
+
### Integration with Existing Infer Class
|
|
203
|
+
|
|
204
|
+
The `getPropagator()` method should:
|
|
205
|
+
1. First check if element has native `propagator` property (roundabout elements)
|
|
206
|
+
2. If not, create PropagatorInferencer instance
|
|
207
|
+
3. Cache the inferencer to avoid recreating
|
|
208
|
+
4. Return the EventTarget that emits property change events
|
|
209
|
+
|
|
210
|
+
### Open Questions
|
|
211
|
+
|
|
212
|
+
1. Should we auto-detect which properties to watch, or require explicit registration?
|
|
213
|
+
2. How do we handle properties that don't exist yet (lazy initialization)?
|
|
214
|
+
3. Should we support watching nested properties (e.g., `style.color`)?
|
|
215
|
+
4. What's the event detail format? `{property: string, oldValue: any, newValue: any}`?
|
|
216
|
+
|
|
217
|
+
### Recommended First Implementation
|
|
218
|
+
|
|
219
|
+
Start with the simplest cases and expand:
|
|
220
|
+
1. Attribute-reflected properties (MutationObserver)
|
|
221
|
+
2. Custom elements with roundabout propagator (already works)
|
|
222
|
+
3. Input elements with hybrid event + setter approach
|
|
223
|
+
4. Then tackle harder cases (iframe.src, etc.)
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Human Response I
|
|
228
|
+
|
|
229
|
+
What are your thoughts on making class PropagatorInferencer the inferenced propagator? First, maybe it should be renamed InferencedPropagator. Second, it should extend EventTarget, and dispatch the events. I think the constructor should be passed in the Infer class, so that it already has access the existing infer methods, like default event type, etc.
|
|
230
|
+
|
|
231
|
+
It is true that some of the logic depends on which property is being observed. I'm thinking the InferencedPropagor could override the addEventListener instead of the watchProperty method. There is a tricky aspect as far as asynchronous logic in that method. I'm thinking make the addEventListenr get a snapshot of the initial value, then do whatever asynchronous wiring is needed, then if the value changed, already dispatch an event with the property name. Would this work?
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Kiro Response I
|
|
236
|
+
|
|
237
|
+
Both suggestions are strong improvements over the original design.
|
|
238
|
+
|
|
239
|
+
### Renaming to InferencedPropagator + Extending EventTarget
|
|
240
|
+
|
|
241
|
+
This is cleaner. The class *is* the propagator—it shouldn't contain one. Extending EventTarget directly means consumers interact with it exactly like they would with a native propagator from a roundabout element. No wrapper indirection, no `.getPropagator()` method on the inferencer itself. The `Infer.getPropagator()` just returns `this` (the InferencedPropagator) or the native propagator—same interface either way.
|
|
242
|
+
|
|
243
|
+
Passing the `Infer` instance into the constructor is the right call. The `Infer` class already knows the element, has `inferEventType`, `inferValueProperty`, etc. No need to duplicate that logic or re-derive it.
|
|
244
|
+
|
|
245
|
+
### Overriding `addEventListener` as the Wiring Entry Point
|
|
246
|
+
|
|
247
|
+
This is elegant and solves the "which properties to watch" open question naturally—you only wire up observation for properties that someone actually subscribes to. Lazy by design.
|
|
248
|
+
|
|
249
|
+
The async concern is real though, and your proposed solution handles it well. Here's how I'd refine the flow:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
export class InferencedPropagator extends EventTarget {
|
|
253
|
+
#infer: Infer;
|
|
254
|
+
#watchedProperties: Set<string> = new Set();
|
|
255
|
+
#abortController: AbortController = new AbortController();
|
|
256
|
+
|
|
257
|
+
constructor(infer: Infer) {
|
|
258
|
+
super();
|
|
259
|
+
this.#infer = infer;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
override addEventListener(
|
|
263
|
+
type: string,
|
|
264
|
+
callback: EventListenerOrEventListenerObject | null,
|
|
265
|
+
options?: AddEventListenerOptions | boolean
|
|
266
|
+
): void {
|
|
267
|
+
// Register the listener immediately so it's ready to receive
|
|
268
|
+
super.addEventListener(type, callback, options);
|
|
269
|
+
|
|
270
|
+
// Only wire up observation once per property
|
|
271
|
+
if (!this.#watchedProperties.has(type)) {
|
|
272
|
+
this.#watchedProperties.add(type);
|
|
273
|
+
this.#wireProperty(type);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async #wireProperty(propName: string): Promise<void> {
|
|
278
|
+
const element = this.#infer.enhancedElement;
|
|
279
|
+
|
|
280
|
+
// Snapshot current value before async work
|
|
281
|
+
const initialValue = (element as any)[propName];
|
|
282
|
+
|
|
283
|
+
// Do async wiring (MutationObserver setup, setter interception, etc.)
|
|
284
|
+
await this.#setupStrategy(propName);
|
|
285
|
+
|
|
286
|
+
// Check if value changed during setup—if so, dispatch immediately
|
|
287
|
+
const currentValue = (element as any)[propName];
|
|
288
|
+
if (currentValue !== initialValue) {
|
|
289
|
+
this.dispatchEvent(new Event(propName));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async #setupStrategy(propName: string): Promise<void> {
|
|
294
|
+
const element = this.#infer.enhancedElement;
|
|
295
|
+
const signal = this.#abortController.signal;
|
|
296
|
+
|
|
297
|
+
// Strategy 1: Attribute-reflected properties
|
|
298
|
+
// Use Infer's knowledge to check reflection...
|
|
299
|
+
|
|
300
|
+
// Strategy 2: Native events (use infer.eventType)
|
|
301
|
+
// For input/textarea/select, the Infer class already
|
|
302
|
+
// knows the relevant event type
|
|
303
|
+
|
|
304
|
+
// Strategy 3: Setter interception for custom elements
|
|
305
|
+
|
|
306
|
+
// Strategy 4: Polling fallback
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Why This Works
|
|
312
|
+
|
|
313
|
+
1. **`addEventListener` is synchronous** — the listener is registered immediately via `super.addEventListener()`, so it won't miss events.
|
|
314
|
+
2. **Wiring is async but safe** — the snapshot/compare pattern catches changes that happen during setup. The only gap is a very brief window where a change might happen *and then revert* during async wiring, but that's an extreme edge case.
|
|
315
|
+
3. **Event name = property name** — this mirrors the roundabout propagator convention. Consumers do `propagator.addEventListener('value', handler)` and it just works regardless of whether it's native or inferred.
|
|
316
|
+
|
|
317
|
+
### One Consideration: Multiple `addEventListener` Calls for Same Property
|
|
318
|
+
|
|
319
|
+
The `#watchedProperties` set prevents re-wiring, which is correct. But we should think about `removeEventListener` — if *all* listeners for a property are removed, should we tear down the watcher? This could matter for memory/performance. An option:
|
|
320
|
+
|
|
321
|
+
- Track listener count per property
|
|
322
|
+
- On `removeEventListener`, decrement and tear down if zero
|
|
323
|
+
- Or: just keep watchers alive until `destroy()` is called (simpler, probably fine for most use cases)
|
|
324
|
+
|
|
325
|
+
### Revised `Infer.getPropagator()`
|
|
326
|
+
|
|
327
|
+
With this design, the integration simplifies to:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
export class Infer<TValue = any, TDisplay = any> {
|
|
331
|
+
#propagator: EventTarget | undefined;
|
|
332
|
+
|
|
333
|
+
async getPropagator(): Promise<EventTarget> {
|
|
334
|
+
if (this.#propagator) return this.#propagator;
|
|
335
|
+
|
|
336
|
+
const {enhancedElement} = this;
|
|
337
|
+
const {localName} = enhancedElement;
|
|
338
|
+
|
|
339
|
+
if (localName.includes('-')) {
|
|
340
|
+
await (
|
|
341
|
+
(enhancedElement as any).customElementRegistry || customElements
|
|
342
|
+
).whenDefined(localName);
|
|
343
|
+
const {propagator} = enhancedElement as any;
|
|
344
|
+
if (propagator instanceof EventTarget) {
|
|
345
|
+
this.#propagator = propagator;
|
|
346
|
+
return propagator;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// No native propagator — create an inferred one
|
|
351
|
+
const {InferencedPropagator} = await import('./InferencedPropagator.js');
|
|
352
|
+
this.#propagator = new InferencedPropagator(this);
|
|
353
|
+
return this.#propagator;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Caching via `#propagator` ensures we don't create multiple instances for the same element.
|
|
359
|
+
|
|
360
|
+
### Strategy Selection Leveraging Infer
|
|
361
|
+
|
|
362
|
+
Since the constructor receives the `Infer` instance, the strategy selection can lean on existing inference logic:
|
|
363
|
+
|
|
364
|
+
- `inferEventType(element)` tells us if there's a native event to listen to (Strategy 2)
|
|
365
|
+
- `inferValueProperty(element)` tells us what property the element considers its "value" — useful for knowing which properties are likely attribute-reflected
|
|
366
|
+
- The `localName` check for `-` (custom element detection) is already a pattern in the codebase
|
|
367
|
+
|
|
368
|
+
This avoids duplicating element-type knowledge across two classes.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { withScopePerimeter } from "./withScopePerimeter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Symbol for smart value assignment
|
|
4
|
+
* When used with element.set[value], it infers and sets the appropriate value property
|
|
5
|
+
*/
|
|
6
|
+
export const value = Symbol.for('inferencer:value');
|
|
7
|
+
/**
|
|
8
|
+
* Symbol for smart display assignment
|
|
9
|
+
* When used with element.set[display], it infers and sets the appropriate display property
|
|
10
|
+
*/
|
|
11
|
+
export const display = Symbol.for('inferencer:display');
|
|
12
|
+
/**
|
|
13
|
+
* Enhancement class that provides smart value and display property inference
|
|
14
|
+
* Automatically determines the correct property to set based on element type
|
|
15
|
+
*/
|
|
16
|
+
export class Infer {
|
|
17
|
+
#weakRef;
|
|
18
|
+
#propName;
|
|
19
|
+
#propagator;
|
|
20
|
+
get enhancedElement() {
|
|
21
|
+
return this.#weakRef.deref();
|
|
22
|
+
}
|
|
23
|
+
constructor(enhancedElement, propName) {
|
|
24
|
+
this.#weakRef = new WeakRef(enhancedElement);
|
|
25
|
+
this.#propName = propName;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get a propagator (EventTarget) that emits events when properties change.
|
|
29
|
+
* For custom elements with a native propagator (roundabout), returns that directly.
|
|
30
|
+
* Otherwise, creates an InferencedPropagator that uses best-effort strategies
|
|
31
|
+
* to detect property changes.
|
|
32
|
+
*/
|
|
33
|
+
async getPropagator() {
|
|
34
|
+
if (this.#propagator)
|
|
35
|
+
return this.#propagator;
|
|
36
|
+
const { enhancedElement } = this;
|
|
37
|
+
const { localName } = enhancedElement;
|
|
38
|
+
if (localName.includes('-')) {
|
|
39
|
+
await (enhancedElement.customElementRegistry || customElements).whenDefined(localName);
|
|
40
|
+
const { propagator } = enhancedElement;
|
|
41
|
+
if (propagator instanceof EventTarget) {
|
|
42
|
+
this.#propagator = propagator;
|
|
43
|
+
return propagator;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// No native propagator — create an inferred one
|
|
47
|
+
const { InferencedPropagator } = await import('./InferencedPropagator.js');
|
|
48
|
+
this.#propagator = new InferencedPropagator(this);
|
|
49
|
+
return this.#propagator;
|
|
50
|
+
}
|
|
51
|
+
#value;
|
|
52
|
+
get value() {
|
|
53
|
+
return this.#value;
|
|
54
|
+
}
|
|
55
|
+
set value(nv) {
|
|
56
|
+
this.#value = nv;
|
|
57
|
+
const { enhancedElement } = this;
|
|
58
|
+
enhancedElement[inferValueProperty(enhancedElement)] = nv;
|
|
59
|
+
}
|
|
60
|
+
#display;
|
|
61
|
+
get display() {
|
|
62
|
+
return this.#display;
|
|
63
|
+
}
|
|
64
|
+
set display(nv) {
|
|
65
|
+
this.#display = nv;
|
|
66
|
+
const { enhancedElement } = this;
|
|
67
|
+
enhancedElement[inferDisplayProperty(enhancedElement)] = nv;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the inferred event type for the element
|
|
71
|
+
* @returns The most appropriate event type for this element
|
|
72
|
+
*/
|
|
73
|
+
get eventType() {
|
|
74
|
+
return inferEventType(this.enhancedElement);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the inferred value property name for the element
|
|
78
|
+
* @returns The property name used for value assignment (e.g. 'value', 'checked', 'dateTime')
|
|
79
|
+
*/
|
|
80
|
+
get valueProperty() {
|
|
81
|
+
return inferValueProperty(this.enhancedElement);
|
|
82
|
+
}
|
|
83
|
+
['|'](itempropAttr, scopeBoundary = '[itemscope]') {
|
|
84
|
+
const candidates = this.enhancedElement.querySelectorAll(`[itemprop="${itempropAttr}"]`);
|
|
85
|
+
return Array.from(candidates)
|
|
86
|
+
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
87
|
+
.map(x => new Infer(x, itempropAttr));
|
|
88
|
+
}
|
|
89
|
+
['@'](nameAttr, scopeBoundary) {
|
|
90
|
+
const candidates = this.enhancedElement.querySelectorAll(`[name="${nameAttr}"]`);
|
|
91
|
+
const filtered = scopeBoundary
|
|
92
|
+
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
93
|
+
: Array.from(candidates);
|
|
94
|
+
return filtered.map(x => new Infer(x, nameAttr));
|
|
95
|
+
}
|
|
96
|
+
['%'](partAttr, scopeBoundary) {
|
|
97
|
+
const candidates = this.enhancedElement.querySelectorAll(`[part~="${partAttr}"]`);
|
|
98
|
+
const filtered = scopeBoundary
|
|
99
|
+
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
100
|
+
: Array.from(candidates);
|
|
101
|
+
return filtered.map(x => new Infer(x, partAttr));
|
|
102
|
+
}
|
|
103
|
+
['#'](id, scopeBoundary) {
|
|
104
|
+
const candidates = this.enhancedElement.querySelectorAll(`#${id}`);
|
|
105
|
+
const filtered = scopeBoundary
|
|
106
|
+
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
107
|
+
: Array.from(candidates);
|
|
108
|
+
return filtered.map(x => new Infer(x, id));
|
|
109
|
+
}
|
|
110
|
+
['.'](className, scopeBoundary) {
|
|
111
|
+
const candidates = this.enhancedElement.querySelectorAll(`.${className}`);
|
|
112
|
+
const filtered = scopeBoundary
|
|
113
|
+
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
114
|
+
: Array.from(candidates);
|
|
115
|
+
return filtered.map(x => new Infer(x, className));
|
|
116
|
+
}
|
|
117
|
+
setDisplay(vm) {
|
|
118
|
+
const val = this.#propName ? vm[this.#propName] : inferBindingProperty(this.enhancedElement);
|
|
119
|
+
this.display = val;
|
|
120
|
+
}
|
|
121
|
+
get defaultRemoteBindingPropName() {
|
|
122
|
+
const { enhancedElement } = this;
|
|
123
|
+
return enhancedElement.getAttribute('itemprop') || enhancedElement.getAttribute('name') || enhancedElement.getAttribute('id') || 'value';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Registry item for the Infer enhancement
|
|
128
|
+
* Register this with customElements.enhancementRegistry to enable smart value/display assignment
|
|
129
|
+
*/
|
|
130
|
+
export const registryItem = {
|
|
131
|
+
spawn: Infer,
|
|
132
|
+
enhKey: 'inferencer',
|
|
133
|
+
symlinks: {
|
|
134
|
+
[value]: 'value',
|
|
135
|
+
[display]: 'display'
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Infer the most appropriate value property for an element
|
|
140
|
+
* @param element - The element to infer the property for
|
|
141
|
+
* @returns The property name to use for value assignment
|
|
142
|
+
*/
|
|
143
|
+
export function inferValueProperty(element) {
|
|
144
|
+
// Non-empty itemscope → route through ish (itemscope manager)
|
|
145
|
+
const itemscope = element.getAttribute('itemscope');
|
|
146
|
+
if (itemscope !== null && itemscope !== '') {
|
|
147
|
+
return 'ish';
|
|
148
|
+
}
|
|
149
|
+
const { localName } = element;
|
|
150
|
+
switch (localName) {
|
|
151
|
+
case 'input': {
|
|
152
|
+
const type = element.getAttribute('type')?.toLowerCase();
|
|
153
|
+
switch (type) {
|
|
154
|
+
case 'checkbox':
|
|
155
|
+
case 'radio':
|
|
156
|
+
return 'checked';
|
|
157
|
+
case 'number':
|
|
158
|
+
case 'range':
|
|
159
|
+
return 'valueAsNumber';
|
|
160
|
+
default:
|
|
161
|
+
return 'value';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
case 'textarea':
|
|
165
|
+
case 'select':
|
|
166
|
+
case 'data':
|
|
167
|
+
case 'meter':
|
|
168
|
+
case 'progress':
|
|
169
|
+
case 'output':
|
|
170
|
+
return 'value';
|
|
171
|
+
case 'time':
|
|
172
|
+
return 'dateTime';
|
|
173
|
+
case 'a':
|
|
174
|
+
case 'area':
|
|
175
|
+
return 'href';
|
|
176
|
+
default: {
|
|
177
|
+
return 'textContent';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Infer the most appropriate display property for an element
|
|
183
|
+
* @param element - The element to infer the property for
|
|
184
|
+
* @returns The property name to use for display assignment
|
|
185
|
+
*/
|
|
186
|
+
export function inferDisplayProperty(element) {
|
|
187
|
+
const { localName } = element;
|
|
188
|
+
switch (localName) {
|
|
189
|
+
case 'input':
|
|
190
|
+
case 'textarea':
|
|
191
|
+
case 'select':
|
|
192
|
+
return 'value';
|
|
193
|
+
case 'meter':
|
|
194
|
+
case 'progress':
|
|
195
|
+
return 'ariaValueText';
|
|
196
|
+
case 'time':
|
|
197
|
+
case 'data':
|
|
198
|
+
default:
|
|
199
|
+
return 'textContent';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Infer the most appropriate event type for an element
|
|
204
|
+
* Used when no explicit event type is provided
|
|
205
|
+
* @param element - The element to infer the event type for
|
|
206
|
+
* @returns The event type name like 'input', 'change', 'click', 'submit'
|
|
207
|
+
*/
|
|
208
|
+
export function inferEventType(element) {
|
|
209
|
+
const { localName } = element;
|
|
210
|
+
switch (localName) {
|
|
211
|
+
case 'input':
|
|
212
|
+
case 'textarea':
|
|
213
|
+
case 'select':
|
|
214
|
+
return 'input';
|
|
215
|
+
case 'form':
|
|
216
|
+
return 'submit';
|
|
217
|
+
case 'details':
|
|
218
|
+
return 'toggle';
|
|
219
|
+
case 'dialog':
|
|
220
|
+
return 'close';
|
|
221
|
+
default:
|
|
222
|
+
return 'click';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Check if an element requires propagator-based observation for value changes.
|
|
227
|
+
* Elements like <data>, <meter>, <output>, <time> have no meaningful user-driven
|
|
228
|
+
* event for value changes — their values change programmatically and reflect to attributes.
|
|
229
|
+
* Custom elements (names containing '-') should also use the propagator path, since they
|
|
230
|
+
* may expose a native propagator (EventTarget) for property change notification.
|
|
231
|
+
* For these elements, consumers should use InferencedPropagator rather than raw addEventListener.
|
|
232
|
+
* @param element - The element to check
|
|
233
|
+
* @returns true if the element needs propagator-based observation
|
|
234
|
+
*/
|
|
235
|
+
export function needsPropagator(element) {
|
|
236
|
+
const { localName } = element;
|
|
237
|
+
switch (localName) {
|
|
238
|
+
case 'data':
|
|
239
|
+
case 'meter':
|
|
240
|
+
case 'output':
|
|
241
|
+
case 'time':
|
|
242
|
+
return true;
|
|
243
|
+
default:
|
|
244
|
+
// Custom elements should use propagator path — Infer.getPropagator()
|
|
245
|
+
// will check for a native propagator or fall back to InferencedPropagator
|
|
246
|
+
if (localName.includes('-'))
|
|
247
|
+
return true;
|
|
248
|
+
return inferEventType(element) === 'click';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function inferBindingProperty(element) {
|
|
252
|
+
return element.getAttribute('itemprop') || element.getAttribute('name') || element.getAttribute('id') || 'value';
|
|
253
|
+
}
|
|
254
|
+
export default registryItem;
|