diff --git a/owl.js b/owl.js
index 4d99e2a6..04588d8c 100644
--- a/owl.js
+++ b/owl.js
@@ -92,14 +92,6 @@
this.weakMap = new WeakMap();
}
notifyCB() { }
- async notifyChange() {
- this.dirty = true;
- await Promise.resolve();
- if (this.dirty) {
- this.dirty = false;
- this.notifyCB();
- }
- }
observe(value, parent) {
if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
@@ -127,7 +119,7 @@
}
self._updateRevNumber(target);
target[key] = newVal;
- self.notifyChange();
+ self.notifyCB();
}
return true;
},
@@ -135,7 +127,7 @@
if (key in target) {
delete target[key];
self._updateRevNumber(target);
- self.notifyChange();
+ self.notifyCB();
}
return true;
}
@@ -186,7 +178,7 @@
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(",");
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
@@ -205,7 +197,9 @@
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
};
- const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
+ // note that the space after typeof is relevant. It makes sure that the formatted
+ // expression has a space after typeof
+ const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ".split(",");
let tokenizeString = function (expr) {
let s = expr[0];
let start = s;
@@ -282,9 +276,9 @@
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
+ tokenizeOperator,
tokenizeSymbol,
- tokenizeStatic,
- tokenizeOperator
+ tokenizeStatic
];
/**
* Convert a javascript expression (as a string) into a list of tokens. For
@@ -419,6 +413,30 @@
const id = this.rootContext.nextID++;
return id;
}
+ /**
+ * This method generates a "template key", which is basically a unique key
+ * which depends on the currently set keys, and on the iteration numbers (if
+ * we are in a loop).
+ *
+ * Such a key is necessary when we need to associate an id to some element
+ * generated by a template (for example, a component)
+ */
+ generateTemplateKey() {
+ const id = this.generateID();
+ let locationExpr = `\`__${this.generateID()}__`;
+ for (let i = 0; i < this.loopNumber - 1; i++) {
+ locationExpr += `\${i${i + 1}}__`;
+ }
+ if (this.lastNodeKey || this.currentKey) {
+ const k = this.lastNodeKey || this.currentKey;
+ this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
+ }
+ else {
+ locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
+ this.addLine(`let k${id} = ${locationExpr};`);
+ }
+ return `k${id}`;
+ }
generateCode() {
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
if (shouldTrackScope) {
@@ -1626,6 +1644,9 @@
// to
node.setAttribute("t-component", node.tagName);
}
+ else if (node.tagName !== 't' && node.hasAttribute('t-component')) {
+ throw new Error(`Directive 't-component' can only be used on nodes (used on a <${node.tagName}>)`);
+ }
const attributes = node.attributes;
const validDirectives = [];
const finalizers = [];
@@ -1939,10 +1960,8 @@
attrs[attr.name] = attr.textContent;
}
const children = [];
- if (node.hasChildNodes) {
- for (let c of node.childNodes) {
- children.push(htmlToVNode(c));
- }
+ for (let c of node.childNodes) {
+ children.push(htmlToVNode(c));
}
return h(node.tagName, { attrs }, children);
}
@@ -1974,7 +1993,7 @@
qweb._compileNode(ctx.caller, ctx);
return;
}
- if (value.xml instanceof NodeList) {
+ if (value.xml instanceof NodeList && !value.id) {
for (let node of Array.from(value.xml)) {
qweb._compileNode(node, ctx);
}
@@ -2014,6 +2033,12 @@
ctx.addElse();
qweb._compileChildren(node, ctx);
}
+ if (value.xml instanceof NodeList && value.id) {
+ ctx.addElse();
+ for (let node of Array.from(value.xml)) {
+ qweb._compileNode(node, ctx);
+ }
+ }
ctx.closeIf();
}
QWeb.addDirective({
@@ -2044,24 +2069,22 @@
atNodeEncounter({ node, ctx }) {
const variable = node.getAttribute("t-set");
let value = node.getAttribute("t-value");
+ ctx.variables[variable] = ctx.variables[variable] || {};
+ let qwebvar = ctx.variables[variable];
if (value) {
const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable)) {
- ctx.addLine(`${ctx.variables[variable].id} = ${formattedValue}`);
+ if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
+ ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
}
else {
const varName = `_${ctx.generateID()}`;
ctx.addLine(`var ${varName} = ${formattedValue};`);
- ctx.variables[variable] = {
- id: varName,
- expr: formattedValue
- };
+ qwebvar.id = varName;
+ qwebvar.expr = formattedValue;
}
}
else {
- ctx.variables[variable] = {
- xml: node.childNodes
- };
+ qwebvar.xml = node.childNodes;
}
return true;
}
@@ -2465,7 +2488,15 @@
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
- const expr = ctx.formatExpression(value);
+ // we keep here a reference to the "base expression" (if the expression
+ // is `t-model="some.expr.value", then the base expression is "some.expr").
+ // This is necessary so we can capture it in the handler closure.
+ let expr = ctx.formatExpression(value);
+ const index = expr.lastIndexOf(".");
+ const baseExpr = expr.slice(0, index);
+ ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
+ expr = `expr${nodeID}.${expr.slice(index + 1)}`;
+ const key = ctx.generateTemplateKey();
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
@@ -2492,8 +2523,8 @@
}
handler = `(ev) => {${expr} = ${valueCode}}`;
}
- ctx.addLine(`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`);
- ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
+ ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
+ ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
}
});
//------------------------------------------------------------------------------
@@ -2526,6 +2557,18 @@
}
});
+ /**
+ * We define here OwlEvent, a subclass of CustomEvent, with an additional
+ * attribute:
+ * - originalComponent: the component that triggered the event
+ */
+ class OwlEvent extends CustomEvent {
+ constructor(component, eventType, options) {
+ super(eventType, options);
+ this.originalComponent = component;
+ }
+ }
+
//------------------------------------------------------------------------------
// t-component
//------------------------------------------------------------------------------
@@ -2748,19 +2791,7 @@
.join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID();
- let locationExpr = `\`__${ctx.generateID()}__`;
- for (let i = 0; i < ctx.loopNumber - 1; i++) {
- locationExpr += `\${i${i + 1}}__`;
- }
- if (ctx.lastNodeKey || ctx.currentKey) {
- const k = ctx.lastNodeKey || ctx.currentKey;
- ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
- }
- else {
- locationExpr += ctx.loopNumber ? `\${i${ctx.loopNumber}}__\`` : "`";
- ctx.addLine(`let templateId${componentID} = ${locationExpr};`);
- }
- const templateId = `templateId${componentID}`;
+ const templateKey = ctx.generateTemplateKey();
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey = "";
@@ -2852,7 +2883,7 @@
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
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;`);
+ ctx.addLine(`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`);
let shouldProxy = !ctx.parentNode;
if (shouldProxy) {
let id = ctx.generateID();
@@ -2924,7 +2955,7 @@
// maybe only do this in dev mode...
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;`);
+ ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
if (hasSlots) {
const clone = node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
@@ -2951,9 +2982,9 @@
}
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
- 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(`let pvnode = h('dummy', {key: ${templateKey}, 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 (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`);
+ ctx.addLine(`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`);
if (registerCode) {
ctx.addLine(registerCode);
}
@@ -2965,6 +2996,7 @@
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
+ ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
return true;
}
@@ -2976,12 +3008,24 @@
this.isRunning = false;
this.requestAnimationFrame = requestAnimationFrame;
}
- addFiber(fiber, callback) {
- this.tasks.push({ fiber, callback });
- if (this.isRunning) {
- return;
- }
- this.scheduleTasks();
+ addFiber(fiber) {
+ return new Promise((resolve, reject) => {
+ if (fiber.error) {
+ return reject(fiber.error);
+ }
+ this.tasks.push({
+ fiber,
+ callback: () => {
+ if (fiber.error) {
+ return reject(fiber.error);
+ }
+ resolve();
+ }
+ });
+ if (!this.isRunning) {
+ this.scheduleTasks();
+ }
+ });
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
@@ -2991,11 +3035,20 @@
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
- if (task.fiber.isCancelled) {
+ if (task.fiber.isCompleted) {
+ task.callback();
return false;
}
if (task.fiber.counter === 0) {
- task.callback(task.fiber.error);
+ if (!task.fiber.error) {
+ try {
+ task.fiber.complete();
+ }
+ catch (e) {
+ task.fiber.handleError(e);
+ }
+ }
+ task.callback();
return false;
}
return true;
@@ -3031,10 +3084,12 @@
* states and in general determine the state of the rendering.
*/
class Fiber {
- constructor(parent, component, 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;
+ constructor(parent, component, scope, vars, force, target) {
+ this.id = Fiber.nextId++;
+ // isCompleted means that the rendering corresponding to this fiber's work is
+ // done, either because the component has been mounted or patched, or because
+ // fiber has been cancelled.
+ this.isCompleted = 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
@@ -3058,15 +3113,43 @@
this.scope = scope;
this.vars = vars;
this.component = component;
+ this.target = target;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = component.__owl__.currentFiber;
- if (oldFiber && !oldFiber.isCancelled) {
- this._remapFiber(oldFiber);
+ if (oldFiber && !oldFiber.isCompleted) {
+ if (oldFiber.root === oldFiber && !parent) {
+ // both oldFiber and this fiber are root fibers
+ this._reuseFiber(oldFiber);
+ return oldFiber;
+ }
+ else {
+ this._remapFiber(oldFiber);
+ }
}
this.root.counter++;
component.__owl__.currentFiber = this;
}
+ /**
+ * When the oldFiber is not completed yet, and both oldFiber and this fiber
+ * are root fibers, we want to reuse the oldFiber instead of creating a new
+ * one. Doing so will guarantee that the initiator(s) of those renderings will
+ * be notified (the promise will resolve) when the last rendering will be done.
+ *
+ * This function thus assumes that oldFiber is a root fiber.
+ */
+ _reuseFiber(oldFiber) {
+ oldFiber.cancel(); // cancel children fibers
+ oldFiber.isCompleted = false; // keep the root fiber alive
+ oldFiber.isRendered = false; // the fiber has to be re-rendered
+ if (oldFiber.child) {
+ // remove relation to children
+ oldFiber.child.parent = null;
+ oldFiber.child = null;
+ }
+ oldFiber.counter = 1; // re-initialize counter
+ oldFiber.id = Fiber.nextId++;
+ }
/**
* 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
@@ -3076,7 +3159,7 @@
_remapFiber(oldFiber) {
oldFiber.cancel();
if (oldFiber === oldFiber.root) {
- oldFiber.root.counter++;
+ oldFiber.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
@@ -3124,7 +3207,26 @@
}
}
/**
- * Apply the given patch queue from a fiber.
+ * Successfully complete the work of the fiber: call the mount or patch hooks
+ * and patch the DOM. This function is called once the fiber and its children
+ * are ready, and the scheduler decides to process it.
+ */
+ complete() {
+ const component = this.component;
+ if (this.target) {
+ component.__patch(this.vnode);
+ this.target.appendChild(component.el);
+ if (document.body.contains(this.target)) {
+ component.__callMounted();
+ }
+ }
+ else if (component.__owl__.isMounted && this === this.root) {
+ this.patchComponents();
+ }
+ this.isCompleted = true;
+ }
+ /**
+ * Compute and apply the patch queue of the 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
@@ -3134,41 +3236,32 @@
const doWork = function (f) {
if (f.shouldPatch) {
patchQueue.push(f);
+ return f.child;
}
- return f.child;
};
this._walk(doWork);
let component = this.component;
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();
+ for (let i = 0; i < patchLen; i++) {
+ component = patchQueue[i].component;
+ if (component.__owl__.willPatchCB) {
+ component.__owl__.willPatchCB();
}
- }
- catch (e) {
- console.error(e);
+ component.willPatch();
}
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
+ component.__owl__.currentFiber = null;
}
- try {
- for (let i = patchLen - 1; i >= 0; i--) {
- component = patchQueue[i].component;
- component.patched();
- if (component.__owl__.patchedCB) {
- component.__owl__.patchedCB();
- }
+ 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);
- }
}
/**
* Cancel a fiber and all its children.
@@ -3178,7 +3271,7 @@
if (!f.isRendered) {
f.root.counter--;
}
- f.isCancelled = true;
+ f.isCompleted = true;
return f.child;
});
}
@@ -3191,20 +3284,18 @@
* being in a corrupted state.
*/
handleError(error) {
- let canCatch = false;
let component = this.component;
- let qweb = component.env.qweb;
+ this.vnode = component.__owl__.vnode || h("div");
+ const qweb = component.env.qweb;
let root = component;
+ let canCatch = false;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent;
}
qweb.trigger("error", error);
if (canCatch) {
- setTimeout(() => {
- console.error(error);
- component.catchError(error);
- });
+ component.catchError(error);
}
else {
// the 3 next lines aim to mark the root fiber as being in error, and
@@ -3216,6 +3307,7 @@
}
}
}
+ Fiber.nextId = 1;
//------------------------------------------------------------------------------
// Prop validation helper
@@ -3308,12 +3400,12 @@
return true;
}
let result = isValidProp(prop, propDef.type);
- if (propDef.type === Array) {
+ if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
}
}
- if (propDef.type === Object) {
+ if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
@@ -3398,6 +3490,7 @@
children: {},
cmap: {},
currentFiber: null,
+ parentLastFiberId: 0,
boundHandlers: {},
mountedCB: null,
willUnmountCB: null,
@@ -3505,29 +3598,14 @@
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
- return new Promise((resolve, reject) => {
- const fiber = new Fiber(null, this, undefined, undefined, false);
- scheduler.addFiber(fiber, err => {
- if (err) {
- reject(err);
- return;
- }
- if (!__owl__.isDestroyed) {
- this.__patch(fiber.vnode);
- target.appendChild(this.el);
- if (document.body.contains(target)) {
- this.__callMounted();
- }
- }
- resolve();
- });
- if (!__owl__.vnode) {
- this.__prepareAndRender(fiber);
- }
- else {
- this.__render(fiber);
- }
- });
+ const fiber = new Fiber(null, this, undefined, undefined, false, target);
+ if (!__owl__.vnode) {
+ this.__prepareAndRender(fiber);
+ }
+ else {
+ this.__render(fiber);
+ }
+ return scheduler.addFiber(fiber);
}
/**
* The unmount method is the opposite of the mount method. It is useful
@@ -3550,24 +3628,35 @@
*/
async render(force = false) {
const __owl__ = this.__owl__;
- if ((!__owl__.isMounted && !__owl__.currentFiber) ||
- (__owl__.currentFiber && !__owl__.currentFiber.isRendered)) {
+ if (!__owl__.isMounted && !__owl__.currentFiber) {
+ // if we get here, this means that the component was either never mounted,
+ // or was unmounted and some state change triggered a render. Either way,
+ // we do not want to actually render anything in this case.
return;
}
- return new Promise((resolve, reject) => {
- const fiber = new Fiber(null, this, undefined, undefined, force);
- scheduler.addFiber(fiber.root, err => {
- if (err) {
- reject(err);
- return;
- }
- if (__owl__.isMounted && fiber === fiber.root) {
- fiber.patchComponents();
- }
- resolve();
- });
- this.__render(fiber);
+ if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
+ return scheduler.addFiber(__owl__.currentFiber.root);
+ }
+ // if we aren't mounted at this point, it implies that there is a
+ // currentFiber that is already rendered (isRendered is true), so we are
+ // about to be mounted
+ const isMounted = __owl__.isMounted;
+ const fiber = new Fiber(null, this, undefined, undefined, force, null);
+ Promise.resolve().then(() => {
+ if (__owl__.isMounted || !isMounted) {
+ // we are mounted (__owl__.isMounted), or if we are currently being
+ // mounted (!isMounted), so we call __render
+ this.__render(fiber);
+ }
+ else {
+ // we were mounted when render was called, but we aren't anymore, so we
+ // were actually about to be unmounted ; we can thus forget about this
+ // fiber
+ fiber.isCompleted = true;
+ __owl__.currentFiber = null;
+ }
});
+ return scheduler.addFiber(fiber);
}
/**
* Destroy the component. This operation is quite complex:
@@ -3604,7 +3693,7 @@
*/
trigger(eventType, payload) {
if (this.el) {
- const ev = new CustomEvent(eventType, {
+ const ev = new OwlEvent(this, eventType, {
bubbles: true,
cancelable: true,
detail: payload
@@ -3648,7 +3737,7 @@
__owl__.isDestroyed = true;
delete __owl__.vnode;
if (__owl__.currentFiber) {
- __owl__.currentFiber.isCancelled = true;
+ __owl__.currentFiber.isCompleted = true;
}
}
__callMounted() {
@@ -3661,14 +3750,10 @@
}
}
__owl__.isMounted = true;
- try {
- this.mounted();
- if (__owl__.mountedCB) {
- __owl__.mountedCB();
- }
- }
- catch (e) {
- console.error(e); // TODO : add a test
+ __owl__.currentFiber = null;
+ this.mounted();
+ if (__owl__.mountedCB) {
+ __owl__.mountedCB();
}
}
__callWillUnmount() {
@@ -3694,7 +3779,7 @@
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
- const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
+ const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
}
@@ -3712,7 +3797,7 @@
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
- if (fiber.isCancelled) {
+ if (fiber.isCompleted) {
return;
}
this.props = nextProps;
@@ -3727,7 +3812,6 @@
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
@@ -3735,7 +3819,7 @@
* parent template.
*/
__prepare(parentFiber, scope, vars, previousSibling) {
- const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
+ const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
@@ -3770,13 +3854,12 @@
}
catch (e) {
fiber.handleError(e);
- fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
- if (!fiber.isCancelled) {
+ if (!fiber.isCompleted) {
this.__render(fiber);
}
}
@@ -3785,29 +3868,45 @@
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
- let vnode;
+ let error;
try {
- vnode = __owl__.renderFn(this, {
+ let vnode = __owl__.renderFn(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
});
+ // we iterate over the children to detect those that no longer belong to the
+ // current rendering: those ones, if not mounted yet, can (and have to) be
+ // destroyed right now, because they are not in the DOM, and thus we won't
+ // be notified later on (when patching), that they are removed from the DOM
+ for (let childKey in __owl__.children) {
+ let child = __owl__.children[childKey];
+ if (!child.__owl__.isMounted && child.__owl__.parentLastFiberId < fiber.id) {
+ child.destroy();
+ }
+ }
+ if (!vnode) {
+ throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
+ }
+ fiber.vnode = vnode;
+ // we apply here the class information described on the component by the
+ // template (so, something like ) to the actual
+ // root vnode
+ if (__owl__.classObj) {
+ const data = vnode.data;
+ data.class = Object.assign(data.class || {}, __owl__.classObj);
+ }
}
catch (e) {
- vnode = __owl__.vnode || h("div");
- fiber.handleError(e);
+ error = e;
}
- fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
- // 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);
- }
fiber.root.counter--;
fiber.isRendered = true;
+ if (error) {
+ fiber.handleError(error);
+ }
}
/**
* Only called by qweb t-component directive
@@ -3859,6 +3958,142 @@
// expose scheduler s.t. it can be mocked for testing purposes
Component.scheduler = scheduler;
+ /**
+ * The `Context` object provides a way to share data between an arbitrary number
+ * of component. Usually, data is passed from a parent to its children component,
+ * but when we have to deal with some mostly global information, this can be
+ * annoying, since each component will need to pass the information to each
+ * children, even though some or most of them will not use the information.
+ *
+ * With a `Context` object, each component can subscribe (with the `useContext`
+ * hook) to its state, and will be updated whenever the context state is updated.
+ */
+ function partitionBy(arr, fn) {
+ let lastGroup = false;
+ let lastValue;
+ return arr.reduce((acc, cur) => {
+ let curVal = fn(cur);
+ if (lastGroup) {
+ if (curVal === lastValue) {
+ lastGroup.push(cur);
+ }
+ else {
+ lastGroup = false;
+ }
+ }
+ if (!lastGroup) {
+ lastGroup = [cur];
+ acc.push(lastGroup);
+ }
+ lastValue = curVal;
+ return acc;
+ }, []);
+ }
+ class Context extends EventBus {
+ constructor(state = {}) {
+ super();
+ this.rev = 1;
+ // mapping from component id to last observed context id
+ this.mapping = {};
+ this.observer = new Observer();
+ this.observer.notifyCB = () => {
+ // notify components in the next microtask tick to ensure that subscribers
+ // are notified only once for all changes that occur in the same micro tick
+ let rev = this.rev;
+ return Promise.resolve().then(() => {
+ if (rev === this.rev) {
+ this.__notifyComponents();
+ }
+ });
+ };
+ this.state = this.observer.observe(state);
+ this.subscriptions.update = [];
+ }
+ /**
+ * 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. More precisely, if an update
+ * as an effect of destroying a children, we do not want to call any code
+ * from the child, and certainly not render it.
+ *
+ * This method implements a simple grouping algorithm by depth. If we have
+ * connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
+ * them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
+ * be updated sequentially, but each components in a given group will be done in
+ * parallel.
+ *
+ * This is a very simple algorithm, but it avoids checking if a given
+ * component is a child of another.
+ */
+ async __notifyComponents() {
+ const rev = ++this.rev;
+ const subscriptions = this.subscriptions.update;
+ const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
+ for (let group of groups) {
+ const proms = group.map(sub => sub.callback.call(sub.owner, rev));
+ // at this point, each component in the current group has registered a
+ // top level fiber in the scheduler. It could happen that rendering these
+ // components is done (if they have no children). This is why we manually
+ // flush the scheduler. This will force the scheduler to check
+ // immediately if they are done, which will cause their rendering
+ // promise to resolve earlier, which means that there is a chance of
+ // processing the next group in the same frame.
+ scheduler.flush();
+ await Promise.all(proms);
+ }
+ }
+ }
+ /**
+ * The`useContext` hook is the normal way for a component to register themselve
+ * to context state changes. The `useContext` method returns the context state
+ */
+ function useContext(ctx) {
+ const component = Component.current;
+ return useContextWithCB(ctx, component, component.render.bind(component));
+ }
+ function useContextWithCB(ctx, component, method) {
+ const __owl__ = component.__owl__;
+ const id = __owl__.id;
+ const mapping = ctx.mapping;
+ 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__.renderFn;
+ __owl__.renderFn = function (comp, params) {
+ mapping[id] = ctx.rev;
+ return renderFn(comp, params);
+ };
+ ctx.on("update", component, async (contextRev) => {
+ if (mapping[id] < contextRev) {
+ mapping[id] = contextRev;
+ await method();
+ }
+ });
+ const __destroy = component.__destroy;
+ component.__destroy = (parent) => {
+ ctx.off("update", component);
+ delete mapping[id];
+ __destroy.call(component, parent);
+ };
+ return ctx.state;
+ }
+
/**
* Owl Hook System
*
@@ -3986,135 +4221,6 @@
useSubEnv: useSubEnv
});
- /**
- * The `Context` object provides a way to share data between an arbitrary number
- * of component. Usually, data is passed from a parent to its children component,
- * but when we have to deal with some mostly global information, this can be
- * annoying, since each component will need to pass the information to each
- * children, even though some or most of them will not use the information.
- *
- * With a `Context` object, each component can subscribe (with the `useContext`
- * hook) to its state, and will be updated whenever the context state is updated.
- */
- function partitionBy(arr, fn) {
- let lastGroup = false;
- let lastValue;
- return arr.reduce((acc, cur) => {
- let curVal = fn(cur);
- if (lastGroup) {
- if (curVal === lastValue) {
- lastGroup.push(cur);
- }
- else {
- lastGroup = false;
- }
- }
- if (!lastGroup) {
- lastGroup = [cur];
- acc.push(lastGroup);
- }
- lastValue = curVal;
- return acc;
- }, []);
- }
- class Context extends EventBus {
- constructor(state = {}) {
- super();
- this.rev = 1;
- // mapping from component id to last observed context id
- this.mapping = {};
- this.observer = new Observer();
- this.observer.notifyCB = this.__notifyComponents.bind(this);
- this.state = this.observer.observe(state);
- this.subscriptions.update = [];
- }
- /**
- * 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. More precisely, if an update
- * as an effect of destroying a children, we do not want to call any code
- * from the child, and certainly not render it.
- *
- * This method implements a simple grouping algorithm by depth. If we have
- * connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
- * them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
- * be updated sequentially, but each components in a given group will be done in
- * parallel.
- *
- * This is a very simple algorithm, but it avoids checking if a given
- * component is a child of another.
- */
- async __notifyComponents() {
- const rev = ++this.rev;
- const subscriptions = this.subscriptions.update;
- const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
- for (let group of groups) {
- const proms = Promise.all(group.map(sub => {
- if (sub.owner ? sub.owner.__owl__.isMounted : true) {
- return sub.callback.call(sub.owner, rev);
- }
- }));
- // at this point, each component in the current group has registered a
- // top level fiber in the scheduler. It could happen that rendering these
- // components is done (if they have no children). This is why we manually
- // flush the scheduler. This will force the scheduler to check
- // immediately if they are done, which will cause their rendering
- // promise to resolve earlier, which means that there is a chance of
- // processing the next group in the same frame.
- scheduler.flush();
- await proms;
- }
- }
- }
- /**
- * The`useContext` hook is the normal way for a component to register themselve
- * to context state changes. The `useContext` method returns the context state
- */
- function useContext(ctx) {
- const component = Component.current;
- return useContextWithCB(ctx, component, component.render.bind(component));
- }
- function useContextWithCB(ctx, component, method) {
- const __owl__ = component.__owl__;
- const id = __owl__.id;
- const mapping = ctx.mapping;
- 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__.renderFn;
- __owl__.renderFn = function (comp, params) {
- mapping[id] = ctx.rev;
- return renderFn(comp, params);
- };
- ctx.on("update", component, async (contextRev) => {
- if (mapping[id] < contextRev) {
- mapping[id] = contextRev;
- await method();
- }
- });
- onWillUnmount(() => {
- ctx.off("update", component);
- delete mapping[id];
- });
- return ctx.state;
- }
-
class Store extends Context {
constructor(config) {
super(config.state);
@@ -4173,9 +4279,9 @@
});
useContextWithCB(store, component, function () {
let shouldRender = false;
- updateFunctions.forEach(function (updateFn) {
- shouldRender = updateFn() || shouldRender;
- });
+ for (let fn of updateFunctions) {
+ shouldRender = fn() || shouldRender;
+ }
if (shouldRender) {
return component.render();
}
@@ -4544,9 +4650,9 @@
exports.useState = useState$1;
exports.utils = utils;
- exports.__info__.version = '1.0.0-alpha3';
- exports.__info__.date = '2019-11-13T14:44:19.699Z';
- exports.__info__.hash = 'd249f50';
+ exports.__info__.version = '1.0.0-alpha4';
+ exports.__info__.date = '2019-11-22T12:03:23.892Z';
+ exports.__info__.hash = 'ff76747';
exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {}));
diff --git a/playground/app.js b/playground/app.js
index f4d841fc..3345f67a 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -120,20 +120,34 @@ async function makeApp(js, css, xml) {
.join("\n");
const JS = `
-async function loadTemplates() {
- try {
- return owl.utils.loadFile('app.xml');
- } catch(e) {
- console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
- }
-}
-
-function start([TEMPLATES]) {
- // Application code
+/**
+ * This is the javascript code defined in the playground.
+ * In a larger application, this code should probably be moved in different
+ * sub files.
+ */
+function app() {
${processedJS}
}
-Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
+/**
+ * Initialization code
+ * This code load templates, and make sure everything is properly connected.
+ */
+async function start() {
+ let templates;
+ try {
+ templates = await owl.utils.loadFile('app.xml');
+ } catch(e) {
+ console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
+ return;
+ }
+ const env = { qweb: new owl.QWeb({templates})};
+ owl.Component.env = env;
+ await owl.utils.whenReady();
+ app();
+}
+
+start();
`;
zip.file("app.js", JS);