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/.gitmodules
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
inclusion: auto
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Project Context
|
|
6
|
+
|
|
7
|
+
This project uses shared type definitions and documentation from the `types` submodule.
|
|
8
|
+
|
|
9
|
+
## Key References
|
|
10
|
+
|
|
11
|
+
#[[file:types/NewCustomElementFeature.md]]
|
|
12
|
+
#[[file:types/NewEnhancementInstructions.md]]
|
|
13
|
+
#[[file:types/EnhancementConversionInstructions.md]]
|
|
14
|
+
|
|
15
|
+
## Coding Standards
|
|
16
|
+
|
|
17
|
+
#[[file:types/.kiro/steering/coding-standards.md]]
|
package/FaceUp.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {FaceUpProps, AllProps, FeatureSpawnContext, FaceUpSharedContext, ValidationFlags} from './types/face-up/types' */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FaceUp — A Custom Element Feature that adds Form Associated behavior
|
|
6
|
+
* to a custom element via the ElementInternals API.
|
|
7
|
+
*
|
|
8
|
+
* This feature enables a custom element to:
|
|
9
|
+
* - Participate in form submission (setFormValue)
|
|
10
|
+
* - Participate in form validation (setValidity)
|
|
11
|
+
* - Respond to form lifecycle callbacks (reset, restore, disabled state)
|
|
12
|
+
* - Be styled with :valid/:invalid/:disabled pseudo-classes
|
|
13
|
+
* - Be labeled with <label> elements
|
|
14
|
+
*
|
|
15
|
+
* The host custom element MUST:
|
|
16
|
+
* - Call `this.attachInternals()` and pass the result via getSharedContext
|
|
17
|
+
*
|
|
18
|
+
* `static formAssociated = true` is set automatically via `static onAssigned`.
|
|
19
|
+
*
|
|
20
|
+
* @implements {FaceUpProps}
|
|
21
|
+
*/
|
|
22
|
+
class FaceUp {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Called once by assignFeatures after registration.
|
|
26
|
+
* Sets `static formAssociated = true` on the host constructor so the
|
|
27
|
+
* consumer doesn't need to declare it manually.
|
|
28
|
+
* @param {Function} ctr - The custom element constructor
|
|
29
|
+
* @param {object} _featureConfig - The feature config (unused)
|
|
30
|
+
*/
|
|
31
|
+
static onAssigned(ctr, _featureConfig) {
|
|
32
|
+
if (!ctr.formAssociated) {
|
|
33
|
+
ctr.formAssociated = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** @type {WeakRef<HTMLElement> | undefined} */
|
|
37
|
+
#hostRef;
|
|
38
|
+
|
|
39
|
+
/** @type {ElementInternals | undefined} */
|
|
40
|
+
#internals;
|
|
41
|
+
|
|
42
|
+
/** @type {AbortController | undefined} */
|
|
43
|
+
#abortController;
|
|
44
|
+
|
|
45
|
+
/** @type {EventTarget | undefined} */
|
|
46
|
+
#hostPropagator;
|
|
47
|
+
|
|
48
|
+
/** @type {boolean} */
|
|
49
|
+
#hasDisconnected = false;
|
|
50
|
+
|
|
51
|
+
/** @type {string | File | FormData | null} */
|
|
52
|
+
#value = null;
|
|
53
|
+
|
|
54
|
+
/** @type {string | File | FormData | null} */
|
|
55
|
+
#state = null;
|
|
56
|
+
|
|
57
|
+
/** @type {boolean} */
|
|
58
|
+
#disabled = false;
|
|
59
|
+
|
|
60
|
+
/** @type {boolean} */
|
|
61
|
+
#required = false;
|
|
62
|
+
|
|
63
|
+
/** @type {string} */
|
|
64
|
+
#validationMessage = '';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {HTMLElement} hostElement
|
|
68
|
+
* @param {FeatureSpawnContext} ctx
|
|
69
|
+
* @param {Partial<FaceUpProps>} [initVals]
|
|
70
|
+
*/
|
|
71
|
+
constructor(hostElement, ctx, initVals) {
|
|
72
|
+
this.#hostRef = new WeakRef(hostElement);
|
|
73
|
+
if (ctx.shared) {
|
|
74
|
+
this.#internals = ctx.shared.internals;
|
|
75
|
+
this.#hostPropagator = ctx.shared.hostPropagator;
|
|
76
|
+
}
|
|
77
|
+
if (initVals) {
|
|
78
|
+
if (initVals.value !== undefined) this.#value = initVals.value;
|
|
79
|
+
if (initVals.state !== undefined) this.#state = initVals.state;
|
|
80
|
+
if (initVals.disabled !== undefined) this.#disabled = initVals.disabled;
|
|
81
|
+
if (initVals.required !== undefined) this.#required = initVals.required;
|
|
82
|
+
if (initVals.validationMessage !== undefined) this.#validationMessage = initVals.validationMessage;
|
|
83
|
+
if (initVals.hostPropagator !== undefined) this.#hostPropagator = initVals.hostPropagator;
|
|
84
|
+
}
|
|
85
|
+
this.#connect();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Public Properties ───────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
get value() { return this.#value; }
|
|
91
|
+
set value(v) {
|
|
92
|
+
this.#value = v;
|
|
93
|
+
this.#syncFormValue();
|
|
94
|
+
this.#validateInternal();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get state() { return this.#state; }
|
|
98
|
+
set state(v) {
|
|
99
|
+
this.#state = v;
|
|
100
|
+
this.#syncFormValue();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get disabled() { return this.#disabled; }
|
|
104
|
+
set disabled(v) {
|
|
105
|
+
this.#disabled = !!v;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get required() { return this.#required; }
|
|
109
|
+
set required(v) {
|
|
110
|
+
this.#required = !!v;
|
|
111
|
+
this.#validateInternal();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get validationMessage() { return this.#validationMessage; }
|
|
115
|
+
set validationMessage(msg) {
|
|
116
|
+
this.#validationMessage = msg;
|
|
117
|
+
this.#validateInternal();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get hostPropagator() { return this.#hostPropagator ?? null; }
|
|
121
|
+
set hostPropagator(v) {
|
|
122
|
+
this.#hostPropagator = v ?? undefined;
|
|
123
|
+
this.#reconnectListeners();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Read-only Form Control Accessors ────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** The form element the host is associated with */
|
|
129
|
+
get form() { return this.#internals?.form ?? null; }
|
|
130
|
+
|
|
131
|
+
/** The ValidityState of the control */
|
|
132
|
+
get validity() { return this.#internals?.validity ?? null; }
|
|
133
|
+
|
|
134
|
+
/** Whether the control will be validated */
|
|
135
|
+
get willValidate() { return this.#internals?.willValidate ?? false; }
|
|
136
|
+
|
|
137
|
+
// ─── Form Control Methods ────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** Runs constraint validation and returns true if valid */
|
|
140
|
+
checkValidity() {
|
|
141
|
+
return this.#internals?.checkValidity() ?? true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Runs constraint validation and shows the browser validation UI if invalid */
|
|
145
|
+
reportValidity() {
|
|
146
|
+
return this.#internals?.reportValidity() ?? true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sets custom validity flags and message on the control.
|
|
151
|
+
* @param {ValidationFlags} flags - ValidityStateFlags object
|
|
152
|
+
* @param {string} [message] - Validation message (required if any flag is true)
|
|
153
|
+
* @param {HTMLElement} [anchor] - Element to anchor the validation popup to
|
|
154
|
+
*/
|
|
155
|
+
setValidity(flags, message, anchor) {
|
|
156
|
+
if (!this.#internals) return;
|
|
157
|
+
if (message) {
|
|
158
|
+
this.#internals.setValidity(flags, message, anchor);
|
|
159
|
+
} else {
|
|
160
|
+
// Clear validity — no flags set
|
|
161
|
+
this.#internals.setValidity({});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Form Lifecycle Callbacks (forwarded via callbackForwarding) ─
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Called when the host element's disabled state changes.
|
|
169
|
+
* @param {boolean} isDisabled
|
|
170
|
+
*/
|
|
171
|
+
formDisabledCallback(isDisabled) {
|
|
172
|
+
this.#disabled = isDisabled;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Called when the form is reset.
|
|
177
|
+
* Resets value and state to null and clears validation.
|
|
178
|
+
*/
|
|
179
|
+
formResetCallback() {
|
|
180
|
+
this.#value = null;
|
|
181
|
+
this.#state = null;
|
|
182
|
+
this.#validationMessage = '';
|
|
183
|
+
this.#syncFormValue();
|
|
184
|
+
if (this.#internals) {
|
|
185
|
+
this.#internals.setValidity({});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Called when the browser restores form state (navigation, restart)
|
|
191
|
+
* or when autofill sets a value.
|
|
192
|
+
* @param {string | File | FormData | null} state
|
|
193
|
+
* @param {'restore' | 'autocomplete'} mode
|
|
194
|
+
*/
|
|
195
|
+
formStateRestoreCallback(state, mode) {
|
|
196
|
+
if (mode === 'restore') {
|
|
197
|
+
this.#state = state;
|
|
198
|
+
this.#value = state;
|
|
199
|
+
this.#syncFormValue();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
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
|
+
// ─── Private Methods ─────────────────────────────────────────────
|
|
218
|
+
|
|
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
|
+
/**
|
|
266
|
+
* Syncs the current value (and optional state) to ElementInternals.
|
|
267
|
+
*/
|
|
268
|
+
#syncFormValue() {
|
|
269
|
+
if (!this.#internals) return;
|
|
270
|
+
if (this.#value === null) {
|
|
271
|
+
this.#internals.setFormValue(null);
|
|
272
|
+
} else if (this.#state !== null) {
|
|
273
|
+
this.#internals.setFormValue(this.#value, this.#state);
|
|
274
|
+
} else {
|
|
275
|
+
this.#internals.setFormValue(this.#value);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Runs internal validation based on required + custom validation message.
|
|
281
|
+
*/
|
|
282
|
+
#validateInternal() {
|
|
283
|
+
if (!this.#internals) return;
|
|
284
|
+
const host = this.#hostRef?.deref();
|
|
285
|
+
if (!host) return;
|
|
286
|
+
|
|
287
|
+
if (this.#validationMessage) {
|
|
288
|
+
this.#internals.setValidity(
|
|
289
|
+
{ customError: true },
|
|
290
|
+
this.#validationMessage,
|
|
291
|
+
host
|
|
292
|
+
);
|
|
293
|
+
} else if (this.#required && (this.#value === null || this.#value === '')) {
|
|
294
|
+
this.#internals.setValidity(
|
|
295
|
+
{ valueMissing: true },
|
|
296
|
+
'Please fill out this field.',
|
|
297
|
+
host
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
this.#internals.setValidity({});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export { FaceUp };
|
package/README.md
CHANGED
|
@@ -1,2 +1,162 @@
|
|
|
1
|
-
# face-
|
|
2
|
-
|
|
1
|
+
# face-up
|
|
2
|
+
|
|
3
|
+
A Custom Element Feature that adds Form Associated behavior to a custom element via the [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) API.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`FaceUp` enables any custom element to fully participate in HTML forms — matching the capabilities described in [More Capable Form Controls](https://web.dev/articles/more-capable-form-controls):
|
|
8
|
+
|
|
9
|
+
- **Form submission** — The control's value is automatically submitted with the form via `setFormValue()`.
|
|
10
|
+
- **Form validation** — The control participates in constraint validation with `:valid`/`:invalid` pseudo-classes.
|
|
11
|
+
- **Form reset** — The control resets to its default state when the form is reset.
|
|
12
|
+
- **Form state restoration** — The browser can restore the control's state after navigation or restart.
|
|
13
|
+
- **Disabled state** — The control responds to `disabled` attribute changes on itself or ancestor `<fieldset>`.
|
|
14
|
+
- **Label association** — The control can be labeled with `<label>` elements.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import 'assign-gingerly/assignFeatures.js';
|
|
20
|
+
import { FaceUp } from 'face-up/FaceUp.js';
|
|
21
|
+
|
|
22
|
+
class MyInput extends HTMLElement {
|
|
23
|
+
propagator = new EventTarget();
|
|
24
|
+
#internals;
|
|
25
|
+
|
|
26
|
+
static supportedFeatures = {
|
|
27
|
+
faceUp: {
|
|
28
|
+
fallbackSpawn: FaceUp,
|
|
29
|
+
callbackForwarding: [
|
|
30
|
+
'connectedCallback',
|
|
31
|
+
'disconnectedCallback',
|
|
32
|
+
'formDisabledCallback',
|
|
33
|
+
'formResetCallback',
|
|
34
|
+
'formStateRestoreCallback'
|
|
35
|
+
],
|
|
36
|
+
getSharedContext(instance) {
|
|
37
|
+
return {
|
|
38
|
+
internals: instance.#internals,
|
|
39
|
+
hostPropagator: instance.propagator
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
this.#internals = this.attachInternals();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// assignFeatures calls FaceUp.onAssigned which sets static formAssociated = true
|
|
52
|
+
await customElements.assignFeatures(MyInput, {
|
|
53
|
+
faceUp: { spawn: FaceUp }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
customElements.define('my-input', MyInput);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Note: `static formAssociated = true` is set automatically by `FaceUp.onAssigned` — you don't need to declare it yourself.
|
|
60
|
+
|
|
61
|
+
Form lifecycle callbacks (`formDisabledCallback`, `formResetCallback`, `formStateRestoreCallback`) are forwarded automatically via `callbackForwarding` — no manual forwarding code needed in the host element.
|
|
62
|
+
|
|
63
|
+
## Setting Values
|
|
64
|
+
|
|
65
|
+
Dispatch events on the host's `propagator` to update the form value:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
// From within the custom element:
|
|
69
|
+
this.propagator.dispatchEvent(
|
|
70
|
+
new Event('value')
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or set the value directly on the feature instance:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
el.faceUp.value = 'new value';
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Validation
|
|
81
|
+
|
|
82
|
+
Set a custom validation message:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
el.faceUp.validationMessage = 'This value is not allowed.';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or use the lower-level `setValidity()` API:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
el.faceUp.setValidity({ rangeUnderflow: true }, 'Value must be at least 0.');
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Clear validation:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
el.faceUp.validationMessage = '';
|
|
98
|
+
// or
|
|
99
|
+
el.faceUp.setValidity({});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Form State Restoration
|
|
103
|
+
|
|
104
|
+
Pass a `state` parameter alongside `value` to enable proper form restoration:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
el.faceUp.state = 'palette/#7fff00';
|
|
108
|
+
el.faceUp.value = '#7fff00';
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `state` is stored internally by the browser and passed back to `formStateRestoreCallback` when the form is restored.
|
|
112
|
+
|
|
113
|
+
## API
|
|
114
|
+
|
|
115
|
+
### Properties
|
|
116
|
+
|
|
117
|
+
| Property | Type | Description |
|
|
118
|
+
|----------|------|-------------|
|
|
119
|
+
| `value` | `string \| File \| FormData \| null` | The submittable form value |
|
|
120
|
+
| `state` | `string \| File \| FormData \| null` | Internal state for form restoration |
|
|
121
|
+
| `disabled` | `boolean` | Whether the control is disabled |
|
|
122
|
+
| `required` | `boolean` | Whether the control requires a value |
|
|
123
|
+
| `validationMessage` | `string` | Custom validation error message |
|
|
124
|
+
| `form` | `HTMLFormElement \| null` | The associated form (read-only) |
|
|
125
|
+
| `validity` | `ValidityState \| null` | The validity state (read-only) |
|
|
126
|
+
| `willValidate` | `boolean` | Whether the control will be validated (read-only) |
|
|
127
|
+
|
|
128
|
+
### Methods
|
|
129
|
+
|
|
130
|
+
| Method | Returns | Description |
|
|
131
|
+
|--------|---------|-------------|
|
|
132
|
+
| `checkValidity()` | `boolean` | Returns true if the control is valid |
|
|
133
|
+
| `reportValidity()` | `boolean` | Shows browser validation UI if invalid |
|
|
134
|
+
| `setValidity(flags, message?, anchor?)` | `void` | Sets custom validity flags |
|
|
135
|
+
|
|
136
|
+
### Form Lifecycle Callbacks (forwarded via `callbackForwarding`)
|
|
137
|
+
|
|
138
|
+
| Callback | Description |
|
|
139
|
+
|----------|-------------|
|
|
140
|
+
| `formDisabledCallback(disabled)` | Called when disabled state changes |
|
|
141
|
+
| `formResetCallback()` | Called when the form is reset |
|
|
142
|
+
| `formStateRestoreCallback(state, mode)` | Called when browser restores form state |
|
|
143
|
+
|
|
144
|
+
These are forwarded automatically by `assign-gingerly`'s `callbackForwarding` mechanism — the host element does not need to implement them manually.
|
|
145
|
+
|
|
146
|
+
## Requirements
|
|
147
|
+
|
|
148
|
+
The host custom element **must**:
|
|
149
|
+
|
|
150
|
+
1. Call `this.attachInternals()` in its constructor
|
|
151
|
+
2. Pass the internals via `getSharedContext` in `supportedFeatures`
|
|
152
|
+
3. Include the form lifecycle callbacks in `callbackForwarding`
|
|
153
|
+
|
|
154
|
+
Both `static formAssociated = true` and form lifecycle callback forwarding are handled automatically — no manual boilerplate needed in the host element.
|
|
155
|
+
|
|
156
|
+
## Dev
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm install
|
|
160
|
+
npm run serve
|
|
161
|
+
# Open http://localhost:8000/tests/test1.html
|
|
162
|
+
```
|
package/imports.html
ADDED
package/package.json
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "face-up",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "A Custom Element Feature that Adds Form Associated Behavior to a Custom Element",
|
|
5
|
-
"homepage": "https://github.com/bahrus/face-
|
|
6
|
-
"bugs": {
|
|
7
|
-
"url": "https://github.com/bahrus/face-
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/bahrus/face-
|
|
12
|
-
},
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"author": "Bruce B. Anderson <anderson.bruce.b@gmail.com>",
|
|
15
|
-
"type": "module",
|
|
16
|
-
"main": "
|
|
17
|
-
"scripts": {
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "face-up",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "A Custom Element Feature that Adds Form Associated Behavior to a Custom Element",
|
|
5
|
+
"homepage": "https://github.com/bahrus/face-up#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/bahrus/face-up/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/bahrus/face-up.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Bruce B. Anderson <anderson.bruce.b@gmail.com>",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "FaceUp.js",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
19
|
+
"test": "playwright test",
|
|
20
|
+
"update": "ncu -u && npm install",
|
|
21
|
+
"safari": "npx playwright wk http://localhost:8000",
|
|
22
|
+
"chrome": "npx playwright cr http://localhost:8000"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"assign-gingerly": "0.0.43",
|
|
26
|
+
"@playwright/test": "1.60.0",
|
|
27
|
+
"spa-ssi": "0.0.27"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {}
|
|
30
|
+
}
|
package/tests/test1.html
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Test - FaceUp Feature</title>
|
|
7
|
+
<!-- #include virtual="/imports.html" -->
|
|
8
|
+
<script type=module>
|
|
9
|
+
import 'assign-gingerly/assignFeatures.js';
|
|
10
|
+
import {FaceUp} from 'face-up/FaceUp.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A test custom element that uses FaceUp to become form-associated.
|
|
14
|
+
* Note: static formAssociated = true is set automatically by FaceUp.onAssigned
|
|
15
|
+
*/
|
|
16
|
+
class TestInput extends HTMLElement {
|
|
17
|
+
propagator = new EventTarget();
|
|
18
|
+
#internals;
|
|
19
|
+
|
|
20
|
+
static supportedFeatures = {
|
|
21
|
+
faceUp: {
|
|
22
|
+
fallbackSpawn: FaceUp,
|
|
23
|
+
callbackForwarding: [
|
|
24
|
+
'connectedCallback',
|
|
25
|
+
'disconnectedCallback',
|
|
26
|
+
'formDisabledCallback',
|
|
27
|
+
'formResetCallback',
|
|
28
|
+
'formStateRestoreCallback'
|
|
29
|
+
],
|
|
30
|
+
getSharedContext(instance) {
|
|
31
|
+
return {
|
|
32
|
+
internals: instance.#internals,
|
|
33
|
+
hostPropagator: instance.propagator
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
super();
|
|
41
|
+
this.#internals = this.attachInternals();
|
|
42
|
+
this.attachShadow({ mode: 'open' });
|
|
43
|
+
this.shadowRoot.innerHTML = `
|
|
44
|
+
<style>
|
|
45
|
+
:host { display: inline-block; }
|
|
46
|
+
input { font: inherit; }
|
|
47
|
+
</style>
|
|
48
|
+
<input type="text" part="input">
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
const input = this.shadowRoot.querySelector('input');
|
|
54
|
+
input.addEventListener('input', () => {
|
|
55
|
+
this.propagator.dispatchEvent(
|
|
56
|
+
new CustomEvent('value', { detail: input.value })
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Expose name for form submission
|
|
62
|
+
get name() { return this.getAttribute('name'); }
|
|
63
|
+
get type() { return this.localName; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Inject the FaceUp feature (onAssigned sets static formAssociated = true)
|
|
67
|
+
await customElements.assignFeatures(TestInput, {
|
|
68
|
+
faceUp: { spawn: FaceUp }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
customElements.define('test-input', TestInput);
|
|
72
|
+
</script>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<h1>FaceUp Feature Test</h1>
|
|
76
|
+
|
|
77
|
+
<form id="test-form">
|
|
78
|
+
<fieldset>
|
|
79
|
+
<legend>Form-Associated Custom Element</legend>
|
|
80
|
+
|
|
81
|
+
<label>
|
|
82
|
+
Name:
|
|
83
|
+
<test-input name="username"></test-input>
|
|
84
|
+
</label>
|
|
85
|
+
|
|
86
|
+
<br><br>
|
|
87
|
+
|
|
88
|
+
<label>
|
|
89
|
+
Required field:
|
|
90
|
+
<test-input name="required-field" required></test-input>
|
|
91
|
+
</label>
|
|
92
|
+
|
|
93
|
+
<br><br>
|
|
94
|
+
|
|
95
|
+
<button type="submit">Submit</button>
|
|
96
|
+
<button type="reset">Reset</button>
|
|
97
|
+
</fieldset>
|
|
98
|
+
</form>
|
|
99
|
+
|
|
100
|
+
<h2>Form Data Output</h2>
|
|
101
|
+
<pre id="output"></pre>
|
|
102
|
+
|
|
103
|
+
<script type=module>
|
|
104
|
+
const form = document.getElementById('test-form');
|
|
105
|
+
const output = document.getElementById('output');
|
|
106
|
+
|
|
107
|
+
form.addEventListener('submit', (e) => {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const formData = new FormData(form);
|
|
110
|
+
const entries = [...formData.entries()];
|
|
111
|
+
output.textContent = JSON.stringify(Object.fromEntries(entries), null, 2);
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|