-
Notifications
You must be signed in to change notification settings - Fork 12
Convert to custom elements spec v1 #30
base: master
Are you sure you want to change the base?
Changes from all commits
af9591e
2671b99
63ad32a
88aaf5d
329eff3
568ee36
106d66a
0b5a395
722040b
723c656
959bff7
71829e6
3f7c02e
374b24f
1a9c7d3
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 |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// | ||
// Strict mode disallows us to overwrite Document.prototype properties. | ||
// This file is to stay out of strict mode. | ||
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. These strict mode errors are happening because these properties have been defined with writable set to false somewhere. Strict mode doesn't aim to change working runtime behaviour - it just exposes issues that are otherwise hidden. Those errors are appearing here because these writes don't actually do anything - they're silently failing. You're not successfully changing createElement here. I'm not totally clear on the goal of this code, but I've had a quick test, and if you remove 'createElement' and 'createElementNS' below here then you can enable strict mode on this file, and all the tests still pass. That suggests either there's a bunch more code involved here (like _createElement) that we could delete too, or that we're missing tests that cover whatever this is doing. 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. I see. My understanding is that the code is supposed to support |
||
// | ||
var domino = require("domino"); | ||
var Document = require('domino/lib/Document'); | ||
var Element = require('domino/lib/Element'); | ||
|
||
|
||
module.exports = function (newHTMLElement, _createElement) { | ||
var result = {}; | ||
|
||
// | ||
// Patch document.createElement | ||
// | ||
Document.prototype.createElement = function(tagName, options) { | ||
return _createElement(this, tagName, options, true); | ||
}; | ||
|
||
// | ||
// Patch HTMLElement | ||
// | ||
result.HTMLElement = newHTMLElement; | ||
result.HTMLElement.prototype = Object.create(domino.impl.HTMLElement.prototype, { | ||
constructor: {value: result.HTMLElement, configurable: true, writable: true}, | ||
}); | ||
|
||
|
||
// | ||
// Patch doc.createElementNS | ||
// | ||
var HTMLNS = 'http://www.w3.org/1999/xhtml'; | ||
var _origCreateElementNS = Document.prototype.createElementNS; | ||
|
||
Document.prototype.createElementNS = function(namespaceURI, qualifiedName) { | ||
if (namespaceURI === 'http://www.w3.org/1999/xhtml') { | ||
return this.createElement(qualifiedName); | ||
} else { | ||
return _origCreateElementNS.call(this, namespaceURI, qualifiedName); | ||
} | ||
}; | ||
|
||
return result; | ||
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. If we're keeping this, it needs some comments. What are these patches doing to Domino's built-in behaviour? Why doesn't Domino's DOM + the polyfill do what we want already? 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. I was just following what the original polyfill did. I believe this is supposed to support programatically creating custom elements. 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. Interesting, ok. We should add tests for that then. If this code is necessary, that's probably broken, because this code doesn't work. The right answer to this might well be that these properties are writable in a browser, but not in Domino. That's probably not supposed to be that case, so we should talk to Domino, make this writable there, and then everything'll be fine. Can you check that that's the problem? If so, I'm happy to look at sorting this in Domino. 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. I agree with you, that is the right approach to take. The browser does in fact allow you to override |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
"use strict"; | ||
|
||
var domino = require("domino"); | ||
var validateElementName = require("validate-element-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. Just spotted this - we should remove the dependency if we're not using this any more. |
||
/** | ||
* The DOM object (components.dom) exposes tradition DOM objects (normally globally available | ||
|
@@ -17,41 +16,39 @@ exports.dom = domino.impl; | |
* with an element name, and options (typically including the prototype returned here as your | ||
* 'prototype' value). | ||
*/ | ||
exports.newElement = function newElement() { | ||
return Object.create(domino.impl.HTMLElement.prototype); | ||
}; | ||
var CustomElementRegistry = require('./registry'); | ||
exports.customElements = CustomElementRegistry.instance(); | ||
exports.HTMLElement = CustomElementRegistry.HTMLElement; | ||
|
||
var registeredElements = {}; | ||
|
||
/** | ||
* Registers an element, so that it will be used when the given element name is found during parsing. | ||
* | ||
* Element names are required to contain a hyphen (to disambiguate them from existing element names), | ||
* be entirely lower-case, and not start with a hyphen. | ||
* | ||
* The only option currently supported is 'prototype', which sets the prototype of the given element. | ||
* This prototype will have its various callbacks called when it is found during document parsing, | ||
* and properties of the prototype will be exposed within the DOM to other elements there in turn. | ||
* Re-export methods for convenience | ||
*/ | ||
exports.registerElement = function registerElement(name, options) { | ||
var nameValidationResult = validateElementName(name); | ||
if (!nameValidationResult.isValid) { | ||
throw new Error(`Registration failed for '${name}'. ${nameValidationResult.message}`); | ||
} | ||
exports.define = function (name, constructor, options) { | ||
return CustomElementRegistry.instance().define(name, constructor, options); | ||
}; | ||
exports.get = function (name) { | ||
return CustomElementRegistry.instance().get(name); | ||
}; | ||
exports.whenDefined = function (name) { | ||
return CustomElementRegistry.instance().whenDefined(name); | ||
}; | ||
exports.reset = function (name) { | ||
return CustomElementRegistry.instance().reset(); | ||
}; | ||
|
||
if (options && options.prototype) { | ||
registeredElements[name] = options.prototype; | ||
} else { | ||
registeredElements[name] = exports.newElement(); | ||
} | ||
|
||
return registeredElements[name].constructor; | ||
}; | ||
const _upgradedProp = '__$CE_upgraded'; | ||
|
||
|
||
function transformTree(document, visitedNodes, currentNode, callback) { | ||
|
||
var task = visitedNodes.has(currentNode) ? undefined : callback(currentNode); | ||
|
||
visitedNodes.add(currentNode); | ||
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. Why 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. It was in the original polyfill. I believe it's possible if a custom element decides to move itself around within the DOM. 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. Ah, ok. Yes, that makes perfect sense. |
||
|
||
function recurseTree(rootNode, callback) { | ||
for (let node of rootNode.childNodes) { | ||
callback(node); | ||
recurseTree(node, callback); | ||
for (var child of currentNode.childNodes) { | ||
transformTree(document, visitedNodes, child, callback); | ||
} | ||
} | ||
|
||
|
@@ -89,24 +86,28 @@ function renderNode(rootNode) { | |
let createdPromises = []; | ||
|
||
var document = getDocument(rootNode); | ||
var visitedNodes = new Set(); | ||
var customElements = exports.customElements; | ||
|
||
recurseTree(rootNode, (foundNode) => { | ||
if (foundNode.tagName) { | ||
let nodeType = foundNode.tagName.toLowerCase(); | ||
let customElement = registeredElements[nodeType]; | ||
if (customElement) { | ||
// TODO: Should probably clone node, not change prototype, for performance | ||
Object.setPrototypeOf(foundNode, customElement); | ||
if (customElement.createdCallback) { | ||
createdPromises.push(new Promise((resolve) => { | ||
resolve(customElement.createdCallback.call(foundNode, document)); | ||
})); | ||
} | ||
transformTree(document, visitedNodes, rootNode, function render (element) { | ||
|
||
const definition = customElements.getDefinition(element.localName); | ||
|
||
if (definition) { | ||
if ( element[_upgradedProp] ) { | ||
return; | ||
} | ||
upgradeElement(element, definition, true); | ||
|
||
if (definition.connectedCallback) { | ||
var p = new Promise(function(resolve, reject) { | ||
resolve( definition.connectedCallback.call(element, document) ); | ||
}); | ||
createdPromises.push(p); | ||
} | ||
} | ||
}); | ||
|
||
return Promise.all(createdPromises).then(() => rootNode); | ||
return Promise.all(createdPromises).then(function(){ return rootNode; }); | ||
} | ||
|
||
/** | ||
|
@@ -154,3 +155,35 @@ function getDocument(rootNode) { | |
return rootNode; | ||
} | ||
} | ||
|
||
function upgradeElement (element, definition, callConstructor) { | ||
const prototype = definition.constructor.prototype; | ||
Object.setPrototypeOf(element, prototype); | ||
if (callConstructor) { | ||
CustomElementRegistry.instance()._setNewInstance(element); | ||
new (definition.constructor)(); | ||
element[_upgradedProp] = true; | ||
} | ||
|
||
const observedAttributes = definition.observedAttributes; | ||
const attributeChangedCallback = definition.attributeChangedCallback; | ||
if (attributeChangedCallback && observedAttributes.length > 0) { | ||
|
||
// Trigger attributeChangedCallback for existing attributes. | ||
// https://html.spec.whatwg.org/multipage/scripting.html#upgrades | ||
for (let i = 0; i < observedAttributes.length; i++) { | ||
const name = observedAttributes[i]; | ||
if (element.hasAttribute(name)) { | ||
const value = element.getAttribute(name); | ||
attributeChangedCallback.call(element, name, null, value, null); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// | ||
// Helpers | ||
// | ||
function map (arrayLike, fn) { | ||
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. It's neater to just convert the array-like into a real array, and then use real map, rather than reimplementing map and any other functions we need all from scratch. 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. It's neater, but also creates an two extra arrays (an empty one and a copy for the actual mapping). 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. Can do Array.prototype.slice instead. 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. Alright, updated. |
||
return Array.prototype.slice.call(arrayLike).map(fn); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
"use strict"; | ||
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. How much of this is the original real polyfill, and how much is extensions on top of it? If at all possible (and I do get that it might not be) I'd like to keep them separate. It's going to be way easier to maintain this if there's a polyfill file that we can trust (and upgrade) independently, and then a separate bunch of code making the any tweaks required to hook it into Domino, and/or wrapping the polyfill to change the interface. 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.
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. It'd be useful to keep all those changes separate from the core polyfill though, rather than mixing them all in together. If we can. If you've got examples where we can't possibly make the changes separately then that's ok too, but we should document those, so we can work out what's going on here in future. 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. But this sort of polyfill modifies the DOM environment. Normally it would modify window and window.document, but since there is no such globals, it modifies domino instead. In other words, modifying domino is a primary concern of the polyfill. 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. It's fine that it modifies the DOM, my concern is that we're modifying the polyfill from within it, rather than externally, which makes this codebase harder to manage. What I'm aiming to separate are the modifications to the DOM that any custom elements polyfill would do for any DOM, from both the modifications to Domino we make to get the polyfill working and the modifications to the polyfill we make to provide this library's API. Some practical reasons:
It's easier to do all that if we can keep the two as separate as possible. 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 changes to the original polyfill were things like this: const origHTMLElement = win.HTMLElement;
// TO
const origHTMLElement = domino.impl.HTMLElement; which were contained in the same file. Are you suggesting we move this to a separate file? Sorry if I'm still misunderstanding. 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. Focusing on the use cases is the key part of this I think. Given the change you have there, if I wanted to pull in a new version of the polyfill, I'd have to recreate that change, and right now I'd have to do that totally by hand from scratch. That'll be painful, and I'd like to avoid this. At the very least, some documentation of exactly what we've changed (even perhaps just as git history) would be a start, but separating our changes from the polyfill source entirely is probably doable, I hope. For example, there's other things we can do to wrap a library and inject our own This might be a bit fiddly. Have a go, but if you really don't want to dig into this, feel free to commit an unchanged version of the polyfill followed by your changed version, and I'll extract the diff out as much as possible. That also conveniently lets me review the changes we're making to the polyfill, which is an important part of this too. |
||
var domino = require("domino"); | ||
var Document = require('domino/lib/Document'); | ||
var Element = require('domino/lib/Element'); | ||
|
||
const _upgradedProp = '__$CE_upgraded'; | ||
|
||
const _customElements = () => CustomElementRegistry.instance(); | ||
|
||
/** | ||
* A registry of custom element definitions. | ||
* | ||
* See https://html.spec.whatwg.org/multipage/scripting.html#customelementsregistry | ||
* | ||
* Implementation based on https://github.com/webcomponents/custom-elements/blob/master/src/custom-elements.js | ||
* | ||
*/ | ||
var _instance = null; | ||
class CustomElementRegistry { | ||
|
||
static instance () { | ||
if ( ! _instance ) _instance = new CustomElementRegistry(); | ||
return _instance; | ||
} | ||
|
||
constructor() { | ||
this._definitions = new Map(); | ||
this._constructors = new Map(); | ||
this._whenDefinedMap = new Map(); | ||
|
||
this._newInstance = null; | ||
} | ||
|
||
// HTML spec part 4.13.4 | ||
// https://html.spec.whatwg.org/multipage/scripting.html#dom-customelementsregistry-define | ||
/** | ||
* @param {string} name | ||
* @param {function(new:HTMLElement)} constructor | ||
* @param {{extends: string}} options | ||
* @return {undefined} | ||
*/ | ||
define(name, constructor, options) { | ||
// 1: | ||
if (typeof constructor !== 'function') { | ||
throw new TypeError('constructor must be a Constructor'); | ||
} | ||
|
||
// 2. If constructor is an interface object whose corresponding interface | ||
// either is HTMLElement or has HTMLElement in its set of inherited | ||
// interfaces, throw a TypeError and abort these steps. | ||
// | ||
// It doesn't appear possible to check this condition from script | ||
|
||
// 3: | ||
const nameError = checkValidCustomElementName(name); | ||
if (nameError) throw nameError; | ||
|
||
// 4, 5: | ||
// Note: we don't track being-defined names and constructors because | ||
// define() isn't normally reentrant. The only time user code can run | ||
// during define() is when getting callbacks off the prototype, which | ||
// would be highly-unusual. We can make define() reentrant-safe if needed. | ||
if (this._definitions.has(name)) { | ||
throw new Error(`An element with name '${name}' is already defined`); | ||
} | ||
|
||
// 6, 7: | ||
if (this._constructors.has(constructor)) { | ||
throw new Error(`Definition failed for '${name}': ` + | ||
`The constructor is already used.`); | ||
} | ||
|
||
// 8: | ||
/** @type {string} */ | ||
const localName = name; | ||
|
||
// 9, 10: We do not support extends currently. | ||
|
||
// 11, 12, 13: Our define() isn't rentrant-safe | ||
|
||
// 14.1: | ||
const prototype = constructor.prototype; | ||
|
||
// 14.2: | ||
if (typeof prototype !== 'object') { | ||
throw new TypeError(`Definition failed for '${name}': ` + | ||
`constructor.prototype must be an object`); | ||
} | ||
|
||
function getCallback(callbackName) { | ||
const callback = prototype[callbackName]; | ||
if (callback !== undefined && typeof callback !== 'function') { | ||
throw new Error(`${localName} '${callbackName}' is not a Function`); | ||
} | ||
return callback; | ||
} | ||
|
||
// 3, 4: | ||
const connectedCallback = getCallback('connectedCallback'); | ||
|
||
// 5, 6: | ||
const disconnectedCallback = getCallback('disconnectedCallback'); | ||
|
||
// Divergence from spec: we always throw if attributeChangedCallback is | ||
// not a function. | ||
|
||
// 7, 9.1: | ||
const attributeChangedCallback = getCallback('attributeChangedCallback'); | ||
|
||
// 8, 9.2, 9.3: | ||
const observedAttributes = | ||
(attributeChangedCallback && constructor.observedAttributes) || []; | ||
|
||
// 15: | ||
/** @type {CustomElementDefinition} */ | ||
const definition = { | ||
name: name, | ||
localName: localName, | ||
constructor: constructor, | ||
connectedCallback: connectedCallback, | ||
disconnectedCallback: disconnectedCallback, | ||
attributeChangedCallback: attributeChangedCallback, | ||
observedAttributes: observedAttributes, | ||
}; | ||
|
||
// 16: | ||
this._definitions.set(localName, definition); | ||
this._constructors.set(constructor, localName); | ||
|
||
// 17, 18, 19: | ||
// Since we are rendering server-side, no need to upgrade doc; | ||
// custom elements will be defined before rendering takes place. | ||
// this._upgradeDoc(); | ||
|
||
// 20: | ||
const deferred = this._whenDefinedMap.get(localName); | ||
if (deferred) { | ||
deferred.resolve(undefined); | ||
this._whenDefinedMap.delete(localName); | ||
} | ||
} | ||
|
||
/** | ||
* Returns the constructor defined for `name`, or `null`. | ||
* | ||
* @param {string} name | ||
* @return {Function|undefined} | ||
*/ | ||
get(name) { | ||
// https://html.spec.whatwg.org/multipage/scripting.html#custom-elements-api | ||
const def = this._definitions.get(name); | ||
return def ? def.constructor : undefined; | ||
} | ||
|
||
/** | ||
* Returns a `Promise` that resolves when a custom element for `name` has | ||
* been defined. | ||
* | ||
* @param {string} name | ||
* @return {!Promise} | ||
*/ | ||
whenDefined(name) { | ||
// https://html.spec.whatwg.org/multipage/scripting.html#dom-customelementsregistry-whendefined | ||
const nameError = checkValidCustomElementName(name); | ||
if (nameError) return Promise.reject(nameError); | ||
if (this._definitions.has(name)) return Promise.resolve(); | ||
|
||
let deferred = this._whenDefinedMap.get(name); | ||
if (deferred) return deferred.promise; | ||
|
||
let resolve; | ||
const promise = new Promise(function(_resolve, _) { | ||
resolve = _resolve; | ||
}); | ||
deferred = {promise, resolve}; | ||
this._whenDefinedMap.set(name, deferred); | ||
return promise; | ||
} | ||
|
||
/** | ||
* @param {?HTMLElement} instance | ||
* @private | ||
*/ | ||
_setNewInstance(instance) { | ||
this._newInstance = instance; | ||
} | ||
|
||
/** | ||
* WARNING: NOT PART OF THE SPEC | ||
* | ||
* @param {string} localName | ||
* @return {?CustomElementDefinition} | ||
*/ | ||
getDefinition(localName) { | ||
return this._definitions.get(localName); | ||
} | ||
|
||
/** | ||
* WARNING: NOT PART OF THE SPEC | ||
* | ||
* @param {string} localName | ||
* @return {undefined} | ||
*/ | ||
reset() { | ||
this._definitions.clear(); | ||
this._constructors.clear(); | ||
this._whenDefinedMap.clear(); | ||
} | ||
} | ||
exports = module.exports = CustomElementRegistry; | ||
|
||
|
||
// | ||
// - Overwrite domino's new element constructor | ||
// - Patch domino's document.createElement | ||
// | ||
const origHTMLElement = domino.impl.HTMLElement; | ||
const _origCreateElement = Document.prototype.createElement; | ||
|
||
const newHTMLElement = function HTMLElement() { | ||
const customElements = _customElements(); | ||
|
||
// If there's an being upgraded, return that | ||
if (customElements._newInstance) { | ||
const i = customElements._newInstance; | ||
customElements._newInstance = null; | ||
return i; | ||
} | ||
if (this.constructor) { | ||
// Find the tagname of the constructor and create a new element with it | ||
const tagName = customElements._constructors.get(this.constructor); | ||
// Domino does not need a doc as a `this` parameter | ||
return _createElement(null, tagName, undefined, false); | ||
} | ||
throw new Error('Unknown constructor. Did you call customElements.define()?'); | ||
}; | ||
|
||
/** | ||
* Creates a new element and upgrades it if it's a custom element. | ||
* @param {!Document} doc | ||
* @param {!string} tagName | ||
* @param {Object|undefined} options | ||
* @param {boolean} callConstructor whether or not to call the elements | ||
* constructor after upgrading. If an element is created by calling its | ||
* constructor, then `callConstructor` should be false to prevent double | ||
* initialization. | ||
*/ | ||
function _createElement(doc, tagName, options, callConstructor) { | ||
const customElements = _customElements(); | ||
const element = options ? _origCreateElement.call(doc, tagName, options) : | ||
_origCreateElement.call(doc, tagName); | ||
const definition = customElements._definitions.get(tagName.toLowerCase()); | ||
if (definition) { | ||
customElements._upgradeElement(element, definition, callConstructor); | ||
} | ||
return element; | ||
} | ||
|
||
|
||
var patched = require('./extend-domino')(newHTMLElement, _createElement); | ||
exports.HTMLElement = patched.HTMLElement; | ||
|
||
|
||
/** | ||
* 2.3 | ||
* http://w3c.github.io/webcomponents/spec/custom/#dfn-element-definition | ||
* @typedef {{ | ||
* name: string, | ||
* localName: string, | ||
* constructor: function(new:HTMLElement), | ||
* connectedCallback: (Function|undefined), | ||
* disconnectedCallback: (Function|undefined), | ||
* attributeChangedCallback: (Function|undefined), | ||
* observedAttributes: Array<string>, | ||
* }} | ||
*/ | ||
let CustomElementDefinition; | ||
|
||
|
||
const reservedTagList = [ | ||
'annotation-xml', | ||
'color-profile', | ||
'font-face', | ||
'font-face-src', | ||
'font-face-uri', | ||
'font-face-format', | ||
'font-face-name', | ||
'missing-glyph', | ||
]; | ||
|
||
function checkValidCustomElementName(name) { | ||
if (!(/^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$/.test(name) && | ||
reservedTagList.indexOf(name) === -1)) { | ||
return new Error(`The element name '${name}' is not valid.`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,42 @@ | ||
"use strict"; | ||
var expect = require('chai').expect; | ||
|
||
var components = require("../src/index.js"); | ||
|
||
describe("Custom element validation", () => { | ||
it("allows elements without options", () => { | ||
components.registerElement("my-element"); | ||
|
||
return components.renderFragment("<my-element></my-element"); | ||
}); | ||
|
||
it("requires a non-empty name", () => { | ||
var InvalidElement = components.newElement(); | ||
class InvalidElement {} | ||
expect(() => { | ||
components.registerElement("", { prototype: InvalidElement }); | ||
components.define("", InvalidElement); | ||
}).to.throw( | ||
/Registration failed for ''. Missing element name./ | ||
/The element name '' is not valid./ | ||
); | ||
}); | ||
|
||
it("requires a hyphen in the element name", () => { | ||
var InvalidElement = components.newElement(); | ||
class InvalidElement {} | ||
expect(() => { | ||
components.registerElement("invalidname", { prototype: InvalidElement }); | ||
components.define("invalidname", InvalidElement); | ||
}).to.throw( | ||
/Registration failed for 'invalidname'. Custom element names must contain a hyphen./ | ||
/The element name 'invalidname' is not valid./ | ||
); | ||
}); | ||
|
||
it("doesn't allow elements to start with a hyphen", () => { | ||
var InvalidElement = components.newElement(); | ||
class InvalidElement {} | ||
expect(() => { | ||
components.registerElement("-invalid-name", { prototype: InvalidElement }); | ||
components.define("-invalid-name", InvalidElement); | ||
}).to.throw( | ||
/Registration failed for '-invalid-name'. Custom element names must not start with a hyphen./ | ||
/The element name '-invalid-name' is not valid./ | ||
); | ||
}); | ||
|
||
it("requires element names to be lower case", () => { | ||
var InvalidElement = components.newElement(); | ||
class InvalidElement {} | ||
expect(() => { | ||
components.registerElement("INVALID-NAME", { prototype: InvalidElement }); | ||
components.define("INVALID-NAME", InvalidElement); | ||
}).to.throw( | ||
/Registration failed for 'INVALID-NAME'. Custom element names must not contain uppercase ASCII characters./ | ||
/The element name 'INVALID-NAME' is not valid./ | ||
); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,16 @@ | ||
"use strict"; | ||
var expect = require('chai').expect; | ||
|
||
var components = require("../src/index.js"); | ||
|
||
describe("Programmatic usage", () => { | ||
|
||
// Pending until we decide what we want from this | ||
it("returns the element constructor from the registration call", () => { | ||
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. Test changes look great, but the name needs updating too. |
||
var NewElement = components.newElement(); | ||
var registrationResult = components.registerElement("my-element", { prototype: NewElement }); | ||
expect(NewElement.constructor).to.equal(registrationResult); | ||
class NewElement extends components.HTMLElement {} | ||
components.define("test-element", NewElement); | ||
|
||
var klass = components.get("test-element"); | ||
expect(klass).to.equal(NewElement); | ||
}); | ||
}); |
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.
Interesting. This makes perfect sense, and I'm not sure why been it's passing without it all this time! Any idea? Right now, it seems to pass fine on my machine and in CI without this.
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.
Adding "use strict" to the top of the test files caused the linter to start complaining.
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.
Ah, that'll do it. 👍