diff --git a/README.md b/README.md
index 296585f..a98f643 100644
--- a/README.md
+++ b/README.md
@@ -26,20 +26,20 @@ You can take the same ideas (and standards), apply them directly server side, to
```javascript
var components = require("server-components");
-// Get the prototype for a new element
-var NewElement = components.newElement();
-
-// When the element is created during DOM parsing, you can transform the HTML inside it.
-// This can be configurable too, either by setting attributes or adding HTML content
-// inside it or elsewhere in the page it can interact with. Elements can fire events
-// that other elements can receive to allow interactions, or even expose methods
-// or data that other elements in the page can access directly.
-NewElement.createdCallback = function () {
- this.innerHTML = "Hi there";
-};
+// Define a new class that extends a native HTML Element
+class NewElement extends components.HTMLElement {
+ // When the element is created during DOM parsing, you can transform the HTML inside it.
+ // This can be configurable too, either by setting attributes or adding HTML content
+ // inside it or elsewhere in the page it can interact with. Elements can fire events
+ // that other elements can receive to allow interactions, or even expose methods
+ // or data that other elements in the page can access directly.
+ connectedCallback() {
+ this.innerHTML = "Hi there";
+ }
+}
// Register the element with an element name
-components.registerElement("my-new-element", { prototype: NewElement });
+components.define("my-new-element", NewElement);
```
For examples of more complex component definitions, take a look at the [example components](https://github.com/pimterry/server-components/blob/master/component-examples.md)
@@ -83,7 +83,7 @@ There aren't many published sharable components to drop in quite yet, as it's st
### Top-level API
-#### `components.newElement()`
+#### `components.HTMLElement`
Creates a returns a new custom HTML element prototype, extending the HTMLElement prototype.
@@ -91,7 +91,7 @@ Note that this does *not* register the element. To do that, call `components.reg
This is broadly equivalent to `Object.create(HTMLElement.prototype)` in browser land, and exactly equivalent here to `Object.create(components.dom.HTMLElement.prototype)`. You can call that yourself instead if you like, but it's a bit of a mouthful.
-#### `components.registerElement(componentName, options)`
+#### `components.define(componentName, Constructor)`
Registers an element, so that it will be used when the given element name is found during parsing.
@@ -131,9 +131,9 @@ These methods are methods you can implement on your component prototype (as retu
Any methods that are implemented, from this selection or otherwise, will be exposed on your element in the DOM during rendering. I.e. you can call `document.querySelector("my-element").setTitle("New Title")` and to call the `setTitle` method on your object, which can then potentially change how your component is rendered.
-#### `yourComponent.createdCallback(document)`
+#### `yourComponentConstructor.prototype.connectedCallback(document)`
-Called when an element is created.
+Called when an element is attached to the DOM.
**This is where you put your magic!** Rewrite the elements contents to dynamically generate what your users will actually see client side. Read configuration from attributes or the initial child nodes to create flexible reconfigurable reusable elements. Register for events to create elements that interact with the rest of the application structure. Build your page.
@@ -141,21 +141,17 @@ This method is called with `this` bound to the element that's being rendered (ju
If this callback returns a promise, the rendering process will not resolve until that promise does, and will fail if that promise fails. You can use this to perform asynchronous actions without your component definitions. Pull tweets from twitter and draw them into the page, or anything else you can imagine.
-These callbacks are called in opening tag order, so a parent's createdCallback is called, then each of its children's, then its next sibling element.
+These callbacks are called in opening tag order, so a parent's connectedCallback is called, then each of its children's, then its next sibling element.
-#### `yourComponent.attachedCallback(document)`
-
-Called when the element is attached to the DOM. This is different to when it's created when your component is being built programmatically, not through HTML parsing. *Not yet implemented*
-
-#### `yourComponent.detachedCallback(document)`
+#### `yourComponentConstructor.prototype.disconnectedCallback(document)`
Called when the element is removed from the DOM. *Not yet implemented*
-#### `yourComponent.attributeChangedCallback(document)`
+#### `yourComponentConstructor.prototype.attributeChangedCallback(document)`
-Called when an attribute of the element is added, changed, or removed. *Not yet implemented*.
+Called when an attribute of the element is added, changed, or removed. *Partially implemented;* runs on component initialization.
-**So far only the createdCallback is implemented here, as the others are less relevant initially for the key simpler cases. Each of those will be coming in time though! Watch this space.**
+**So far only the connectedCallback is implemented here, as the others are less relevant initially for the key simpler cases. Each of those will be coming in time though! Watch this space.**
## Why does this exist?
diff --git a/component-examples.md b/component-examples.md
index e873e0a..7aa3932 100644
--- a/component-examples.md
+++ b/component-examples.md
@@ -14,14 +14,14 @@ With the web component below, rendering `` will resul
`Hi there`.
```javascript
-var components = require("server-components");
-
-var StaticElement = components.newElement();
-StaticElement.createdCallback = function () {
- this.innerHTML = "Hi there";
-};
-
-components.registerElement("my-greeting", { prototype: StaticElement });
+var customElements = require("server-components");
+
+class StaticElement extends customElements.HTMLElement {
+ connectedCallback() {
+ this.innerHTML = "Hi there"
+ }
+}
+customElements.define("my-greeting", StaticElement);
```
This is very basic, and toy cases like this aren't immediately useful, but this can be helpful for standard
@@ -40,17 +40,17 @@ example is below: a visitor counter. All the rage in the 90s, with web component
comeback!
```javascript
-var components = require("server-components");
+var customElements = require("server-components");
-var CounterElement = components.newElement();
var currentCount = 0;
-CounterElement.createdCallback = function () {
- currentCount += 1;
- this.innerHTML = "There have been " + currentCount + " visitors.";
-};
-
-components.registerElement("visitor-counter", { prototype: CounterElement });
+class CounterElement extends customElements.HTMLElement {
+ connectedCallback() {
+ currentCount += 1;
+ this.innerHTML = "There have been " + currentCount + " visitors.";
+ }
+}
+customElements.define("visitor-counter", CounterElement);
```
After a few visitors, this will render `` into something like
@@ -81,17 +81,16 @@ Components can be parameterized in all sorts of ways. One interesting pattern is
For example, you might want a component that wraps HTML, parses all the text within, and replaces URL strings with actual links (using the excellent [Linkify library](https://github.com/SoapBox/linkifyjs), but here in a server side DOM, not a real one):
```javascript
-var components = require("server-components");
+var customElements = require("server-components");
var linkify = require("linkifyjs/element");
-var LinkifyElement = components.newElement();
-
-LinkifyElement.createdCallback = function (document) {
- // Delegate the whole thing to a real normal front-end library!
- linkify(this, { target: () => null, linkClass: "autolinked" }, document);
- };
-
-components.registerElement("linkify-urls", { prototype: LinkifyElement });
+class LinkifyElement extends customElements.HTMLElement {
+ connectedCallback() {
+ // Delegate the whole thing to a real front-end library!
+ linkify(this, { target: () => null, linkClass: "autolinked" }, document);
+ }
+}
+customElements.define("linkify-urls", LinkifyElement);
```
With this, we can pass HTML into Server Components that looks like
diff --git a/package.json b/package.json
index 326553f..e26117f 100644
--- a/package.json
+++ b/package.json
@@ -39,12 +39,22 @@
"watch": "^0.18.0"
},
"dependencies": {
- "domino": "^1.0.23",
- "validate-element-name": "^1.0.0"
+ "domino": "^1.0.23"
},
"jshintConfig": {
"esversion": 6,
- "node": true
+ "node": true,
+ "globals": {
+ "describe": false,
+ "xdescribe": false,
+ "it": false,
+ "xit": false,
+ "before": false,
+ "beforeEach": false,
+ "after": false,
+ "afterEach": false,
+ "expect": false
+ }
},
"engines": {
"node": ">= 4.0.0"
diff --git a/src/extend-domino.js b/src/extend-domino.js
new file mode 100644
index 0000000..e06305b
--- /dev/null
+++ b/src/extend-domino.js
@@ -0,0 +1,44 @@
+//
+// Strict mode disallows us to overwrite Document.prototype properties.
+// This file is to stay out of strict mode.
+//
+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;
+};
diff --git a/src/index.js b/src/index.js
index 4ecfa28..8b0a459 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,6 @@
"use strict";
var domino = require("domino");
-var validateElementName = require("validate-element-name");
/**
* 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);
-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) {
+ return Array.prototype.slice.call(arrayLike).map(fn);
+}
diff --git a/src/registry.js b/src/registry.js
new file mode 100644
index 0000000..6e1c589
--- /dev/null
+++ b/src/registry.js
@@ -0,0 +1,296 @@
+"use strict";
+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,
+ * }}
+ */
+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.`);
+ }
+}
diff --git a/test/asynchrony-test.js b/test/asynchrony-test.js
index 4d6bf88..522420b 100644
--- a/test/asynchrony-test.js
+++ b/test/asynchrony-test.js
@@ -1,19 +1,25 @@
+"use strict";
var expect = require('chai').expect;
var components = require("../src/index.js");
describe("An asynchronous element", () => {
+ beforeEach(() => {
+ components.reset();
+ });
+
it("blocks rendering until they complete", () => {
- var SlowElement = components.newElement();
- SlowElement.createdCallback = function () {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- this.textContent = "loaded!";
- resolve();
- }, 1);
- });
- };
- components.registerElement("slow-element", { prototype: SlowElement });
+ class SlowElement extends components.HTMLElement {
+ connectedCallback() {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ this.textContent = "loaded!";
+ resolve();
+ }, 1);
+ });
+ }
+ }
+ components.define("slow-element", SlowElement);
return components.renderFragment("").then((output) => {
expect(output).to.equal("loaded!");
@@ -21,9 +27,12 @@ describe("An asynchronous element", () => {
});
it("throw an async error if a component fails to render synchronously", () => {
- var FailingElement = components.newElement();
- FailingElement.createdCallback = () => { throw new Error(); };
- components.registerElement("failing-element", { prototype: FailingElement });
+ class FailingElement extends components.HTMLElement {
+ connectedCallback() {
+ throw new Error();
+ }
+ }
+ components.define("failing-element", FailingElement);
return components.renderFragment(
""
@@ -33,9 +42,12 @@ describe("An asynchronous element", () => {
});
it("throw an async error if a component fails to render asynchronously", () => {
- var FailingElement = components.newElement();
- FailingElement.createdCallback = () => Promise.reject(new Error());
- components.registerElement("failing-element", { prototype: FailingElement });
+ class FailingElement extends components.HTMLElement {
+ connectedCallback() {
+ return Promise.reject(new Error());
+ }
+ }
+ components.define("failing-element", FailingElement);
return components.renderFragment(
""
diff --git a/test/basics-test.js b/test/basics-test.js
index 3282670..bfa6a66 100644
--- a/test/basics-test.js
+++ b/test/basics-test.js
@@ -1,3 +1,4 @@
+"use strict";
var expect = require('chai').expect;
var components = require("../src/index.js");
@@ -12,9 +13,12 @@ describe("Basic component functionality", () => {
});
it("replaces components with their rendered result", () => {
- var NewElement = components.newElement();
- NewElement.createdCallback = function () { this.textContent = "hi there"; };
- components.registerElement("my-element", { prototype: NewElement });
+ class NewElement extends components.HTMLElement {
+ connectedCallback() {
+ this.textContent = "hi there";
+ }
+ }
+ components.define("my-element", NewElement);
return components.renderFragment("").then((output) => {
expect(output).to.equal("hi there");
@@ -22,13 +26,12 @@ describe("Basic component functionality", () => {
});
it("can wrap existing content", () => {
- var PrefixedElement = components.newElement();
- PrefixedElement.createdCallback = function () {
- this.innerHTML = "prefix:" + this.innerHTML;
- };
- components.registerElement("prefixed-element", {
- prototype: PrefixedElement
- });
+ class PrefixedElement extends components.HTMLElement {
+ connectedCallback() {
+ this.innerHTML = "prefix:" + this.innerHTML;
+ }
+ }
+ components.define("prefixed-element", PrefixedElement);
return components.renderFragment(
"existing-content"
@@ -38,12 +41,13 @@ describe("Basic component functionality", () => {
});
it("allows attribute access", () => {
- var BadgeElement = components.newElement();
- BadgeElement.createdCallback = function () {
- var name = this.getAttribute("name");
- this.innerHTML = "My name is: " + name + "
";
- };
- components.registerElement("name-badge", { prototype: BadgeElement });
+ class BadgeElement extends components.HTMLElement {
+ connectedCallback() {
+ var name = this.getAttribute("name");
+ this.innerHTML = "My name is: " + name + "
";
+ }
+ }
+ components.define("name-badge", BadgeElement);
return components.renderFragment(
''
@@ -53,13 +57,14 @@ describe("Basic component functionality", () => {
});
it("can use normal document methods like QuerySelector", () => {
- var SelfFindingElement = components.newElement();
- SelfFindingElement.createdCallback = function (document) {
- var hopefullyThis = document.querySelector("self-finding-element");
- if (hopefullyThis === this) this.innerHTML = "Found!";
- else this.innerHTML = "Not found, found " + hopefullyThis;
- };
- components.registerElement("self-finding-element", { prototype: SelfFindingElement });
+ class SelfFindingElement extends components.HTMLElement {
+ connectedCallback(document) {
+ var hopefullyThis = document.querySelector("self-finding-element");
+ if (hopefullyThis === this) this.innerHTML = "Found!";
+ else this.innerHTML = "Not found, found " + hopefullyThis;
+ }
+ }
+ components.define("self-finding-element", SelfFindingElement);
return components.renderFragment(
''
diff --git a/test/element-validation-test.js b/test/element-validation-test.js
index d3523c3..5fb0406 100644
--- a/test/element-validation-test.js
+++ b/test/element-validation-test.js
@@ -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(" {
- 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./
);
});
});
diff --git a/test/example-components.js b/test/example-components.js
index 67f03af..c1f8efd 100644
--- a/test/example-components.js
+++ b/test/example-components.js
@@ -1,17 +1,22 @@
+"use strict";
var expect = require('chai').expect;
var components = require("../src/index.js");
var linkify = require("linkifyjs/element");
describe("An example component:", () => {
- describe("using static rendering", () => {
- before(() => {
- var StaticElement = components.newElement();
- StaticElement.createdCallback = function () {
- this.innerHTML = "Hi there";
- };
+ beforeEach(() => {
+ components.reset();
+ });
- components.registerElement("my-greeting", { prototype: StaticElement });
+ describe("using static rendering", () => {
+ beforeEach(() => {
+ class StaticElement extends components.HTMLElement {
+ connectedCallback() {
+ this.innerHTML = "Hi there";
+ }
+ }
+ components.define("my-greeting", StaticElement);
});
it("replaces its content with the given text", () => {
@@ -22,16 +27,16 @@ describe("An example component:", () => {
});
describe("using dynamic logic for rendering", () => {
- before(() => {
- var CounterElement = components.newElement();
+ beforeEach(() => {
var currentCount = 0;
- CounterElement.createdCallback = function () {
- currentCount += 1;
- this.innerHTML = "There have been " + currentCount + " visitors.";
- };
-
- components.registerElement("visitor-counter", { prototype: CounterElement });
+ class CounterElement extends components.HTMLElement {
+ connectedCallback() {
+ currentCount += 1;
+ this.innerHTML = "There have been " + currentCount + " visitors.";
+ }
+ }
+ components.define("visitor-counter", CounterElement);
});
it("dynamically changes its content", () => {
@@ -48,15 +53,14 @@ describe("An example component:", () => {
});
describe("parameterised by HTML content", () => {
- before(() => {
- var LinkifyElement = components.newElement();
-
- LinkifyElement.createdCallback = function (document) {
- // Delegate the whole thing to a real normal front-end library!
- linkify(this, { target: () => null, linkClass: "autolinked" }, document);
- };
-
- components.registerElement("linkify-urls", { prototype: LinkifyElement });
+ beforeEach(() => {
+ class LinkifyElement extends components.HTMLElement {
+ connectedCallback(document) {
+ // Delegate the whole thing to a real front-end library!
+ linkify(this, { target: () => null, linkClass: "autolinked" }, document);
+ }
+ }
+ components.define("linkify-urls", LinkifyElement);
});
it("should be able to parse and manipulate it's content", () => {
diff --git a/test/multiple-element-interactions-test.js b/test/multiple-element-interactions-test.js
index bcb8063..80976f0 100644
--- a/test/multiple-element-interactions-test.js
+++ b/test/multiple-element-interactions-test.js
@@ -1,17 +1,21 @@
+"use strict";
var expect = require('chai').expect;
var components = require("../src/index.js");
describe("When multiple DOM elements are present", () => {
+ beforeEach(() => {
+ components.reset();
+ });
+
describe("nested elements", () => {
it("are rendered correctly", () => {
- var PrefixedElement = components.newElement();
- PrefixedElement.createdCallback = function () {
- this.innerHTML = "prefix:" + this.innerHTML;
- };
- components.registerElement("prefixed-element", {
- prototype: PrefixedElement
- });
+ class PrefixedElement extends components.HTMLElement {
+ connectedCallback() {
+ this.innerHTML = "prefix:" + this.innerHTML;
+ }
+ }
+ components.define("prefixed-element", PrefixedElement);
return components.renderFragment(
"existing-content"
@@ -25,13 +29,14 @@ describe("When multiple DOM elements are present", () => {
describe("parent elements", () => {
it("can see child elements", () => {
- var ChildCountElement = components.newElement();
- ChildCountElement.createdCallback = function () {
- var newNode = this.doc.createElement("div");
- newNode.textContent = this.childNodes.length + " children";
- this.insertBefore(newNode, this.firstChild);
- };
- components.registerElement("child-count", { prototype: ChildCountElement });
+ class ChildCountElement extends components.HTMLElement {
+ connectedCallback() {
+ var newNode = this.doc.createElement("div");
+ newNode.textContent = this.childNodes.length + " children";
+ this.insertBefore(newNode, this.firstChild);
+ }
+ }
+ components.define("child-count", ChildCountElement);
return components.renderFragment(
"A child
Another child
"
@@ -43,52 +48,59 @@ describe("When multiple DOM elements are present", () => {
});
it("can read attributes from custom child element's prototypes", () => {
- var DataSource = components.newElement();
- DataSource.data = [1, 2, 3];
- components.registerElement("data-source", { prototype: DataSource });
+ class DataSource extends components.HTMLElement {
+ get data() {
+ return [10, 20, 30];
+ }
+ }
+ components.define("data-source", DataSource);
+
+ class DataDisplayer extends components.HTMLElement {
+ connectedCallback() {
+ return new Promise((resolve) => {
+ // Has to be async, as child node prototypes aren't set: http://stackoverflow.com/questions/36187227/
+ // This is a web components limitation generally. TODO: Find a nicer pattern for handle this.
+ setTimeout(() => {
+ var data = this.childNodes[0].data;
+ this.textContent = "Data: " + JSON.stringify(data);
+ resolve();
+ }, 0);
+ });
+ }
+ }
- var DataDisplayer = components.newElement();
- DataDisplayer.createdCallback = function () {
- return new Promise((resolve) => {
- // Has to be async, as child node prototypes aren't set: http://stackoverflow.com/questions/36187227/
- // This is a web components limitation generally. TODO: Find a nicer pattern for handle this.
- setTimeout(() => {
- var data = this.childNodes[0].data;
- this.textContent = "Data: " + JSON.stringify(data);
- resolve();
- }, 0);
- });
- };
- components.registerElement("data-displayer", { prototype: DataDisplayer });
+ components.define("data-displayer", DataDisplayer);
return components.renderFragment(
""
).then((output) => {
expect(output).to.equal(
- "Data: [1,2,3]"
+ "Data: [10,20,30]"
);
});
});
it("receive bubbling events from child elements", () => {
- var EventRecorder = components.newElement();
- EventRecorder.createdCallback = function (document) {
- var resultsNode = document.createElement("p");
- this.appendChild(resultsNode);
+ class EventRecorder extends components.HTMLElement {
+ connectedCallback(document) {
+ var resultsNode = document.createElement("p");
+ this.appendChild(resultsNode);
- this.addEventListener("my-event", (event) => {
- resultsNode.innerHTML = "Event received";
- });
- };
- components.registerElement("event-recorder", { prototype: EventRecorder });
+ this.addEventListener("my-event", (event) => {
+ resultsNode.innerHTML = "Event received";
+ });
+ }
+ }
+ components.define("event-recorder", EventRecorder);
- var EventElement = components.newElement();
- EventElement.createdCallback = function () {
- this.dispatchEvent(new components.dom.CustomEvent('my-event', {
- bubbles: true
- }));
- };
- components.registerElement("event-source", { prototype: EventElement });
+ class EventElement extends components.HTMLElement {
+ connectedCallback() {
+ this.dispatchEvent(new components.dom.CustomEvent('my-event', {
+ bubbles: true
+ }));
+ }
+ }
+ components.define("event-source", EventElement);
return components.renderFragment(
""
diff --git a/test/programmatic-usage-test.js b/test/programmatic-usage-test.js
index 076bfca..d006e33 100644
--- a/test/programmatic-usage-test.js
+++ b/test/programmatic-usage-test.js
@@ -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", () => {
- 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);
});
});