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

automatically calling tagHandler for tags in the document #77

Merged
merged 4 commits into from
Jan 12, 2018
Merged
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
5 changes: 4 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
"start": true,
"stop": true,
"global": true,
"Promise": true
"Promise": true,
"customElements": true,
"Reflect": true,
"WeakSet": true
},
"strict": false,
"curly": true,
Expand Down
111 changes: 105 additions & 6 deletions can-view-callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,68 @@ var makeFrag = require("can-util/dom/frag/frag");
var requestedAttributes = {};
//!steal-remove-end

var tags = {};

// WeakSet containing elements that have been rendered already
// and therefore do not need to be rendered again
var renderedElements = new WeakSet();

var renderNodeAndChildren = function(node) {
var tagName = node.tagName && node.tagName.toLowerCase();
var tagHandler = tags[tagName];
var children;

// skip elements that already have a viewmodel or elements whose tags don't match a registered tag
// or elements that have already been rendered
if (tagHandler && !renderedElements.has(node)) {
tagHandler(node, tagName, {});
}

if (node.getElementsByTagName) {
children = node.getElementsByTagName("*");
for (var k=0, child; (child = children[k]) !== undefined; k++) {
renderNodeAndChildren(child);
}
}
};

var mutationObserverEnabled = false;
var enableMutationObserver = function() {
if (mutationObserverEnabled) {
return;
}

var mutationHandler = function(mutationsList) {
var addedNodes;

for (var i=0, mutation; (mutation = mutationsList[i]) !== undefined; i++) {
if (mutation.type === "childList") {
addedNodes = mutation.addedNodes;

for (var j=0, addedNode; (addedNode = addedNodes[j]) !== undefined; j++) {
// skip elements that have already been rendered
if (!renderedElements.has(addedNode)) {
renderNodeAndChildren(addedNode);
}
}
}
}
};

var obs = new MutationObserver(mutationHandler);
obs.observe(getGlobal().document.documentElement, { childList: true, subtree: true });

mutationObserverEnabled = true;
};

var renderTagsInDocument = function(tagName) {
var nodes = getGlobal().document.getElementsByTagName(tagName);

for (var i=0, node; (node = nodes[i]) !== undefined; i++) {
renderNodeAndChildren(node);
}
};

