Skip to content

Commit

Permalink
Convert to custom elements spec v1. Closes pimterry#27
Browse files Browse the repository at this point in the history
  • Loading branch information
gilbert committed Nov 9, 2016
1 parent af9591e commit 8c3de3e
Show file tree
Hide file tree
Showing 9 changed files with 638 additions and 210 deletions.
173 changes: 105 additions & 68 deletions src/index.js
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");

/**
* The DOM object (components.dom) exposes tradition DOM objects (normally globally available
Expand All @@ -17,62 +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 = {};
const _upgradedProp = '__$CE_upgraded';

/**
* 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.
* Registers a transformer for a tag that is intended to run server-side.
*
* 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.
* At the moment, only one transformer is permitted per tag.
*/
exports.registerElement = function registerElement(name, options) {
var nameValidationResult = validateElementName(name);
if (!nameValidationResult.isValid) {
throw new Error(`Registration failed for '${name}'. ${nameValidationResult.message}`);
}

if (options && options.prototype) {
registeredElements[name] = options.prototype;
} else {
registeredElements[name] = exports.newElement();
}

return registeredElements[name].constructor;
};
var transformers = {};

/**
* Registers an element that is intended to run server-side only, and thus
* replaced with a resolved value or nothing.
*
* Server-side elements ARE NOT required to contain a hyphen.
*/
exports.registerServerElement = function registerServerElement(name, handler) {
if ( registeredElements[name] && typeof registeredElements[name] !== 'function' ) {
throw new Error(`Registration failed for '${name}'. Name is already taken by a non-server-side element.`);
exports.registerTransformer = function registerTransformer (name, handler) {
if ( transformers[name] && typeof transformers[name] !== 'function' ) {
throw new Error(`Registration failed for '${name}'. Name is already taken by another transformer.`);
}
registeredElements[name] = handler;
transformers[name] = handler;
return handler;
};


function transformTree(document, visitedNodes, currentNode, callback) {

function transformTree(document, currentNode, callback) {
var task = visitedNodes.has(currentNode) ? undefined : callback(currentNode);

var task = callback(currentNode);
visitedNodes.add(currentNode);

if ( task !== undefined ) {
let replaceNode = function replaceNode (results) {
if (results === null) {
currentNode.parentNode.removeChild(currentNode)
return Promise.resolve()
currentNode.parentNode.removeChild(currentNode);
return Promise.resolve();
}
if (typeof results === 'string') {
var temp = document.createElement('template');
Expand All @@ -84,24 +60,24 @@ function transformTree(document, currentNode, callback) {
var newNodes = results.length ? slice.call(results) : [results];

newNodes.map( (newNode) => {
newNode.parentNode === currentNode && currentNode.removeChild(newNode);
if (newNode.parentNode === currentNode) currentNode.removeChild(newNode);
fragment.appendChild(newNode);
});
currentNode.parentNode.replaceChild(fragment, currentNode);

return Promise.all(
newNodes.map((child) => transformTree(document, child, callback))
newNodes.map((child) => transformTree(document, visitedNodes, child, callback))
);
}
else {
return Promise.all(
map(currentNode.childNodes, (child) => transformTree(document, child, callback))
map(currentNode.childNodes, (child) => transformTree(document, visitedNodes, child, callback))
);
}
};

if ( task === null ) {
return replaceNode(null)
return replaceNode(null);
}
if ( task.then ) {
// Promise task; potential transformation
Expand All @@ -116,7 +92,7 @@ function transformTree(document, currentNode, callback) {
// This element has opted to do nothing to itself.
// Recurse on its children.
return Promise.all(
map(currentNode.childNodes, (child) => transformTree(document, child, callback))
map(currentNode.childNodes, (child) => transformTree(document, visitedNodes, child, callback))
);
}
}
Expand Down Expand Up @@ -155,35 +131,63 @@ function renderNode(rootNode) {
let createdPromises = [];

var document = getDocument(rootNode);
var visitedNodes = new Set();
var upgradedNodes = new Set();
var customElements = exports.customElements;

return transformTree(document, visitedNodes, rootNode, function render (element) {

var transformer = transformers[element.localName];

return transformTree(document, rootNode, (foundNode) => {
if (foundNode.tagName) {
let nodeType = foundNode.tagName.toLowerCase();
let customElement = registeredElements[nodeType];
if (transformer && ! element.serverTransformed) {
let result = transformer(element, document);
element.serverTransformed = true;

if (customElement && typeof customElement === 'function') {
var subResult = customElement(foundNode, document);
let handleTransformerResult = (result) => {
if ( result === undefined && customElements.get(element.localName) ) {
// Re-render the transformed element as a custom element,
// since a corresponding custom tag is defined.
return render(element);
}
if ( result === undefined ) {
// Replace the element with its children; its server-side duties are fulfilled.
return element.childNodes;
}
else {
// The transformer has opted to do something specific.
return result;
}
};

// Replace with children by default
return (subResult === undefined) ? null : subResult;
if ( result && result.then ) {
return result.then(handleTransformerResult);
}
else if (customElement) {
// TODO: Should probably clone node, not change prototype, for performance
Object.setPrototypeOf(foundNode, customElement);

if (customElement.createdCallback) {
try {
var result = customElement.createdCallback.call(foundNode, document);
if ( result && result.then ) {
// Client-side custom elements never replace themselves;
// resolve with undefined to prevent such a scenario.
return result.then( () => undefined );
}
}
catch (err) {
return Promise.reject(err);
else {
return handleTransformerResult(result);
}
}

const definition = customElements.getDefinition(element.localName);

if (definition) {
if ( upgradedNodes.has(element[_upgradedProp]) ) {
return;
}
upgradeElement(element, definition, true);
upgradedNodes.add(element);

if (definition.connectedCallback) {
try {
let result = definition.connectedCallback.call(element, document);
if ( result && result.then ) {
// Client-side custom elements never replace themselves;
// resolve with undefined to prevent such a scenario.
return result.then( () => undefined );
}
}
catch (err) {
return Promise.reject(err);
}
}
}
})
Expand Down Expand Up @@ -236,6 +240,35 @@ function getDocument(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;
console.assert(CustomElementRegistry.instance()._newInstance === null);
}

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) {
var results = [];
for (var i=0; i < arrayLike.length; i++) {
Expand All @@ -244,4 +277,8 @@ function map (arrayLike, fn) {
return results;
}

function isClass(v) {
return typeof v === 'function' && /^\s*class\s+/.test(v.toString());
}

var slice = Array.prototype.slice;
Loading

0 comments on commit 8c3de3e

Please sign in to comment.