From d048348e073680148af8866a61314a5c4b64a6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 29 May 2019 11:29:25 +0200 Subject: [PATCH] [IMP] owl: update to v0.12.0 --- owl.js | 1595 ++++++++++++++++++++++---------------- playground/app.js | 6 +- playground/samples.js | 350 +++------ playground/templates.xml | 6 +- 4 files changed, 1047 insertions(+), 910 deletions(-) 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 = ` -
- -
-
Hello
+ +
+ +

Transition on DOM element

+ +
+ +
+
Hello
+
+ +

Transition on sub widgets

+ +
+ +
+ +
+
+ +

Transition on lists

+

Transitions can also be applied on lists

+
+ +
+ + + +
+
+ +

Simple CSS animation

+

Remember, normal CSS still apply: for example, a simple flash animation with pure css

+
`; const ANIMATION_CSS = `button { - width: 100px; - height: 30px; - font-size: 20px; + font-size: 18px; + height: 35px; } +.btn { + cursor: pointer; + padding: 5px; + margin: 5px; + background-color: #dddddd; +} + + +.flash { + background-position: center; + transition: background 0.5s; +} + +.flash:active { + background-color: gray; + transition: background 0s; +} + .square { background-color: red; width: 100px; - height: 100px; + height: 70px; color: white; - margin: 20px; + margin: 0 20px; font-size: 24px; - line-height: 100px; + line-height: 70px; text-align: center; - line-height: 100px; } .fade-enter-active, .fade-leave-active { @@ -141,6 +213,24 @@ const ANIMATION_CSS = `button { .fade-enter, .fade-leave-to { opacity: 0; } + +.demo { + display: flex; + height: 80px; +} + +.clickcounter { + margin-left: 20px; + height: 50px; + background-color: blue; + color: white; +} + +.numberspan { + border: 1px solid green; + margin: 5px; + padding: 5px; +} `; @@ -203,182 +293,6 @@ const LIFECYCLE_DEMO_XML = `
Demo Sub Widget. Props: . State: . (click on me to update me)
`; -const BENCHMARK_APP = `//------------------------------------------------------------------------------ -// Generating demo data -//------------------------------------------------------------------------------ -const messages = []; -const authors = ["Aaron", "David", "Vincent"]; -const content = [ - "Lorem ipsum dolor sit amet", - "Sed ut perspiciatis unde omnis iste natus error sit voluptatem", - "Excepteur sint occaecat cupidatat non proident" -]; - -function chooseRandomly(array) { - const index = Math.floor(Math.random() * array.length); - return array[index]; -} - -for (let i = 1; i < 16000; i++) { - messages.push({ - id: i, - author: chooseRandomly(authors), - msg: \`\${i}: \${chooseRandomly(content)}\`, - likes: 0 - }); -} - -//------------------------------------------------------------------------------ -// Counter Widget -//------------------------------------------------------------------------------ -class Counter extends owl.Component { - constructor(parent, props) { - super(parent, props); - this.state = { counter: props.initialState || 0 }; - } - - increment(delta) { - this.state.counter += delta; - } -} - -//------------------------------------------------------------------------------ -// Message Widget -//------------------------------------------------------------------------------ -class Message extends owl.Component { - constructor() { - super(...arguments); - this.widgets = { Counter }; - } - - removeMessage() { - this.trigger("remove_message", { - id: this.props.id - }); - } -} - -//------------------------------------------------------------------------------ -// Root Widget -//------------------------------------------------------------------------------ -class App extends owl.Component { - constructor() { - super(...arguments); - this.widgets = { Message }; - this.state = { messages: messages.slice(0, 10) }; - } - - setMessageCount(n) { - this.state.messages = messages.slice(0, n); - } - - removeMessage(data) { - const index = messages.findIndex(m => m.id === data.id); - this.state.messages.splice(index, 1); - } - - increment(delta) { - const n = this.state.messages.length + delta; - this.setMessageCount(n); - } -} - -//------------------------------------------------------------------------------ -// Application initialization -//------------------------------------------------------------------------------ -const env = { - qweb: new owl.QWeb(TEMPLATES) -}; - -const app = new App(env); -app.mount(document.body); -`; - -const BENCHMARK_APP_CSS = `.main { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - - display: grid; - grid-template-columns: 220px 1fr; -} - -.left-thing { - background-color: gray; - padding: 20px; -} - -.left-thing button { - width: 100%; -} - -.left-thing .counter span { - color: white; -} - -.left-thing .counter button { - width: 40px; -} - -.right-thing { - padding: 20px; - overflow: auto; -} - -/* Message widget */ -.message .author { - font-weight: bold; -} - -.message { - width: 400px; - background-color: lightblue; - margin: 10px 5px; - border-radius: 5px; - padding: 5px; -}`; - -const BENCHMARK_APP_XML = ` -
-
-
- - Value: - -
- - - - - - -
-
-
- - - -
-
-
- -
- - - - -
- -
- - Value: - -
- -
`; - const TODO_APP_STORE = `const ENTER_KEY = 13; const ESC_KEY = 27; const LOCALSTORAGE_KEY = "todos-odoo"; @@ -637,7 +551,7 @@ const TODO_APP_STORE_XML = `
- + `; @@ -1205,12 +1119,6 @@ export const SAMPLES = [ code: LIFECYCLE_DEMO, xml: LIFECYCLE_DEMO_XML }, - { - description: "Benchmark application", - code: BENCHMARK_APP, - css: BENCHMARK_APP_CSS, - xml: BENCHMARK_APP_XML - }, { description: "Todo List App (with store)", code: TODO_APP_STORE, diff --git a/playground/templates.xml b/playground/templates.xml index f7ee67de..5b384e6b 100644 --- a/playground/templates.xml +++ b/playground/templates.xml @@ -2,12 +2,12 @@
-
+
@@ -48,7 +48,7 @@
-
+
\ No newline at end of file