Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow use of ElementInternals with Web Components #8473

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions site/content/docs/03-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
* `accessors={false}` — the default
* `namespace="..."` — the namespace where this component will be used, most commonly "svg"; use the "foreign" namespace to opt out of case-insensitive attribute names and HTML-specific warnings
* `tag="..."` — the name to use when compiling this component as a custom element
* `elementInternals` - adds formAssociated static property for custom elements to participate in forms

```sv
<svelte:options tag="my-custom-element"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ The options that can be set here are:
* `accessors={false}` — the default
* `namespace="..."` — the namespace where this component will be used, most commonly `"svg"`
* `tag="..."` — the name to use when compiling this component as a custom element
* `elementInternals` - adds formAssociated static property for custom elements to participate in forms

Consult the [API reference](/docs) for more information on these options.
12 changes: 12 additions & 0 deletions src/compiler/compile/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface ComponentOptions {
immutable?: boolean;
accessors?: boolean;
preserveWhitespace?: boolean;
elementInternals?: boolean;
}

const regex_leading_directory_separator = /^[/\\]/;
Expand Down Expand Up @@ -1570,6 +1571,17 @@ function process_component_options(component: Component, nodes) {
break;
}

case 'elementInternals': {
const value = get_value(attribute, compiler_errors.invalid_attribute_value(name));

if (typeof value !== 'boolean') {
return component.error(attribute, compiler_errors.invalid_attribute_value(name));
}

component_options.elementInternals = value;
break;
}

case 'namespace': {
const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute);

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/compile/compiler_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export default {
},
invalid_options_attribute: {
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable', 'preserveWhitespace' and 'elementInternals' attributes"
},
css_invalid_global: {
code: 'css-invalid-global',
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const valid_options = [
'loopGuardTimeout',
'preserveComments',
'preserveWhitespace',
'cssHash'
'cssHash',
'elementInternals'
];

const valid_css_values = [
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/compile/render_dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,16 @@ export default function dom(
}
`[0] as ClassDeclaration;

if (component.component_options.elementInternals) {
declaration.body.body.push({
type: 'PropertyDefinition',
static: true,
computed: false,
key: { type: 'Identifier', name: 'formAssociated' },
value: x`true`
});
}

if (props.length > 0) {
declaration.body.body.push({
type: 'MethodDefinition',
Expand Down
1 change: 1 addition & 0 deletions src/compiler/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export interface CompileOptions {
legacy?: boolean;
customElement?: boolean;
tag?: string;
elementInternals?: boolean;
css?: 'injected' | 'external' | 'none' | boolean;
loopGuardTimeout?: number;
namespace?: string;
Expand Down
88 changes: 54 additions & 34 deletions src/runtime/internal/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ export function get_current_component() {

/**
* Schedules a callback to run immediately before the component is updated after any state change.
*
*
* The first time the callback runs will be before the initial `onMount`
*
*
* https://svelte.dev/docs#run-time-svelte-beforeupdate
*/
export function beforeUpdate(fn: () => any) {
get_current_component().$$.before_update.push(fn);
}

/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
* It must be called during the component's initialisation (but doesn't need to live *inside* the component;
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
* It must be called during the component's initialisation (but doesn't need to live *inside* the component;
* it can be called from an external module).
*
*
* `onMount` does not run inside a [server-side component](/docs#run-time-server-side-component-api).
*
*
* https://svelte.dev/docs#run-time-svelte-onmount
*/
export function onMount(fn: () => any) {
Expand All @@ -37,19 +37,19 @@ export function onMount(fn: () => any) {

/**
* Schedules a callback to run immediately after the component has been updated.
*
* The first time the callback runs will be after the initial `onMount`
*
* The first time the callback runs will be after the initial `onMount`
*/
export function afterUpdate(fn: () => any) {
get_current_component().$$.after_update.push(fn);
}

/**
/**
* Schedules a callback to run immediately before the component is unmounted.
*
* Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the
*
* Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the
* only one that runs inside a server-side component.
*
*
* https://svelte.dev/docs#run-time-svelte-ondestroy
*/
export function onDestroy(fn: () => any) {
Expand All @@ -61,15 +61,15 @@ export interface DispatchOptions {
}

/**
* Creates an event dispatcher that can be used to dispatch [component events](/docs#template-syntax-component-directives-on-eventname).
* Creates an event dispatcher that can be used to dispatch [component events](/docs#template-syntax-component-directives-on-eventname).
* Event dispatchers are functions that can take two arguments: `name` and `detail`.
*
* Component events created with `createEventDispatcher` create a
* [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent).
*
* Component events created with `createEventDispatcher` create a
* [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent).
* These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture).
* The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail)
* property and can contain any type of data.
*
* The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail)
* property and can contain any type of data.
*
* https://svelte.dev/docs#run-time-svelte-createeventdispatcher
*/
export function createEventDispatcher<EventMap extends {} = any>(): <
Expand Down Expand Up @@ -99,12 +99,12 @@ export function createEventDispatcher<EventMap extends {} = any>(): <
}

/**
* Associates an arbitrary `context` object with the current component and the specified `key`
* and returns that object. The context is then available to children of the component
* Associates an arbitrary `context` object with the current component and the specified `key`
* and returns that object. The context is then available to children of the component
* (including slotted content) with `getContext`.
*
* Like lifecycle functions, this must be called during component initialisation.
*
*
* Like lifecycle functions, this must be called during component initialisation.
*
* https://svelte.dev/docs#run-time-svelte-setcontext
*/
export function setContext<T>(key, context: T): T {
Expand All @@ -113,34 +113,34 @@ export function setContext<T>(key, context: T): T {
}

/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* https://svelte.dev/docs#run-time-svelte-getcontext
*/
export function getContext<T>(key): T {
return get_current_component().$$.context.get(key);
}

/**
* Retrieves the whole context map that belongs to the closest parent component.
* Must be called during component initialisation. Useful, for example, if you
* Retrieves the whole context map that belongs to the closest parent component.
* Must be called during component initialisation. Useful, for example, if you
* programmatically create a component and want to pass the existing context to it.
*
*
* https://svelte.dev/docs#run-time-svelte-getallcontexts
*/
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T {
return get_current_component().$$.context;
}

/**
* Checks whether a given `key` has been set in the context of a parent component.
* Must be called during component initialisation.
*
* Checks whether a given `key` has been set in the context of a parent component.
* Must be called during component initialisation.
*
* https://svelte.dev/docs#run-time-svelte-hascontext
*/
export function hasContext(key): boolean {
return get_current_component().$$.context.has(key);
return get_current_component().$$.context.has(key);
}

// TODO figure out if we still want to support
Expand All @@ -154,3 +154,23 @@ export function bubble(component, event) {
callbacks.slice().forEach(fn => fn.call(this, event));
}
}

// Element internals not in TS3.x.x
declare global {
// eslint-disable-next-line
interface ElementInternals {}
interface HTMLElement {
attachInternals(): ElementInternals;
}
}

/**
* Creates element internals for Web Components to participate in forms.
*/
export function createCustomElementInternals(): ElementInternals {
const component = get_current_component();
if (!(component instanceof HTMLElement)) {
throw ReferenceError('NotHTMLElement');
}
return component.attachInternals();
}