var attr = function (attributeName, attrHandler) {
if(attrHandler) {
if (typeof attributeName === "string") {
Expand Down Expand Up @@ -62,21 +124,56 @@ var tag = function (tagName, tagHandler) {
if(tagHandler) {
var GLOBAL = getGlobal();

var validCustomElementName = automaticCustomElementCharacters.test(tagName);

//!steal-remove-start
if (typeof tags[tagName.toLowerCase()] !== 'undefined') {
dev.warn("Custom tag: " + tagName.toLowerCase() + " is already defined");
return;
}
if (!automaticCustomElementCharacters.test(tagName) && tagName !== "content") {

if (!validCustomElementName && tagName !== "content") {
dev.warn("Custom tag: " + tagName.toLowerCase() + " hyphen missed");
return;
}
//!steal-remove-end

// if we have html5shiv ... re-generate
if (GLOBAL.html5) {
GLOBAL.html5.elements += " " + tagName;
GLOBAL.html5.shivDocument();
}

tags[tagName.toLowerCase()] = tagHandler;

// automatically render elements that have tagHandlers
// If browser supports customElements, register the tag as a custom element
if ("customElements" in GLOBAL) {
if (validCustomElementName) {
var CustomElement = function() {
return Reflect.construct(HTMLElement, [], CustomElement);
};

CustomElement.prototype.connectedCallback = function() {
// don't re-render an element that has been rendered already
if (!renderedElements.has(this)) {
tagHandler(this, tagName, {});
}
};

Object.setPrototypeOf(CustomElement.prototype, HTMLElement.prototype);
Object.setPrototypeOf(CustomElement, HTMLElement);

customElements.define(tagName, CustomElement);
}
}
// If browser doesn't support customElements, set up MutationObserver for
// rendering elements when they are inserted in the page
// and rendering elements that are already in the page
else {
enableMutationObserver();
renderTagsInDocument(tagName);
}
} else {
var cb;

Expand All @@ -95,7 +192,6 @@ var tag = function (tagName, tagHandler) {
}

};
var tags = {};

var callbacks = {
_tags: tags,
Expand All @@ -106,15 +202,18 @@ var callbacks = {
attr: attr,
// handles calling back a tag callback
tagHandler: function(el, tagName, tagData){
var helperTagCallback = tagData.scope.templateContext.tags.get(tagName),
tagCallback = helperTagCallback || tags[tagName];

// If this was an element like <foo-bar> that doesn't have a component, just render its content
var scope = tagData.scope,
helperTagCallback = scope && scope.templateContext.tags.get(tagName),
tagCallback = helperTagCallback || tags[tagName],
res;

// If this was an element like <foo-bar> that doesn't have a component, just render its content
if(tagCallback) {
res = ObservationRecorder.ignore(tagCallback)(el, tagData);

// add the element to the Set of elements that have had their handlers called
// this will prevent the handler from being called again when the element is inserted
renderedElements.add(el);
} else {
res = scope;
}
Expand Down
129 changes: 129 additions & 0 deletions test/callbacks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ var can = require('can-namespace');
var clone = require('steal-clone');
var testHelpers = require("can-test-helpers/lib/dev");
var Scope = require("can-view-scope");
var domMutate = require('can-dom-mutate');
var domMutateNode = require('can-dom-mutate/node');
var globals = require('can-globals');

function afterMutation(cb) {
var doc = globals.getKeyValue('document');
var div = doc.createElement("div");
domMutate.onNodeInsertion(div, function(){
doc.body.removeChild(div);
setTimeout(cb, 5);
});
domMutateNode.appendChild.call(doc.body, div);
}

QUnit.module('can-view-callbacks');

Expand Down Expand Up @@ -176,3 +189,119 @@ QUnit.test("Passes through nodeList", function(){
}
});
});

QUnit.test("tag handler is called automatically when elements are inserted into the page", function() {
var fixture = document.getElementById('qunit-fixture');

callbacks.tag("the-el", function(el) {
el.innerHTML = "This is the el";
});

// <the-el />
// <div>
// <the-el />
// <the-el />
// </div>
var elOne = document.createElement("the-el");
var div = document.createElement("div");
var elTwo = document.createElement("the-el");
var elThree = document.createElement("the-el");

div.appendChild(elTwo);
div.appendChild(elThree);

fixture.appendChild(elOne);
fixture.appendChild(div);

QUnit.stop();
afterMutation(function() {
QUnit.start();
var els = fixture.getElementsByTagName("the-el");

for (var i=0; i<els.length; i++) {
QUnit.equal(els[i].innerHTML, "This is the el", "<the-el>[" + i + "] rendered correctly");
}
});
});

QUnit.test("tag handler is not called again when elements are inserted into the page if it has been called already", function() {
var fixture = document.getElementById('qunit-fixture');

callbacks.tag("another-el", function(el) {
var textNode = document.createTextNode("This is another el");
el.appendChild(textNode);
});

// <another-el />
// <div>
// <another-el />
// <another-el />
// </div>
var elOne = document.createElement("another-el");
var div = document.createElement("div");
var elTwo = document.createElement("another-el");
var elThree = document.createElement("another-el");

callbacks.tagHandler(elOne, "another-el", {});
callbacks.tagHandler(elTwo, "another-el", {});
callbacks.tagHandler(elThree, "another-el", {});

div.appendChild(elTwo);
div.appendChild(elThree);

fixture.appendChild(elOne);
fixture.appendChild(div);

QUnit.stop();
afterMutation(function() {
QUnit.start();
var els = fixture.getElementsByTagName("another-el");

for (var i=0; i<els.length; i++) {
QUnit.equal(els[i].innerHTML, "This is another el", "<another-el>[" + i + "] not rendered");
}
});
});

QUnit.test("when tagHandler is registered, it is called automatically for elements already in the page", function() {
var fixture = document.getElementById('qunit-fixture');

// <existing-el />
// <div>
// <existing-el />
// <existing-el />
// </div>
var elOne = document.createElement("existing-el");
var div = document.createElement("div");
var elTwo = document.createElement("existing-el");
var elThree = document.createElement("existing-el");

div.appendChild(elTwo);
div.appendChild(elThree);

fixture.appendChild(elOne);
fixture.appendChild(div);

callbacks.tag("existing-el", function(el) {
el.innerHTML = "This is an existing el";
});

QUnit.stop();
afterMutation(function() {
QUnit.start();
var els = fixture.getElementsByTagName("existing-el");

for (var i=0; i<els.length; i++) {
QUnit.equal(els[i].innerHTML, "This is an existing el", "<existing-el>[" + i + "] rendered correctly");
}
});
});

QUnit.test("creating a tag for `content` should work", function() {
callbacks.tag("content", function() {
var textNode = document.createTextNode("This is another el");
el.appendChild(textNode);
});

ok(true, "did not throw error");
});