From d6aad9c67328f05bd06d3d7bc5e4d7179624b284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 25 Oct 2019 17:10:17 +0200 Subject: [PATCH] [IMP] owl: update to v0.24.0 --- owl.js | 748 +++++++++++++++++++++++++++--------------- playground/samples.js | 14 +- 2 files changed, 493 insertions(+), 269 deletions(-) diff --git a/owl.js b/owl.js index a2df2191..3cdf12d3 100644 --- a/owl.js +++ b/owl.js @@ -405,6 +405,7 @@ this.shouldDefineUtils = false; this.shouldDefineRefs = false; this.shouldDefineResult = true; + this.shouldDefineSibling = true; this.shouldProtectContext = false; this.shouldTrackScope = false; this.loopNumber = 0; @@ -441,6 +442,9 @@ if (this.shouldDefineResult) { this.code.unshift(" let result;"); } + if (this.shouldDefineSibling) { + this.code.unshift(" let sibling = null;"); + } if (this.shouldDefineRefs) { this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); } @@ -837,20 +841,10 @@ vnode.elm = api.createComment(vnode.text); } else if (sel !== undefined) { - // Parse selector - const hashIdx = sel.indexOf("#"); - 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 elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, tag) - : api.createElement(tag)); - if (hash < dot) - elm.setAttribute("id", sel.slice(hash + 1, dot)); - if (dotIdx > 0) - elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); + ? api.createElementNS(i, sel) + : api.createElement(sel)); for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode); if (array(children)) { @@ -1135,13 +1129,15 @@ setTextContent }; function addNS(data, children, sel) { + if (sel === "dummy") { + // we do not need to add the namespace on dummy elements, they come from a + // subcomponent, which will handle the namespace itself + return; + } data.ns = "http://www.w3.org/2000/svg"; if (sel !== "foreignObject" && children !== undefined) { for (let i = 0, iLen = children.length; i < iLen; ++i) { const child = children[i]; - if (child === null) { - continue; - } let childData = child.data; if (childData !== undefined) { addNS(childData, child.children, child.sel); @@ -1288,7 +1284,6 @@ } var _utils = /*#__PURE__*/Object.freeze({ - __proto__: null, whenReady: whenReady, loadJS: loadJS, loadFile: loadFile, @@ -1803,7 +1798,7 @@ if (name.startsWith("t-att-")) { let attName = name.slice(6); const v = ctx.getValue(value); - let formattedValue = typeof v === 'string' ? ctx.formatExpression(v) : v.id; + let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id; if (attName === "class") { ctx.rootContext.shouldDefineUtils = true; formattedValue = `utils.toObj(${formattedValue})`; @@ -2080,7 +2075,7 @@ priority: 20, atNodeEncounter({ node, ctx }) { let cond = ctx.getValue(node.getAttribute("t-if")); - ctx.addIf(`${ctx.formatExpression(typeof cond === 'string' ? cond : cond.expr)}`); + ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id); return false; }, finalize({ ctx }) { @@ -2092,7 +2087,7 @@ priority: 30, atNodeEncounter({ node, ctx }) { let cond = ctx.getValue(node.getAttribute("t-elif")); - ctx.addLine(`else if (${ctx.formatExpression(typeof cond === 'string' ? cond : cond.expr)}) {`); + ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`); ctx.indent(); return false; }, @@ -2310,38 +2305,32 @@ extraArgs = args.slice(1, -1); return ""; }); - 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"; - if (mods.length > 0) { - let handler = `function (e) {`; - handler += mods - .map(function (mod) { - return MODS_CODE[mod]; - }) - .join(""); + let handler = `function (e) {`; + handler += mods + .map(function (mod) { + return MODS_CODE[mod]; + }) + .join(""); + if (handlerName) { if (!extraArgs) { - handler += `context['${handlerName}'].call(${params}, e);}`; + handler += `const fn = context['${handlerName}'];`; + handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`; + handler += `}`; ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`); ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); } else { const handlerKey = `handler${ctx.generateID()}`; - ctx.addLine(`const ${handlerKey} = context['${handlerName}'].bind(${params});`); - handler += `${handlerKey}(e);}`; + ctx.addLine(`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`); + handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`; + handler += `}`; ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); } } else { - const 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}];`); - } + handler += "}"; + ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); } } }); @@ -2465,7 +2454,7 @@ } ctx.addLine(`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`); if (!ctx.parentNode) { - ctx.addLine(`Promise.all(extra.promises).then(() => utils.defineProxy(result, ${parentNode}[0]))`); + ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`); } ctx.closeIf(); return true; @@ -2697,7 +2686,7 @@ */ QWeb.addDirective({ name: "component", - extraNames: ["props", "keepalive", "asyncroot"], + extraNames: ["props", "keepalive"], priority: 100, atNodeEncounter({ ctx, value, node, qweb }) { ctx.addLine("//COMPONENT"); @@ -2707,7 +2696,6 @@ ctx.rootContext.shouldDefineUtils = true; let keepAlive = node.getAttribute("t-keepalive") ? true : false; let hasDynamicProps = node.getAttribute("t-props") ? true : false; - let async = node.getAttribute("t-asyncroot") ? true : false; // t-on- events and t-transition const events = []; let transition = ""; @@ -2719,11 +2707,11 @@ if (name.startsWith("t-on-")) { const [eventName, ...mods] = name.slice(5).split("."); let extraArgs; - let handlerName = value.replace(/\(.*\)/, function (args) { + let handlerValue = value.replace(/\(.*\)/, function (args) { extraArgs = args.slice(1, -1); return ""; }); - events.push([eventName, mods, handlerName, extraArgs]); + events.push([eventName, mods, handlerValue, extraArgs]); } else if (name === "t-transition") { transition = value; @@ -2743,7 +2731,6 @@ let propStr = Object.keys(props) .map(k => k + ":" + props[k]) .join(","); - let dummyID = ctx.generateID(); let defID = ctx.generateID(); let componentID = ctx.generateID(); let keyID = key && ctx.generateID(); @@ -2752,7 +2739,6 @@ // want to evaluate it only once) ctx.addLine(`let key${keyID} = 'key' + ${key};`); } - ctx.addLine(`let def${defID};`); let locationExpr = `\`__${ctx.generateID()}__`; for (let i = 0; i < ctx.loopNumber - 1; i++) { locationExpr += `\${i${i + 1}}__`; @@ -2824,7 +2810,7 @@ } } let eventsCode = events - .map(function ([eventName, mods, handlerName, extraArgs]) { + .map(function ([eventName, mods, handlerValue, extraArgs]) { let params = "owner"; if (extraArgs) { if (ctx.loopNumber) { @@ -2839,19 +2825,17 @@ params = `owner, ${ctx.formatExpression(extraArgs)}`; } } - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function (mod) { - return T_COMPONENT_MODS_CODE[mod]; - }) - .join(""); - handler += `owner['${handlerName}'].call(${params}, e);}`; - } - else { - handler = `owner['${handlerName}'].bind(${params})`; + let handler = `function (e) {`; + handler += mods + .map(function (mod) { + return T_COMPONENT_MODS_CODE[mod]; + }) + .join(""); + if (handlerValue) { + handler += `const fn = owner['${handlerValue}'];`; + handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`; } + handler += `}`; return `vn.elm.addEventListener('${eventName}', ${handler});`; }) .join(""); @@ -2860,28 +2844,17 @@ createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`; } ctx.addLine(`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`); - if (ctx.parentNode) { - ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); - } - let shouldProxy = false; - if (async || keepAlive) { + let shouldProxy = !ctx.parentNode; + if (keepAlive) { ctx.addLine(`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`); } - if (async) { - ctx.addLine(`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`); - } - else { - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push(null);`); - } - else { - let id = ctx.generateID(); - ctx.rootContext.rootNode = id; - shouldProxy = true; - ctx.rootContext.shouldDefineResult = true; - ctx.addLine(`let vn${id} = {};`); - ctx.addLine(`result = vn${id};`); - } + if (shouldProxy) { + let id = ctx.generateID(); + ctx.rootContext.rootNode = id; + shouldProxy = true; + ctx.rootContext.shouldDefineResult = true; + ctx.addLine(`let vn${id} = {};`); + ctx.addLine(`result = vn${id};`); } if (hasDynamicProps) { const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")); @@ -2891,14 +2864,65 @@ ctx.addLine(`let props${componentID} = {${propStr}};`); } ctx.addIf(`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`); - ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`); - ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`); - ctx.addElse(); ctx.addLine(`w${componentID}.destroy();`); ctx.addLine(`w${componentID} = false;`); ctx.closeIf(); - ctx.closeIf(); - ctx.addIf(`!w${componentID}`); + let registerCode = ""; + if (shouldProxy) { + registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`; + } + // SLOTS + const varDefs = []; + const hasSlots = node.childNodes.length; + if (hasSlots) { + ctx.rootContext.shouldTrackScope = true; + for (let v of Object.values(ctx.variables)) { + if (v["id"]) { + varDefs.push(v["id"]); + } + } + } + let scopeVars; + if (hasSlots) { + let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`; + let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined"; + scopeVars = `${scope}, ${vars}`; + } + else { + scopeVars = "undefined, undefined"; + } + ctx.addIf(`w${componentID}`); + // need to update component + let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber"; + if (keepAlive) { + // if we have t-keepalive="1", the component could be unmounted, but then + // we __updateProps is called. This is ok, but we do not want to call + // the willPatch/patched hooks of the component in this case, so we + // disable the patch queue + patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`; + } + if (QWeb.dev) { + ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`); + } + let styleCode = ""; + if (tattStyle) { + styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`; + } + ctx.addLine(`w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars && + ", " + scopeVars}, sibling)${styleCode};`); + ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`); + 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();};`; + ctx.addLine(keepAliveCode); + } + if (registerCode) { + ctx.addLine(registerCode); + } + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push(pvnode);`); + } + ctx.addElse(); // new component let dynamicFallback = ""; if (!value.match(INTERP_REGEXP)) { @@ -2911,16 +2935,7 @@ ctx.addLine(`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`); ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`); ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`); - // SLOTS - const varDefs = []; - const hasSlots = node.childNodes.length; if (hasSlots) { - ctx.rootContext.shouldTrackScope = true; - for (let v of Object.values(ctx.variables)) { - if (v["id"]) { - varDefs.push(v["id"]); - } - } const clone = node.cloneNode(true); const slotNodes = clone.querySelectorAll("[t-set]"); const slotId = QWeb.nextSlotId++; @@ -2944,56 +2959,202 @@ QWeb.slots[`${slotId}_default`] = slotFn; } } - let scopeVars; - if (hasSlots) { - let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`; - let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined"; - scopeVars = `${scope}, ${vars}`; - } - else { - scopeVars = "undefined, undefined"; - } - ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`); + ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`); // hack: specify empty remove hook to prevent the node from being removed from the DOM - let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`; - if (shouldProxy) { - registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`; + ctx.addLine(`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`); + ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`); + ctx.addLine(`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`); + if (registerCode) { + ctx.addLine(registerCode); } - ctx.addLine(`def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${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}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`); - ctx.addElse(); - // need to update component - let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber"; - if (keepAlive) { - // if we have t-keepalive="1", the component could be unmounted, but then - // we __updateProps is called. This is ok, but we do not want to call - // the willPatch/patched hooks of the component in this case, so we - // disable the patch queue - patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`; + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push(pvnode);`); } - if (QWeb.dev) { - ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`); - } - ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars && - ", " + scopeVars});`); - 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();};`; - } - 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}${registerCode}});`); + ctx.addLine(`w${componentID}.__owl__.pvnode = 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}, fiber${componentID}));`); - } - else { - ctx.addLine(`extra.promises.push(def${defID});`); - } + ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`); return true; } }); + /** + * Owl Fiber Class + * + * Fibers are small abstractions designed to contain all the internal state + * associated with a "rendering work unit", relative to a specific component. + * + * A rendering will cause the creation of a fiber for each impacted components. + * + * Fibers capture all that necessary information, which is critical to owl + * asynchronous rendering pipeline. Fibers can be cancelled, can be in different + * states and in general determine the state of the rendering. + */ + class Fiber { + constructor(parent, component, props, scope, vars, force) { + // isCancelled means that the rendering corresponding to this fiber and its + // children is cancelled. No extra work should be done. + this.isCancelled = false; + // the fibers corresponding to component updates (updateProps) need to call + // the willPatch and patched hooks from the corresponding component. However, + // fibers corresponding to a new component do not need to do that. So, the + // shouldPatch hook is the boolean that we check whenever we need to apply + // a patch. + this.shouldPatch = true; + // isRendered is the last state of a fiber. If true, this means that it has + // been rendered and is inert (so, it should not be taken into account when + // counting the number of active fibers). + this.isRendered = false; + // the counter number is a critical information. It is only necessary for a + // root fiber. For that fiber, this number counts the number of active sub + // fibers. When that number reaches 0, the fiber can be applied by the + // scheduler. + this.counter = 0; + this.vnode = null; + this.child = null; + this.sibling = null; + this.parent = null; + this.force = force; + this.scope = scope; + this.vars = vars; + this.props = props; + this.component = component; + this.root = parent ? parent.root : this; + this.parent = parent; + let oldFiber = component.__owl__.currentFiber; + if (oldFiber && !oldFiber.isCancelled) { + this._remapFiber(oldFiber); + } + this.root.counter++; + component.__owl__.currentFiber = this; + } + /** + * In some cases, a rendering initiated at some component can detect that it + * should be part of a larger rendering initiated somewhere up the component + * tree. In that case, it needs to cancel the previous rendering and + * remap itself as a part of the current parent rendering. + */ + _remapFiber(oldFiber) { + oldFiber.cancel(); + if (oldFiber === oldFiber.root) { + oldFiber.root.counter++; + } + if (oldFiber.parent && !this.parent) { + // re-map links + this.parent = oldFiber.parent; + this.root = this.parent.root; + this.sibling = oldFiber.sibling; + if (this.parent.child === oldFiber) { + this.parent.child = this; + } + else { + let current = this.parent.child; + while (true) { + if (current.sibling === oldFiber) { + current.sibling = this; + break; + } + current = current.sibling; + } + } + } + } + /** + * This function has been taken from + * https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7 + */ + _walk(doWork) { + let root = this; + let current = this; + while (true) { + const child = doWork(current); + if (child) { + current = child; + continue; + } + if (current === root) { + return; + } + while (!current.sibling) { + if (!current.parent || current.parent === root) { + return; + } + current = current.parent; + } + current = current.sibling; + } + } + /** + * Apply the given patch queue from a fiber. + * 1) Call 'willPatch' 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 reverse order + */ + patchComponents() { + const patchQueue = []; + const doWork = function (f) { + if (f.shouldPatch) { + patchQueue.push(f); + } + return f.child; + }; + this._walk(doWork); + let component = this.component; + this.shouldPatch = false; + const patchLen = patchQueue.length; + try { + for (let i = 0; i < patchLen; i++) { + component = patchQueue[i].component; + if (component.__owl__.willPatchCB) { + component.__owl__.willPatchCB(); + } + component.willPatch(); + } + } + catch (e) { + console.error(e); + } + try { + for (let i = 0; i < patchLen; i++) { + const fiber = patchQueue[i]; + component = fiber.component; + component.__patch(fiber.vnode); + } + } + catch (e) { + this.handleError(e); + } + try { + for (let i = patchLen - 1; i >= 0; i--) { + component = patchQueue[i].component; + component.patched(); + if (component.__owl__.patchedCB) { + component.__owl__.patchedCB(); + } + } + } + catch (e) { + console.error(e); + } + this.shouldPatch = true; + } + /** + * Cancel a fiber and all its children. + */ + cancel() { + this._walk(f => { + if (!f.isRendered) { + f.root.counter--; + } + f.isCancelled = true; + return f.child; + }); + } + handleError(e) { } + } + //------------------------------------------------------------------------------ // Prop validation helper //------------------------------------------------------------------------------ @@ -3089,6 +3250,67 @@ return result; } + class Scheduler { + constructor(requestAnimationFrame) { + this.tasks = []; + this.isRunning = false; + this.requestAnimationFrame = requestAnimationFrame; + } + addFiber(fiber, callback) { + this.tasks.push({ fiber, callback }); + if (this.isRunning) { + return; + } + this.scheduleTasks(); + } + /** + * Process all current tasks. This only applies to the fibers that are ready. + * Other tasks are left unchanged. + */ + flush() { + let tasks = this.tasks; + this.tasks = []; + tasks = tasks.filter(task => { + if (task.fiber.isCancelled) { + return false; + } + if (task.fiber.counter === 0) { + task.callback(); + return false; + } + return true; + }); + this.tasks = tasks.concat(this.tasks); + } + scheduleTasks() { + this.isRunning = true; + this.requestAnimationFrame(() => { + this.flush(); + if (this.tasks.length > 0) { + this.scheduleTasks(); + } + else { + this.isRunning = false; + } + }); + } + } + + /** + * Owl Component System + * + * This file introduces a declarative and composable component system. It + * contains: + * + * - the Env interface (generic type for the environment) + * - the Internal interface (the owl specific metadata attached to a component) + * - the Component class + */ + //------------------------------------------------------------------------------ + // Types/helpers + //------------------------------------------------------------------------------ + const raf = window.requestAnimationFrame.bind(window); + const scheduler = new Scheduler(raf); //------------------------------------------------------------------------------ // Component //------------------------------------------------------------------------------ @@ -3159,6 +3381,7 @@ this.__owl__ = { id: id, vnode: null, + pvnode: null, isMounted: false, isDestroyed: false, parent: p, @@ -3173,7 +3396,7 @@ willStartCB: null, willUpdatePropsCB: null, observer: null, - render: qweb.render.bind(qweb, this.__getTemplate(qweb)), + renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)), classObj: null, refs: null }; @@ -3270,28 +3493,34 @@ async mount(target, renderBeforeRemount = false) { const __owl__ = this.__owl__; if (__owl__.isMounted) { + return Promise.resolve(); + } + if (__owl__.vnode && !renderBeforeRemount) { + target.appendChild(this.el); + if (document.body.contains(target)) { + this.__callMounted(); + } return; } - const fiber = this.__createFiber(false, undefined, undefined, undefined); + const fiber = new Fiber(null, this, this.props, undefined, undefined, false); if (!__owl__.vnode) { - fiber.promise = this.__prepareAndRender(fiber); - const vnode = await fiber.promise; - if (__owl__.isDestroyed) { - // component was destroyed before we get here... - return; - } - this.__patch(vnode); + this.__prepareAndRender(fiber); } - else if (renderBeforeRemount) { - fiber.patchQueue.push(fiber); - fiber.promise = this.__render(fiber); - await fiber.promise; - this.__applyPatchQueue(fiber); - } - target.appendChild(this.el); - if (document.body.contains(target)) { - this.__callMounted(); + else { + this.__render(fiber); } + return new Promise(resolve => { + scheduler.addFiber(fiber, () => { + if (!__owl__.isDestroyed) { + this.__patch(fiber.vnode); + target.appendChild(this.el); + if (document.body.contains(target)) { + this.__callMounted(); + } + } + resolve(); + }); + }); } /** * The unmount method is the opposite of the mount method. It is useful @@ -3314,18 +3543,20 @@ */ async render(force = false) { const __owl__ = this.__owl__; - if (!__owl__.isMounted) { + if ((!__owl__.isMounted && !__owl__.currentFiber) || + (__owl__.currentFiber && !__owl__.currentFiber.isRendered)) { return; } - const fiber = this.__createFiber(force, undefined, undefined, undefined); - fiber.patchQueue.push(fiber); - fiber.promise = this.__render(fiber); - await fiber.promise; - if (__owl__.isMounted && fiber === __owl__.currentFiber) { - // 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(fiber); - } + const fiber = new Fiber(null, this, this.props, undefined, undefined, force); + this.__render(fiber); + return new Promise(resolve => { + scheduler.addFiber(fiber.root, () => { + if (__owl__.isMounted && fiber === fiber.root) { + fiber.patchComponents(); + } + resolve(); + }); + }); } /** * Destroy the component. This operation is quite complex: @@ -3373,26 +3604,6 @@ //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - /** - * This method is a helper to create a fiber element. - */ - __createFiber(force, scope, vars, parent) { - const fiber = { - force, - scope, - vars, - rootFiber: null, - isCancelled: false, - component: this, - vnode: null, - patchQueue: parent ? parent.patchQueue : [], - props: this.props, - promise: null - }; - fiber.rootFiber = parent ? parent.rootFiber : fiber; - this.__owl__.currentFiber = fiber; - return fiber; - } /** * Private helper to perform a full destroy, from the point of view of an Owl * component. It does not remove the el (this is done only once on the top @@ -3443,7 +3654,7 @@ } } catch (e) { - errorHandler(e, this); + console.error(e); // TODO : add a test } } __callWillUnmount() { @@ -3465,21 +3676,30 @@ * The __updateProps method is called by the t-component directive whenever * it updates a component (so, when the parent template is rerendered). */ - async __updateProps(nextProps, parentFiber, scope, vars) { + async __updateProps(nextProps, parentFiber, scope, vars, previousSibling) { const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); if (shouldUpdate) { + const __owl__ = this.__owl__; + const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force); + if (!parentFiber.child) { + parentFiber.child = fiber; + } + else { + previousSibling.sibling = fiber; + } const defaultProps = this.constructor.defaultProps; if (defaultProps) { nextProps = this.__applyDefaultProps(nextProps, defaultProps); } await Promise.all([ this.willUpdateProps(nextProps), - this.__owl__.willUpdatePropsCB && this.__owl__.willUpdatePropsCB(nextProps) + __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) ]); + if (fiber.isCancelled) { + return; + } this.props = nextProps; - const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber); - fiber.patchQueue.push(fiber); - await this.__render(fiber); + this.__render(fiber); } } /** @@ -3490,16 +3710,23 @@ const __owl__ = this.__owl__; const target = __owl__.vnode || document.createElement(vnode.sel); __owl__.vnode = patch(target, vnode); + __owl__.currentFiber = null; } /** * The __prepare method is only called by the t-component directive, when a * subcomponent is created. It gets its scope and vars, if any, from the * parent template. */ - __prepare(parentFiber, scope, vars) { - const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber); - fiber.promise = this.__prepareAndRender(fiber); - return fiber.promise; + __prepare(parentFiber, scope, vars, previousSibling) { + const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force); + fiber.shouldPatch = false; + if (!parentFiber.child) { + parentFiber.child = fiber; + } + else { + previousSibling.sibling = fiber; + } + return this.__prepareAndRender(fiber); } __getTemplate(qweb) { let p = this.constructor; @@ -3530,60 +3757,60 @@ await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]); } catch (e) { - errorHandler(e, this); - return Promise.resolve(h("div")); + errorHandler(e, fiber); + fiber.vnode = h("div"); // -> we render this div at the end + return Promise.resolve(); } - const __owl__ = this.__owl__; - if (__owl__.isDestroyed) { - return Promise.resolve(h("div")); + if (this.__owl__.isDestroyed) { + return Promise.resolve(); + } + if (!fiber.isCancelled) { + this.__render(fiber); } - return this.__render(fiber); } __render(fiber) { const __owl__ = this.__owl__; - const promises = []; if (__owl__.observer) { __owl__.observer.allowMutations = false; } let vnode; try { - vnode = __owl__.render(this, { - promises, + vnode = __owl__.renderFn(this, { handlers: __owl__.boundHandlers, fiber: fiber }); } catch (e) { vnode = __owl__.vnode || h("div"); - errorHandler(e, this); + errorHandler(e, fiber); } fiber.vnode = vnode; 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 component can be rerendered on its own, which - // will update its own vnode representation without the knowledge of the - // parent component. With this, we make sure that the parent component will be - // able to patch itself properly after - vnode.key = __owl__.id; - // we applly here the class information described on the component by the + // we apply here the class information described on the component by the // template (so, something like ) to the actual // root vnode if (__owl__.classObj) { vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj); } - return Promise.all(promises).then(() => vnode); + fiber.root.counter--; + fiber.isRendered = true; } /** * Only called by qweb t-component directive */ - __mount(vnode, elm) { + __mount(fiber, elm) { + if (fiber !== this.__owl__.currentFiber) { + fiber = this.__owl__.currentFiber; // TODO: check if we can remove fiber arg + } + const vnode = fiber.vnode; const __owl__ = this.__owl__; if (__owl__.classObj) { vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj); } __owl__.vnode = patch(elm, vnode); + __owl__.currentFiber = null; if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) { this.__callMounted(); } @@ -3614,41 +3841,6 @@ } return props; } - /** - * Apply the given patch queue from a fiber. - * 1) Call 'willPatch' 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 reverse order - */ - __applyPatchQueue(fiber) { - const patchQueue = fiber.patchQueue; - let component = this; - try { - const patchLen = patchQueue.length; - for (let i = 0; i < patchLen; i++) { - component = patchQueue[i].component; - if (component.__owl__.willPatchCB) { - component.__owl__.willPatchCB(); - } - component.willPatch(); - } - for (let i = 0; i < patchLen; i++) { - const fiber = patchQueue[i]; - component = fiber.component; - component.__patch(fiber.vnode); - } - for (let i = patchLen - 1; i >= 0; i--) { - component = patchQueue[i].component; - component.patched(); - if (component.__owl__.patchedCB) { - component.__owl__.patchedCB(); - } - } - } - catch (e) { - errorHandler(e, component); - } - } } Component.template = null; Component._template = null; @@ -3657,6 +3849,9 @@ //------------------------------------------------------------------------------ // Error handling //------------------------------------------------------------------------------ + Fiber.prototype.handleError = function (error) { + errorHandler(error, this); + }; /** * This is the global error handler for errors occurring in Owl main lifecycle * methods. Caught errors are triggered on the QWeb instance, and are @@ -3665,8 +3860,9 @@ * If there are no such component, we destroy everything. This is better than * being in a corrupted state. */ - function errorHandler(error, component) { + function errorHandler(error, fiber) { let canCatch = false; + let component = fiber.component; let qweb = component.env.qweb; let root = component; while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) { @@ -3674,7 +3870,6 @@ component = component.__owl__.parent; } console.error(error); - // we trigger error on QWeb so it can be logged/handled qweb.trigger("error", error); if (canCatch) { setTimeout(() => { @@ -3795,7 +3990,6 @@ } var _hooks = /*#__PURE__*/Object.freeze({ - __proto__: null, useState: useState, onMounted: onMounted, onWillUnmount: onWillUnmount, @@ -3820,7 +4014,7 @@ class Context extends EventBus { constructor(state = {}) { super(); - this.id = 1; + this.rev = 1; // mapping from component id to last observed context id this.mapping = {}; this.observer = new Observer(); @@ -3847,13 +4041,15 @@ * with the same depth in parallel. */ async __notifyComponents() { - const id = ++this.id; + const rev = ++this.rev; 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, id); + const render = sub.callback.call(sub.owner, rev); + scheduler.flush(); + await render; } } } @@ -3873,15 +4069,29 @@ if (id in mapping) { return ctx.state; } + if (!__owl__.observer) { + __owl__.observer = new Observer(); + __owl__.observer.notifyCB = component.render.bind(component); + } + const currentCB = __owl__.observer.notifyCB; + __owl__.observer.notifyCB = function () { + if (ctx.rev > mapping[id]) { + // in this case, the context has been updated since we were rendering + // last, and we do not need to render here with the observer. A + // rendering is coming anyway, with the correct props. + return; + } + currentCB(); + }; mapping[id] = 0; - const renderFn = __owl__.render; - __owl__.render = function (comp, params) { - mapping[id] = ctx.id; + const renderFn = __owl__.renderFn; + __owl__.renderFn = function (comp, params) { + mapping[id] = ctx.rev; return renderFn(comp, params); }; - ctx.on("update", component, async (contextId) => { - if (mapping[id] < contextId) { - mapping[id] = contextId; + ctx.on("update", component, async (contextRev) => { + if (mapping[id] < contextRev) { + mapping[id] = contextRev; await method(); } }); @@ -4004,10 +4214,24 @@ } var _tags = /*#__PURE__*/Object.freeze({ - __proto__: null, xml: xml }); + /** + * AsyncRoot + * + * Owl is by default asynchronous, and the user interface will wait for all its + * subcomponents to be rendered before updating the DOM. This is most of the + * time what we want, but in some cases, it makes sense to "detach" a component + * from this coordination. This is the goal of the AsyncRoot component. + */ + class AsyncRoot extends Component { + async __updateProps(nextProps, parentFiber) { + this.render(parentFiber.force); + } + } + AsyncRoot.template = xml ``; + class Link extends Component { constructor() { super(...arguments); @@ -4281,6 +4505,7 @@ const Store$1 = Store; const utils = _utils; const tags = _tags; + const misc = { AsyncRoot }; const hooks$1 = Object.assign({}, _hooks, { useContext: useContext, useDispatch: useDispatch, @@ -4311,14 +4536,15 @@ exports.__info__ = __info__; exports.core = core; exports.hooks = hooks$1; + exports.misc = misc; exports.router = router; exports.tags = tags; exports.useState = useState$1; exports.utils = utils; - exports.__info__.version = '0.23.0'; - exports.__info__.date = '2019-10-24T07:34:00.585Z'; - exports.__info__.hash = 'a3317ab'; + exports.__info__.version = '0.24.0'; + exports.__info__.date = '2019-10-25T15:08:12.840Z'; + exports.__info__.hash = 'f0b5a55'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/playground/samples.js b/playground/samples.js index 28585542..a36ae5bf 100644 --- a/playground/samples.js +++ b/playground/samples.js @@ -466,10 +466,6 @@ class TodoItem extends Component { useAutofocus("input"); } - editTodo() { - this.state.isEditing = true; - } - handleKeyup(ev) { if (ev.keyCode === ENTER_KEY) { this.updateTitle(ev.target.value); @@ -603,7 +599,7 @@ const TODO_APP_STORE_XML = `
  • -