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 = `