Skip to content

Commit

Permalink
Merge pull request #77 from canjs/automatic-mounting
Browse files Browse the repository at this point in the history
automatically calling tagHandler for tags in the document
  • Loading branch information
phillipskevin authored Jan 12, 2018
2 parents 5b963cb + 549974e commit a9f1514
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 7 deletions.
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");
});

0 comments on commit a9f1514

Please sign in to comment.