-
Notifications
You must be signed in to change notification settings - Fork 94
Implementing customized builtin elements. #88
Changes from all commits
81009a8
b7b6f39
64e072c
acd7b88
673c269
c02b1c1
881ce96
3c53e49
fed4381
d9c59ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,7 +47,7 @@ export default class CustomElementRegistry { | |
* @private | ||
* @type {!Array<string>} | ||
*/ | ||
this._unflushedLocalNames = []; | ||
this._unflushedNames = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
/** | ||
* @private | ||
|
@@ -57,20 +57,39 @@ export default class CustomElementRegistry { | |
} | ||
|
||
/** | ||
* @param {string} localName | ||
* @param {string} name | ||
* @param {!Function} constructor | ||
*/ | ||
define(localName, constructor) { | ||
define(name, constructor, options) { | ||
if (!(constructor instanceof Function)) { | ||
throw new TypeError('Custom element constructors must be functions.'); | ||
} | ||
|
||
if (!Utilities.isValidCustomElementName(localName)) { | ||
throw new SyntaxError(`The element name '${localName}' is not valid.`); | ||
if (!Utilities.isValidCustomElementName(name)) { | ||
throw new SyntaxError(`The element name '${name}' is not valid.`); | ||
} | ||
|
||
if (this._internals.localNameToDefinition(localName)) { | ||
throw new Error(`A custom element with name '${localName}' has already been defined.`); | ||
if (this._internals.nameToDefinition(name)) { | ||
throw new Error(`A custom element with name '${name}' has already been defined.`); | ||
} | ||
|
||
let localName = name; | ||
|
||
if (options && options.extends) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See |
||
if (!this['enableCustomizedBuiltins']) { | ||
throw new Error(`Customized builtin elements are disabled by default. Set customElements.enableCustomizedBuiltins = true.`); | ||
} | ||
|
||
if (Utilities.isValidCustomElementName(options.extends)) { | ||
throw new Error(`A customized builtin element may not extend a custom element.`); | ||
} | ||
|
||
const el = document.createElement(options.extends); | ||
if (el instanceof window['HTMLUnknownElement']) { | ||
throw new Error(`Cannot extend '${options.extends}': is not a real HTML element`); | ||
} | ||
|
||
localName = options.extends; | ||
} | ||
|
||
if (this._elementDefinitionIsRunning) { | ||
|
@@ -110,6 +129,7 @@ export default class CustomElementRegistry { | |
} | ||
|
||
const definition = { | ||
name, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 updated |
||
localName, | ||
constructor, | ||
connectedCallback, | ||
|
@@ -120,9 +140,9 @@ export default class CustomElementRegistry { | |
constructionStack: [], | ||
}; | ||
|
||
this._internals.setDefinition(localName, definition); | ||
this._internals.setDefinition(name, definition); | ||
|
||
this._unflushedLocalNames.push(localName); | ||
this._unflushedNames.push(name); | ||
|
||
// If we've already called the flush callback and it hasn't called back yet, | ||
// don't call it again. | ||
|
@@ -141,21 +161,21 @@ export default class CustomElementRegistry { | |
this._flushPending = false; | ||
this._internals.patchAndUpgradeTree(document); | ||
|
||
while (this._unflushedLocalNames.length > 0) { | ||
const localName = this._unflushedLocalNames.shift(); | ||
const deferred = this._whenDefinedDeferred.get(localName); | ||
while (this._unflushedNames.length > 0) { | ||
const name = this._unflushedNames.shift(); | ||
const deferred = this._whenDefinedDeferred.get(name); | ||
if (deferred) { | ||
deferred.resolve(undefined); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} localName | ||
* @param {string} name | ||
* @return {Function|undefined} | ||
*/ | ||
get(localName) { | ||
const definition = this._internals.localNameToDefinition(localName); | ||
get(name) { | ||
const definition = this._internals.nameToDefinition(name); | ||
if (definition) { | ||
return definition.constructor; | ||
} | ||
|
@@ -164,27 +184,27 @@ export default class CustomElementRegistry { | |
} | ||
|
||
/** | ||
* @param {string} localName | ||
* @param {string} name | ||
* @return {!Promise<undefined>} | ||
*/ | ||
whenDefined(localName) { | ||
if (!Utilities.isValidCustomElementName(localName)) { | ||
return Promise.reject(new SyntaxError(`'${localName}' is not a valid custom element name.`)); | ||
whenDefined(name) { | ||
if (!Utilities.isValidCustomElementName(name)) { | ||
return Promise.reject(new SyntaxError(`'${name}' is not a valid custom element name.`)); | ||
} | ||
|
||
const prior = this._whenDefinedDeferred.get(localName); | ||
const prior = this._whenDefinedDeferred.get(name); | ||
if (prior) { | ||
return prior.toPromise(); | ||
} | ||
|
||
const deferred = new Deferred(); | ||
this._whenDefinedDeferred.set(localName, deferred); | ||
this._whenDefinedDeferred.set(name, deferred); | ||
|
||
const definition = this._internals.localNameToDefinition(localName); | ||
const definition = this._internals.nameToDefinition(name); | ||
// Resolve immediately only if the given local name has a definition *and* | ||
// the full document walk to upgrade elements with that local name has | ||
// already happened. | ||
if (definition && this._unflushedLocalNames.indexOf(localName) === -1) { | ||
if (definition && this._unflushedNames.indexOf(name) === -1) { | ||
deferred.resolve(undefined); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,17 +14,22 @@ export default function(internals) { | |
* @param {string} localName | ||
* @return {!Element} | ||
*/ | ||
function(localName) { | ||
function(localName, options) { | ||
// Only create custom elements if this document is associated with the registry. | ||
if (this.__CE_hasRegistry) { | ||
const definition = internals.localNameToDefinition(localName); | ||
const name = options && options.is ? options.is : localName; | ||
const definition = internals.nameToDefinition(name); | ||
if (definition) { | ||
return new (definition.constructor)(); | ||
} | ||
} | ||
|
||
const result = /** @type {!Element} */ | ||
(Native.Document_createElement.call(this, localName)); | ||
if (options && options.is) { | ||
result.__CE_is = options.is; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec's definition of |
||
result.setAttribute('is', options.is) | ||
} | ||
internals.patch(result); | ||
return result; | ||
}); | ||
|
@@ -56,17 +61,22 @@ export default function(internals) { | |
* @param {string} localName | ||
* @return {!Element} | ||
*/ | ||
function(namespace, localName) { | ||
function(namespace, localName, options) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method also needs to add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 updated. Also I added a test for this. |
||
// Only create custom elements if this document is associated with the registry. | ||
if (this.__CE_hasRegistry && (namespace === null || namespace === NS_HTML)) { | ||
const definition = internals.localNameToDefinition(localName); | ||
const name = options && options.is ? options.is : localName; | ||
const definition = internals.nameToDefinition(name); | ||
if (definition) { | ||
return new (definition.constructor)(); | ||
} | ||
} | ||
|
||
const result = /** @type {!Element} */ | ||
(Native.Document_createElementNS.call(this, namespace, localName)); | ||
if (options && options.is) { | ||
result.__CE_is = options.is; | ||
result.setAttribute('is', options.is) | ||
} | ||
internals.patch(result); | ||
return result; | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import Native from './Native.js'; | ||
import CustomElementInternals from '../CustomElementInternals.js'; | ||
import CEState from '../CustomElementState.js'; | ||
import AlreadyConstructedMarker from '../AlreadyConstructedMarker.js'; | ||
|
||
/** | ||
* @param {!CustomElementInternals} internals | ||
*/ | ||
export default function(internals) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Browsers currently throw errors if you try to subclass native DOM elements. So we patch each of the DOM element constructors so that they don't throw errors. |
||
for (let subclass in Native.HTMLElement_subclasses) { | ||
patchElement(`HTML${subclass}Element`, Native.HTMLElement_subclasses[subclass]); | ||
} | ||
|
||
function patchElement(constructorName, NativeElement) { | ||
if (!NativeElement) { | ||
return; | ||
} | ||
|
||
window[constructorName] = (function() { | ||
/** | ||
* @type {function(new: HTMLElement): !HTMLElement} | ||
*/ | ||
function PatchedElement() { | ||
// This should really be `new.target` but `new.target` can't be emulated | ||
// in ES5. Assuming the user keeps the default value of the constructor's | ||
// prototype's `constructor` property, this is equivalent. | ||
const constructor = /** @type {!Function} */ (this.constructor); | ||
|
||
const definition = internals.constructorToDefinition(constructor); | ||
if (!definition) { | ||
throw new Error('The custom element being constructed was not registered with `customElements`.'); | ||
} | ||
|
||
const constructionStack = definition.constructionStack; | ||
|
||
if (constructionStack.length === 0) { | ||
let element = Native.Document_createElement.call(document, definition.localName); | ||
element.setAttribute('is', definition.name); | ||
Object.setPrototypeOf(element, constructor.prototype); | ||
element.__CE_state = CEState.custom; | ||
element.__CE_definition = definition; | ||
internals.patch(element); | ||
return /** @type {!HTMLElement} */ (element); | ||
} | ||
|
||
const lastIndex = constructionStack.length - 1; | ||
const element = constructionStack[lastIndex]; | ||
if (element === AlreadyConstructedMarker) { | ||
throw new Error(`The ${constructorName} constructor was either called reentrantly for this constructor or called multiple times.`); | ||
} | ||
constructionStack[lastIndex] = AlreadyConstructedMarker; | ||
|
||
Object.setPrototypeOf(element, constructor.prototype); | ||
internals.patch(/** @type {!HTMLElement} */ (element)); | ||
|
||
return /** @type {!HTMLElement} */ (element); | ||
} | ||
|
||
PatchedElement.prototype = NativeElement.prototype; | ||
|
||
return PatchedElement; | ||
})(); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For autonomous custom elements, the custom element name is the same as
localName
. But for customized builtin elements, thelocalName
is different than the name of the custom element. I have changed this Map so that it looks thing up byname
instead oflocalName
, so that it works with both autonomous and customized builtin elements.