Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hyperHTML.adopt(liveNode) #185

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions cjs/hyper/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
unique
} = require('../shared/utils.js');

const {selfClosing} = require('../shared/re.js');
const {selfClosing: SC_RE} = require('../shared/re.js');

// a weak collection of contexts that
// are already known to hyperHTML
Expand Down Expand Up @@ -39,14 +39,22 @@ function render(template) {
// to the current context, and render it after cleaning the context up
function upgrade(template) {
template = unique(template);
const adopt = render.adopt;
const info = templates.get(template) ||
createTemplate.call(this, template);
const fragment = importNode(this.ownerDocument, info.fragment);
const updates = Updates.create(fragment, info.paths);
let fragment, updates;
if (adopt) {
updates = Updates.create(this, info.paths, adopt);
} else {
fragment = importNode(this.ownerDocument, info.fragment);
updates = Updates.create(fragment, info.paths, adopt);
}
bewitched.set(this, {template, updates});
update.apply(updates, arguments);
this.textContent = '';
this.appendChild(fragment);
if (!adopt) {
this.textContent = '';
this.appendChild(fragment);
}
}

// an update simply loops over all mapped DOM operations
Expand All @@ -73,7 +81,6 @@ function createTemplate(template) {

// some node could be special though, like a custom element
// with a self closing tag, which should work through these changes.
const SC_RE = selfClosing;
const SC_PLACE = ($0, $1, $2) => {
return VOID_ELEMENTS.test($1) ? $0 : ('<' + $1 + $2 + '></' + $1 + '>');
};
Expand Down
11 changes: 10 additions & 1 deletion cjs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ const diff = (m => m.__esModule ? m.default : m)(require('./shared/domdiff.js'))
// you can do the following
// const {bind, wire} = hyperHTML;
// and use them right away: bind(node)`hello!`;
const bind = context => render.bind(context);
const adopt = context => function () {
render.adopt = true;
return render.apply(context, arguments);
};
const bind = context => function () {
render.adopt = false;
return render.apply(context, arguments);
};
const define = Intent.define;

hyper.Component = Component;
hyper.adopt = adopt;
hyper.bind = bind;
hyper.define = define;
hyper.diff = diff;
Expand All @@ -30,6 +38,7 @@ setup(content);
// everything is exported directly or through the
// hyperHTML callback, when used as top level script
exports.Component = Component;
exports.adopt = adopt;
exports.bind = bind;
exports.define = define;
exports.diff = diff;
Expand Down
2 changes: 1 addition & 1 deletion cjs/objects/Path.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ Object.defineProperty(exports, '__esModule', {value: true}).default = {
for (let i = 0; i < length; i++) {
node = node.childNodes[path[i]];
}
return node;
return {node, childNodes: []};
}
}
77 changes: 68 additions & 9 deletions cjs/objects/Updates.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,54 @@ value instanceof Component;
// Updates can be related to any kind of content,
// attributes, or special text-only cases such <style>
// elements or <textarea>
const create = (root, paths) => {
const create = (root, paths, adopt) => {
const level = adopt ? [] : null;
const updates = [];
const length = paths.length;
for (let i = 0; i < length; i++) {
const info = paths[i];
const node = Path.find(root, info.path);
const {node, childNodes} = adopt ?
findNode(root, info.path, level) :
Path.find(root, info.path);
switch (info.type) {
case 'any':
updates.push(setAnyContent(node, []));
updates.push(setAnyContent(node, childNodes));
break;
case 'attr':
updates.push(setAttribute(node, info.name, info.node));
updates.push(
setAttribute(
node,
info.name,
adopt ?
(
node.getAttributeNode(info.name) ||
createAttribute(node, info.node.cloneNode(true))
) :
info.node,
adopt
)
);
break;
case 'text':
updates.push(setTextContent(node));
updates.push(
setTextContent(
adopt ?
childNodes[0] :
node
)
);
break;
}
}
return updates;
};

// set an attribute node and return it
const createAttribute = (node, attr) => {
node.setAttributeNode(attr);
return attr;
};

// finding all paths is a one-off operation performed
// when a new template literal is used.
// The goal is to map all target nodes that will be
Expand Down Expand Up @@ -180,6 +207,32 @@ const findAttributes = (node, paths, parts) => {
}
};

// used to adopt live nodes from virtual paths
const findNode = (node, path, level) => {
const childNodes = [];
const length = path.length;
for (let i = 0; i < length; i++) {
let index = path[i] + (level[i] || 0);
node = node.childNodes[index];
if (
node.nodeType === COMMENT_NODE &&
/^\u0001:[0-9a-zA-Z]+$/.test(node.textContent)
) {
const textContent = node.textContent;
while ((node = node.nextSibling)) {
index++;
if (node.nodeType === COMMENT_NODE && node.textContent === textContent) {
break;
} else {
childNodes.push(node);
}
}
}
level[i] = index - path[i];
}
return {node, childNodes};
};

// when a Promise is used as interpolation value
// its result must be parsed once resolved.
// This callback is in charge of understanding what to do
Expand Down Expand Up @@ -336,12 +389,13 @@ const setAnyContent = (node, childNodes) => {
// * style, the only regular attribute that also accepts an object as value
// so that you can style=${{width: 120}}. In this case, the behavior has been
// fully inspired by Preact library and its simplicity.
const setAttribute = (node, name, original) => {
const setAttribute = (node, name, original, adopt) => {
const isSVG = OWNER_SVG_ELEMENT in node;
let oldValue;
// if the attribute is the style one
// handle it differently from others
if (name === 'style') {
if (adopt) node.removeAttribute(name);
return Style(node, original, isSVG);
}
// the name is an event one,
Expand All @@ -358,6 +412,7 @@ const setAttribute = (node, name, original) => {
else if (name.toLowerCase() in node) {
type = type.toLowerCase();
}
if (adopt) node.removeAttribute(name);
return newValue => {
if (oldValue !== newValue) {
if (oldValue) node.removeEventListener(type, oldValue, false);
Expand All @@ -371,7 +426,11 @@ const setAttribute = (node, name, original) => {
// in this case assign the value directly
else if (name === 'data' || (!isSVG && name in node)) {
return newValue => {
if (oldValue !== newValue) {
if (adopt) {
adopt = false;
oldValue = node[name];
}
else if (oldValue !== newValue) {
oldValue = newValue;
if (node[name] !== newValue) {
node[name] = newValue;
Expand All @@ -385,8 +444,8 @@ const setAttribute = (node, name, original) => {
// in every other case, use the attribute node as it is
// update only the value, set it as node only when/if needed
else {
let owner = false;
const attribute = original.cloneNode(true);
let owner = adopt;
const attribute = adopt ? original : original.cloneNode(true);
return newValue => {
if (oldValue !== newValue) {
oldValue = newValue;
Expand Down
2 changes: 1 addition & 1 deletion coverage/coverage.json

Large diffs are not rendered by default.

Loading