face-up 0.0.2 → 0.0.3
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/FaceUp.js +12 -79
- package/RAConfig.mjs +88 -0
- package/README.md +46 -17
- package/package.json +5 -1
- package/requirements/SupportAutoForwardingWithRoundabout.md +57 -0
- package/tests/test1.html +5 -9
- package/types/NewCustomElementFeature.md +13 -3
- package/types/face-up/types.d.ts +7 -15
package/FaceUp.js
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
*
|
|
18
18
|
* `static formAssociated = true` is set automatically via `static onAssigned`.
|
|
19
19
|
*
|
|
20
|
+
* Integration is seamless — because the feature is a getter-only property,
|
|
21
|
+
* `assignGingerly` merges directly into the instance. The consumer simply
|
|
22
|
+
* sets properties on the feature (e.g., `el.faceUp.value = x`) and the
|
|
23
|
+
* setters handle syncing to ElementInternals automatically.
|
|
24
|
+
*
|
|
20
25
|
* @implements {FaceUpProps}
|
|
21
26
|
*/
|
|
22
27
|
class FaceUp {
|
|
@@ -25,29 +30,23 @@ class FaceUp {
|
|
|
25
30
|
* Called once by assignFeatures after registration.
|
|
26
31
|
* Sets `static formAssociated = true` on the host constructor so the
|
|
27
32
|
* consumer doesn't need to declare it manually.
|
|
28
|
-
* @param {
|
|
33
|
+
* @param {typeof HTMLElement} ctr - The custom element constructor
|
|
29
34
|
* @param {object} _featureConfig - The feature config (unused)
|
|
30
35
|
*/
|
|
31
36
|
static onAssigned(ctr, _featureConfig) {
|
|
37
|
+
// @ts-ignore — formAssociated is a custom element static property
|
|
32
38
|
if (!ctr.formAssociated) {
|
|
39
|
+
// @ts-ignore
|
|
33
40
|
ctr.formAssociated = true;
|
|
34
41
|
}
|
|
35
42
|
}
|
|
43
|
+
|
|
36
44
|
/** @type {WeakRef<HTMLElement> | undefined} */
|
|
37
45
|
#hostRef;
|
|
38
46
|
|
|
39
47
|
/** @type {ElementInternals | undefined} */
|
|
40
48
|
#internals;
|
|
41
49
|
|
|
42
|
-
/** @type {AbortController | undefined} */
|
|
43
|
-
#abortController;
|
|
44
|
-
|
|
45
|
-
/** @type {EventTarget | undefined} */
|
|
46
|
-
#hostPropagator;
|
|
47
|
-
|
|
48
|
-
/** @type {boolean} */
|
|
49
|
-
#hasDisconnected = false;
|
|
50
|
-
|
|
51
50
|
/** @type {string | File | FormData | null} */
|
|
52
51
|
#value = null;
|
|
53
52
|
|
|
@@ -72,7 +71,6 @@ class FaceUp {
|
|
|
72
71
|
this.#hostRef = new WeakRef(hostElement);
|
|
73
72
|
if (ctx.shared) {
|
|
74
73
|
this.#internals = ctx.shared.internals;
|
|
75
|
-
this.#hostPropagator = ctx.shared.hostPropagator;
|
|
76
74
|
}
|
|
77
75
|
if (initVals) {
|
|
78
76
|
if (initVals.value !== undefined) this.#value = initVals.value;
|
|
@@ -80,9 +78,10 @@ class FaceUp {
|
|
|
80
78
|
if (initVals.disabled !== undefined) this.#disabled = initVals.disabled;
|
|
81
79
|
if (initVals.required !== undefined) this.#required = initVals.required;
|
|
82
80
|
if (initVals.validationMessage !== undefined) this.#validationMessage = initVals.validationMessage;
|
|
83
|
-
if (initVals.hostPropagator !== undefined) this.#hostPropagator = initVals.hostPropagator;
|
|
84
81
|
}
|
|
85
|
-
|
|
82
|
+
// Sync initial state to internals
|
|
83
|
+
this.#syncFormValue();
|
|
84
|
+
this.#validateInternal();
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
// ─── Public Properties ───────────────────────────────────────────
|
|
@@ -117,12 +116,6 @@ class FaceUp {
|
|
|
117
116
|
this.#validateInternal();
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
get hostPropagator() { return this.#hostPropagator ?? null; }
|
|
121
|
-
set hostPropagator(v) {
|
|
122
|
-
this.#hostPropagator = v ?? undefined;
|
|
123
|
-
this.#reconnectListeners();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
119
|
// ─── Read-only Form Control Accessors ────────────────────────────
|
|
127
120
|
|
|
128
121
|
/** The form element the host is associated with */
|
|
@@ -200,68 +193,8 @@ class FaceUp {
|
|
|
200
193
|
}
|
|
201
194
|
}
|
|
202
195
|
|
|
203
|
-
// ─── Lifecycle (callbackForwarding) ──────────────────────────────
|
|
204
|
-
|
|
205
|
-
connectedCallback() {
|
|
206
|
-
if (this.#hasDisconnected) {
|
|
207
|
-
this.#hasDisconnected = false;
|
|
208
|
-
this.#connect();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
disconnectedCallback() {
|
|
213
|
-
this.#hasDisconnected = true;
|
|
214
|
-
this.#cleanup();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
196
|
// ─── Private Methods ─────────────────────────────────────────────
|
|
218
197
|
|
|
219
|
-
#connect() {
|
|
220
|
-
this.#abortController = new AbortController();
|
|
221
|
-
const signal = this.#abortController.signal;
|
|
222
|
-
|
|
223
|
-
// Listen for value/state/required changes from the host propagator
|
|
224
|
-
if (this.#hostPropagator) {
|
|
225
|
-
this.#hostPropagator.addEventListener('value', (/** @type {CustomEvent} */ e) => {
|
|
226
|
-
this.#value = e.detail?.value ?? e.detail;
|
|
227
|
-
this.#syncFormValue();
|
|
228
|
-
this.#validateInternal();
|
|
229
|
-
}, { signal });
|
|
230
|
-
|
|
231
|
-
this.#hostPropagator.addEventListener('state', (/** @type {CustomEvent} */ e) => {
|
|
232
|
-
this.#state = e.detail?.value ?? e.detail;
|
|
233
|
-
this.#syncFormValue();
|
|
234
|
-
}, { signal });
|
|
235
|
-
|
|
236
|
-
this.#hostPropagator.addEventListener('required', (/** @type {CustomEvent} */ e) => {
|
|
237
|
-
this.#required = !!(e.detail?.value ?? e.detail);
|
|
238
|
-
this.#validateInternal();
|
|
239
|
-
}, { signal });
|
|
240
|
-
|
|
241
|
-
this.#hostPropagator.addEventListener('disabled', (/** @type {CustomEvent} */ e) => {
|
|
242
|
-
this.#disabled = !!(e.detail?.value ?? e.detail);
|
|
243
|
-
}, { signal });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Sync initial value if already set
|
|
247
|
-
this.#syncFormValue();
|
|
248
|
-
this.#validateInternal();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
#cleanup() {
|
|
252
|
-
if (this.#abortController) {
|
|
253
|
-
this.#abortController.abort();
|
|
254
|
-
this.#abortController = undefined;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
#reconnectListeners() {
|
|
259
|
-
this.#cleanup();
|
|
260
|
-
if (!this.#hasDisconnected) {
|
|
261
|
-
this.#connect();
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
198
|
/**
|
|
266
199
|
* Syncs the current value (and optional state) to ElementInternals.
|
|
267
200
|
*/
|
package/RAConfig.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {Merges} from './types/roundabout/types.d.ts' */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} FaceUpForwardingProps
|
|
6
|
+
* @property {string | File | FormData | null} value
|
|
7
|
+
* @property {string | File | FormData | null} state
|
|
8
|
+
* @property {boolean} disabled
|
|
9
|
+
* @property {boolean} required
|
|
10
|
+
* @property {string} validationMessage
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Property key constants for FaceUp-relevant forwarding.
|
|
15
|
+
* @type {{ [K in keyof FaceUpForwardingProps]: K }}
|
|
16
|
+
*/
|
|
17
|
+
export const props = {
|
|
18
|
+
value: 'value',
|
|
19
|
+
state: 'state',
|
|
20
|
+
disabled: 'disabled',
|
|
21
|
+
required: 'required',
|
|
22
|
+
validationMessage: 'validationMessage',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates merges configuration for forwarding properties from the host view model
|
|
27
|
+
* to the faceUp feature instance via roundabout's reactive system.
|
|
28
|
+
*
|
|
29
|
+
* When any of these properties change on the view model, the corresponding
|
|
30
|
+
* setter on the feature fires, which syncs to ElementInternals automatically.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} [featureKey='faceUp'] - The feature property name on the host element.
|
|
33
|
+
* @returns {Merges<FaceUpForwardingProps>}
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```js
|
|
37
|
+
* import { getFaceUpMerges } from 'face-up/RAConfig.mjs';
|
|
38
|
+
*
|
|
39
|
+
* export const raConfig = {
|
|
40
|
+
* merges: [
|
|
41
|
+
* ...getFaceUpMerges(), // uses default 'faceUp' key
|
|
42
|
+
* // ...or with a custom key:
|
|
43
|
+
* // ...getFaceUpMerges('formControl'),
|
|
44
|
+
* ]
|
|
45
|
+
* };
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function getFaceUpMerges(featureKey = 'faceUp') {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
ifKeyIn: [props.value],
|
|
52
|
+
assign: {
|
|
53
|
+
[`?.${featureKey}?.value`]: '?.value'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
ifKeyIn: [props.state],
|
|
58
|
+
assign: {
|
|
59
|
+
[`?.${featureKey}?.state`]: '?.state'
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
ifKeyIn: [props.disabled],
|
|
64
|
+
assign: {
|
|
65
|
+
[`?.${featureKey}?.disabled`]: '?.disabled'
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
ifKeyIn: [props.required],
|
|
70
|
+
assign: {
|
|
71
|
+
[`?.${featureKey}?.required`]: '?.required'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
ifKeyIn: [props.validationMessage],
|
|
76
|
+
assign: {
|
|
77
|
+
[`?.${featureKey}?.validationMessage`]: '?.validationMessage'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pre-built merges using the default 'faceUp' feature key.
|
|
85
|
+
* Convenience export for the common case.
|
|
86
|
+
* @type {Merges<FaceUpForwardingProps>}
|
|
87
|
+
*/
|
|
88
|
+
export const faceUpMerges = getFaceUpMerges();
|
package/README.md
CHANGED
|
@@ -20,23 +20,19 @@ import 'assign-gingerly/assignFeatures.js';
|
|
|
20
20
|
import { FaceUp } from 'face-up/FaceUp.js';
|
|
21
21
|
|
|
22
22
|
class MyInput extends HTMLElement {
|
|
23
|
-
propagator = new EventTarget();
|
|
24
23
|
#internals;
|
|
25
24
|
|
|
26
25
|
static supportedFeatures = {
|
|
27
26
|
faceUp: {
|
|
28
27
|
fallbackSpawn: FaceUp,
|
|
29
28
|
callbackForwarding: [
|
|
30
|
-
'connectedCallback',
|
|
31
|
-
'disconnectedCallback',
|
|
32
29
|
'formDisabledCallback',
|
|
33
30
|
'formResetCallback',
|
|
34
31
|
'formStateRestoreCallback'
|
|
35
32
|
],
|
|
36
33
|
getSharedContext(instance) {
|
|
37
34
|
return {
|
|
38
|
-
internals: instance.#internals
|
|
39
|
-
hostPropagator: instance.propagator
|
|
35
|
+
internals: instance.#internals
|
|
40
36
|
};
|
|
41
37
|
}
|
|
42
38
|
}
|
|
@@ -56,27 +52,26 @@ await customElements.assignFeatures(MyInput, {
|
|
|
56
52
|
customElements.define('my-input', MyInput);
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
That's it. No propagator, no manual callback forwarding, no event wiring.
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+
- `static formAssociated = true` is set automatically by `FaceUp.onAssigned`.
|
|
58
|
+
- Form lifecycle callbacks are forwarded automatically via `callbackForwarding`.
|
|
59
|
+
- Property changes sync to `ElementInternals` via the feature's setters.
|
|
62
60
|
|
|
63
61
|
## Setting Values
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
Because `faceUp` is a getter-only property, `assignGingerly` merges directly into the feature instance. Set properties however you like:
|
|
66
64
|
|
|
67
65
|
```js
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
new Event('value')
|
|
71
|
-
);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Or set the value directly on the feature instance:
|
|
66
|
+
// Direct property access
|
|
67
|
+
el.faceUp.value = 'hello';
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
el.faceUp
|
|
69
|
+
// Via assignGingerly (merges into the feature instance)
|
|
70
|
+
el.assignGingerly({ faceUp: { value: 'hello', required: true } });
|
|
78
71
|
```
|
|
79
72
|
|
|
73
|
+
Both approaches trigger the setter, which calls `setFormValue()` on the internals automatically.
|
|
74
|
+
|
|
80
75
|
## Validation
|
|
81
76
|
|
|
82
77
|
Set a custom validation message:
|
|
@@ -99,6 +94,8 @@ el.faceUp.validationMessage = '';
|
|
|
99
94
|
el.faceUp.setValidity({});
|
|
100
95
|
```
|
|
101
96
|
|
|
97
|
+
Built-in `required` validation is automatic — if `required` is `true` and `value` is `null` or `''`, the control is marked invalid with `valueMissing`.
|
|
98
|
+
|
|
102
99
|
## Form State Restoration
|
|
103
100
|
|
|
104
101
|
Pass a `state` parameter alongside `value` to enable proper form restoration:
|
|
@@ -143,6 +140,20 @@ The `state` is stored internally by the browser and passed back to `formStateRes
|
|
|
143
140
|
|
|
144
141
|
These are forwarded automatically by `assign-gingerly`'s `callbackForwarding` mechanism — the host element does not need to implement them manually.
|
|
145
142
|
|
|
143
|
+
## How it integrates
|
|
144
|
+
|
|
145
|
+
The key insight: because `assignFeatures` installs `faceUp` as a **getter-only** property on the prototype, `assignGingerly` automatically merges object values into the existing feature instance rather than replacing it. This means:
|
|
146
|
+
|
|
147
|
+
1. The feature's setters fire on every property assignment.
|
|
148
|
+
2. Each setter syncs to `ElementInternals` immediately.
|
|
149
|
+
3. No event bus, no propagator, no intermediate layer — just property access.
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// All of these trigger the setter → sync to internals:
|
|
153
|
+
el.faceUp.value = 'x';
|
|
154
|
+
el.assignGingerly({ faceUp: { value: 'x' } });
|
|
155
|
+
```
|
|
156
|
+
|
|
146
157
|
## Requirements
|
|
147
158
|
|
|
148
159
|
The host custom element **must**:
|
|
@@ -153,6 +164,24 @@ The host custom element **must**:
|
|
|
153
164
|
|
|
154
165
|
Both `static formAssociated = true` and form lifecycle callback forwarding are handled automatically — no manual boilerplate needed in the host element.
|
|
155
166
|
|
|
167
|
+
## Roundabout Integration
|
|
168
|
+
|
|
169
|
+
For projects using [roundabout](https://github.com/bahrus/roundabout), `face-up` exports merge rules that wire up property forwarding automatically:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
import { faceUpMerges } from 'face-up/RAConfig.mjs';
|
|
173
|
+
|
|
174
|
+
export const raConfig = {
|
|
175
|
+
propagate: ['value', 'disabled', 'required', /* ... */],
|
|
176
|
+
merges: [
|
|
177
|
+
...faceUpMerges,
|
|
178
|
+
// ...your other merges
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
When `value`, `state`, `disabled`, `required`, or `validationMessage` change on the view model, roundabout forwards them to the `faceUp` feature's setters, which sync to `ElementInternals` automatically.
|
|
184
|
+
|
|
156
185
|
## Dev
|
|
157
186
|
|
|
158
187
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "face-up",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A Custom Element Feature that Adds Form Associated Behavior to a Custom Element",
|
|
5
5
|
"homepage": "https://github.com/bahrus/face-up#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"author": "Bruce B. Anderson <anderson.bruce.b@gmail.com>",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "FaceUp.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
"./FaceUp.js": "./FaceUp.js",
|
|
19
|
+
"./RAConfig.mjs": "./RAConfig.mjs"
|
|
20
|
+
},
|
|
17
21
|
"scripts": {
|
|
18
22
|
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
19
23
|
"test": "playwright test",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Support Auto Forwarding With Roundabout Merging
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Human Ask
|
|
6
|
+
|
|
7
|
+
I am interested in making it really easy to provide property forwarding to this feature for those who make use of the [roundabout library](https://github.com/bahrus/roundabout).
|
|
8
|
+
|
|
9
|
+
The *time-ticker* custom element library is such a scenario. I've copied that project into this folder temporarily for easy inspection. Note how, in that project, the cef.json file is built from cef.mjs via npm run build for good typescript support with a kiro hook. Look closely at the "merges" section to see how property forwarding is done. The roundabout library actually creates the properties automaically from these merges.
|
|
10
|
+
|
|
11
|
+
Please create an RAConfig.mjs in this project that provides similar property forwarding configuration for all the relevant forwarding, which can then be imported by projects like time-ticker.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation Notes
|
|
16
|
+
|
|
17
|
+
### What was created
|
|
18
|
+
|
|
19
|
+
`RAConfig.mjs` — exports `faceUpMerges` and `props` for consumers using roundabout.
|
|
20
|
+
|
|
21
|
+
### How it works
|
|
22
|
+
|
|
23
|
+
In the roundabout pattern, `merges` are reactive rules: when a property on the view model changes (detected via `ifKeyIn`), the `assign` block runs `assignFrom(vm, assign, { from: vm })` — resolving the RHS path against the vm and assigning the result into the LHS path on the vm.
|
|
24
|
+
|
|
25
|
+
Since `faceUp` is a getter-only property (installed by `assignFeatures`), `assignGingerly` merges into the feature instance. This means the LHS path `?.faceUp?.value` triggers the feature's `value` setter, which calls `setFormValue()` on the internals.
|
|
26
|
+
|
|
27
|
+
The forwarding properties exposed are:
|
|
28
|
+
- `value` → `?.faceUp?.value` — the submittable form value
|
|
29
|
+
- `state` → `?.faceUp?.state` — internal state for form restoration
|
|
30
|
+
- `disabled` → `?.faceUp?.disabled` — disabled state
|
|
31
|
+
- `required` → `?.faceUp?.required` — required constraint
|
|
32
|
+
- `validationMessage` → `?.faceUp?.validationMessage` — custom validation error
|
|
33
|
+
|
|
34
|
+
### Consumer usage (e.g., time-ticker's cef.mjs)
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { faceUpMerges } from 'face-up/RAConfig.mjs';
|
|
38
|
+
|
|
39
|
+
export const raConfig = {
|
|
40
|
+
propagate: ['value', 'disabled', 'required', /* ...other props */],
|
|
41
|
+
merges: [
|
|
42
|
+
...faceUpMerges,
|
|
43
|
+
// ...project-specific merges
|
|
44
|
+
],
|
|
45
|
+
// ...rest of config
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This eliminates the need for consumers to manually write the merge rules for form-associated behavior — they just spread `faceUpMerges` into their config.
|
|
50
|
+
|
|
51
|
+
### Relationship to time-ticker
|
|
52
|
+
|
|
53
|
+
In time-ticker's `cef.mjs`, the existing merges forward `duration` and `disabled` to the `timeTicker` feature. With `faceUpMerges`, the time-ticker project could additionally spread these merges to forward `value`, `disabled`, etc. to the `faceUp` feature. The `disabled` merge would forward to both features (roundabout handles this naturally since each merge is independent).
|
|
54
|
+
|
|
55
|
+
### Note on `hostPropagator`
|
|
56
|
+
|
|
57
|
+
The time-ticker's `time-ticker-element.js` still passes `hostPropagator` in `getSharedContext` for `faceUp`. Since we've removed the propagator dependency from FaceUp (it now relies purely on property setters + roundabout merges for reactivity), that `hostPropagator` field in `getSharedContext` is no longer needed for `faceUp`. The `getSharedContext` only needs to pass `internals`.
|
package/tests/test1.html
CHANGED
|
@@ -11,26 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* A test custom element that uses FaceUp to become form-associated.
|
|
14
|
-
* Note: static formAssociated = true is set automatically by FaceUp.onAssigned
|
|
14
|
+
* Note: static formAssociated = true is set automatically by FaceUp.onAssigned.
|
|
15
|
+
* Form lifecycle callbacks are forwarded automatically via callbackForwarding.
|
|
15
16
|
*/
|
|
16
17
|
class TestInput extends HTMLElement {
|
|
17
|
-
propagator = new EventTarget();
|
|
18
18
|
#internals;
|
|
19
19
|
|
|
20
20
|
static supportedFeatures = {
|
|
21
21
|
faceUp: {
|
|
22
22
|
fallbackSpawn: FaceUp,
|
|
23
23
|
callbackForwarding: [
|
|
24
|
-
'connectedCallback',
|
|
25
|
-
'disconnectedCallback',
|
|
26
24
|
'formDisabledCallback',
|
|
27
25
|
'formResetCallback',
|
|
28
26
|
'formStateRestoreCallback'
|
|
29
27
|
],
|
|
30
28
|
getSharedContext(instance) {
|
|
31
29
|
return {
|
|
32
|
-
internals: instance.#internals
|
|
33
|
-
hostPropagator: instance.propagator
|
|
30
|
+
internals: instance.#internals
|
|
34
31
|
};
|
|
35
32
|
}
|
|
36
33
|
}
|
|
@@ -52,9 +49,8 @@
|
|
|
52
49
|
connectedCallback() {
|
|
53
50
|
const input = this.shadowRoot.querySelector('input');
|
|
54
51
|
input.addEventListener('input', () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
52
|
+
// Simply set the feature property — the setter syncs to internals
|
|
53
|
+
this.faceUp.value = input.value;
|
|
58
54
|
});
|
|
59
55
|
}
|
|
60
56
|
|
|
@@ -481,7 +481,6 @@ This pattern eliminates all manual wiring in the consumer's constructor — the
|
|
|
481
481
|
|
|
482
482
|
```javascript
|
|
483
483
|
class MyElement extends HTMLElement {
|
|
484
|
-
propagator = new EventTarget();
|
|
485
484
|
#internals;
|
|
486
485
|
|
|
487
486
|
static supportedFeatures = {
|
|
@@ -490,8 +489,7 @@ class MyElement extends HTMLElement {
|
|
|
490
489
|
callbackForwarding: ['connectedCallback', 'disconnectedCallback'],
|
|
491
490
|
getSharedContext(instance) {
|
|
492
491
|
return {
|
|
493
|
-
internals: instance.#internals
|
|
494
|
-
hostPropagator: instance.propagator
|
|
492
|
+
internals: instance.#internals
|
|
495
493
|
};
|
|
496
494
|
}
|
|
497
495
|
}
|
|
@@ -511,6 +509,18 @@ customElements.assignFeatures(MyElement, {
|
|
|
511
509
|
customElements.define('my-element', MyElement);
|
|
512
510
|
```
|
|
513
511
|
|
|
512
|
+
Because the feature is a getter-only property, `assignGingerly` merges directly into the instance. The consumer simply sets properties on the feature and the setters handle side effects:
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
// Direct property access — triggers the setter
|
|
516
|
+
el.myFeature.myProp = 'new value';
|
|
517
|
+
|
|
518
|
+
// Via assignGingerly — merges into the existing instance
|
|
519
|
+
el.assignGingerly({ myFeature: { myProp: 'new value' } });
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
This eliminates the need for an intermediate event bus or propagator — property access is the integration point.
|
|
523
|
+
|
|
514
524
|
**Async features and `callbackForwarding`:**
|
|
515
525
|
|
|
516
526
|
For async features (where `spawn` is an async function), the feature is instantiated when the async import resolves. Since the constructor already handles initial connection, `callbackForwarding` only needs to forward *subsequent* lifecycle events. If you wrap an async feature around a sync one (lazy-loading pattern), delegate lifecycle calls to the inner feature once it's loaded:
|
package/types/face-up/types.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { SpawnContext } from "../assign-gingerly/types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Shared context passed via getSharedContext — contains
|
|
5
|
-
*
|
|
4
|
+
* Shared context passed via getSharedContext — contains the
|
|
5
|
+
* ElementInternals instance needed for form control APIs.
|
|
6
6
|
*/
|
|
7
7
|
export interface FaceUpSharedContext {
|
|
8
8
|
/** The ElementInternals instance for form control APIs */
|
|
9
9
|
internals: ElementInternals;
|
|
10
|
-
/** The EventTarget the host dispatches property change events on */
|
|
11
|
-
hostPropagator: EventTarget;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -43,6 +41,11 @@ export interface ValidationFlags {
|
|
|
43
41
|
* Properties that the FaceUp feature exposes.
|
|
44
42
|
* These allow the host custom element to participate in HTML forms
|
|
45
43
|
* via the ElementInternals API.
|
|
44
|
+
*
|
|
45
|
+
* Because the feature is installed as a getter-only property,
|
|
46
|
+
* assignGingerly merges directly into the instance. The consumer
|
|
47
|
+
* simply sets properties (e.g., el.faceUp.value = x) and the
|
|
48
|
+
* setters handle syncing to ElementInternals automatically.
|
|
46
49
|
*/
|
|
47
50
|
export interface FaceUpProps {
|
|
48
51
|
/**
|
|
@@ -72,12 +75,6 @@ export interface FaceUpProps {
|
|
|
72
75
|
* marks the control as invalid with customError.
|
|
73
76
|
*/
|
|
74
77
|
validationMessage: string;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The EventTarget that the host element uses to propagate property change events.
|
|
78
|
-
* Provided via getSharedContext.
|
|
79
|
-
*/
|
|
80
|
-
hostPropagator: EventTarget | null;
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
/**
|
|
@@ -93,11 +90,6 @@ export interface AllProps extends FaceUpProps {
|
|
|
93
90
|
* The ElementInternals instance from the host (for form control APIs)
|
|
94
91
|
*/
|
|
95
92
|
internals: ElementInternals | null;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* AbortController for cleaning up event listeners
|
|
99
|
-
*/
|
|
100
|
-
abortController: AbortController | null;
|
|
101
93
|
}
|
|
102
94
|
|
|
103
95
|
export type AP = AllProps;
|