-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Adds static
define
implementation.
This follows the initial draft of the [static `define` community protocol](webcomponents-cg/community-protocols#67). Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used. The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve. The current implementation does presumably support scoped custom element registries, but this is not tested as the spec has not been implemented by any browser just yet. The polyfill can potentially be used to verify behavior.
- Loading branch information
Showing
7 changed files
with
206 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
"dictionaries": [], | ||
"words": [ | ||
"Clazz", | ||
"Defineable", | ||
"hydroactive", | ||
"prerendered", | ||
"templating" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>`define-protocol` tests</title> | ||
<meta charset="utf8"> | ||
</head> | ||
<body></body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { createDefine, defineIfSupported } from './define-protocol.js'; | ||
|
||
describe('define-protocol', () => { | ||
describe('defineIfSupported', () => { | ||
it('calls static `define` on a supporting class', () => { | ||
class MyElement extends HTMLElement { | ||
static define = jasmine.createSpy<() => void>('define'); | ||
} | ||
|
||
defineIfSupported(MyElement); | ||
|
||
expect(MyElement.define).toHaveBeenCalledOnceWith(); | ||
}); | ||
|
||
it('ignores classes which do not implement the protocol', () => { | ||
class MyElement extends HTMLElement { | ||
// No `define` property. | ||
// static define(): void { /* ... */ } | ||
} | ||
|
||
expect(() => defineIfSupported(MyElement)).not.toThrow(); | ||
}); | ||
}); | ||
|
||
describe('createDefine', () => { | ||
it('defines in the global registry', () => { | ||
class MyElement extends HTMLElement { | ||
static define = createDefine('define-protocol--global-reg', this); | ||
} | ||
|
||
expect(customElements.get('define-protocol--global-reg')).toBeUndefined(); | ||
|
||
MyElement.define(); | ||
|
||
expect(customElements.get('define-protocol--global-reg')).toBe(MyElement); | ||
}); | ||
|
||
it('no-ops when called multiple times', () => { | ||
class MyElement extends HTMLElement { | ||
static define = createDefine('define-protocol--multi', this); | ||
} | ||
|
||
MyElement.define(); | ||
expect(() => MyElement.define()).not.toThrow(); | ||
}); | ||
|
||
it('no-ops when `customElements.define` was already called', () => { | ||
class MyElement extends HTMLElement { | ||
static define = createDefine('define-protocol--already-defined', this); | ||
} | ||
|
||
customElements.define('define-protocol--already-defined', MyElement); | ||
|
||
expect(() => MyElement.define()).not.toThrow(); | ||
}); | ||
|
||
it('throws when the element was already defined with a different class', () => { | ||
class MyElement extends HTMLElement { | ||
static define = createDefine('define-protocol--conflict', this); | ||
} | ||
|
||
customElements.define( | ||
'define-protocol--conflict', class extends HTMLElement {}); | ||
|
||
expect(() => MyElement.define()).toThrowError(/already defined/); | ||
}); | ||
|
||
it('passes through element definition options', () => { | ||
class MyElement extends HTMLParagraphElement { | ||
static define = createDefine('define-protocol--options', this, { | ||
extends: 'p', | ||
}); | ||
} | ||
|
||
MyElement.define(); | ||
|
||
const p = document.createElement('p', { | ||
is: 'define-protocol--options', | ||
}); | ||
expect(p).toBeInstanceOf(MyElement); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* @fileoverview Provides primitives to easily implement the static `define` | ||
* community protocol. | ||
* | ||
* @see https://github.com/webcomponents-cg/community-protocols/pull/67 | ||
*/ | ||
|
||
/** | ||
* Defines the custom element. | ||
* | ||
* @param registry The registry to define the custom element in. Defaults to the | ||
* global {@link customElements} registry. | ||
* @param tagName The tag name to define the custom element as. Uses a default | ||
* tag name when not specified. Using an explicit tag name is only supported | ||
* when using a non-global registry | ||
*/ | ||
export type Define = | ||
(registry?: CustomElementRegistry, tagName?: string) => void; | ||
|
||
/** | ||
* A class definition which implements the static `define` community protocol. | ||
* | ||
* Note that because `define` is static, this type should be applied to the | ||
* custom element class type, not the instance type. | ||
* | ||
* ```typescript | ||
* class MyElement extends HTMLElement { | ||
* static define() { ... } | ||
* } | ||
* | ||
* const definable = MyElement as Defineable; | ||
* ``` | ||
*/ | ||
export interface Defineable { | ||
/** | ||
* Defines the custom element. | ||
* | ||
* @param registry The registry to define the custom element in. Defaults to | ||
* the global {@link customElements} registry. | ||
* @param tagName The tag name to define the custom element as. Uses a default | ||
* tag name when not specified. Using an explicit tag name is only | ||
* supported when a using non-global registry | ||
*/ | ||
define: Define; | ||
} | ||
|
||
/** | ||
* Defines the provided custom element in the global registry if that element | ||
* implements the static `define` community protocol. | ||
* | ||
* @param Clazz The custom element class to define. | ||
*/ | ||
export function defineIfSupported(Clazz: typeof Element): void { | ||
(Clazz as Partial<Defineable>).define?.(); | ||
} | ||
|
||
/** | ||
* Creates a {@link Define} function which defines the given custom element with | ||
* the default tag name. The returned function should be used as the static | ||
* `define` function in a {@link Defineable} custom element. | ||
* | ||
* @param defaultTagName The tag name to use in the global registry and by | ||
* default for scoped registries. | ||
* @param Clazz The custom element class to define. | ||
* @param options Options for the {@link CustomElementRegistry.prototype.define} | ||
* call. | ||
*/ | ||
export function createDefine( | ||
defaultTagName: string, | ||
Clazz: typeof HTMLElement, | ||
options?: ElementDefinitionOptions, | ||
): Define { | ||
return (registry = customElements, tagName = defaultTagName) => { | ||
// Tag name can only be modified when not in the global registry. | ||
if (registry === customElements && tagName !== defaultTagName) { | ||
throw new Error('Cannot use a non-default tag name in the global custom element registry.'); | ||
} | ||
|
||
// Check if the tag name was already defined by another class. | ||
const existing = registry.get(tagName); | ||
if (existing) { | ||
if (existing === Clazz) { | ||
return; // Already defined as the correct class, no-op. | ||
} else { | ||
throw new Error(`Tag name \`${tagName}\` already defined as \`${ | ||
existing.name}\`.`); | ||
} | ||
} | ||
|
||
// Define the class. | ||
registry.define(tagName, Clazz, options); | ||
}; | ||
} |