From eccd1d4a93116d16d96f72c60a5dabb266302738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 28 Jun 2019 11:41:03 +0200 Subject: [PATCH] [IMP] owl: update to v0.16.0 --- owl.js | 632 +++++++++++++++++++++++---------------- playground/app.js | 41 ++- playground/samples.js | 37 ++- playground/templates.xml | 2 +- 4 files changed, 413 insertions(+), 299 deletions(-) diff --git a/owl.js b/owl.js index fb1bbce6..fd8d41ed 100644 --- a/owl.js +++ b/owl.js @@ -22,15 +22,7 @@ //------------------------------------------------------------------------------ // we define here a new modified Array prototype, which basically override all // Array methods that change some state to be able to track their changes - const methodsToPatch = [ - "push", - "pop", - "shift", - "unshift", - "splice", - "sort", - "reverse" - ]; + const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]; const methodLen = methodsToPatch.length; const ArrayProto = Array.prototype; const ModifiedArrayProto = Object.create(ArrayProto); @@ -118,8 +110,7 @@ } } set(target, key, value) { - let alreadyDefined = key in target && - Object.getOwnPropertyDescriptor(target, key).configurable === false; + let alreadyDefined = key in target && Object.getOwnPropertyDescriptor(target, key).configurable === false; if (alreadyDefined) { target[key] = value; } @@ -240,14 +231,7 @@ } return map; } - const hooks = [ - "create", - "update", - "remove", - "destroy", - "pre", - "post" - ]; + const hooks = ["create", "update", "remove", "destroy", "pre", "post"]; function init(modules, domApi) { let i, j, cbs = {}; const api = domApi !== undefined ? domApi : htmlDomApi; @@ -294,9 +278,7 @@ const dotIdx = sel.indexOf(".", hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; - const tag = hashIdx !== -1 || dotIdx !== -1 - ? sel.slice(0, Math.min(hash, dot)) - : sel; + const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; const elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) ? api.createElementNS(i, tag) @@ -366,9 +348,7 @@ rm = createRmCb(ch.elm, listeners); for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm); - if (isDef((i = ch.data)) && - isDef((i = i.hook)) && - isDef((i = i.remove))) { + if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) { i(ch, rm); } else { @@ -467,9 +447,7 @@ } function patchVnode(oldVnode, vnode, insertedVnodeQueue) { let i, iLen, hook; - if (isDef((i = vnode.data)) && - isDef((hook = i.hook)) && - isDef((i = hook.prepatch))) { + if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) { i(oldVnode, vnode); } const elm = (vnode.elm = oldVnode.elm); @@ -752,8 +730,7 @@ // add new listeners which has not already attached if (on) { // reuse existing listener or create new - var listener = (vnode.listener = - oldVnode.listener || createListener()); + var listener = (vnode.listener = oldVnode.listener || createListener()); // update vnode for listener listener.vnode = vnode; // if element changed or added we add all needed listeners unconditionally @@ -832,7 +809,35 @@ create: updateAttrs, update: updateAttrs }; - const patch = init([eventListenersModule, attrsModule, propsModule]); + //------------------------------------------------------------------------------ + // class.ts + //------------------------------------------------------------------------------ + function updateClass(oldVnode, vnode) { + var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class; + if (!oldClass && !klass) + return; + if (oldClass === klass) + return; + oldClass = oldClass || {}; + klass = klass || {}; + elm = vnode.elm; + for (name in oldClass) { + if (!klass[name]) { + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + elm.classList[cur ? "add" : "remove"](name); + } + } + } + const classModule = { create: updateClass, update: updateClass }; + //------------------------------------------------------------------------------ + // patch + //------------------------------------------------------------------------------ + const patch = init([eventListenersModule, attrsModule, propsModule, classModule]); /** * Owl QWeb Expression Parser @@ -878,7 +883,7 @@ "(": "LEFT_PAREN", ")": "RIGHT_PAREN" }; - const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(','); + const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(","); let tokenizeString = function (expr) { let s = expr[0]; let start = s; @@ -1031,8 +1036,7 @@ if (prevToken.type === "OPERATOR" && prevToken.value === ".") { isVar = false; } - else if (prevToken.type === "LEFT_BRACE" || - prevToken.type === "COMMA") { + else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { let nextToken = tokens[i + 1]; if (nextToken && nextToken.type === "COLON") { isVar = false; @@ -1053,17 +1057,75 @@ return result; } + /** + * We define here a simple event bus: it can + * - emit events + * - add/remove listeners. + * + * This is a useful pattern of communication in many cases. For OWL, each + * components and stores are event buses. + */ + //------------------------------------------------------------------------------ + // EventBus + //------------------------------------------------------------------------------ + class EventBus { + constructor() { + this.subscriptions = {}; + } + /** + * Add a listener for the 'eventType' events. + * + * Note that the 'owner' of this event can be anything, but will more likely + * be a component or a class. The idea is that the callback will be called with + * the proper owner bound. + * + * Also, the owner should be kind of unique. This will be used to remove the + * listener. + */ + on(eventType, owner, callback) { + if (!callback) { + throw new Error("Missing callback"); + } + if (!this.subscriptions[eventType]) { + this.subscriptions[eventType] = []; + } + this.subscriptions[eventType].push({ + owner, + callback + }); + } + /** + * Remove a listener + */ + off(eventType, owner) { + const subs = this.subscriptions[eventType]; + if (subs) { + this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); + } + } + /** + * Emit an event of type 'eventType'. Any extra arguments will be passed to + * the listeners callback. + */ + trigger(eventType, ...args) { + const subs = this.subscriptions[eventType] || []; + for (let i = 0, iLen = subs.length; i < iLen; i++) { + const sub = subs[i]; + sub.callback.call(sub.owner, ...args); + } + } + /** + * Remove all subscriptions. + */ + clear() { + this.subscriptions = {}; + } + } + //------------------------------------------------------------------------------ // Const/global stuff/helpers //------------------------------------------------------------------------------ - const DISABLED_TAGS = [ - "input", - "textarea", - "button", - "select", - "option", - "optgroup" - ]; + const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"]; const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; const DIRECTIVE_NAMES = { @@ -1080,14 +1142,20 @@ }; const UTILS = { h: h, - objectToAttrString(obj) { - let classes = []; - for (let k in obj) { - if (obj[k]) { - classes.push(k); + toObj(expr) { + if (typeof expr === "string") { + expr = expr.trim(); + if (!expr) { + return {}; } + let words = expr.split(/\s+/); + let result = {}; + for (let i = 0; i < words.length; i++) { + result[words[i]] = true; + } + return result; } - return classes.join(" "); + return expr; }, shallowEqual(p1, p2) { for (let k in p1) { @@ -1110,8 +1178,9 @@ //------------------------------------------------------------------------------ // QWeb rendering engine //------------------------------------------------------------------------------ - class QWeb { + class QWeb extends EventBus { constructor(data) { + super(); this.templates = {}; this.utils = UTILS; // the id field is useful to be able to hash qweb instances. The current @@ -1144,7 +1213,10 @@ * Add a template to the internal template map. Note that it is not * immediately compiled. */ - addTemplate(name, xmlString) { + addTemplate(name, xmlString, allowDuplicate) { + if (allowDuplicate && name in this.templates) { + return; + } const doc = parseXML(xmlString); if (!doc.firstChild) { throw new Error("Invalid template (should not be empty)"); @@ -1228,9 +1300,15 @@ } return template.fn.call(this, context, extra); } - _compile(name, elem) { + _compile(name, elem, parentNode) { const isDebug = elem.attributes.hasOwnProperty("t-debug"); const ctx = new Context(name); + if (parentNode) { + ctx.nextID = parentNode + 1; + ctx.parentNode = parentNode; + ctx.allowMultipleRoots = true; + ctx.addLine(`let c${parentNode} = extra.parentNode;`); + } this._compileNode(elem, ctx); if (ctx.shouldProtectContext) { ctx.code.unshift(" context = Object.create(context);"); @@ -1246,10 +1324,12 @@ if (ctx.shouldDefineUtils) { ctx.code.unshift(" let utils = this.utils;"); } - if (!ctx.rootNode) { - throw new Error("A template should have one root node"); + if (!parentNode) { + if (!ctx.rootNode) { + throw new Error("A template should have one root node"); + } + ctx.addLine(`return vn${ctx.rootNode};`); } - ctx.addLine(`return vn${ctx.rootNode};`); let template; try { template = new Function("context", "extra", ctx.code.join("\n")); @@ -1262,8 +1342,11 @@ throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`); } if (isDebug) { - console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` + - template.toString()); + const tpl = this.templates[name]; + if (tpl) { + const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`; + console.log(msg); + } } return template; } @@ -1301,8 +1384,7 @@ if (firstLetter === firstLetter.toUpperCase()) { // this is a component, we modify in place the xml document to change // to - node.setAttribute('t-component', node.tagName); - node.nodeValue = 't'; + node.setAttribute("t-component", node.tagName); } const attributes = node.attributes; const validDirectives = []; @@ -1332,7 +1414,7 @@ fullName = name; value = attributes[j].textContent; validDirectives.push({ directive, value, fullName }); - if (directive.name === "on" || directive.name === 'model') { + if (directive.name === "on" || directive.name === "model") { withHandlers = true; } } @@ -1418,56 +1500,74 @@ if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) { isProp = true; } - if ((key === "readonly" && node.nodeName === "input") || - node.nodeName === "textarea") { + if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") { isProp = true; } if (isProp) { props.push(`${key}: _${val}`); } } + let classObj = ""; 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)) { + 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 + '"'; + if (name === "class") { + let classDef = value + .trim() + .split(/\s+/) + .map(a => `'${a}':true`) + .join(","); + classObj = `_${ctx.generateID()}`; + ctx.addLine(`let ${classObj} = {${classDef}};`); + } + else { + 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); } - attrs.push(`${name}: _${attID}`); - handleBooleanProps(name, attID); } // dynamic attributes if (name.startsWith("t-att-")) { let attName = name.slice(6); const v = ctx.getValue(value); let formattedValue = v.id || ctx.formatExpression(v); - if (formattedValue[0] === "{" && - formattedValue[formattedValue.length - 1] === "}") { - formattedValue = `this.utils.objectToAttrString(${formattedValue})`; + if (attName === "class") { + formattedValue = `this.utils.toObj(${formattedValue})`; + if (classObj) { + ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); + } + else { + classObj = `_${ctx.generateID()}`; + ctx.addLine(`let ${classObj} = ${formattedValue};`); + } } - const attID = ctx.generateID(); - if (!attName.match(/^[a-zA-Z]+$/)) { - // attribute contains 'non letters' => we want to quote it - attName = '"' + attName + '"'; + else { + 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); } - // 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); @@ -1508,6 +1608,9 @@ if (props.length > 0) { parts.push(`props:{${props.join(",")}}`); } + if (classObj) { + parts.push(`class:${classObj}`); + } if (withHandlers) { parts.push(`on:{}`); } @@ -1559,6 +1662,7 @@ this.shouldProtectContext = false; this.inLoop = false; this.inPreTag = false; + this.allowMultipleRoots = false; this.rootContext = this; this.templateName = name || "noname"; this.addLine("var h = this.utils.h;"); @@ -1568,7 +1672,9 @@ return id; } withParent(node) { - if (this === this.rootContext && (this.parentNode || this.parentTextNode)) { + if (!this.allowMultipleRoots && + this === this.rootContext && + (this.parentNode || this.parentTextNode)) { throw new Error("A template should not have more than one root node"); } if (!this.rootContext.rootNode) { @@ -1670,10 +1776,10 @@ this.refs = {}; const defaultProps = this.constructor.defaultProps; if (defaultProps) { - props = this._applyDefaultProps(props, defaultProps); + props = this.__applyDefaultProps(props, defaultProps); } if (QWeb.dev) { - this._validateProps(props || {}); + this.__validateProps(props || {}); } // is this a good idea? // Pro: if props is empty, we can create easily a component @@ -1689,6 +1795,19 @@ } else { this.env = parent; + this.env.qweb.on("update", this, () => { + if (this.__owl__.isMounted) { + this.render(true); + } + if (this.__owl__.isDestroyed) { + // this is unlikely to happen, but if a root widget is destroyed, + // we want to remove our subscription. The usual way to do that + // would be to perform some check in the destroy method, but since + // it is very performance sensitive, and since this is a rare event, + // we simply do it lazily + this.env.qweb.off("update", this); + } + }); } this.__owl__ = { id: id, @@ -1792,20 +1911,20 @@ * created declaratively in templates are managed by the Owl system. */ async mount(target) { - const vnode = await this._prepare(); + const vnode = await this.__prepare(); if (this.__owl__.isDestroyed) { // component was destroyed before we get here... return; } - this._patch(vnode); + this.__patch(vnode); target.appendChild(this.el); if (document.body.contains(target)) { - this._callMounted(); + this.__callMounted(); } } unmount() { if (this.__owl__.isMounted) { - this._callWillUnmount(); + this.__callWillUnmount(); this.el.remove(); } } @@ -1818,13 +1937,13 @@ if (shouldPatch) { patchQueue = []; } - const renderVDom = this._render(force, patchQueue); + const renderVDom = this.__render(force, patchQueue); const renderId = __owl__.renderId; await renderVDom; 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. - this._applyPatchQueue(patchQueue); + this.__applyPatchQueue(patchQueue); } } /** @@ -1840,7 +1959,7 @@ const __owl__ = this.__owl__; if (!__owl__.isDestroyed) { const el = this.el; - this._destroy(__owl__.parent); + this.__destroy(__owl__.parent); if (el) { el.remove(); } @@ -1892,7 +2011,7 @@ //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - _destroy(parent) { + __destroy(parent) { const __owl__ = this.__owl__; const isMounted = __owl__.isMounted; if (isMounted) { @@ -1901,7 +2020,7 @@ } const children = __owl__.children; for (let key in children) { - children[key]._destroy(this); + children[key].__destroy(this); } if (parent) { let id = __owl__.id; @@ -1911,13 +2030,13 @@ __owl__.isDestroyed = true; delete __owl__.vnode; } - _callMounted() { + __callMounted() { 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(); + comp.__callMounted(); } } __owl__.isMounted = true; @@ -1927,7 +2046,7 @@ } this.mounted(); } - _callWillUnmount() { + __callWillUnmount() { this.willUnmount(); const __owl__ = this.__owl__; __owl__.isMounted = false; @@ -1935,38 +2054,41 @@ for (let id in children) { const comp = children[id]; if (comp.__owl__.isMounted) { - comp._callWillUnmount(); + comp.__callWillUnmount(); } } } - async _updateProps(nextProps, forceUpdate = false, patchQueue) { + 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); + nextProps = this.__applyDefaultProps(nextProps, defaultProps); } if (QWeb.dev) { - this._validateProps(nextProps); + this.__validateProps(nextProps); } await this.willUpdateProps(nextProps); this.props = nextProps; await this.render(forceUpdate, patchQueue); } } - _patch(vnode) { + __patch(vnode) { const __owl__ = this.__owl__; __owl__.renderPromise = null; const target = __owl__.vnode || document.createElement(vnode.sel); + if (this.__owl__.classObj) { + vnode.data.class = Object.assign(vnode.data.class || {}, this.__owl__.classObj); + } __owl__.vnode = patch(target, vnode); } - _prepare() { + __prepare() { const __owl__ = this.__owl__; __owl__.renderProps = this.props; - __owl__.renderPromise = this._prepareAndRender(); + __owl__.renderPromise = this.__prepareAndRender(); return __owl__.renderPromise; } - async _prepareAndRender() { + async __prepareAndRender() { await this.willStart(); const __owl__ = this.__owl__; if (__owl__.isDestroyed) { @@ -1986,9 +2108,7 @@ this.template = template; } else { - while ((template = p.name) && - !(template in qweb.templates) && - p !== Component) { + while ((template = p.name) && !(template in qweb.templates) && p !== Component) { p = p.__proto__; } if (p === Component) { @@ -2001,10 +2121,10 @@ } } __owl__.render = qweb.render.bind(qweb, this.template); - this._observeState(); - return this._render(); + this.__observeState(); + return this.__render(); } - async _render(force = false, patchQueue = []) { + async __render(force = false, patchQueue = []) { const __owl__ = this.__owl__; __owl__.renderId++; const promises = []; @@ -2039,25 +2159,28 @@ /** * Only called by qweb t-component directive */ - _mount(vnode, elm) { + __mount(vnode, elm) { const __owl__ = this.__owl__; + if (__owl__.classObj) { + vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj); + } __owl__.vnode = patch(elm, vnode); if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) { - this._callMounted(); + this.__callMounted(); } return __owl__.vnode; } /** * Only called by qweb t-component directive (when t-keepalive is set) */ - _remount() { + __remount() { const __owl__ = this.__owl__; if (!__owl__.isMounted) { __owl__.isMounted = true; this.mounted(); } } - _observeState() { + __observeState() { if (this.state) { const __owl__ = this.__owl__; __owl__.observer = new Observer(); @@ -2071,7 +2194,7 @@ * Note that this method does not modify in place the props, it returns a new * prop object */ - _applyDefaultProps(props, defaultProps) { + __applyDefaultProps(props, defaultProps) { props = props ? Object.create(props) : {}; for (let propName in defaultProps) { if (props[propName] === undefined) { @@ -2084,10 +2207,10 @@ * Apply the given patch queue. A patch is a pair [c, vn], where c is a * Component instance and vn a VNode. * 1) Call 'willPatch' on the component of each patch - * 2) Call '_patch' on the component of each patch + * 2) Call '__patch' on the component of each patch * 3) Call 'patched' on the component of each patch, in inverse order */ - _applyPatchQueue(patchQueue) { + __applyPatchQueue(patchQueue) { const patchLen = patchQueue.length; for (let i = 0; i < patchLen; i++) { const patch = patchQueue[i]; @@ -2095,7 +2218,7 @@ } for (let i = 0; i < patchLen; i++) { const patch = patchQueue[i]; - patch[0]._patch(patch[1]); + patch[0].__patch(patch[1]); } for (let i = patchLen - 1; i >= 0; i--) { const patch = patchQueue[i]; @@ -2108,7 +2231,7 @@ * visit recursively the props and all the children to check if they are valid. * This is why it is only done in 'dev' mode. */ - _validateProps(props) { + __validateProps(props) { const propsDef = this.constructor.props; if (propsDef instanceof Array) { // list of strings (prop names) @@ -2180,71 +2303,6 @@ return result; } - /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. - */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; - } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a component or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback - }); - } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); - } - } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - sub.callback.call(sub.owner, ...args); - } - } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; - } - } - /** * Owl QWeb Directives * @@ -2452,9 +2510,7 @@ } } // compile sub template - const subCtx = ctx - .subContext("caller", nodeCopy) - .subContext("variables", Object.create(vars)); + const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars)); qweb._compileNode(nodeTemplate.elem, subCtx); // close new scope if (hasNewVariables) { @@ -2522,7 +2578,7 @@ //------------------------------------------------------------------------------ QWeb.addDirective({ name: "debug", - priority: 99, + priority: 1, atNodeEncounter({ ctx }) { ctx.addLine("debugger;"); } @@ -2532,7 +2588,7 @@ //------------------------------------------------------------------------------ QWeb.addDirective({ name: "log", - priority: 99, + priority: 1, atNodeEncounter({ ctx, value }) { const expr = ctx.formatExpression(value); ctx.addLine(`console.log(${expr})`); @@ -2580,9 +2636,7 @@ 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 params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner"; let handler; if (mods.length > 0) { handler = `function (e) {`; @@ -2627,8 +2681,7 @@ const elm = vn.elm; // remove potential duplicated vnode that is currently being removed, to // prevent from having twice the same node in the DOM during an animation - const dup = elm.parentElement && - elm.parentElement.querySelector(`*[data-owl-key='${vn.key}']`); + const dup = elm.parentElement && elm.parentElement.querySelector(`*[data-owl-key='${vn.key}']`); if (dup) { dup.remove(); } @@ -2795,9 +2848,9 @@ * // perspective. * context.__owl__.cmap[key5] = w4.__owl__.id; * - * // _prepare is called, to basically call willStart, then render the + * // __prepare is called, to basically call willStart, then render the * // component - * def3 = w4._prepare(); + * def3 = w4.__prepare(); * * def3 = def3.then(vnode => { * // we create here a virtual node for the parent (NOT the component). This @@ -2811,11 +2864,11 @@ * // component at the proper time * pvnode.data.hook = { * insert(vn) { - * // the _mount method will patch the component vdom into the elm vn.elm, + * // the __mount method will patch the component vdom into the elm vn.elm, * // then call the mounted hooks. However, suprisingly, the snabbdom * // patch method actually replace the elm by a new elm, so we need * // to synchronise the pvnode elm with the resulting elm - * let nvn = w4._mount(vnode, vn.elm); + * let nvn = w4.__mount(vnode, vn.elm); * pvnode.elm = nvn.elm; * // what follows is only present if there are animations on the component * utils.transitionInsert(vn, "fade"); @@ -2843,10 +2896,10 @@ * }); * } else { * // this is the 'update' path of the directive. - * // the call to _updateProps is the actual component update + * // the call to __updateProps is the actual component update * // Note that we only update the props if we cannot reuse the previous * // rendering work (in the case it was rendered with the same props) - * def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + * def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue); * def3 = def3.then(() => { * // if component was destroyed in the meantime, we do nothing (so, this * // means that the parent's element children list will have a null in @@ -2929,6 +2982,17 @@ : ctx.inLoop ? `String(-${componentID} - i)` : String(componentID); + if (ctx.allowMultipleRoots) { + // necessary to prevent collisions + if (!key && ctx.inLoop) { + let id = ctx.generateID(); + ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`); + templateID = `template${id}`; + } + else { + templateID = `"_slot_${templateID}"`; + } + } let ref = node.getAttribute("t-ref"); let refExpr = ""; let refKey = ""; @@ -2961,25 +3025,29 @@ ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`); tattStyle = attVar; } - let updateClassCode = ""; + let classObj = ""; if (classAttr || tattClass || styleAttr || tattStyle || events.length) { - let classCode = ""; if (classAttr) { - classCode = - classAttr - .split(" ") - .map(c => `vn.elm.classList.add('${c}')`) - .join(";") + ";"; + let classDef = classAttr + .trim() + .split(/\s+/) + .map(a => `'${a}':true`) + .join(","); + classObj = `_${ctx.generateID()}`; + ctx.addLine(`let ${classObj} = {${classDef}};`); } if (tattClass) { - const attVar = `_${ctx.generateID()}`; - ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`); - classCode = `for (let k in ${attVar}) { - if (${attVar}[k]) { - vn.elm.classList.add(k); - } - }`; - updateClassCode = `let cl=w${componentID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; + let tattExpr = ctx.formatExpression(tattClass); + if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") { + tattExpr = `this.utils.toObj(${tattExpr})`; + } + if (classAttr) { + ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`); + } + else { + classObj = `_${ctx.generateID()}`; + ctx.addLine(`let ${classObj} = ${tattExpr};`); + } } let eventsCode = events .map(function ([eventName, mods, handlerName, extraArgs]) { @@ -3015,7 +3083,7 @@ .join(""); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : ""; - createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`; + createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`; } ctx.addLine(`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`); ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); @@ -3044,7 +3112,7 @@ ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`); ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`); // SLOTS - if (node.childElementCount) { + if (node.childNodes.length) { const clone = node.cloneNode(true); const slotNodes = clone.querySelectorAll("[t-set]"); const slotId = qweb.nextSlotId++; @@ -3055,40 +3123,42 @@ slotNode.parentElement.removeChild(slotNode); const key = slotNode.getAttribute("t-set"); slotNode.removeAttribute("t-set"); - const slotFn = qweb._compile(`slot_${key}_template`, slotNode); + const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode); qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb); } } - if (clone.childElementCount) { - const content = clone.children[0]; - const slotFn = qweb._compile(`slot_default_template`, content); + if (clone.childNodes.length) { + const t = clone.ownerDocument.createElement("t"); + for (let child of Object.values(clone.childNodes)) { + t.appendChild(child); + } + const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode); qweb.slots[`${slotId}_default`] = slotFn.bind(qweb); } } - ctx.addLine(`def${defID} = w${componentID}._prepare();`); + ctx.addLine(`def${defID} = w${componentID}.__prepare();`); // hack: specify empty remove hook to prevent the node from being removed from the DOM - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`); + ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`); ctx.addElse(); // need to update component - const patchQueueCode = async - ? `patchQueue${componentID}` - : "extra.patchQueue"; - ctx.addLine(`def${defID} = def${defID} || w${componentID}._updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`); + const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; + ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`); let keepAliveCode = ""; if (keepAlive) { - keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}._remount();};`; + keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`; } - ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`); + ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`); ctx.closeIf(); + if (classObj) { + ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`); + } if (async) { - ctx.addLine(`def${defID}.then(w${componentID}._applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`); + ctx.addLine(`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`); } else { ctx.addLine(`extra.promises.push(def${defID});`); } - if (node.hasAttribute("t-if") || - node.hasAttribute("t-else") || - node.hasAttribute("t-elif")) { + if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) { ctx.closeIf(); } return true; @@ -3130,7 +3200,9 @@ atNodeEncounter({ ctx, value }) { const slotKey = ctx.generateID(); ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); - ctx.addLine(`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`); + ctx.addIf(`slot${slotKey}`); + ctx.addLine(`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`); + ctx.closeIf(); return true; } }); @@ -3183,16 +3255,14 @@ super(); this._commitLevel = 0; this.history = []; + this._updateId = 1; this.debug = options.debug || false; this.state = config.state || {}; this.actions = config.actions; this.mutations = config.mutations; this.env = config.env; this.observer = new Observer(); - this.observer.notifyCB = () => { - this._gettersCache = {}; - this.trigger("update"); - }; + this.observer.notifyCB = this.__notifyComponents.bind(this); this.observer.allowMutations = false; this.observer.observe(this.state); this.getters = {}; @@ -3216,7 +3286,7 @@ }; } } - dispatch(action, payload) { + dispatch(action, ...payload) { if (!this.actions[action]) { throw new Error(`[Error] action ${action} is undefined`); } @@ -3226,7 +3296,7 @@ env: this.env, state: this.state, getters: this.getters - }, payload); + }, ...payload); if (result instanceof Promise) { return new Promise((resolve, reject) => { result.then(() => resolve()); @@ -3234,7 +3304,7 @@ }); } } - commit(type, payload) { + commit(type, ...payload) { if (!this.mutations[type]) { throw new Error(`[Error] mutation ${type} is undefined`); } @@ -3244,20 +3314,47 @@ commit: this.commit.bind(this), state: this.state, getters: this.getters - }, payload); + }, ...payload); if (this._commitLevel === 1) { this.observer.allowMutations = false; if (this.debug) { this.history.push({ state: this.state, mutation: type, - payload: payload + payload: [...payload] }); } } this._commitLevel--; return res; } + /** + * Instead of using trigger to emit an update event, we actually implement + * our own function to do that. The reason is that we need to be smarter than + * a simple trigger function: we need to wait for parent components to be + * done before doing children components. The reason is that if an update + * as an effect of destroying a children, we do not want to call the + * mapStoreToProps function of the child, nor rendering it. + * + * This method is not optimal if we have a bunch of asynchronous components: + * we wait sequentially for each component to be completed before updating the + * next. However, the only things that matters is that children are updated + * after their parents. So, this could be optimized by being smarter, and + * updating all widgets concurrently, except for parents/children. + */ + async __notifyComponents() { + this._updateId++; + const current = this._updateId; + this._gettersCache = {}; + const subs = this.subscriptions.update || []; + for (let i = 0, iLen = subs.length; i < iLen; i++) { + const sub = subs[i]; + const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true; + if (shouldCallback) { + await sub.callback.call(sub.owner, current); + } + } + } } //------------------------------------------------------------------------------ // Connect function @@ -3326,15 +3423,18 @@ * updates to be called for the parents before the children. However, * if we use the mounted hook, this will be done in the reverse order. */ - _callMounted() { - this.__owl__.store.on("update", this, this._checkUpdate); - super._callMounted(); + __callMounted() { + this.__owl__.store.on("update", this, this.__checkUpdate); + super.__callMounted(); } willUnmount() { this.__owl__.store.off("update", this); super.willUnmount(); } - _checkUpdate() { + async __checkUpdate(updateId) { + if (updateId === this.__owl__.currentUpdateId) { + return; + } const ownProps = this.__owl__.ownProps; const storeProps = mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters); const options = { @@ -3353,16 +3453,18 @@ } if (didChange) { this.__owl__.currentStoreProps = storeProps; - this._updateProps(ownProps, false); + await this.__updateProps(ownProps, false); } } - _updateProps(nextProps, forceUpdate, patchQueue) { - if (this.__owl__.ownProps !== nextProps) { - this.__owl__.currentStoreProps = mapStoreToProps(this.__owl__.store.state, nextProps, this.__owl__.store.getters); + __updateProps(nextProps, forceUpdate, patchQueue) { + const __owl__ = this.__owl__; + __owl__.currentUpdateId = __owl__.store._updateId; + if (__owl__.ownProps !== nextProps) { + __owl__.currentStoreProps = mapStoreToProps(__owl__.store.state, nextProps, __owl__.store.getters); } - this.__owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps); - return super._updateProps(mergedProps, forceUpdate, patchQueue); + __owl__.ownProps = nextProps; + const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps); + return super.__updateProps(mergedProps, forceUpdate, patchQueue); } }; // we assign here a unique name to the resulting anonymous class. @@ -3508,9 +3610,9 @@ exports.connect = connect; exports.utils = utils; - exports.__info__.version = '0.15.0'; - exports.__info__.date = '2019-06-21T19:47:14.131Z'; - exports.__info__.hash = '63a8fcd'; + exports.__info__.version = '0.16.0'; + exports.__info__.date = '2019-06-28T09:20:47.478Z'; + exports.__info__.hash = 'd381e85'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/playground/app.js b/playground/app.js index 49a5e87d..629c7051 100644 --- a/playground/app.js +++ b/playground/app.js @@ -97,10 +97,7 @@ function makeCodeIframe(js, css, xml, errorHandler) { setTimeout(function() { if (iframe.contentWindow) { iframe.contentWindow.removeEventListener("error", errorHandler); - iframe.contentWindow.removeEventListener( - "unhandledrejection", - errorHandler - ); + iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler); } }, 200); doc.body.appendChild(script); @@ -200,6 +197,8 @@ class App extends owl.Component { } catch (e) { //probably problem with the templates error = e; + // we still log the error, always useful to have it available + console.error(e); } if (error) { this.displayError(error.message); @@ -284,15 +283,7 @@ class TabbedEditor extends owl.Component { this.setTab = owl.utils.debounce(this.setTab, 250, true); this.sessions = {}; - for (let tab of ["js", "xml", "css"]) { - if (props[tab]) { - this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]); - this.sessions[tab].setOption("useWorker", false); - const tabSize = tab === "xml" ? 2 : 4; - this.sessions[tab].setOption("tabSize", tabSize); - this.sessions[tab].setUndoManager(new ace.UndoManager()); - } - } + this._setupSessions(props); this.editor = null; } @@ -317,9 +308,19 @@ class TabbedEditor extends owl.Component { }); } + willUpdateProps(nextProps) { + this._setupSessions(nextProps); + } + patched() { const session = this.sessions[this.state.currentTab]; - session.setValue(this.props[this.state.currentTab], -1); + let content = this.props[this.state.currentTab]; + if (content === false) { + const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css"; + content = this.props[tab]; + this.state.currentTab = tab; + } + session.setValue(content, -1); this.editor.setSession(session); this.editor.resize(); } @@ -347,6 +348,18 @@ class TabbedEditor extends owl.Component { }); } } + + _setupSessions(props) { + for (let tab of ["js", "xml", "css"]) { + if (props[tab] && !this.sessions[tab]) { + this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]); + this.sessions[tab].setOption("useWorker", false); + const tabSize = tab === "xml" ? 2 : 4; + this.sessions[tab].setOption("tabSize", tabSize); + this.sessions[tab].setUndoManager(new ace.UndoManager()); + } + } + } } //------------------------------------------------------------------------------ diff --git a/playground/samples.js b/playground/samples.js index e037e42e..68980b28 100644 --- a/playground/samples.js +++ b/playground/samples.js @@ -942,31 +942,30 @@ class App extends owl.Component { } //------------------------------------------------------------------------------ -// Application Startup +// Responsive plugin //------------------------------------------------------------------------------ -function isMobile() { - return window.innerWidth <= 768; +function setupResponsivePlugin(env) { + const isMobile = () => window.innerWidth <= 768; + env.isMobile = isMobile(); + const updateEnv = owl.utils.debounce(() => { + if (env.isMobile !== isMobile()) { + env.isMobile = !env.isMobile; + env.qweb.trigger('update'); + } + }, 15); + window.addEventListener("resize", updateEnv); } +//------------------------------------------------------------------------------ +// Application Startup +//------------------------------------------------------------------------------ const env = { qweb: new owl.QWeb(TEMPLATES), - isMobile: isMobile() }; - +setupResponsivePlugin(env); const app = new App(env); app.mount(document.body); - -function updateEnv() { - const _isMobile = isMobile(); - if (_isMobile !== env.isMobile) { - app.updateEnv({ - isMobile: _isMobile - }); - } -} - -window.addEventListener("resize", owl.utils.debounce(updateEnv, 20)); `; const RESPONSIVE_XML = ` @@ -1133,10 +1132,10 @@ const SLOTS_XML = ` -
+
Card 2... []
-
+
@@ -1352,7 +1351,7 @@ export const SAMPLES = [ description: "Lifecycle demo", code: LIFECYCLE_DEMO, xml: LIFECYCLE_DEMO_XML, - css: LIFECYCLE_CSS, + css: LIFECYCLE_CSS }, { description: "Todo List App (with store)", diff --git a/playground/templates.xml b/playground/templates.xml index 1c67cc6d..ffeb442d 100644 --- a/playground/templates.xml +++ b/playground/templates.xml @@ -27,7 +27,7 @@