diff --git a/owl.js b/owl.js
index b7a45bda..cbe006b6 100644
--- a/owl.js
+++ b/owl.js
@@ -53,7 +53,8 @@
*/
trigger(eventType, ...args) {
const subs = this.subscriptions[eventType] || [];
- for (let sub of subs) {
+ for (let i = 0, iLen = subs.length; i < iLen; i++) {
+ const sub = subs[i];
sub.callback.call(sub.owner, ...args);
}
}
@@ -95,9 +96,11 @@
"sort",
"reverse"
];
+ const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype;
const ModifiedArrayProto = Object.create(ArrayProto);
- for (let method of methodsToPatch) {
+ for (let i = 0; i < methodLen; i++) {
+ const method = methodsToPatch[i];
const initialMethod = ArrayProto[method];
ModifiedArrayProto[method] = function (...args) {
if (!this.__observer__.allowMutations) {
@@ -121,8 +124,8 @@
break;
}
if (inserted) {
- for (let elem of inserted) {
- this.__observer__.observe(elem, this);
+ for (let i = 0, iLen = inserted.length; i < iLen; i++) {
+ this.__observer__.observe(inserted[i], this);
}
}
return initialMethod.call(this, ...args);
@@ -174,10 +177,9 @@
this.notifyChange();
}
_observeObj(obj, parent) {
- const keys = Object.keys(obj);
obj.__owl__ = { rev: this.rev, deepRev: this.rev, parent };
Object.defineProperty(obj, "__owl__", { enumerable: false });
- for (let key of keys) {
+ for (let key in obj) {
this._addProp(obj, key, obj[key]);
}
}
@@ -186,7 +188,7 @@
Object.defineProperty(arr, "__owl__", { enumerable: false });
arr.__proto__ = Object.create(ModifiedArrayProto);
arr.__proto__.__observer__ = this;
- for (let i = 0; i < arr.length; i++) {
+ for (let i = 0, iLen = arr.length; i < iLen; i++) {
this.observe(arr[i], arr);
}
}
@@ -307,7 +309,7 @@
};
}
function createElm(vnode, insertedVnodeQueue) {
- let i, data = vnode.data;
+ let i, iLen, data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.init))) {
i(vnode);
@@ -338,10 +340,10 @@
elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
- for (i = 0; i < cbs.create.length; ++i)
+ for (i = 0, iLen = cbs.create.length; i < iLen; ++i)
cbs.create[i](emptyNode, vnode);
if (array(children)) {
- for (i = 0; i < children.length; ++i) {
+ for (i = 0, iLen = children.length; i < iLen; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch, insertedVnodeQueue));
@@ -373,14 +375,14 @@
}
}
function invokeDestroyHook(vnode) {
- let i, j, data = vnode.data;
+ let i, iLen, j, jLen, data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.destroy)))
i(vnode);
- for (i = 0; i < cbs.destroy.length; ++i)
+ for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i)
cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
- for (j = 0; j < vnode.children.length; ++j) {
+ for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
i = vnode.children[j];
if (i != null && typeof i !== "string") {
invokeDestroyHook(i);
@@ -391,13 +393,13 @@
}
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
- let i, listeners, rm, ch = vnodes[startIdx];
+ let i, iLen, listeners, rm, ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm, listeners);
- for (i = 0; i < cbs.remove.length; ++i)
+ for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
cbs.remove[i](ch, rm);
if (isDef((i = ch.data)) &&
isDef((i = i.hook)) &&
@@ -499,7 +501,7 @@
}
}
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
- let i, hook;
+ let i, iLen, hook;
if (isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))) {
@@ -511,7 +513,7 @@
if (oldVnode === vnode)
return;
if (vnode.data !== undefined) {
- for (i = 0; i < cbs.update.length; ++i)
+ for (i = 0, iLen = cbs.update.length; i < iLen; ++i)
cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update)))
@@ -545,9 +547,9 @@
}
}
return function patch(oldVnode, vnode) {
- let i, elm, parent;
+ let i, iLen, elm, parent;
const insertedVnodeQueue = [];
- for (i = 0; i < cbs.pre.length; ++i)
+ for (i = 0, iLen = cbs.pre.length; i < iLen; ++i)
cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
@@ -564,10 +566,10 @@
removeVnodes(parent, [oldVnode], 0, 0);
}
}
- for (i = 0; i < insertedVnodeQueue.length; ++i) {
+ for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
- for (i = 0; i < cbs.post.length; ++i)
+ for (i = 0, iLen = cbs.post.length; i < iLen; ++i)
cbs.post[i]();
return vnode;
};
@@ -644,7 +646,7 @@
function addNS(data, children, sel) {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
- for (let i = 0; i < children.length; ++i) {
+ for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, children[i].children, children[i].sel);
@@ -653,7 +655,7 @@
}
}
function h(sel, b, c) {
- var data = {}, children, text, i;
+ var data = {}, children, text, i, iLen;
if (c !== undefined) {
data = b;
if (array(c)) {
@@ -681,7 +683,7 @@
}
}
if (children !== undefined) {
- for (i = 0; i < children.length; ++i) {
+ for (i = 0, iLen = children.length; i < iLen; ++i) {
if (primitive(children[i]))
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
@@ -740,7 +742,7 @@
}
else {
// call multiple handlers
- for (var i = 0; i < handler.length; i++) {
+ for (let i = 0, iLen = handler.length; i < iLen; i++) {
invokeHandler(handler[i], vnode, event);
}
}
@@ -867,6 +869,630 @@
};
const patch = init([eventListenersModule, attrsModule, propsModule]);
+ //------------------------------------------------------------------------------
+ // Const/global stuff/helpers
+ //------------------------------------------------------------------------------
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
+ const WORD_REPLACEMENT = {
+ and: "&&",
+ or: "||",
+ gt: ">",
+ gte: ">=",
+ lt: "<",
+ lte: "<="
+ };
+ const DISABLED_TAGS = [
+ "input",
+ "textarea",
+ "button",
+ "select",
+ "option",
+ "optgroup"
+ ];
+ const lineBreakRE = /[\r\n]/;
+ const whitespaceRE = /\s+/g;
+ const DIRECTIVE_NAMES = {
+ name: 1,
+ att: 1,
+ attf: 1,
+ key: 1
+ };
+ const DIRECTIVES = [];
+ const NODE_HOOKS_PARAMS = {
+ create: "(_, n)",
+ insert: "vn",
+ remove: "(vn, rm)"
+ };
+ const UTILS = {
+ h: h,
+ objectToAttrString(obj) {
+ let classes = [];
+ for (let k in obj) {
+ if (obj[k]) {
+ classes.push(k);
+ }
+ }
+ return classes.join(" ");
+ }
+ };
+ function parseXML(xml) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(xml, "text/xml");
+ if (doc.getElementsByTagName("parsererror").length) {
+ throw new Error("Invalid XML in template");
+ }
+ return doc;
+ }
+ let nextID = 1;
+ //------------------------------------------------------------------------------
+ // QWeb rendering engine
+ //------------------------------------------------------------------------------
+ class QWeb {
+ constructor(data) {
+ this.templates = {};
+ this.utils = UTILS;
+ // the id field is useful to be able to hash qweb instances. The current
+ // use case is that component's templates are qweb dependant, and need to be
+ // able to map a qweb instance to a template name.
+ this.id = nextID++;
+ if (data) {
+ this.addTemplates(data);
+ }
+ this.addTemplate("default", "
");
+ }
+ static addDirective(directive) {
+ DIRECTIVES.push(directive);
+ DIRECTIVE_NAMES[directive.name] = 1;
+ DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
+ if (directive.extraNames) {
+ directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
+ }
+ }
+ static register(name, Component) {
+ if (QWeb.widgets[name]) {
+ throw new Error(`Component '${name}' has already been registered`);
+ }
+ QWeb.widgets[name] = Component;
+ }
+ /**
+ * Add a template to the internal template map. Note that it is not
+ * immediately compiled.
+ */
+ addTemplate(name, xmlString) {
+ const doc = parseXML(xmlString);
+ if (!doc.firstChild) {
+ throw new Error("Invalid template (should not be empty)");
+ }
+ this._addTemplate(name, doc.firstChild);
+ }
+ /**
+ * Load templates from a xml (as a string). This will look up for the first
+ * tag, and will consider each child of this as a template, with
+ * the name given by the t-name attribute.
+ */
+ addTemplates(xmlstr) {
+ const doc = parseXML(xmlstr);
+ const templates = doc.getElementsByTagName("templates")[0];
+ if (!templates) {
+ return;
+ }
+ for (let elem of templates.children) {
+ const name = elem.getAttribute("t-name");
+ this._addTemplate(name, elem);
+ }
+ }
+ _addTemplate(name, elem) {
+ if (name in this.templates) {
+ throw new Error(`Template ${name} already defined`);
+ }
+ this._processTemplate(elem);
+ const template = {
+ elem,
+ fn: (context, extra) => {
+ const compiledFunction = this._compile(name, elem);
+ template.fn = compiledFunction;
+ return compiledFunction.call(this, context, extra);
+ }
+ };
+ this.templates[name] = template;
+ }
+ _processTemplate(elem) {
+ let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
+ for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
+ let node = tbranch[i];
+ let prevElem = node.previousElementSibling;
+ let pattr = function (name) {
+ return prevElem.getAttribute(name);
+ };
+ let nattr = function (name) {
+ return +!!node.getAttribute(name);
+ };
+ if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
+ if (pattr("t-foreach")) {
+ throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
+ }
+ if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
+ return a + b;
+ }) > 1) {
+ throw new Error("Only one conditional branching directive is allowed per node");
+ }
+ // All text nodes between branch nodes are removed
+ let textNode;
+ while ((textNode = node.previousSibling) !== prevElem) {
+ if (textNode.nodeValue.trim().length) {
+ throw new Error("text is not allowed between branching directives");
+ }
+ textNode.remove();
+ }
+ }
+ else {
+ throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
+ }
+ }
+ }
+ /**
+ * Render a template
+ *
+ * @param {string} name the template should already have been added
+ */
+ render(name, context = {}, extra = null) {
+ const template = this.templates[name];
+ if (!template) {
+ throw new Error(`Template ${name} does not exist`);
+ }
+ return template.fn.call(this, context, extra);
+ }
+ _compile(name, elem) {
+ const isDebug = elem.attributes.hasOwnProperty("t-debug");
+ const ctx = new Context(name);
+ this._compileNode(elem, ctx);
+ if (ctx.shouldProtectContext) {
+ ctx.code.unshift(" context = Object.create(context);");
+ }
+ if (ctx.shouldDefineOwner) {
+ // this is necessary to prevent some directives (t-forach for ex) to
+ // pollute the rendering context by adding some keys in it.
+ ctx.code.unshift(" let owner = context;");
+ }
+ if (ctx.shouldDefineQWeb) {
+ ctx.code.unshift(" let QWeb = this.constructor;");
+ }
+ if (ctx.shouldDefineUtils) {
+ ctx.code.unshift(" let utils = this.utils;");
+ }
+ if (!ctx.rootNode) {
+ throw new Error("A template should have one root node");
+ }
+ ctx.addLine(`return vn${ctx.rootNode};`);
+ let template;
+ try {
+ template = new Function("context", "extra", ctx.code.join("\n"));
+ }
+ catch (e) {
+ throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`);
+ }
+ if (isDebug) {
+ console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
+ template.toString());
+ }
+ return template;
+ }
+ /**
+ * Generate code from an xml node
+ *
+ */
+ _compileNode(node, ctx) {
+ if (!(node instanceof Element)) {
+ // this is a text node, there are no directive to apply
+ let text = node.textContent;
+ if (!ctx.inPreTag) {
+ if (lineBreakRE.test(text) && !text.trim()) {
+ return;
+ }
+ text = text.replace(whitespaceRE, " ");
+ }
+ if (ctx.parentNode) {
+ ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
+ }
+ else {
+ // this is an unusual situation: this text node is the result of the
+ // template rendering.
+ let nodeID = ctx.generateID();
+ ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
+ ctx.rootContext.rootNode = nodeID;
+ ctx.rootContext.parentNode = nodeID;
+ }
+ return;
+ }
+ const attributes = node.attributes;
+ const validDirectives = [];
+ let withHandlers = false;
+ // maybe this is not optimal: we iterate on all attributes here, and again
+ // just after for each directive.
+ for (let i = 0; i < attributes.length; i++) {
+ let attrName = attributes[i].name;
+ if (attrName.startsWith("t-")) {
+ let dName = attrName.slice(2).split("-")[0];
+ if (!(dName in DIRECTIVE_NAMES)) {
+ throw new Error(`Unknown QWeb directive: '${attrName}'`);
+ }
+ }
+ }
+ const DIR_N = DIRECTIVES.length;
+ const ATTR_N = attributes.length;
+ for (let i = 0; i < DIR_N; i++) {
+ let directive = DIRECTIVES[i];
+ let fullName;
+ let value;
+ for (let j = 0; j < ATTR_N; j++) {
+ const name = attributes[j].name;
+ if (name === "t-" + directive.name ||
+ name.startsWith("t-" + directive.name + "-")) {
+ fullName = name;
+ value = attributes[j].textContent;
+ validDirectives.push({ directive, value, fullName });
+ if (directive.name === "on") {
+ withHandlers = true;
+ }
+ }
+ }
+ }
+ for (let { directive, value, fullName } of validDirectives) {
+ if (directive.atNodeEncounter) {
+ const isDone = directive.atNodeEncounter({
+ node,
+ qweb: this,
+ ctx,
+ fullName,
+ value
+ });
+ if (isDone) {
+ return;
+ }
+ }
+ }
+ if (node.nodeName !== "t") {
+ let nodeID = this._compileGenericNode(node, ctx, withHandlers);
+ ctx = ctx.withParent(nodeID);
+ let nodeHooks = {};
+ let addNodeHook = function (hook, handler) {
+ nodeHooks[hook] = nodeHooks[hook] || [];
+ nodeHooks[hook].push(handler);
+ };
+ for (let { directive, value, fullName } of validDirectives) {
+ if (directive.atNodeCreation) {
+ directive.atNodeCreation({
+ node,
+ qweb: this,
+ ctx,
+ fullName,
+ value,
+ nodeID,
+ addNodeHook
+ });
+ }
+ }
+ if (Object.keys(nodeHooks).length) {
+ ctx.addLine(`p${nodeID}.hook = {`);
+ for (let hook in nodeHooks) {
+ ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
+ for (let handler of nodeHooks[hook]) {
+ ctx.addLine(` ${handler}`);
+ }
+ ctx.addLine(` },`);
+ }
+ ctx.addLine(`};`);
+ }
+ }
+ if (node.nodeName === "pre") {
+ ctx = ctx.subContext("inPreTag", true);
+ }
+ this._compileChildren(node, ctx);
+ for (let { directive, value, fullName } of validDirectives) {
+ if (directive.finalize) {
+ directive.finalize({ node, qweb: this, ctx, fullName, value });
+ }
+ }
+ }
+ _compileGenericNode(node, ctx, withHandlers = true) {
+ // nodeType 1 is generic tag
+ if (node.nodeType !== 1) {
+ throw new Error("unsupported node type");
+ }
+ const attributes = node.attributes;
+ const attrs = [];
+ const props = [];
+ const tattrs = [];
+ function handleBooleanProps(key, val) {
+ let isProp = false;
+ if (node.nodeName === "input" && key === "checked") {
+ let type = node.getAttribute("type");
+ if (type === "checkbox" || type === "radio") {
+ isProp = true;
+ }
+ }
+ if (node.nodeName === "option" && key === "selected") {
+ isProp = true;
+ }
+ if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
+ isProp = true;
+ }
+ if ((key === "readonly" && node.nodeName === "input") ||
+ node.nodeName === "textarea") {
+ isProp = true;
+ }
+ if (isProp) {
+ props.push(`${key}: _${val}`);
+ }
+ }
+ for (let i = 0; i < attributes.length; i++) {
+ let name = attributes[i].name;
+ const value = attributes[i].textContent;
+ // regular attributes
+ if (!name.startsWith("t-") &&
+ !node.getAttribute("t-attf-" + name)) {
+ const attID = ctx.generateID();
+ ctx.addLine(`var _${attID} = '${value}';`);
+ if (!name.match(/^[a-zA-Z]+$/)) {
+ // attribute contains 'non letters' => we want to quote it
+ name = '"' + name + '"';
+ }
+ attrs.push(`${name}: _${attID}`);
+ handleBooleanProps(name, attID);
+ }
+ // dynamic attributes
+ if (name.startsWith("t-att-")) {
+ let attName = name.slice(6);
+ let formattedValue = ctx.formatExpression(ctx.getValue(value));
+ if (formattedValue[0] === "{" &&
+ formattedValue[formattedValue.length - 1] === "}") {
+ formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
+ }
+ const attID = ctx.generateID();
+ if (!attName.match(/^[a-zA-Z]+$/)) {
+ // attribute contains 'non letters' => we want to quote it
+ attName = '"' + attName + '"';
+ }
+ // we need to combine dynamic with non dynamic attributes:
+ // class="a" t-att-class="'yop'" should be rendered as class="a yop"
+ const attValue = node.getAttribute(attName);
+ if (attValue) {
+ const attValueID = ctx.generateID();
+ ctx.addLine(`var _${attValueID} = ${formattedValue};`);
+ formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
+ const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
+ attrs.splice(attrIndex, 1);
+ }
+ ctx.addLine(`var _${attID} = ${formattedValue};`);
+ attrs.push(`${attName}: _${attID}`);
+ handleBooleanProps(attName, attID);
+ }
+ if (name.startsWith("t-attf-")) {
+ let attName = name.slice(7);
+ if (!attName.match(/^[a-zA-Z]+$/)) {
+ // attribute contains 'non letters' => we want to quote it
+ attName = '"' + attName + '"';
+ }
+ const formattedExpr = ctx.interpolate(value);
+ const attID = ctx.generateID();
+ let staticVal = node.getAttribute(attName);
+ if (staticVal) {
+ ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
+ }
+ else {
+ ctx.addLine(`var _${attID} = ${formattedExpr};`);
+ }
+ attrs.push(`${attName}: _${attID}`);
+ }
+ // t-att= attributes
+ if (name === "t-att") {
+ let id = ctx.generateID();
+ ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`);
+ tattrs.push(id);
+ }
+ }
+ let nodeID = ctx.generateID();
+ let nodeKey = node.getAttribute("t-key");
+ if (nodeKey) {
+ nodeKey = ctx.formatExpression(nodeKey);
+ }
+ else {
+ nodeKey = nodeID;
+ }
+ const parts = [`key:${nodeKey}`];
+ if (attrs.length + tattrs.length > 0) {
+ parts.push(`attrs:{${attrs.join(",")}}`);
+ }
+ if (props.length > 0) {
+ parts.push(`props:{${props.join(",")}}`);
+ }
+ if (withHandlers) {
+ parts.push(`on:{}`);
+ }
+ ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
+ for (let id of tattrs) {
+ ctx.addIf(`_${id} instanceof Array`);
+ ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
+ ctx.addElse();
+ ctx.addLine(`for (let key in _${id}) {`);
+ ctx.indent();
+ ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
+ ctx.dedent();
+ ctx.addLine(`}`);
+ ctx.closeIf();
+ }
+ ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
+ if (ctx.parentNode) {
+ ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
+ }
+ return nodeID;
+ }
+ _compileChildren(node, ctx) {
+ if (node.childNodes.length > 0) {
+ for (let child of Array.from(node.childNodes)) {
+ this._compileNode(child, ctx);
+ }
+ }
+ }
+ }
+ QWeb.widgets = Object.create(null);
+ // dev mode enables better error messages or more costly validations
+ QWeb.dev = false;
+ //------------------------------------------------------------------------------
+ // Compilation Context
+ //------------------------------------------------------------------------------
+ class Context {
+ constructor(name) {
+ this.nextID = 1;
+ this.code = [];
+ this.variables = {};
+ this.definedVariables = {};
+ this.escaping = false;
+ this.parentNode = null;
+ this.rootNode = null;
+ this.indentLevel = 0;
+ this.shouldDefineOwner = false;
+ this.shouldDefineQWeb = false;
+ this.shouldDefineUtils = false;
+ this.shouldProtectContext = false;
+ this.inLoop = false;
+ this.inPreTag = false;
+ this.rootContext = this;
+ this.templateName = name || "noname";
+ this.addLine("var h = this.utils.h;");
+ }
+ generateID() {
+ const id = this.rootContext.nextID++;
+ return id;
+ }
+ withParent(node) {
+ if (this === this.rootContext && this.parentNode) {
+ throw new Error("A template should not have more than one root node");
+ }
+ if (!this.rootContext.rootNode) {
+ this.rootContext.rootNode = node;
+ }
+ return this.subContext("parentNode", node);
+ }
+ subContext(key, value) {
+ const newContext = Object.create(this);
+ newContext[key] = value;
+ return newContext;
+ }
+ indent() {
+ this.indentLevel++;
+ }
+ dedent() {
+ this.indentLevel--;
+ }
+ addLine(line) {
+ const prefix = new Array(this.indentLevel + 2).join(" ");
+ this.code.push(prefix + line);
+ }
+ addIf(condition) {
+ this.addLine(`if (${condition}) {`);
+ this.indent();
+ }
+ addElse() {
+ this.dedent();
+ this.addLine("} else {");
+ this.indent();
+ }
+ closeIf() {
+ this.dedent();
+ this.addLine("}");
+ }
+ getValue(val) {
+ return val in this.variables ? this.getValue(this.variables[val]) : val;
+ }
+ formatExpression(e) {
+ e = e.trim();
+ if (e[0] === "{" && e[e.length - 1] === "}") {
+ const innerExpr = e
+ .slice(1, -1)
+ .split(",")
+ .map(p => {
+ let [key, val] = p.trim().split(":");
+ if (key === "") {
+ return "";
+ }
+ if (!val) {
+ val = key;
+ }
+ return `${key}: ${this.formatExpression(val)}`;
+ })
+ .join(",");
+ return "{" + innerExpr + "}";
+ }
+ // Thanks CHM for this code...
+ const chars = e.split("");
+ let instring = "";
+ let invar = "";
+ let invarPos = 0;
+ let r = "";
+ chars.push(" ");
+ for (let i = 0, ilen = chars.length; i < ilen; i++) {
+ let c = chars[i];
+ if (instring.length) {
+ if (c === instring && chars[i - 1] !== "\\") {
+ instring = "";
+ }
+ }
+ else if (c === '"' || c === "'") {
+ instring = c;
+ }
+ else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
+ invar = c;
+ invarPos = i;
+ continue;
+ }
+ else if (c.match(/\W/) && invar.length) {
+ // TODO: Should check for possible spaces before dot
+ if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
+ if (!(invar in this.definedVariables)) {
+ invar =
+ WORD_REPLACEMENT[invar] ||
+ (invar in this.variables &&
+ this.formatExpression(this.variables[invar])) ||
+ "context['" + invar + "']";
+ }
+ }
+ r += invar;
+ invar = "";
+ }
+ else if (invar.length) {
+ invar += c;
+ continue;
+ }
+ r += c;
+ }
+ const result = r.slice(0, -1);
+ return result;
+ }
+ /**
+ * Perform string interpolation on the given string. Note that if the whole
+ * string is an expression, it simply returns it (formatted).
+ * For instance:
+ * 'Hello {{x}}!' -> `Hello ${x}`
+ * '{{x}}' -> x
+ */
+ interpolate(s) {
+ let matches = s.match(/\{\{.*?\}\}/g);
+ if (matches && matches[0].length === s.length) {
+ return this.formatExpression(s.slice(2, -2));
+ }
+ matches = s.match(/\#\{.*?\}/g);
+ if (matches && matches[0].length === s.length) {
+ return this.formatExpression(s.slice(2, -1));
+ }
+ let formatter = expr => "${" + this.formatExpression(expr) + "}";
+ let r = s
+ .replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
+ .replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
+ return "`" + r + "`";
+ }
+ }
+
// If a component does not define explicitely a template
// key, it needs to find a template with its name (or a parent's). This is
// qweb dependant, so we need a place to store this information indexed by
@@ -902,6 +1528,13 @@
constructor(parent, props) {
super();
this.refs = {};
+ const defaultProps = this.constructor.defaultProps;
+ if (defaultProps) {
+ props = this._applyDefaultProps(props, defaultProps);
+ }
+ if (QWeb.dev) {
+ this._validateProps(props || {});
+ }
// is this a good idea?
// Pro: if props is empty, we can create easily a widget
// Con: this is not really safe
@@ -1021,23 +1654,26 @@
}
}
_callMounted() {
- const children = this.__owl__.children;
+ const __owl__ = this.__owl__;
+ const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el.contains(comp.el)) {
comp._callMounted();
}
}
- this.__owl__.isMounted = true;
- for (let key in this.__owl__.mountedHandlers) {
- this.__owl__.mountedHandlers[key]();
+ __owl__.isMounted = true;
+ const handlers = __owl__.mountedHandlers;
+ for (let key in handlers) {
+ handlers[key]();
}
this.mounted();
}
_callWillUnmount() {
this.willUnmount();
- this.__owl__.isMounted = false;
- const children = this.__owl__.children;
+ const __owl__ = this.__owl__;
+ __owl__.isMounted = false;
+ const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
@@ -1052,7 +1688,8 @@
}
}
async render(force = false, patchQueue) {
- if (!this.__owl__.isMounted) {
+ const __owl__ = this.__owl__;
+ if (!__owl__.isMounted) {
return;
}
const shouldPatch = !patchQueue;
@@ -1060,54 +1697,55 @@
patchQueue = [];
}
const renderVDom = this._render(force, patchQueue);
- const renderId = this.__owl__.renderId;
+ const renderId = __owl__.renderId;
await renderVDom;
- if (shouldPatch &&
- this.__owl__.isMounted &&
- renderId === this.__owl__.renderId) {
+ if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
- for (let i = 0; i < patchQueue.length; i++) {
+ const patchLen = patchQueue.length;
+ for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch.push(patch[0].willPatch());
}
- for (let i = 0; i < patchQueue.length; i++) {
+ for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0]._patch(patch[1]);
}
- for (let i = patchQueue.length - 1; i >= 0; i--) {
+ for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
}
}
}
destroy() {
- if (!this.__owl__.isDestroyed) {
+ const __owl__ = this.__owl__;
+ if (!__owl__.isDestroyed) {
const el = this.el;
- this._destroy(this.__owl__.parent);
+ this._destroy(__owl__.parent);
if (el) {
el.remove();
}
}
}
_destroy(parent) {
- const isMounted = this.__owl__.isMounted;
+ const __owl__ = this.__owl__;
+ const isMounted = __owl__.isMounted;
if (isMounted) {
this.willUnmount();
- this.__owl__.isMounted = false;
+ __owl__.isMounted = false;
}
- const children = Object.values(this.__owl__.children);
- for (let child of children) {
- child._destroy(this);
+ const children = __owl__.children;
+ for (let key in children) {
+ children[key]._destroy(this);
}
if (parent) {
- let id = this.__owl__.id;
+ let id = __owl__.id;
delete parent.__owl__.children[id];
- this.__owl__.parent = null;
+ __owl__.parent = null;
}
this.clear();
- this.__owl__.isDestroyed = true;
- delete this.__owl__.vnode;
+ __owl__.isDestroyed = true;
+ delete __owl__.vnode;
}
shouldUpdate(nextProps) {
return true;
@@ -1122,11 +1760,12 @@
* mode or not.
*/
async updateEnv(nextEnv) {
- if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
+ const __owl__ = this.__owl__;
+ if (__owl__.parent && __owl__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
- if (this.__owl__.isMounted) {
+ if (__owl__.isMounted) {
await this.render(true);
}
}
@@ -1139,24 +1778,34 @@
async _updateProps(nextProps, forceUpdate = false, patchQueue) {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
+ const defaultProps = this.constructor.defaultProps;
+ if (defaultProps) {
+ nextProps = this._applyDefaultProps(nextProps, defaultProps);
+ }
+ if (QWeb.dev) {
+ this._validateProps(nextProps);
+ }
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.render(forceUpdate, patchQueue);
}
}
_patch(vnode) {
- this.__owl__.renderPromise = null;
- const target = this.__owl__.vnode || document.createElement(vnode.sel);
- this.__owl__.vnode = patch(target, vnode);
+ const __owl__ = this.__owl__;
+ __owl__.renderPromise = null;
+ const target = __owl__.vnode || document.createElement(vnode.sel);
+ __owl__.vnode = patch(target, vnode);
}
_prepare() {
- this.__owl__.renderProps = this.props;
- this.__owl__.renderPromise = this._prepareAndRender();
- return this.__owl__.renderPromise;
+ const __owl__ = this.__owl__;
+ __owl__.renderProps = this.props;
+ __owl__.renderPromise = this._prepareAndRender();
+ return __owl__.renderPromise;
}
async _prepareAndRender() {
await this.willStart();
- if (this.__owl__.isDestroyed) {
+ const __owl__ = this.__owl__;
+ if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
}
const qweb = this.env.qweb;
@@ -1187,652 +1836,162 @@
}
}
}
- this.__owl__.render = qweb.render.bind(qweb, this.template);
+ __owl__.render = qweb.render.bind(qweb, this.template);
this._observeState();
return this._render();
}
async _render(force = false, patchQueue = []) {
- this.__owl__.renderId++;
+ const __owl__ = this.__owl__;
+ __owl__.renderId++;
const promises = [];
const patch = [this];
- if (this.__owl__.isMounted) {
+ if (__owl__.isMounted) {
patchQueue.push(patch);
}
- if (this.__owl__.observer) {
- this.__owl__.observer.allowMutations = false;
+ if (__owl__.observer) {
+ __owl__.observer.allowMutations = false;
}
- let vnode = this.__owl__.render(this, {
+ let vnode = __owl__.render(this, {
promises,
- handlers: this.__owl__.boundHandlers,
- mountedHandlers: this.__owl__.mountedHandlers,
+ handlers: __owl__.boundHandlers,
+ mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue
});
patch.push(vnode);
- if (this.__owl__.observer) {
- this.__owl__.observer.allowMutations = true;
+ if (__owl__.observer) {
+ __owl__.observer.allowMutations = true;
}
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child widget can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent widget. With this, we make sure that the parent widget will be
// able to patch itself properly after
- vnode.key = this.__owl__.id;
- this.__owl__.renderProps = this.props;
- this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
- return this.__owl__.renderPromise;
+ vnode.key = __owl__.id;
+ __owl__.renderProps = this.props;
+ __owl__.renderPromise = Promise.all(promises).then(() => vnode);
+ return __owl__.renderPromise;
}
/**
* Only called by qweb t-widget directive
*/
_mount(vnode, elm) {
- this.__owl__.vnode = patch(elm, vnode);
- if (this.__owl__.parent.__owl__.isMounted &&
- !this.__owl__.isMounted) {
+ const __owl__ = this.__owl__;
+ __owl__.vnode = patch(elm, vnode);
+ if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) {
this._callMounted();
}
- return this.__owl__.vnode;
+ return __owl__.vnode;
}
- __mount() {
- if (!this.__owl__.isMounted) {
- this.__owl__.isMounted = true;
+ /**
+ * Only called by qweb t-widget directive (when t-keepalive is set)
+ */
+ _remount() {
+ const __owl__ = this.__owl__;
+ if (!__owl__.isMounted) {
+ __owl__.isMounted = true;
this.mounted();
}
}
_observeState() {
if (this.state) {
- this.__owl__.observer = new Observer();
- this.__owl__.observer.observe(this.state);
- this.__owl__.observer.notifyCB = this.render.bind(this);
- }
- }
- }
-
- //------------------------------------------------------------------------------
- // Const/global stuff/helpers
- //------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const DISABLED_TAGS = [
- "input",
- "textarea",
- "button",
- "select",
- "option",
- "optgroup"
- ];
- const lineBreakRE = /[\r\n]/;
- const whitespaceRE = /\s+/g;
- const DIRECTIVE_NAMES = {
- name: 1,
- att: 1,
- attf: 1,
- key: 1
- };
- const DIRECTIVES = [];
- const NODE_HOOKS_PARAMS = {
- create: "(_, n)",
- insert: "vn",
- remove: "(vn, rm)"
- };
- const UTILS = {
- h: h,
- objectToAttrString(obj) {
- let classes = [];
- for (let k in obj) {
- if (obj[k]) {
- classes.push(k);
- }
- }
- return classes.join(" ");
- }
- };
- function parseXML(xml) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- if (doc.getElementsByTagName("parsererror").length) {
- throw new Error("Invalid XML in template");
- }
- return doc;
- }
- let nextID = 1;
- //------------------------------------------------------------------------------
- // QWeb rendering engine
- //------------------------------------------------------------------------------
- class QWeb {
- constructor(data) {
- this.templates = {};
- this.utils = UTILS;
- // the id field is useful to be able to hash qweb instances. The current
- // use case is that component's templates are qweb dependant, and need to be
- // able to map a qweb instance to a template name.
- this.id = nextID++;
- if (data) {
- this.addTemplates(data);
- }
- this.addTemplate("default", "");
- }
- static addDirective(directive) {
- DIRECTIVES.push(directive);
- DIRECTIVE_NAMES[directive.name] = 1;
- DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
- if (directive.extraNames) {
- directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
+ const __owl__ = this.__owl__;
+ __owl__.observer = new Observer();
+ __owl__.observer.observe(this.state);
+ __owl__.observer.notifyCB = this.render.bind(this);
}
}
/**
- * Add a template to the internal template map. Note that it is not
- * immediately compiled.
- */
- addTemplate(name, xmlString) {
- const doc = parseXML(xmlString);
- if (!doc.firstChild) {
- throw new Error("Invalid template (should not be empty)");
- }
- this._addTemplate(name, doc.firstChild);
- }
- /**
- * Load templates from a xml (as a string). This will look up for the first
- * tag, and will consider each child of this as a template, with
- * the name given by the t-name attribute.
- */
- addTemplates(xmlstr) {
- const doc = parseXML(xmlstr);
- const templates = doc.getElementsByTagName("templates")[0];
- if (!templates) {
- return;
- }
- for (let elem of templates.children) {
- const name = elem.getAttribute("t-name");
- this._addTemplate(name, elem);
- }
- }
- _addTemplate(name, elem) {
- if (name in this.templates) {
- throw new Error(`Template ${name} already defined`);
- }
- this._processTemplate(elem);
- const template = {
- elem,
- fn: (context, extra) => {
- const compiledFunction = this._compile(name, elem);
- template.fn = compiledFunction;
- return compiledFunction.call(this, context, extra);
- }
- };
- this.templates[name] = template;
- }
- _processTemplate(elem) {
- let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
- let node = tbranch[i];
- let prevElem = node.previousElementSibling;
- let pattr = function (name) {
- return prevElem.getAttribute(name);
- };
- let nattr = function (name) {
- return +!!node.getAttribute(name);
- };
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
- if (pattr("t-foreach")) {
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
- }
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
- return a + b;
- }) > 1) {
- throw new Error("Only one conditional branching directive is allowed per node");
- }
- // All text nodes between branch nodes are removed
- let textNode;
- while ((textNode = node.previousSibling) !== prevElem) {
- if (textNode.nodeValue.trim().length) {
- throw new Error("text is not allowed between branching directives");
- }
- textNode.remove();
- }
- }
- else {
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
- }
- }
- }
- /**
- * Render a template
+ * Apply default props (only top level).
*
- * @param {string} name the template should already have been added
+ * Note that this method does not modify in place the props, it returns a new
+ * prop object
*/
- render(name, context = {}, extra = null) {
- const template = this.templates[name];
- if (!template) {
- throw new Error(`Template ${name} does not exist`);
+ _applyDefaultProps(props, defaultProps) {
+ props = props ? Object.create(props) : {};
+ for (let propName in defaultProps) {
+ if (props[propName] === undefined) {
+ props[propName] = defaultProps[propName];
+ }
}
- return template.fn.call(this, context, extra);
- }
- _compile(name, elem) {
- const isDebug = elem.attributes.hasOwnProperty("t-debug");
- const ctx = new Context(name);
- this._compileNode(elem, ctx);
- if (ctx.shouldProtectContext) {
- ctx.code.unshift(" context = Object.create(context);");
- }
- if (ctx.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- ctx.code.unshift(" let owner = context;");
- }
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
- }
- ctx.addLine(`return vn${ctx.rootNode};`);
- let template;
- try {
- template = new Function("context", "extra", ctx.code.join("\n"));
- }
- catch (e) {
- throw new Error(`Invalid generated code while compiling template '${ctx.templateName.replace(/`/g, "'")}': ${e.message}`);
- }
- if (isDebug) {
- console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
- template.toString());
- }
- return template;
+ return props;
}
/**
- * Generate code from an xml node
- *
+ * Validate the component props (or next props) against the (static) props
+ * description. This is potentially an expensive operation: it may needs to
+ * visit recursively the props and all the children to check if they are valid.
+ * This is why it is only done in 'dev' mode.
*/
- _compileNode(node, ctx) {
- if (!(node instanceof Element)) {
- // this is a text node, there are no directive to apply
- let text = node.textContent;
- if (!ctx.inPreTag) {
- if (lineBreakRE.test(text) && !text.trim()) {
- return;
- }
- text = text.replace(whitespaceRE, " ");
- }
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
- }
- else {
- // this is an unusual situation: this text node is the result of the
- // template rendering.
- let nodeID = ctx.generateID();
- ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
- ctx.rootContext.rootNode = nodeID;
- ctx.rootContext.parentNode = nodeID;
- }
- return;
- }
- const attributes = node.attributes;
- const validDirectives = [];
- let withHandlers = false;
- // maybe this is not optimal: we iterate on all attributes here, and again
- // just after for each directive.
- for (let i = 0; i < attributes.length; i++) {
- let attrName = attributes[i].name;
- if (attrName.startsWith("t-")) {
- let dName = attrName.slice(2).split("-")[0];
- if (!(dName in DIRECTIVE_NAMES)) {
- throw new Error(`Unknown QWeb directive: '${attrName}'`);
+ _validateProps(props) {
+ const propsDef = this.constructor.props;
+ if (propsDef instanceof Array) {
+ // list of strings (prop names)
+ for (let i = 0, l = propsDef.length; i < l; i++) {
+ if (!(propsDef[i] in props)) {
+ throw new Error(`Missing props '${propsDef[i]}' (widget '${this.constructor.name}')`);
}
}
}
- for (let directive of DIRECTIVES) {
- let fullName;
- let value;
- for (let i = 0; i < attributes.length; i++) {
- const name = attributes[i].name;
- if (name === "t-" + directive.name ||
- name.startsWith("t-" + directive.name + "-")) {
- fullName = name;
- value = attributes[i].textContent;
- validDirectives.push({ directive, value, fullName });
- if (directive.name === "on") {
- withHandlers = true;
+ else if (propsDef) {
+ // propsDef is an object now
+ for (let propName in propsDef) {
+ if (!(propName in props)) {
+ if (propsDef[propName] && !propsDef[propName].optional) {
+ throw new Error(`Missing props '${propName}' (widget '${this.constructor.name}')`);
+ }
+ else {
+ break;
}
}
- }
- }
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeEncounter) {
- const isDone = directive.atNodeEncounter({
- node,
- qweb: this,
- ctx,
- fullName,
- value
- });
- if (isDone) {
- return;
+ let isValid = isValidProp(props[propName], propsDef[propName]);
+ if (!isValid) {
+ throw new Error(`Props '${propName}' of invalid type in widget '${this.constructor.name}'`);
}
}
}
- if (node.nodeName !== "t") {
- let nodeID = this._compileGenericNode(node, ctx, withHandlers);
- ctx = ctx.withParent(nodeID);
- let nodeHooks = {};
- let addNodeHook = function (hook, handler) {
- nodeHooks[hook] = nodeHooks[hook] || [];
- nodeHooks[hook].push(handler);
- };
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.atNodeCreation) {
- directive.atNodeCreation({
- node,
- qweb: this,
- ctx,
- fullName,
- value,
- nodeID,
- addNodeHook
- });
- }
- }
- if (Object.keys(nodeHooks).length) {
- ctx.addLine(`p${nodeID}.hook = {`);
- for (let hook in nodeHooks) {
- ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
- for (let handler of nodeHooks[hook]) {
- ctx.addLine(` ${handler}`);
- }
- ctx.addLine(` },`);
- }
- ctx.addLine(`};`);
- }
- }
- if (node.nodeName === "pre") {
- ctx = ctx.subContext("inPreTag", true);
- }
- this._compileChildren(node, ctx);
- for (let { directive, value, fullName } of validDirectives) {
- if (directive.finalize) {
- directive.finalize({ node, qweb: this, ctx, fullName, value });
- }
- }
- }
- _compileGenericNode(node, ctx, withHandlers = true) {
- // nodeType 1 is generic tag
- if (node.nodeType !== 1) {
- throw new Error("unsupported node type");
- }
- const attributes = node.attributes;
- const attrs = [];
- const props = [];
- const tattrs = [];
- function handleBooleanProps(key, val) {
- let isProp = false;
- if (node.nodeName === "input" && key === "checked") {
- let type = node.getAttribute("type");
- if (type === "checkbox" || type === "radio") {
- isProp = true;
- }
- }
- if (node.nodeName === "option" && key === "selected") {
- isProp = true;
- }
- if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
- isProp = true;
- }
- if ((key === "readonly" && node.nodeName === "input") ||
- node.nodeName === "textarea") {
- isProp = true;
- }
- if (isProp) {
- props.push(`${key}: _${val}`);
- }
- }
- function formatter(expr) {
- return "${" + ctx.formatExpression(expr) + "}";
- }
- for (let i = 0; i < attributes.length; i++) {
- let name = attributes[i].name;
- const value = attributes[i].textContent;
- // regular attributes
- if (!name.startsWith("t-") &&
- !node.getAttribute("t-attf-" + name)) {
- const attID = ctx.generateID();
- ctx.addLine(`var _${attID} = '${value}';`);
- if (!name.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- name = '"' + name + '"';
- }
- attrs.push(`${name}: _${attID}`);
- handleBooleanProps(name, attID);
- }
- // dynamic attributes
- if (name.startsWith("t-att-")) {
- let attName = name.slice(6);
- let formattedValue = ctx.formatExpression(ctx.getValue(value));
- if (formattedValue[0] === "{" &&
- formattedValue[formattedValue.length - 1] === "}") {
- formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
- }
- const attID = ctx.generateID();
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- // we need to combine dynamic with non dynamic attributes:
- // class="a" t-att-class="'yop'" should be rendered as class="a yop"
- const attValue = node.getAttribute(attName);
- if (attValue) {
- const attValueID = ctx.generateID();
- ctx.addLine(`var _${attValueID} = ${formattedValue};`);
- formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
- const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
- attrs.splice(attrIndex, 1);
- }
- ctx.addLine(`var _${attID} = ${formattedValue};`);
- attrs.push(`${attName}: _${attID}`);
- handleBooleanProps(attName, attID);
- }
- if (name.startsWith("t-attf-")) {
- let attName = name.slice(7);
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
- }
- const formattedExpr = value
- .replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
- .replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
- const attID = ctx.generateID();
- let staticVal = node.getAttribute(attName);
- if (staticVal) {
- ctx.addLine(`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`);
- }
- else {
- ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
- }
- attrs.push(`${attName}: _${attID}`);
- }
- // t-att= attributes
- if (name === "t-att") {
- let id = ctx.generateID();
- ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`);
- tattrs.push(id);
- }
- }
- let nodeID = ctx.generateID();
- let nodeKey = node.getAttribute("t-key");
- if (nodeKey) {
- nodeKey = ctx.formatExpression(nodeKey);
- }
- else {
- nodeKey = nodeID;
- }
- const parts = [`key:${nodeKey}`];
- if (attrs.length + tattrs.length > 0) {
- parts.push(`attrs:{${attrs.join(",")}}`);
- }
- if (props.length > 0) {
- parts.push(`props:{${props.join(",")}}`);
- }
- if (withHandlers) {
- parts.push(`on:{}`);
- }
- ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
- for (let id of tattrs) {
- ctx.addIf(`_${id} instanceof Array`);
- ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
- ctx.addElse();
- ctx.addLine(`for (let key in _${id}) {`);
- ctx.indent();
- ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
- ctx.dedent();
- ctx.addLine(`}`);
- ctx.closeIf();
- }
- ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
- if (ctx.parentNode) {
- ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
- }
- return nodeID;
- }
- _compileChildren(node, ctx) {
- if (node.childNodes.length > 0) {
- for (let child of Array.from(node.childNodes)) {
- this._compileNode(child, ctx);
- }
- }
}
}
//------------------------------------------------------------------------------
- // Compilation Context
+ // Prop validation helper
//------------------------------------------------------------------------------
- class Context {
- constructor(name) {
- this.nextID = 1;
- this.code = [];
- this.variables = {};
- this.definedVariables = {};
- this.escaping = false;
- this.parentNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldProtectContext = false;
- this.inLoop = false;
- this.inPreTag = false;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.addLine("var h = this.utils.h;");
- }
- generateID() {
- const id = this.rootContext.nextID++;
- return id;
- }
- withParent(node) {
- if (this === this.rootContext && this.parentNode) {
- throw new Error("A template should not have more than one root node");
+ /**
+ * Check if an invidual prop value matches its (static) prop definition
+ */
+ function isValidProp(prop, propDef) {
+ if (typeof propDef === "function") {
+ // Check if a value is constructed by some Constructor. Note that there is a
+ // slight abuse of language: we want to consider primitive values as well.
+ //
+ // So, even though 1 is not an instance of Number, we want to consider that
+ // it is valid.
+ if (typeof prop === "object") {
+ return prop instanceof propDef;
}
- if (!this.rootContext.rootNode) {
- this.rootContext.rootNode = node;
+ return typeof prop === propDef.name.toLowerCase();
+ }
+ else if (propDef instanceof Array) {
+ // If this code is executed, this means that we want to check if a prop
+ // matches at least one of its descriptor.
+ let result = false;
+ for (let i = 0, iLen = propDef.length; i < iLen; i++) {
+ result = result || isValidProp(prop, propDef[i]);
}
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- formatExpression(e) {
- e = e.trim();
- if (e[0] === "{" && e[e.length - 1] === "}") {
- const innerExpr = e
- .slice(1, -1)
- .split(",")
- .map(p => {
- let [key, val] = p.trim().split(":");
- if (key === "") {
- return "";
- }
- if (!val) {
- val = key;
- }
- return `${key}: ${this.formatExpression(val)}`;
- })
- .join(",");
- return "{" + innerExpr + "}";
- }
- // Thanks CHM for this code...
- const chars = e.split("");
- let instring = "";
- let invar = "";
- let invarPos = 0;
- let r = "";
- chars.push(" ");
- for (var i = 0, ilen = chars.length; i < ilen; i++) {
- var c = chars[i];
- if (instring.length) {
- if (c === instring && chars[i - 1] !== "\\") {
- instring = "";
- }
- }
- else if (c === '"' || c === "'") {
- instring = c;
- }
- else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
- invar = c;
- invarPos = i;
- continue;
- }
- else if (c.match(/\W/) && invar.length) {
- // TODO: Should check for possible spaces before dot
- if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
- if (!(invar in this.definedVariables)) {
- invar =
- WORD_REPLACEMENT[invar] ||
- (invar in this.variables &&
- this.formatExpression(this.variables[invar])) ||
- "context['" + invar + "']";
- }
- }
- r += invar;
- invar = "";
- }
- else if (invar.length) {
- invar += c;
- continue;
- }
- r += c;
- }
- const result = r.slice(0, -1);
return result;
}
+ // propsDef is an object
+ let result = isValidProp(prop, propDef.type);
+ if (propDef.type === Array) {
+ for (let i = 0, iLen = prop.length; i < iLen; i++) {
+ result = result && isValidProp(prop[i], propDef.element);
+ }
+ }
+ if (propDef.type === Object) {
+ const shape = propDef.shape;
+ for (let key in shape) {
+ result = result && isValidProp(prop[key], shape[key]);
+ }
+ }
+ return result;
}
/**
@@ -2057,12 +2216,13 @@
ctx.addLine(`if (typeof _${arrayID} === 'number') { _${arrayID} = Array.from(Array(_${arrayID}).keys())}`);
let keysID = ctx.generateID();
ctx.addLine(`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`);
+ ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let valuesID = ctx.generateID();
ctx.addLine(`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`);
- ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
+ ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
ctx.indent();
ctx.addLine(`context.${name}_first = i === 0;`);
- ctx.addLine(`context.${name}_last = i === _${keysID}.length - 1;`);
+ ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd';`);
ctx.addLine(`context.${name}_index = i;`);
ctx.addLine(`context.${name} = _${keysID}[i];`);
@@ -2127,26 +2287,51 @@
//------------------------------------------------------------------------------
// t-on
//------------------------------------------------------------------------------
+ // these are pieces of code that will be injected into the event handler if
+ // modifiers are specified
+ const MODS_CODE = {
+ prevent: "e.preventDefault();",
+ self: "if (e.target !== this.elm) {return}",
+ stop: "e.stopPropagation();"
+ };
QWeb.addDirective({
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
ctx.rootContext.shouldDefineOwner = true;
- const eventName = fullName.slice(5);
+ const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
- let handler = value.replace(/\(.*\)/, function (args) {
+ let handlerName = value.replace(/\(.*\)/, function (args) {
extraArgs = args.slice(1, -1);
return "";
});
- let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)})()`;
- if (extraArgs) {
- ctx.addLine(`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(extraArgs)});`);
+ ctx.addIf(`!context['${handlerName}']`);
+ ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`);
+ ctx.closeIf();
+ let params = extraArgs
+ ? `owner, ${ctx.formatExpression(extraArgs)}`
+ : "owner";
+ let handler;
+ if (mods.length > 0) {
+ handler = `function (e) {`;
+ handler += mods
+ .map(function (mod) {
+ return MODS_CODE[mod];
+ })
+ .join("");
+ handler += `context['${handlerName}'].call(${params}, e);}`;
}
else {
- ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`);
+ handler = `context['${handlerName}'].bind(${params})`;
+ }
+ if (extraArgs) {
+ ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
+ }
+ else {
+ ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`);
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
}
}
@@ -2157,9 +2342,9 @@
QWeb.addDirective({
name: "ref",
priority: 95,
- atNodeCreation({ ctx, nodeID, value, addNodeHook }) {
+ atNodeCreation({ ctx, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`);
+ ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
}
});
@@ -2169,11 +2354,9 @@
UTILS.nextFrame = function (cb) {
requestAnimationFrame(() => requestAnimationFrame(cb));
};
- UTILS.transitionCreate = function (elm, name) {
+ UTILS.transitionInsert = function (elm, name) {
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
- };
- UTILS.transitionInsert = function (elm, name) {
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
@@ -2189,7 +2372,7 @@
elm.classList.add(name + "-leave-active");
const finalize = () => {
elm.classList.remove(name + "-leave-active");
- elm.classList.remove(name + "-enter-to");
+ elm.classList.remove(name + "-leave-to");
rm();
};
this.nextFrame(() => {
@@ -2229,10 +2412,9 @@
QWeb.addDirective({
name: "transition",
priority: 96,
- atNodeCreation({ ctx, value, addNodeHook }) {
+ atNodeCreation({ value, addNodeHook }) {
let name = value;
const hooks = {
- create: `this.utils.transitionCreate(n.elm, '${name}');`,
insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
};
@@ -2254,7 +2436,10 @@
* explanation of the code generated by the t-widget directive for the following
* situation:
* ```xml
- *
+ *
* ```
*
* ```js
@@ -2314,6 +2499,8 @@
* }
* w4 = new W4(owner, props4);
*
+ * let utils = this.utils;
+ *
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
@@ -2343,15 +2530,21 @@
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
+ * // what follows is only present if there are animations on the widget
+ * utils.transitionInsert(vn.elm, "fade");
* },
* remove() {
- * // apparently, in some cases, it is necessary to call the destroy
- * // method here
- * w4.destroy();
+ * // override with empty function to prevent from removing the node
+ * // directly. It will be removed when destroy is called anyway, which
+ * // delays the removal if there are animations.
* },
* destroy() {
- * // and here...
- * w4.destroy();
+ * // if there are animations, we delay the call to destroy on the
+ * // widget, if not, we call it directly.
+ * let finalize = () => {
+ * w4.destroy();
+ * };
+ * utils.transitionRemove(vn.elm, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
@@ -2393,16 +2586,22 @@
atNodeEncounter({ ctx, value, node }) {
ctx.addLine("//WIDGET");
ctx.rootContext.shouldDefineOwner = true;
+ ctx.rootContext.shouldDefineQWeb = true;
+ ctx.rootContext.shouldDefineUtils = true;
let props = node.getAttribute("t-props");
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- // t-on- events...
+ // t-on- events and t-transition
const events = [];
+ let transition = "";
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
if (name.startsWith("t-on-")) {
events.push([name.slice(5), attributes[i].textContent]);
}
+ else if (name === "t-transition") {
+ transition = attributes[i].textContent;
+ }
}
let key = node.getAttribute("t-key");
if (key) {
@@ -2433,12 +2632,22 @@
let refKey = "";
if (ref) {
refKey = `ref${ctx.generateID()}`;
- ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`);
+ ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${widgetID};`;
}
- let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}()`;
+ let transitionsInsertCode = "";
+ if (transition) {
+ transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
+ }
+ let finalizeWidgetCode = `w${widgetID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref) {
- finalizeWidgetCode += `;delete context.refs[${refKey}]`;
+ finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
+ }
+ if (transition) {
+ finalizeWidgetCode = `let finalize = () => {
+ ${finalizeWidgetCode}
+ };
+ utils.transitionRemove(vn.elm, '${transition}', finalize);`;
}
let createHook = "";
let classAttr = node.getAttribute("class");
@@ -2482,22 +2691,25 @@
ctx.closeIf();
ctx.addIf(`!w${widgetID}`);
// new widget
- ctx.addLine(`let W${widgetID} = context.widgets['${value}'];`);
+ ctx.addLine(`let widgetKey${widgetID} = ${ctx.interpolate(value)};`);
+ ctx.addLine(`let W${widgetID} = context.widgets && context.widgets[widgetKey${widgetID}] || QWeb.widgets[widgetKey${widgetID}];`);
// maybe only do this in dev mode...
- ctx.addLine(`if (!W${widgetID}) {throw new Error(\`Cannot find the definition of widget "${value}"\`)}`);
+ ctx.addLine(`if (!W${widgetID}) {throw new Error('Cannot find the definition of widget "' + widgetKey${widgetID} + '"')}`);
ctx.addLine(`w${widgetID} = new W${widgetID}(owner, props${widgetID});`);
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`);
for (let [event, method] of events) {
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn){let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`);
+ // hack: specify empty remove hook to prevent the node from being removed from the DOM
+ // FIXME: click to re-add widget during remove transition -> leak
+ ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`);
ctx.addElse();
// need to update widget
ctx.addLine(`def${defID} = w${widgetID}._updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`);
let keepAliveCode = "";
if (keepAlive) {
- keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}.__mount();};`;
+ keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${widgetID}.el,vn.elm);vn.elm=w${widgetID}.el;w${widgetID}._remount();};`;
}
ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${tattStyle ? `w${widgetID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${widgetID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`);
ctx.closeIf();
@@ -2829,19 +3041,36 @@
*
* Note that dynamic values, such as a date or a commit hash are added by rollup
*/
+ const __info__ = {};
+ Object.defineProperty(__info__, "mode", {
+ get() {
+ return QWeb.dev ? "dev" : "prod";
+ },
+ set(mode) {
+ QWeb.dev = mode === "dev";
+ if (QWeb.dev) {
+ const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
+ console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
+ }
+ else {
+ console.log(`Owl is now running in 'prod' mode.`);
+ }
+ }
+ });
const utils = _utils;
- exports.utils = utils;
exports.Component = Component;
exports.EventBus = EventBus;
exports.Observer = Observer;
exports.QWeb = QWeb;
- exports.connect = connect;
exports.Store = Store;
+ exports.__info__ = __info__;
+ exports.connect = connect;
+ exports.utils = utils;
- exports._version = '0.11.0';
- exports._date = '2019-05-17T21:35:18.307Z';
- exports._hash = '6719650';
- exports._url = 'https://github.com/odoo/owl';
+ exports.__info__.version = '0.12.0';
+ exports.__info__.date = '2019-05-29T09:26:54.764Z';
+ exports.__info__.hash = 'f60904b';
+ exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {}));
diff --git a/playground/app.js b/playground/app.js
index 5c3ec3df..1a211c83 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -84,7 +84,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
- const content = `window.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
+ const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`\n${js}`;
script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
@@ -153,7 +153,7 @@ owl.utils.whenReady(startApp);`;
class App extends owl.Component {
constructor(...args) {
super(...args);
- this.version = owl._version;
+ this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.widgets = { TabbedEditor };
@@ -349,7 +349,7 @@ class TabbedEditor extends owl.Component {
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
-document.title = `${document.title} (v${owl._version})`;
+document.title = `${document.title} (v${owl.__info__.version})`;
document.addEventListener("DOMContentLoaded", async function() {
const templates = await owl.utils.loadTemplates("templates.xml");
const qweb = new owl.QWeb(templates);
diff --git a/playground/samples.js b/playground/samples.js
index 36dd3118..3351b395 100644
--- a/playground/samples.js
+++ b/playground/samples.js
@@ -40,31 +40,34 @@ const counter = new ClickCounter({ qweb });
counter.mount(document.body);
`;
-const WIDGET_COMPOSITION = `class ClickCounter extends owl.Component {
- constructor(parent, props) {
- super(parent, props);
- this.state = { value: props.initialState || 0 };
- }
+const WIDGET_COMPOSITION = `// This example will not work if your browser does not support ESNext class fields
+
+class ClickCounter extends owl.Component {
+ state = { value: 0 };
increment() {
this.state.value++;
}
}
-let nextId = 1;
-
-class App extends owl.Component {
- constructor() {
- super(...arguments);
- this.state = { counters: [] }
- this.widgets = { ClickCounter };
- }
-
- addCounter() {
- this.state.counters.push(nextId++);
+class InputWidget extends owl.Component {
+ state = {text: ""};
+
+ updateVal(event) {
+ let value = event.target.value;
+ if (this.props.reverse) {
+ value = value.split("").reverse().join("");
+ }
+ this.state.text = value
}
}
+// Main root widget
+class App extends owl.Component {
+ widgets = {ClickCounter, InputWidget};
+}
+
+// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
@@ -75,29 +78,49 @@ const WIDGET_COMPOSITION_XML = `
Click Me! []
+
+
+
+
+
-
-
-
-
-
-
+
+
`;
-const WIDGET_COMPOSITION_CSS = `button {
- color: darkred;
- font-size: 30px;
+const WIDGET_COMPOSITION_CSS = `button, input {
+ font-size: 20px;
width: 220px;
+ margin: 5px;
}`;
const ANIMATION = `// This example will not work if your browser does not support ESNext class fields
+class ClickCounter extends owl.Component {
+ state = { value: 0 };
+
+ increment() {
+ this.state.value++;
+ }
+}
+
class App extends owl.Component {
- state = {flag: 0};
+ state = {flag: false, widgetFlag: false, numbers: []};
+ widgets = {ClickCounter};
toggle() {
this.state.flag = !this.state.flag;
}
+
+ toggleWidget() {
+ this.state.widgetFlag = !this.state.widgetFlag;
+ }
+
+ addNumber() {
+ const n = this.state.numbers.length + 1;
+ this.state.numbers.push(n);
+ }
+
}
const qweb = new owl.QWeb(TEMPLATES);
@@ -106,33 +129,82 @@ app.mount(document.body);
`;
const ANIMATION_XML = `
-
-
\ No newline at end of file