/** @module delite/Template */
define(["./register"], function (register) {
/**
* Given an AST representation of the template, generates a function that:
*
* 1. generates DOM corresponding to the template
* 2. returns an object including a function to be called to update that DOM
* when widget properties have changed.
*
* The function is available through `this.func`, i.e.:
*
* ```js
* var template = new Template(ast);
* template.func(document, register);
* ```
*
* See the reference documentation for details on the AST format.
*
* @param {Object} tree - AST representing the template.
* @param {string} rootNodeName - Name of variable for the root node of the tree, typically `this`.
* @param {boolean} createRootNode - If true, create node; otherwise assume node exists in variable `nodeName`.
* @class module:delite/Template
*/
var Template = register.dcl(null, /** @lends module:delite/Template# */ {
constructor: function (tree, rootNodeName, createRootNode) {
this.buildText = []; // code to build the initial DOM
this.observeText = []; // code to update the DOM when widget properties change
this.dependsOn = {}; // set of properties referenced in the template
this.generateNodeCode(rootNodeName || "this", createRootNode, tree);
// Generate text of function.
this.text = this.buildText.join("\n") + "\n" +
"return {\n" +
"\tdependencies: " + JSON.stringify(Object.keys(this.dependsOn)) + ",\n" +
"\trefresh: function(props){\n\t\t" +
this.observeText.join("\n\t\t") +
"\n\t}.bind(this)\n" +
"};\n";
/* jshint evil:true */
this.func = new Function("document", "register", this.text);
},
/**
* Text of the generated function.
* @member {string}
* @readonly
*/
text: "",
/**
* Generated function.
* @member {Function}
* @readonly
*/
func: null,
/**
* Generate code that executes `statement` if any of the properties in `dependencies` change.
* @param {string[]} dependencies - List of variables referenced in `statement`.
* Must have at least one entry.
* @param {string} statement - Content inside if() statement.
* @private
*/
generateWatchCode: function (dependencies, statement) {
this.observeText.push(
"if(" + dependencies.map(function (prop) {
return "'" + prop + "' in props";
}).join(" || ") + ")",
"\t" + statement + ";"
);
dependencies.forEach(function (prop) { this.dependsOn[prop] = true; }, this);
},
/**
* Generate JS code to create and add children to a node named nodeName.
* @param {string} nodeName
* @param {Object[]} children
* @private
*/
generateNodeChildrenCode: function (nodeName, children) {
children.forEach(function (child, idx) {
var childName = (nodeName === "this" ? "" : nodeName) + "c" + (idx + 1);
if (child.tag) {
// Standard DOM node, recurse
this.generateNodeCode(childName, true, child);
this.buildText.push(
nodeName + ".appendChild(" + childName + ");"
);
} else {
// JS code to compute text value
var textNodeName = childName + "t" + (idx + 1);
// Generate code to create DOM text node. If the text contains property references, just
// leave it blank for now, and set the real value in refreshRendering().\
this.buildText.push(
"var " + textNodeName + " = document.createTextNode(" +
(child.dependsOn.length ? "''" : child.expr) + ");",
nodeName + ".appendChild(" + textNodeName + ");"
);
// watch for widget property changes and update DOM text node
if (child.dependsOn.length) {
this.generateWatchCode(child.dependsOn, textNodeName + ".nodeValue = " + child.expr);
}
}
}, this);
},
/**
* Generate JS code to create a node called nodeName based on templateNode, then
* set its properties, attributes, and children, according to descendants of templateNode.
* @param {string} nodeName - The node will be in a variable with this name.
* @param {boolean} createNode - If true, create node; otherwise assume node exists in variable `nodeName`
* @param {Object} templateNode - An object representing a node in the template, as described in module summary.
* @private
*/
generateNodeCode: function (nodeName, createNode, templateNode) {
/* jshint maxcomplexity:15*/
// Helper string for setting up attach-point(s), ex: "this.foo = this.bar = ".
var ap = (templateNode.attachPoints || []).map(function (n) {
return "this." + n + " = ";
}).join("");
// Create node
if (createNode) {
this.buildText.push(
"var " + nodeName + " = " + ap + (templateNode.xmlns ?
"document.createElementNS('" + templateNode.xmlns + "', '" + templateNode.tag + "');" :
"register.createElement('" + templateNode.tag + "');")
);
} else if (ap) {
// weird case that someone set attach-point on root node
this.buildText.push(ap + nodeName + ";");
}
// Set attributes/properties
for (var attr in templateNode.attributes) {
var info = templateNode.attributes[attr];
// Generate code to set this property or attribute
var propName = Template.getProp(templateNode.tag, attr),
js = info.expr; // code to compute property value
if (attr === "class" && !templateNode.xmlns) {
// Special path for class to not overwrite classes set by application or by other code.
if (info.dependsOn.length) {
// Value depends on widget properties that may not be set yet.
// Watch for changes to those widget properties and reflect them to the DOM.
this.generateWatchCode(info.dependsOn,
"this.setClassComponent('template', " + js + ", " + nodeName + ")");
} else {
// Value is a constant; set it during render().
this.buildText.push("this.setClassComponent('template', " + js + ", " + nodeName + ")");
}
} else {
if (info.dependsOn.length) {
// Value depends on widget properties that may not be set yet.
// Watch for changes to those widget properties and reflect them to the DOM.
this.generateWatchCode(info.dependsOn, propName ? nodeName + "." + propName + " = " + js :
"this.setOrRemoveAttribute(" + nodeName + ", '" + attr + "', " + js + ")");
} else {
// Value is a constant; set it during render().
this.buildText.push(propName ? nodeName + "." + propName + " = " + js :
nodeName + ".setAttribute('" + attr + "', " + js + ");");
}
}
}
// If this node is a custom element, make it immediately display the property changes I've made
if (/-/.test(templateNode.tag)) {
this.buildText.push(nodeName + ".deliver();");
this.observeText.push(nodeName + ".deliver();");
}
// Setup connections
for (var type in templateNode.connects) {
var handler = templateNode.connects[type];
var callback = /^[a-zA-Z0-9_]+$/.test(handler) ?
"this." + handler + ".bind(this)" : // standard case, connecting to a method in the widget
"function(event){" + handler + "}"; // connect to anon func, ex: on-click="g++;". used by dapp.
this.buildText.push("this.on('" + type + "', " + callback + ", " + nodeName + ");");
}
// Create descendant Elements and text nodes
this.generateNodeChildrenCode(nodeName, templateNode.children);
}
});
// Export helper funcs so they can be used by handlebars.js
/**
* Return cached reference to Element with given tag name.
* @function module:delite/Template.getElement
* @param {string} tag
* @returns {Element}
*/
var elementCache = {};
Template.getElement = function (tag) {
if (!(tag in elementCache)) {
elementCache[tag] = register.createElement(tag);
}
return elementCache[tag];
};
/**
* Given a tag and attribute name, return the associated property name,
* or undefined if no such property exists, for example:
*
* - getProp("div", "tabindex") --> "tabIndex"
* - getProp("div", "role") --> undefined
*
* Note that in order to support SVG, getProp("svg", "class") returns null instead of className.
*
* @function module:delite/Template.getProp
* @param {string} tag - Tag name.
* @param {string} attrName - Attribute name.
* @returns {string}
*/
var attrMap = {};
Template.getProp = function (tag, attrName) {
if (!(tag in attrMap)) {
var proto = Template.getElement(tag),
map = attrMap[tag] = {};
for (var prop in proto) {
map[prop.toLowerCase()] = prop;
}
map.style = "style.cssText";
}
return attrMap[tag][attrName];
};
return Template;
});