face-up 0.0.0 → 0.0.2
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/.gitmodules +3 -0
- package/.kiro/steering/project-context.md +17 -0
- package/.vscode/settings.json +3 -0
- package/FaceUp.js +305 -0
- package/README.md +162 -2
- package/imports.html +8 -0
- package/package.json +30 -20
- package/tests/test1.html +115 -0
- package/types/.kiro/specs/conversion-template/README.md +128 -0
- package/types/.kiro/specs/conversion-template/design.md +360 -0
- package/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/types/.kiro/steering/coding-standards.md +17 -0
- package/types/.kiro/steering/conversion-guide.md +103 -0
- package/types/.kiro/steering/declarative-configuration.md +108 -0
- package/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/types/EnhancementConversionInstructions.md +1626 -0
- package/types/LICENSE +21 -0
- package/types/NewCustomElementFeature.md +673 -0
- package/types/NewEnhancementInstructions.md +395 -0
- package/types/README.md +2 -0
- package/types/agrace/types.d.ts +11 -0
- package/types/assign-gingerly/types.d.ts +328 -0
- package/types/be-a-beacon/types.d.ts +17 -0
- package/types/be-bound/types.d.ts +61 -0
- package/types/be-buttoned-up/types.d.ts +19 -0
- package/types/be-clonable/types.d.ts +36 -0
- package/types/be-committed/types.d.ts +22 -0
- package/types/be-decked-with/types.d.ts +26 -0
- package/types/be-delible/types.d.ts +25 -0
- package/types/be-reflective/types.d.ts +80 -0
- package/types/be-render-neutral/types.d.ts +29 -0
- package/types/be-typed/types.d.ts +31 -0
- package/types/do-inc/types.d.ts +56 -0
- package/types/do-invoke/types.d.ts +38 -0
- package/types/do-merge/types.d.ts +28 -0
- package/types/do-toggle/types.d.ts +31 -0
- package/types/face-up/types.d.ts +104 -0
- package/types/global.d.ts +29 -0
- package/types/id-generation/types.d.ts +26 -0
- package/types/inferencer/types.d.ts +46 -0
- package/types/mount-observer/types.d.ts +363 -0
- package/types/nested-regex-groups/types.d.ts +101 -0
- package/types/roundabout/types.d.ts +255 -0
- package/types/time-ticker/types.d.ts +66 -0
- package/types/truth-sourcer/types.d.ts +46 -0
package/types/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruce B. Anderson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
# New Custom Element Feature Instructions
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
This document provides step-by-step instructions for creating a **brand new** custom element feature project. Custom Element Features provide dependency injection of composable feature classes onto custom element prototypes via lazy getters. They allow decomposing large components into smaller, testable units without mixins or subclassing.
|
|
6
|
+
|
|
7
|
+
**Note:** This guide is specifically for **custom element features** (composable behavior injected into custom elements via `assignFeatures`). It does NOT apply to enhancements (which use `be-hive` and `mount-observer` to attach behavior to existing elements via attributes). For enhancements, see [NewEnhancementInstructions.md](./NewEnhancementInstructions.md).
|
|
8
|
+
|
|
9
|
+
## What is a Custom Element Feature?
|
|
10
|
+
|
|
11
|
+
A custom element feature is a class that:
|
|
12
|
+
|
|
13
|
+
1. Gets lazily instantiated as a getter-only property on a custom element's prototype.
|
|
14
|
+
2. Receives the host element, a spawn context, and optional initial values in its constructor.
|
|
15
|
+
3. Can be swapped out for mocks in tests without subclassing.
|
|
16
|
+
4. Integrates with `assignGingerly` automatically — because the property is getter-only, `assignGingerly` merges into the spawned instance.
|
|
17
|
+
5. Supports async lazy-loading of the implementation.
|
|
18
|
+
6. Can parse element attributes into initial values via `withAttrs`.
|
|
19
|
+
|
|
20
|
+
## Reference Implementation
|
|
21
|
+
|
|
22
|
+
- **[truth-sourcer](https://github.com/bahrus/truth-sourcer)** — The world's first custom element feature
|
|
23
|
+
- **[be-reflective](https://github.com/bahrus/be-reflective)** — Demonstrates `callbackForwarding` and `getSharedContext` for features that need DOM context (computed styles)
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- Node.js installed
|
|
28
|
+
- npm installed
|
|
29
|
+
- `ncu` (npm-check-updates) installed globally: `npm install -g npm-check-updates`
|
|
30
|
+
- Chrome 146+ for testing (scoped custom element registry support required)
|
|
31
|
+
|
|
32
|
+
## Step 1: Initialize the Project
|
|
33
|
+
|
|
34
|
+
1. Create a new repository (e.g., `truth-sourcer` or `my-feature`)
|
|
35
|
+
2. Run `npm init` or create a `package.json` manually
|
|
36
|
+
3. Add the `types` submodule:
|
|
37
|
+
```bash
|
|
38
|
+
git submodule add https://github.com/bahrus/types.git types
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Step 2: Configure package.json
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"name": "my-feature",
|
|
46
|
+
"version": "0.0.0",
|
|
47
|
+
"description": "Description of what the feature does",
|
|
48
|
+
"type": "module",
|
|
49
|
+
"main": "MyFeature.js",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
52
|
+
"test": "playwright test",
|
|
53
|
+
"update": "ncu -u && npm install",
|
|
54
|
+
"safari": "npx playwright wk http://localhost:8000",
|
|
55
|
+
"chrome": "npx playwright cr http://localhost:8000"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"assign-gingerly": "0.0.39",
|
|
59
|
+
"@playwright/test": "1.60.0",
|
|
60
|
+
"spa-ssi": "0.0.27"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Notes:**
|
|
68
|
+
- Use exact versions, not ranges (no `^` or `~`)
|
|
69
|
+
- `assign-gingerly` is a devDependency — the consuming custom element project brings it in
|
|
70
|
+
- Run `npm run update` after creating package.json to install dependencies
|
|
71
|
+
|
|
72
|
+
## Step 3: Create Type Definitions
|
|
73
|
+
|
|
74
|
+
Create `types/[project-name]/types.d.ts` with the feature structure:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { SpawnContext } from "../assign-gingerly/types";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Configuration/properties that the feature exposes
|
|
81
|
+
*/
|
|
82
|
+
export interface FeatureProps {
|
|
83
|
+
// Properties the feature manages
|
|
84
|
+
myProp: string;
|
|
85
|
+
anotherProp: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Internal state (not exposed to consumers)
|
|
90
|
+
*/
|
|
91
|
+
export interface AllProps extends FeatureProps {
|
|
92
|
+
host: WeakRef<Element>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type AP = AllProps;
|
|
96
|
+
export type PAP = Partial<AP>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Context passed to the feature constructor
|
|
100
|
+
*/
|
|
101
|
+
export interface FeatureSpawnContext extends SpawnContext {
|
|
102
|
+
key: string;
|
|
103
|
+
optIn: any;
|
|
104
|
+
injection: any;
|
|
105
|
+
featuresRegistry: any;
|
|
106
|
+
shared?: any;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Key points:**
|
|
111
|
+
- `FeatureProps` — the public API of the feature
|
|
112
|
+
- `AllProps` — includes internal state like a WeakRef to the host element
|
|
113
|
+
- The feature class does NOT need to extend any base class
|
|
114
|
+
|
|
115
|
+
## Step 4: Create the Feature Class
|
|
116
|
+
|
|
117
|
+
Create `[FeatureName].js` (e.g., `AttrMgr.js`):
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// @ts-check
|
|
121
|
+
/** @import {FeatureProps, AllProps, FeatureSpawnContext} from './types/[project-name]/types' */
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @implements {FeatureProps}
|
|
125
|
+
*/
|
|
126
|
+
class MyFeature {
|
|
127
|
+
/** @type {WeakRef<Element> | undefined} */
|
|
128
|
+
#host;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {Element} hostElement
|
|
132
|
+
* @param {FeatureSpawnContext} ctx
|
|
133
|
+
* @param {Partial<FeatureProps>} [initVals]
|
|
134
|
+
*/
|
|
135
|
+
constructor(hostElement, ctx, initVals) {
|
|
136
|
+
this.#host = new WeakRef(hostElement);
|
|
137
|
+
if (initVals) {
|
|
138
|
+
Object.assign(this, initVals);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Feature methods and properties here
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { MyFeature };
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Key patterns:**
|
|
149
|
+
- No base class — plain JavaScript class
|
|
150
|
+
- Constructor signature: `(hostElement, ctx, initVals)`
|
|
151
|
+
- Store host as a `WeakRef` to avoid preventing garbage collection
|
|
152
|
+
- Apply `initVals` via `Object.assign` in the constructor
|
|
153
|
+
- Use `@ts-check` with JSDoc type imports from the `types/` folder
|
|
154
|
+
- No compiled TypeScript — ship raw `.js` files
|
|
155
|
+
|
|
156
|
+
## Step 5: Create imports.html
|
|
157
|
+
|
|
158
|
+
```html
|
|
159
|
+
<script type=importmap>
|
|
160
|
+
{
|
|
161
|
+
"imports": {
|
|
162
|
+
"assign-gingerly/": "/node_modules/assign-gingerly/",
|
|
163
|
+
"[project-name]/": "/"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
</script>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Notes:**
|
|
170
|
+
- Import maps use trailing slashes for package-style resolution
|
|
171
|
+
- The feature project maps to `/` (root) for local development
|
|
172
|
+
- Only include packages actually needed
|
|
173
|
+
|
|
174
|
+
## Step 6: Create Test HTML
|
|
175
|
+
|
|
176
|
+
Create a test file (e.g., `tests/test1.html`):
|
|
177
|
+
|
|
178
|
+
```html
|
|
179
|
+
<!DOCTYPE html>
|
|
180
|
+
<html lang="en">
|
|
181
|
+
<head>
|
|
182
|
+
<meta charset="UTF-8">
|
|
183
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
184
|
+
<title>Test - My Feature</title>
|
|
185
|
+
<!-- #include virtual="/imports.html" -->
|
|
186
|
+
<script type=module>
|
|
187
|
+
import 'assign-gingerly/assignFeatures.js';
|
|
188
|
+
import {MyFeature} from '[project-name]/MyFeature.js';
|
|
189
|
+
|
|
190
|
+
// Define a test custom element that uses the feature
|
|
191
|
+
class TestElement extends HTMLElement {
|
|
192
|
+
static supportedFeatures = {
|
|
193
|
+
myFeature: {
|
|
194
|
+
fallbackSpawn: MyFeature
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
constructor() {
|
|
199
|
+
super();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Inject the feature
|
|
204
|
+
customElements.assignFeatures(TestElement, {
|
|
205
|
+
myFeature: { spawn: MyFeature }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
customElements.define('test-element', TestElement);
|
|
209
|
+
</script>
|
|
210
|
+
</head>
|
|
211
|
+
<body>
|
|
212
|
+
<test-element></test-element>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Step 7: Configure VS Code
|
|
218
|
+
|
|
219
|
+
Create `.vscode/settings.json`:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"explorer.fileNesting.enabled": true
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Step 8: Set Up .kiro Directory
|
|
228
|
+
|
|
229
|
+
Create `.kiro/steering/project-context.md` to reference the shared types documentation:
|
|
230
|
+
|
|
231
|
+
```markdown
|
|
232
|
+
---
|
|
233
|
+
inclusion: auto
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
# Project Context
|
|
237
|
+
|
|
238
|
+
This project uses shared type definitions and documentation from the `types` submodule.
|
|
239
|
+
|
|
240
|
+
## Key References
|
|
241
|
+
|
|
242
|
+
#[[file:types/NewCustomElementFeature.md]]
|
|
243
|
+
|
|
244
|
+
## Coding Standards
|
|
245
|
+
|
|
246
|
+
#[[file:types/.kiro/steering/coding-standards.md]]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Architecture Overview
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
[project-name]/
|
|
253
|
+
├── .kiro/
|
|
254
|
+
│ └── steering/
|
|
255
|
+
│ └── project-context.md
|
|
256
|
+
├── .vscode/
|
|
257
|
+
│ └── settings.json
|
|
258
|
+
├── types/ (git submodule)
|
|
259
|
+
│ └── [project-name]/
|
|
260
|
+
│ └── types.d.ts
|
|
261
|
+
├── [FeatureName].js (feature class - browser code)
|
|
262
|
+
├── imports.html (import map for browser)
|
|
263
|
+
├── package.json
|
|
264
|
+
├── tests/
|
|
265
|
+
│ └── test1.html
|
|
266
|
+
└── README.md
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## How Custom Element Features Work
|
|
270
|
+
|
|
271
|
+
### The Consumer Side (Custom Element Author)
|
|
272
|
+
|
|
273
|
+
A custom element declares which feature "slots" it supports:
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
// @ts-check
|
|
277
|
+
import 'assign-gingerly/assignFeatures.js';
|
|
278
|
+
import {MyFeature} from 'my-feature/MyFeature.js';
|
|
279
|
+
|
|
280
|
+
class MyElement extends HTMLElement {
|
|
281
|
+
static supportedFeatures = {
|
|
282
|
+
myFeature: {
|
|
283
|
+
fallbackSpawn: MyFeature,
|
|
284
|
+
// Optional: validate the spawned instance
|
|
285
|
+
validateShape(instance) {
|
|
286
|
+
return typeof instance.myMethod === 'function';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
constructor() {
|
|
292
|
+
super();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Inject feature implementations
|
|
297
|
+
customElements.assignFeatures(MyElement, {
|
|
298
|
+
myFeature: { spawn: MyFeature }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
customElements.define('my-element', MyElement);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Lazy Instantiation
|
|
305
|
+
|
|
306
|
+
The feature is NOT instantiated until first property access:
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
const el = document.createElement('my-element');
|
|
310
|
+
// Feature not yet instantiated
|
|
311
|
+
|
|
312
|
+
console.log(el.myFeature.myProp);
|
|
313
|
+
// NOW the feature is instantiated (lazy getter fires)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Integration with assignGingerly
|
|
317
|
+
|
|
318
|
+
Because `assignFeatures` installs getter-only properties (no setter), `assignGingerly` automatically merges into the feature instance:
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
import 'assign-gingerly/object-extension.js';
|
|
322
|
+
|
|
323
|
+
el.assignGingerly({
|
|
324
|
+
myFeature: { myProp: 'updated value' }
|
|
325
|
+
});
|
|
326
|
+
// Merges into the existing feature instance
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Attribute Parsing with withAttrs
|
|
330
|
+
|
|
331
|
+
Features can declare attribute patterns to parse element attributes into `initVals`:
|
|
332
|
+
|
|
333
|
+
```javascript
|
|
334
|
+
customElements.assignFeatures(MyElement, {
|
|
335
|
+
myFeature: {
|
|
336
|
+
spawn: MyFeature,
|
|
337
|
+
withAttrs: {
|
|
338
|
+
base: 'my-feature',
|
|
339
|
+
myProp: '${base}-my-prop',
|
|
340
|
+
count: '${base}-count',
|
|
341
|
+
_count: { instanceOf: 'Number' }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```html
|
|
348
|
+
<my-element my-feature-my-prop="hello" my-feature-count="42"></my-element>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The parsed attributes (`{ myProp: 'hello', count: 42 }`) are passed as `initVals` to the constructor.
|
|
352
|
+
|
|
353
|
+
### Async Spawn (Lazy Loading)
|
|
354
|
+
|
|
355
|
+
Feature implementations can be loaded asynchronously:
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
customElements.assignFeatures(MyElement, {
|
|
359
|
+
myFeature: {
|
|
360
|
+
spawn: () => import('my-feature/MyFeature.js').then(m => m.MyFeature)
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
During the loading window, a placeholder `{}` is returned. Once the async import resolves, the real instance replaces it (with any accumulated properties passed as `initVals`).
|
|
366
|
+
|
|
367
|
+
### Shared Context (Access to Private Fields)
|
|
368
|
+
|
|
369
|
+
Features can receive private data from the host element via `getSharedContext`:
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
class MyElement extends HTMLElement {
|
|
373
|
+
#internals;
|
|
374
|
+
|
|
375
|
+
static supportedFeatures = {
|
|
376
|
+
myFeature: {
|
|
377
|
+
fallbackSpawn: MyFeature,
|
|
378
|
+
getSharedContext(instance) {
|
|
379
|
+
return { internals: instance.#internals };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
constructor() {
|
|
385
|
+
super();
|
|
386
|
+
this.#internals = this.attachInternals();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The feature receives this via `ctx.shared`:
|
|
392
|
+
|
|
393
|
+
```javascript
|
|
394
|
+
class MyFeature {
|
|
395
|
+
constructor(hostElement, ctx, initVals) {
|
|
396
|
+
this.internals = ctx.shared?.internals;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Lifecycle Callback Forwarding with `callbackForwarding`
|
|
402
|
+
|
|
403
|
+
Features that need DOM context (computed styles, layout info) or cleanup on disconnect can declare `callbackForwarding` in `static supportedFeatures` to receive the host element's lifecycle callbacks automatically:
|
|
404
|
+
|
|
405
|
+
```javascript
|
|
406
|
+
class MyElement extends HTMLElement {
|
|
407
|
+
static supportedFeatures = {
|
|
408
|
+
myFeature: {
|
|
409
|
+
fallbackSpawn: MyFeature,
|
|
410
|
+
callbackForwarding: ['connectedCallback', 'disconnectedCallback']
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
customElements.assignFeatures(MyElement, {
|
|
416
|
+
myFeature: { spawn: MyFeature }
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`callbackForwarding` can also be specified in the injection config passed to `assignFeatures`. A union is taken between both — so the feature author can guarantee the callbacks they need in `supportedFeatures`, and the injector can add additional ones if needed.
|
|
421
|
+
|
|
422
|
+
**How it works:**
|
|
423
|
+
|
|
424
|
+
1. `assignFeatures` patches the custom element's lifecycle callbacks on the prototype (once per callback type).
|
|
425
|
+
2. The original callback runs first, then all registered features are forwarded.
|
|
426
|
+
3. On first `connectedCallback`, the lazy getter is triggered — spawning the feature at the correct lifecycle moment (when the element is in the DOM and computed styles are available).
|
|
427
|
+
4. For async features, forwarding is skipped until the real instance is available.
|
|
428
|
+
|
|
429
|
+
**Supported callbacks:** `connectedCallback`, `disconnectedCallback`, `attributeChangedCallback`, `adoptedCallback`, `formAssociatedCallback`, `formDisabledCallback`, `formResetCallback`, `formStateRestoreCallback`
|
|
430
|
+
|
|
431
|
+
**When to use it:**
|
|
432
|
+
|
|
433
|
+
- The feature needs `attributeChangedCallback` forwarded (e.g., truth-sourcer)
|
|
434
|
+
- The feature needs `getComputedStyle` (which requires the element to be in the DOM)
|
|
435
|
+
- The feature sets up event listeners that should be cleaned up on disconnect
|
|
436
|
+
- The feature needs to handle elements created via cloned templates (where the constructor fires before DOM insertion)
|
|
437
|
+
- The feature manages form-associated behavior (`formDisabledCallback`, `formResetCallback`, `formStateRestoreCallback`) — e.g., face-up
|
|
438
|
+
|
|
439
|
+
**Avoiding double-connect on initial spawn:**
|
|
440
|
+
|
|
441
|
+
Since the feature is *spawned* during the first `connectedCallback` (the getter fires), the constructor already has the opportunity to self-connect. When `callbackForwarding` then immediately forwards `connectedCallback` to the freshly-spawned instance, you need to guard against double-initialization. The standard pattern is a `#hasDisconnected` flag:
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
class MyFeature {
|
|
445
|
+
#hasDisconnected = false;
|
|
446
|
+
|
|
447
|
+
constructor(hostElement, ctx, initVals) {
|
|
448
|
+
// Self-connect on construction (we know we're in the DOM
|
|
449
|
+
// because connectedCallback triggered our spawn)
|
|
450
|
+
this.#connect();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
connectedCallback() {
|
|
454
|
+
// Only re-connect after a prior disconnection
|
|
455
|
+
if (this.#hasDisconnected) {
|
|
456
|
+
this.#hasDisconnected = false;
|
|
457
|
+
this.#connect();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
disconnectedCallback() {
|
|
462
|
+
this.#hasDisconnected = true;
|
|
463
|
+
this.#cleanup();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
#connect() {
|
|
467
|
+
// Safe to call getComputedStyle here — element is in the DOM
|
|
468
|
+
const styles = getComputedStyle(this.#hostRef.deref());
|
|
469
|
+
// ... wire up listeners, parse CSS, etc.
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
#cleanup() {
|
|
473
|
+
// Abort listeners, clear state
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Complete example with `getSharedContext` + `callbackForwarding`:**
|
|
479
|
+
|
|
480
|
+
This pattern eliminates all manual wiring in the consumer's constructor — the feature self-activates at the correct lifecycle moment with all dependencies provided declaratively:
|
|
481
|
+
|
|
482
|
+
```javascript
|
|
483
|
+
class MyElement extends HTMLElement {
|
|
484
|
+
propagator = new EventTarget();
|
|
485
|
+
#internals;
|
|
486
|
+
|
|
487
|
+
static supportedFeatures = {
|
|
488
|
+
myFeature: {
|
|
489
|
+
fallbackSpawn: MyFeature,
|
|
490
|
+
callbackForwarding: ['connectedCallback', 'disconnectedCallback'],
|
|
491
|
+
getSharedContext(instance) {
|
|
492
|
+
return {
|
|
493
|
+
internals: instance.#internals,
|
|
494
|
+
hostPropagator: instance.propagator
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
constructor() {
|
|
501
|
+
super();
|
|
502
|
+
this.#internals = this.attachInternals();
|
|
503
|
+
// No manual feature activation needed!
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
customElements.assignFeatures(MyElement, {
|
|
508
|
+
myFeature: { spawn: MyFeature }
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
customElements.define('my-element', MyElement);
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Async features and `callbackForwarding`:**
|
|
515
|
+
|
|
516
|
+
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:
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
class MyFeatureLazy {
|
|
520
|
+
#delegate = null;
|
|
521
|
+
#hasDisconnected = false;
|
|
522
|
+
|
|
523
|
+
constructor(hostElement, ctx, initVals) {
|
|
524
|
+
this.#maybeActivate(hostElement, ctx);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
connectedCallback() {
|
|
528
|
+
if (this.#hasDisconnected) {
|
|
529
|
+
this.#hasDisconnected = false;
|
|
530
|
+
this.#delegate?.connectedCallback();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
disconnectedCallback() {
|
|
535
|
+
this.#hasDisconnected = true;
|
|
536
|
+
this.#delegate?.disconnectedCallback();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async #maybeActivate(hostElement, ctx) {
|
|
540
|
+
// Guard: only load if actually needed
|
|
541
|
+
const computed = getComputedStyle(hostElement);
|
|
542
|
+
if (!computed.getPropertyValue('--my-config').trim()) return;
|
|
543
|
+
|
|
544
|
+
const { MyFeature } = await import('./MyFeature.js');
|
|
545
|
+
this.#delegate = new MyFeature(hostElement, ctx);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Class-Level Setup with `static onAssigned`
|
|
551
|
+
|
|
552
|
+
Features that need one-time class-level setup before any instances are created can define a `static onAssigned` method. This is called by `assignFeatures` immediately after registration, receiving the host constructor and the feature config:
|
|
553
|
+
|
|
554
|
+
```javascript
|
|
555
|
+
class MyFeature {
|
|
556
|
+
/**
|
|
557
|
+
* Called once when assignFeatures processes this feature.
|
|
558
|
+
* Use for one-time class-level setup: installing prototype properties,
|
|
559
|
+
* setting static flags, or pre-loading modules.
|
|
560
|
+
*/
|
|
561
|
+
static onAssigned(ctr, featureConfig) {
|
|
562
|
+
// Set static properties on the host constructor
|
|
563
|
+
ctr.formAssociated = true;
|
|
564
|
+
// Or install prototype getter/setters, pre-load modules, etc.
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
constructor(hostElement, ctx, initVals) {
|
|
568
|
+
// Instance-level setup (runs on first getter access)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Usage:**
|
|
574
|
+
|
|
575
|
+
```javascript
|
|
576
|
+
// await is safe — returns undefined if no async onAssigned hooks exist
|
|
577
|
+
await customElements.assignFeatures(MyElement, {
|
|
578
|
+
myFeature: { spawn: MyFeature }
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Now define — class is fully set up
|
|
582
|
+
customElements.define('my-element', MyElement);
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**How it works:**
|
|
586
|
+
|
|
587
|
+
- `assignFeatures` checks if the spawn class defines `static onAssigned` (via `Object.hasOwn`).
|
|
588
|
+
- If found, calls `SpawnClass.onAssigned(ctr, featureConfig)` after installing the getter.
|
|
589
|
+
- If `onAssigned` returns a Promise, `assignFeatures` returns a `Promise<void>` that resolves when all async hooks complete.
|
|
590
|
+
- If no `onAssigned` hooks are async (or none exist), `assignFeatures` returns `undefined` (backward compatible).
|
|
591
|
+
- Only applies to synchronous spawners (the class must be available at registration time).
|
|
592
|
+
|
|
593
|
+
**When to use it:**
|
|
594
|
+
|
|
595
|
+
- Setting `static formAssociated = true` on the host (for form-associated custom elements)
|
|
596
|
+
- Installing prototype getter/setters that the feature depends on
|
|
597
|
+
- Pre-loading modules or resources needed at spawn time
|
|
598
|
+
- Any setup that must happen once per class, not once per instance
|
|
599
|
+
|
|
600
|
+
**`await` is always safe:**
|
|
601
|
+
|
|
602
|
+
```javascript
|
|
603
|
+
// These are equivalent for features without onAssigned:
|
|
604
|
+
customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
|
|
605
|
+
await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
|
|
606
|
+
// Both work — await on undefined is a no-op
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Pre-upgrade Property Capture
|
|
610
|
+
|
|
611
|
+
If properties are set on an element before it upgrades, use `captureFeatureInitVals`:
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
import { captureFeatureInitVals } from 'assign-gingerly/assignFeatures.js';
|
|
615
|
+
|
|
616
|
+
class MyElement extends HTMLElement {
|
|
617
|
+
static supportedFeatures = {
|
|
618
|
+
myFeature: { fallbackSpawn: MyFeature }
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
constructor() {
|
|
622
|
+
super();
|
|
623
|
+
captureFeatureInitVals(this);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Property Forwarding with installForwarding
|
|
629
|
+
|
|
630
|
+
To expose nested feature properties at the top level of the custom element:
|
|
631
|
+
|
|
632
|
+
```javascript
|
|
633
|
+
import { installForwarding } from 'assign-gingerly/installForwarding.js';
|
|
634
|
+
|
|
635
|
+
class MyElement extends HTMLElement {
|
|
636
|
+
static supportedFeatures = {
|
|
637
|
+
myFeature: { fallbackSpawn: MyFeature }
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
static propLinks = {
|
|
641
|
+
'myProp': '?.myFeature?.myProp'
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
customElements.assignFeatures(MyElement, {
|
|
646
|
+
myFeature: { spawn: MyFeature }
|
|
647
|
+
});
|
|
648
|
+
installForwarding(MyElement);
|
|
649
|
+
customElements.define('my-element', MyElement);
|
|
650
|
+
|
|
651
|
+
// Now el.myProp delegates to el.myFeature.myProp
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Key Differences from Enhancements
|
|
655
|
+
|
|
656
|
+
| Aspect | Enhancement | Custom Element Feature |
|
|
657
|
+
|--------|-------------|----------------------|
|
|
658
|
+
| Target | Any existing HTML element | Custom element prototypes |
|
|
659
|
+
| Attachment | Via attributes + mount-observer | Via `assignFeatures` + lazy getters |
|
|
660
|
+
| Registration | `enhancementRegistry` | `featuresRegistry` |
|
|
661
|
+
| Discovery | DOM observation (be-hive) | Explicit injection before `define()` |
|
|
662
|
+
| Dependencies | mount-observer, be-hive, roundabout | assign-gingerly only |
|
|
663
|
+
| Build step | emc.mjs → emc.json | None required |
|
|
664
|
+
| Attribute prefix | `enh-` for isolation | Unprefixed (features own their element) |
|
|
665
|
+
|
|
666
|
+
## Tips
|
|
667
|
+
|
|
668
|
+
- **Call `assignFeatures` before `customElements.define()`** — getters must be on the prototype before instances exist
|
|
669
|
+
- **Use `@ts-check`** — catches type errors early in `.js` files
|
|
670
|
+
- **Store host as WeakRef** — prevents memory leaks
|
|
671
|
+
- **Keep features focused** — one responsibility per feature class
|
|
672
|
+
- **Use `validateShape`** — catches injection errors early in development
|
|
673
|
+
- **Test with mocks** — swap real implementations for test doubles without subclassing
|