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 1 commit
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
101 changes: 97 additions & 4 deletions can-view-callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,78 @@ var domMutate = require('can-dom-mutate/node');
var namespace = require('can-namespace');
var nodeLists = require('can-view-nodelist');
var makeFrag = require("can-util/dom/frag/frag");
var canSymbol = require("can-symbol");

//!steal-remove-start
var requestedAttributes = {};
//!steal-remove-end

var tags = {};

var GLOBAL = getGlobal();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't cache this. Even thought this feature will (likely) never run in SSR, having this here might mean someone else uses it in code that will.

var supportsCustomElements = "customElements" in GLOBAL;
var viewmodelSymbol = canSymbol.for("can.viewModel");

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

var mountNodeAndChildrenIfNecessary = function(node) {
var tagName = node.tagName && node.tagName.toLowerCase();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: node.localName is already lower case

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 mounted
if (!node[viewmodelSymbol] && tagHandler && !mountedElements.has(node)) {
tagHandler(node, tagName, {});
}

if (node.getElementsByTagName) {
children = node.getElementsByTagName("*");
for (var k=0, child; (child = children[k]) !== undefined; k++) {
mountNodeAndChildrenIfNecessary(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 mounted
if (!mountedElements.has(addedNode)) {
mountNodeAndChildrenIfNecessary(addedNode);
}
}
}
}
};

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use getGlobal or getDocument


mutationObserverEnabled = true;
};

var mountExistingElements = function(tagName) {
var nodes = GLOBAL.document.getElementsByTagName(tagName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getGlobal or getDocument


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

var attr = function (attributeName, attrHandler) {
if(attrHandler) {
if (typeof attributeName === "string") {
Expand Down Expand Up @@ -60,14 +127,14 @@ var defaultCallback = function () {};

var tag = function (tagName, tagHandler) {
if(tagHandler) {
var GLOBAL = getGlobal();

//!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") {
dev.warn("Custom tag: " + tagName.toLowerCase() + " hyphen missed");
return;
}
//!steal-remove-end
// if we have html5shiv ... re-generate
Expand All @@ -77,6 +144,33 @@ var tag = function (tagName, tagHandler) {
}

tags[tagName.toLowerCase()] = tagHandler;

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

CustomElement.prototype.connectedCallback = function() {
// don't re-mount an element that has been mounted already
if (!mountedElements.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
// mounting elements when they are inserted in the page
// and mount elements that are already in the page
else {
enableMutationObserver();
mountExistingElements(tagName);
}
} else {
var cb;

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

};
var tags = {};

var callbacks = {
_tags: tags,
Expand All @@ -104,6 +197,7 @@ var callbacks = {
defaultCallback: defaultCallback,
tag: tag,
attr: attr,
mountedElements: mountedElements,
// handles calling back a tag callback
tagHandler: function(el, tagName, tagData){
var helperTagCallback = tagData.scope.templateContext.tags.get(tagName),
Expand All @@ -121,7 +215,6 @@ var callbacks = {

//!steal-remove-start
if (!tagCallback) {
var GLOBAL = getGlobal();
var ceConstructor = GLOBAL.document.createElement(tagName).constructor;
// If not registered as a custom element, the browser will use default constructors
if (ceConstructor === GLOBAL.HTMLElement || ceConstructor === GLOBAL.HTMLUnknownElement) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"can-log": "<2.0.0",
"can-namespace": "1.0.0",
"can-observation-recorder": "<2.0.0",
"can-symbol": "^1.5.0",
"can-util": "^3.9.5",
"can-view-nodelist": "^4.0.0-pre.5"
},
Expand Down
119 changes: 119 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,109 @@ QUnit.test("Passes through nodeList", function(){
}
});
});

QUnit.test("tag handler is called automatically for elements 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 automatically for elements in mountedElements Set", function() {
var fixture = document.getElementById('qunit-fixture');

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

// <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.mountedElements.add(elOne);
callbacks.mountedElements.add(elTwo);
callbacks.mountedElements.add(elThree);

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, "", "<another-el>[" + i + "] not rendered");
}
});
});

QUnit.test("tag handler is called automatically for elements already in the page when it is registered", 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");
}
});
});