[IMP] owl: update to v1.0.0-alpha

This commit is contained in:
Géry Debongnie
2019-10-30 16:24:51 +01:00
parent d6aad9c673
commit 71a6091772
5 changed files with 305 additions and 324 deletions
+1 -2
View File
@@ -63,7 +63,7 @@ class Counter extends Component {
} }
} }
const counter = new Counter({ qweb: new QWeb(TEMPLATES) }); const counter = new Counter();
counter.mount(document.body);</code></pre> counter.mount(document.body);</code></pre>
</section> </section>
<section class="column"> <section class="column">
@@ -74,7 +74,6 @@ counter.mount(document.body);</code></pre>
&lt;/templates&gt;</code></pre> &lt;/templates&gt;</code></pre>
</section> </section>
</div> </div>
<p>Note that we assume here that the xml templates are available in the <tt>TEMPLATES</tt> string. More interesting examples can be found on the <a href="playground/">Playground</a> application.</p>
</div> </div>
</article> </article>
</main> </main>
+252 -265
View File
@@ -112,10 +112,6 @@
const metadata = this.weakMap.get(value); const metadata = this.weakMap.get(value);
return metadata ? metadata.rev : 0; return metadata ? metadata.rev : 0;
} }
deepRevNumber(value) {
const metadata = this.weakMap.get(value);
return metadata ? metadata.deepRev : 0;
}
_observe(value, parent) { _observe(value, parent) {
var self = this; var self = this;
const proxy = new Proxy(value, { const proxy = new Proxy(value, {
@@ -148,7 +144,6 @@
value, value,
proxy, proxy,
rev: this.rev, rev: this.rev,
deepRev: this.rev,
parent parent
}; };
this.weakMap.set(value, metadata); this.weakMap.set(value, metadata);
@@ -158,11 +153,10 @@
_updateRevNumber(target) { _updateRevNumber(target) {
this.rev++; this.rev++;
let metadata = this.weakMap.get(target); let metadata = this.weakMap.get(target);
metadata.rev++;
let parent = target; let parent = target;
do { do {
metadata = this.weakMap.get(parent); metadata = this.weakMap.get(parent);
metadata.deepRev++; metadata.rev++;
} while ((parent = metadata.parent) && parent !== target); } while ((parent = metadata.parent) && parent !== target);
} }
} }
@@ -1296,6 +1290,7 @@
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"]; const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/; const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
const NODE_HOOKS_PARAMS = { const NODE_HOOKS_PARAMS = {
@@ -1357,7 +1352,7 @@
// QWeb rendering engine // QWeb rendering engine
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class QWeb extends EventBus { class QWeb extends EventBus {
constructor(data) { constructor(config = {}) {
super(); super();
this.h = h; this.h = h;
// recursiveTemplates contains sub templates called with t-call, but which // recursiveTemplates contains sub templates called with t-call, but which
@@ -1366,8 +1361,11 @@
this.recursiveFns = {}; this.recursiveFns = {};
this.isUpdating = false; this.isUpdating = false;
this.templates = Object.create(QWeb.TEMPLATES); this.templates = Object.create(QWeb.TEMPLATES);
if (data) { if (config.templates) {
this.addTemplates(data); this.addTemplates(config.templates);
}
if (config.translateFn) {
this.translateFn = config.translateFn;
} }
} }
static addDirective(directive) { static addDirective(directive) {
@@ -1594,6 +1592,11 @@
} }
text = text.replace(whitespaceRE, " "); text = text.replace(whitespaceRE, " ");
} }
if (this.translateFn) {
if (node.parentNode.getAttribute("t-translation") !== "off") {
text = this.translateFn(text);
}
}
if (ctx.parentNode) { if (ctx.parentNode) {
if (node.nodeType === 3) { if (node.nodeType === 3) {
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`); ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
@@ -1771,7 +1774,10 @@
let classObj = ""; let classObj = "";
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name; let name = attributes[i].name;
const value = attributes[i].textContent; let value = attributes[i].textContent;
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
value = this.translateFn(value);
}
// regular attributes // regular attributes
if (!name.startsWith("t-") && !node.getAttribute("t-attf-" + name)) { if (!name.startsWith("t-") && !node.getAttribute("t-attf-" + name)) {
const attID = ctx.generateID(); const attID = ctx.generateID();
@@ -1856,15 +1862,7 @@
} }
} }
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
let nodeKey = node.getAttribute("t-key"); let nodeKey = ctx.lastNodeKey || nodeID;
if (nodeKey) {
ctx.addLine(`const nodeKey${nodeID} = ${ctx.formatExpression(nodeKey)}`);
nodeKey = `nodeKey${nodeID}`;
ctx.lastNodeKey = nodeKey;
}
else {
nodeKey = nodeID;
}
const parts = [`key:${nodeKey}`]; const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) { if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`); parts.push(`attrs:{${attrs.join(",")}}`);
@@ -1910,7 +1908,7 @@
name: 1, name: 1,
att: 1, att: 1,
attf: 1, attf: 1,
key: 1 translation: 1
}; };
QWeb.DIRECTIVES = []; QWeb.DIRECTIVES = [];
QWeb.TEMPLATES = {}; QWeb.TEMPLATES = {};
@@ -2224,18 +2222,10 @@
ctx.addToScope(name, `_${keysID}[${loopVar}]`); ctx.addToScope(name, `_${keysID}[${loopVar}]`);
ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`); ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
const nodeCopy = node.cloneNode(true); const nodeCopy = node.cloneNode(true);
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); let shouldWarn = !nodeCopy.hasAttribute("t-key") &&
if (!shouldWarn && node.tagName === "t") {
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
shouldWarn = true;
}
if (!shouldWarn &&
node.children.length === 1 && node.children.length === 1 &&
node.children[0].tagName !== "t" && node.children[0].tagName !== "t" &&
!node.children[0].hasAttribute("t-key")) { !node.children[0].hasAttribute("t-key");
shouldWarn = true;
}
}
if (shouldWarn) { if (shouldWarn) {
console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`); console.warn(`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`);
} }
@@ -2505,6 +2495,50 @@
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`); ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
} }
}); });
//------------------------------------------------------------------------------
// t-key
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.lastNodeKey = `nodeKey${id}`;
}
});
const config = {};
Object.defineProperty(config, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
}
else {
console.log(`Owl is now running in 'prod' mode.`);
}
},
});
let env;
Object.defineProperty(config, "env", {
get() {
if (!env) {
env = {};
}
if (!env.qweb) {
env.qweb = new QWeb();
}
return env;
},
set(newEnv) {
env = newEnv;
},
});
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// t-component // t-component
@@ -2686,7 +2720,7 @@
*/ */
QWeb.addDirective({ QWeb.addDirective({
name: "component", name: "component",
extraNames: ["props", "keepalive"], extraNames: ["props"],
priority: 100, priority: 100,
atNodeEncounter({ ctx, value, node, qweb }) { atNodeEncounter({ ctx, value, node, qweb }) {
ctx.addLine("//COMPONENT"); ctx.addLine("//COMPONENT");
@@ -2694,7 +2728,6 @@
ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true; ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false; let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition // t-on- events and t-transition
const events = []; const events = [];
@@ -2723,28 +2756,18 @@
} }
} }
} }
let key = node.getAttribute("t-key");
if (key) {
key = ctx.formatExpression(key);
}
// computing the props string representing the props object // computing the props string representing the props object
let propStr = Object.keys(props) let propStr = Object.keys(props)
.map(k => k + ":" + props[k]) .map(k => k + ":" + props[k])
.join(","); .join(",");
let defID = ctx.generateID(); let defID = ctx.generateID();
let componentID = ctx.generateID(); let componentID = ctx.generateID();
let keyID = key && ctx.generateID();
if (key) {
// we bind a variable to the key (could be a complex expression, so we
// want to evaluate it only once)
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
}
let locationExpr = `\`__${ctx.generateID()}__`; let locationExpr = `\`__${ctx.generateID()}__`;
for (let i = 0; i < ctx.loopNumber - 1; i++) { for (let i = 0; i < ctx.loopNumber - 1; i++) {
locationExpr += `\${i${i + 1}}__`; locationExpr += `\${i${i + 1}}__`;
} }
if (key || ctx.currentKey) { if (ctx.lastNodeKey || ctx.currentKey) {
const k = key ? `key${keyID}` : ctx.currentKey; const k = ctx.lastNodeKey || ctx.currentKey;
ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`); ctx.addLine(`let templateId${componentID} = ${locationExpr}\` + ${k};`);
} }
else { else {
@@ -2765,8 +2788,8 @@
if (transition) { if (transition) {
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`; transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
} }
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; let finalizeComponentCode = `w${componentID}.destroy();`;
if (ref && !keepAlive) { if (ref) {
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`; finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
} }
if (transition) { if (transition) {
@@ -2845,9 +2868,6 @@
} }
ctx.addLine(`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`); ctx.addLine(`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`);
let shouldProxy = !ctx.parentNode; let shouldProxy = !ctx.parentNode;
if (keepAlive) {
ctx.addLine(`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`);
}
if (shouldProxy) { if (shouldProxy) {
let id = ctx.generateID(); let id = ctx.generateID();
ctx.rootContext.rootNode = id; ctx.rootContext.rootNode = id;
@@ -2893,29 +2913,13 @@
} }
ctx.addIf(`w${componentID}`); ctx.addIf(`w${componentID}`);
// need to update component // 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 = ""; let styleCode = "";
if (tattStyle) { if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`; styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
} }
ctx.addLine(`w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars && ctx.addLine(`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
", " + scopeVars}, sibling)${styleCode};`); ", " + scopeVars}, sibling)${styleCode};`);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`); 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) { if (registerCode) {
ctx.addLine(registerCode); ctx.addLine(registerCode);
} }
@@ -2980,6 +2984,54 @@
} }
}); });
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(task.fiber.error);
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;
}
});
}
}
const raf = window.requestAnimationFrame.bind(window);
const scheduler = new Scheduler(raf);
/** /**
* Owl Fiber Class * Owl Fiber Class
* *
@@ -2993,7 +3045,7 @@
* states and in general determine the state of the rendering. * states and in general determine the state of the rendering.
*/ */
class Fiber { class Fiber {
constructor(parent, component, props, scope, vars, force) { constructor(parent, component, scope, vars, force) {
// isCancelled means that the rendering corresponding to this fiber and its // isCancelled means that the rendering corresponding to this fiber and its
// children is cancelled. No extra work should be done. // children is cancelled. No extra work should be done.
this.isCancelled = false; this.isCancelled = false;
@@ -3019,7 +3071,6 @@
this.force = force; this.force = force;
this.scope = scope; this.scope = scope;
this.vars = vars; this.vars = vars;
this.props = props;
this.component = component; this.component = component;
this.root = parent ? parent.root : this; this.root = parent ? parent.root : this;
this.parent = parent; this.parent = parent;
@@ -3102,7 +3153,6 @@
}; };
this._walk(doWork); this._walk(doWork);
let component = this.component; let component = this.component;
this.shouldPatch = false;
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
try { try {
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
@@ -3116,16 +3166,11 @@
catch (e) { catch (e) {
console.error(e); console.error(e);
} }
try {
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i]; const fiber = patchQueue[i];
component = fiber.component; component = fiber.component;
component.__patch(fiber.vnode); component.__patch(fiber.vnode);
} }
}
catch (e) {
this.handleError(e);
}
try { try {
for (let i = patchLen - 1; i >= 0; i--) { for (let i = patchLen - 1; i >= 0; i--) {
component = patchQueue[i].component; component = patchQueue[i].component;
@@ -3138,7 +3183,6 @@
catch (e) { catch (e) {
console.error(e); console.error(e);
} }
this.shouldPatch = true;
} }
/** /**
* Cancel a fiber and all its children. * Cancel a fiber and all its children.
@@ -3152,7 +3196,39 @@
return f.child; return f.child;
}); });
} }
handleError(e) { } /**
* This is the global error handler for errors occurring in Owl main lifecycle
* methods. Caught errors are triggered on the QWeb instance, and are
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
handleError(error) {
let canCatch = false;
let component = this.component;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent;
}
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
console.error(error);
component.catchError(error);
});
}
else {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error;
scheduler.flush();
root.destroy();
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -3174,7 +3250,7 @@
// optional prop // optional prop
break; break;
} }
if (!props[propName]) { if (!(propName in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`); throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
} }
} }
@@ -3250,67 +3326,6 @@
return result; 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 // Component
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -3322,47 +3337,32 @@
/** /**
* Creates an instance of Component. * Creates an instance of Component.
* *
* The root component of a component tree needs an environment:
*
* ```javascript
* const root = new RootComponent(env, props);
* ```
*
* Every other component simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeComponent(parent, props);
* ```
*
* Note that most of the time, only the root component needs to be created by * Note that most of the time, only the root component needs to be created by
* hand. Other components should be created automatically by the framework (with * hand. Other components should be created automatically by the framework (with
* the t-component directive in a template) * the t-component directive in a template)
*/ */
constructor(parent, props) { constructor(parent, props) {
const defaultProps = this.constructor.defaultProps;
Component.current = this; Component.current = this;
const id = nextId++;
let depth;
if (parent) {
const defaultProps = this.constructor.defaultProps;
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
} }
// is this a good idea? this.props = props;
// Pro: if props is empty, we can create easily a component
// Con: this is not really safe
// Pro: but creating component (by a template) is always unsafe anyway
this.props = props || {};
let id = nextId++;
let p = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__owl__.children[id] = this;
}
else {
this.env = parent;
if (QWeb.dev) { if (QWeb.dev) {
// we only validate props for root widgets here. "Regular" widget
// props are validated by the t-component directive
QWeb.utils.validateProps(this.constructor, this.props); QWeb.utils.validateProps(this.constructor, this.props);
} }
this.env = parent.env;
const __powl__ = parent.__owl__;
__powl__.children[id] = this;
depth = __powl__.depth + 1;
}
else {
// we are the root component
this.env = config.env;
this.props = undefined;
this.env.qweb.on("update", this, () => { this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) { if (this.__owl__.isMounted) {
this.render(true); this.render(true);
@@ -3376,15 +3376,17 @@
this.env.qweb.off("update", this); this.env.qweb.off("update", this);
} }
}); });
depth = 0;
} }
const qweb = this.env.qweb; const qweb = this.env.qweb;
this.__owl__ = { this.__owl__ = {
id: id, id: id,
depth: depth,
vnode: null, vnode: null,
pvnode: null, pvnode: null,
isMounted: false, isMounted: false,
isDestroyed: false, isDestroyed: false,
parent: p, parent: parent || null,
children: {}, children: {},
cmap: {}, cmap: {},
currentFiber: null, currentFiber: null,
@@ -3474,11 +3476,6 @@
* @see mounted * @see mounted
*/ */
willUnmount() { } willUnmount() { }
/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error) { }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Public // Public
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -3502,15 +3499,13 @@
} }
return; return;
} }
const fiber = new Fiber(null, this, this.props, undefined, undefined, false); return new Promise((resolve, reject) => {
if (!__owl__.vnode) { const fiber = new Fiber(null, this, undefined, undefined, false);
this.__prepareAndRender(fiber); scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
} }
else {
this.__render(fiber);
}
return new Promise(resolve => {
scheduler.addFiber(fiber, () => {
if (!__owl__.isDestroyed) { if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode); this.__patch(fiber.vnode);
target.appendChild(this.el); target.appendChild(this.el);
@@ -3520,6 +3515,12 @@
} }
resolve(); resolve();
}); });
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
}
else {
this.__render(fiber);
}
}); });
} }
/** /**
@@ -3547,15 +3548,19 @@
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)) { (__owl__.currentFiber && !__owl__.currentFiber.isRendered)) {
return; return;
} }
const fiber = new Fiber(null, this, this.props, undefined, undefined, force); return new Promise((resolve, reject) => {
this.__render(fiber); const fiber = new Fiber(null, this, undefined, undefined, force);
return new Promise(resolve => { scheduler.addFiber(fiber.root, err => {
scheduler.addFiber(fiber.root, () => { if (err) {
reject(err);
return;
}
if (__owl__.isMounted && fiber === fiber.root) { if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents(); fiber.patchComponents();
} }
resolve(); resolve();
}); });
this.__render(fiber);
}); });
} }
/** /**
@@ -3680,7 +3685,7 @@
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force); const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
if (!parentFiber.child) { if (!parentFiber.child) {
parentFiber.child = fiber; parentFiber.child = fiber;
} }
@@ -3691,6 +3696,9 @@
if (defaultProps) { if (defaultProps) {
nextProps = this.__applyDefaultProps(nextProps, defaultProps); nextProps = this.__applyDefaultProps(nextProps, defaultProps);
} }
if (QWeb.dev) {
QWeb.utils.validateProps(this.constructor, nextProps);
}
await Promise.all([ await Promise.all([
this.willUpdateProps(nextProps), this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps) __owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
@@ -3718,7 +3726,7 @@
* parent template. * parent template.
*/ */
__prepare(parentFiber, scope, vars, previousSibling) { __prepare(parentFiber, scope, vars, previousSibling) {
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force); const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
fiber.shouldPatch = false; fiber.shouldPatch = false;
if (!parentFiber.child) { if (!parentFiber.child) {
parentFiber.child = fiber; parentFiber.child = fiber;
@@ -3757,7 +3765,7 @@
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]); await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} }
catch (e) { catch (e) {
errorHandler(e, fiber); fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve(); return Promise.resolve();
} }
@@ -3782,7 +3790,7 @@
} }
catch (e) { catch (e) {
vnode = __owl__.vnode || h("div"); vnode = __owl__.vnode || h("div");
errorHandler(e, fiber); fiber.handleError(e);
} }
fiber.vnode = vnode; fiber.vnode = vnode;
if (__owl__.observer) { if (__owl__.observer) {
@@ -3846,40 +3854,6 @@
Component._template = null; Component._template = null;
Component.current = null; Component.current = null;
Component.components = {}; Component.components = {};
//------------------------------------------------------------------------------
// 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
* potentially given to some parent component which implements `catchError`.
*
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
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)) {
root = component;
component = component.__owl__.parent;
}
console.error(error);
qweb.trigger("error", error);
if (canCatch) {
setTimeout(() => {
component.catchError(error);
});
}
else {
root.destroy();
}
}
/** /**
* Owl Hook System * Owl Hook System
@@ -4011,6 +3985,27 @@
* With a `Context` object, each component can subscribe (with the `useContext` * 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. * 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 { class Context extends EventBus {
constructor(state = {}) { constructor(state = {}) {
super(); super();
@@ -4020,37 +4015,44 @@
this.observer = new Observer(); this.observer = new Observer();
this.observer.notifyCB = this.__notifyComponents.bind(this); this.observer.notifyCB = this.__notifyComponents.bind(this);
this.state = this.observer.observe(state); this.state = this.observer.observe(state);
this.subscriptions.update = [];
} }
/** /**
* Instead of using trigger to emit an update event, we actually implement * 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 * 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 * a simple trigger function: we need to wait for parent components to be
* done before doing children components. The reason is that if an update * done before doing children components. More precisely, if an update
* as an effect of destroying a children, we do not want to call the * as an effect of destroying a children, we do not want to call any code
* mapStoreToProps function of the child, nor rendering it. * from the child, and certainly not render it.
* *
* This method is not optimal if we have a bunch of asynchronous components: * This method implements a simple grouping algorithm by depth. If we have
* we wait sequentially for each component to be completed before updating the * connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
* next. However, the only things that matters is that children are updated * them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
* after their parents. So, this could be optimized by being smarter, and * be updated sequentially, but each components in a given group will be done in
* updating all widgets concurrently, except for parents/children. * parallel.
* *
* A potential cheap way to improve this situation is to keep track of the * This is a very simple algorithm, but it avoids checking if a given
* depth of a component in the component tree. A root component has a depth of * component is a child of another.
* 1, then its children of 2 and so on... Then, we can update all components
* with the same depth in parallel.
*/ */
async __notifyComponents() { async __notifyComponents() {
const rev = ++this.rev; const rev = ++this.rev;
const subs = this.subscriptions.update || []; const subscriptions = this.subscriptions.update;
for (let i = 0, iLen = subs.length; i < iLen; i++) { const groups = partitionBy(subscriptions, s => (s.owner ? s.owner.__owl__.depth : -1));
const sub = subs[i]; for (let group of groups) {
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true; const proms = Promise.all(group.map(sub => {
if (shouldCallback) { if (sub.owner ? sub.owner.__owl__.isMounted : true) {
const render = sub.callback.call(sub.owner, rev); return sub.callback.call(sub.owner, rev);
scheduler.flush();
await render;
} }
}));
// 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;
} }
} }
} }
@@ -4137,7 +4139,7 @@
const component = Component.current; const component = Component.current;
const store = options.store || component.env.store; const store = options.store || component.env.store;
let result = selector(store.state, component.props); let result = selector(store.state, component.props);
const hashFn = store.observer.deepRevNumber.bind(store.observer); const hashFn = store.observer.revNumber.bind(store.observer);
let revNumber = hashFn(result) || result; let revNumber = hashFn(result) || result;
const isEqual = options.isEqual || isStrictEqual; const isEqual = options.isEqual || isStrictEqual;
if (!store.updateFunctions[component.__owl__.id]) { if (!store.updateFunctions[component.__owl__.id]) {
@@ -4165,7 +4167,6 @@
} }
}); });
onWillUpdateProps(props => { onWillUpdateProps(props => {
// FIXME: only do that if not keepalive + do it in destroy in that case
delete store.updateFunctions[component.__owl__.id]; delete store.updateFunctions[component.__owl__.id];
result = selector(store.state, props); result = selector(store.state, props);
}); });
@@ -4373,7 +4374,7 @@
// Private helpers // Private helpers
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
setUrlFromPath(path) { setUrlFromPath(path) {
const separator = this.mode === "hash" ? "/" : ""; const separator = this.mode === "hash" ? location.pathname : "";
const url = location.origin + separator + path; const url = location.origin + separator + path;
if (url !== window.location.href) { if (url !== window.location.href) {
window.history.pushState({}, path, url); window.history.pushState({}, path, url);
@@ -4513,27 +4514,13 @@
useStore: useStore useStore: useStore
}); });
const __info__ = {}; const __info__ = {};
Object.defineProperty(__info__, "mode", {
get() {
return QWeb.dev ? "dev" : "prod";
},
set(mode) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
console.warn(`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`);
}
else {
console.log(`Owl is now running in 'prod' mode.`);
}
}
});
exports.Component = Component; exports.Component = Component;
exports.Context = Context$1; exports.Context = Context$1;
exports.QWeb = QWeb; exports.QWeb = QWeb;
exports.Store = Store$1; exports.Store = Store$1;
exports.__info__ = __info__; exports.__info__ = __info__;
exports.config = config;
exports.core = core; exports.core = core;
exports.hooks = hooks$1; exports.hooks = hooks$1;
exports.misc = misc; exports.misc = misc;
@@ -4542,9 +4529,9 @@
exports.useState = useState$1; exports.useState = useState$1;
exports.utils = utils; exports.utils = utils;
exports.__info__.version = '0.24.0'; exports.__info__.version = '1.0.0-alpha';
exports.__info__.date = '2019-10-25T15:08:12.840Z'; exports.__info__.date = '2019-10-30T15:41:04.324Z';
exports.__info__.hash = 'f0b5a55'; exports.__info__.hash = '2fc71cf';
exports.__info__.url = 'https://github.com/odoo/owl'; exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {})); }(this.owl = this.owl || {}));
+13 -3
View File
@@ -90,7 +90,14 @@ function makeCodeIframe(js, css, xml, errorHandler) {
owlScript.addEventListener("load", () => { owlScript.addEventListener("load", () => {
const script = doc.createElement("script"); const script = doc.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`\n${js}`; const content = `
{
owl.__info__.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.config.env = { qweb };
}
${js}`;
script.innerHTML = content; script.innerHTML = content;
iframe.contentWindow.addEventListener("error", errorHandler); iframe.contentWindow.addEventListener("error", errorHandler);
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler); iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
@@ -429,12 +436,15 @@ App.components = { TabbedEditor };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
async function start() { async function start() {
document.title = `${document.title} (v${owl.__info__.version})`; document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([ const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"), owl.utils.loadFile("templates.xml"),
owl.utils.whenReady() owl.utils.whenReady()
]); ]);
const qweb = new owl.QWeb(templates); const qweb = new owl.QWeb({ templates });
const app = new App({ qweb }); owl.config.env = { qweb };
const app = new App();
app.mount(document.body); app.mount(document.body);
} }
+20 -35
View File
@@ -16,9 +16,7 @@ class App extends Component {
App.components = { Greeter }; App.components = { Greeter };
// Application setup // Application setup
// Note that the xml templates are injected into the global TEMPLATES variable. const app = new App();
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -70,8 +68,7 @@ class App extends Component {
} }
App.components = { Counter }; App.components = { Counter };
const qweb = new owl.QWeb(TEMPLATES); const app = new App();
const app = new App({qweb});
app.mount(document.body); app.mount(document.body);
`; `;
@@ -228,8 +225,7 @@ class App extends Component {
} }
App.components = { DemoComponent }; App.components = { DemoComponent };
const qweb = new owl.QWeb(TEMPLATES); const app = new App();
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -298,8 +294,7 @@ class App extends owl.Component {
} }
// Application setup // Application setup
const qweb = new owl.QWeb(TEMPLATES); const app = new App();
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -349,11 +344,9 @@ const themeContext = new Context({
background: '#000', background: '#000',
foreground: '#fff', foreground: '#fff',
}); });
const env = { // Add the themeContext the environment to make it available to all components
qweb: new owl.QWeb(TEMPLATES), owl.config.env.themeContext = themeContext;
themeContext: themeContext, const app = new App();
};
const app = new App(env);
app.mount(document.body); app.mount(document.body);
`; `;
@@ -534,26 +527,25 @@ TodoApp.components = { TodoItem };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// App Initialization // App Initialization
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function makeStore() {
function saveState(state) { function saveState(state) {
const str = JSON.stringify(state); const str = JSON.stringify(state);
window.localStorage.setItem(LOCALSTORAGE_KEY, str); window.localStorage.setItem(LOCALSTORAGE_KEY, str);
} }
function loadState() { function loadState() {
const localState = window.localStorage.getItem(LOCALSTORAGE_KEY); const localState = window.localStorage.getItem(LOCALSTORAGE_KEY);
return localState ? JSON.parse(localState) : initialState; return localState ? JSON.parse(localState) : initialState;
} }
function makeEnv() {
const state = loadState(); const state = loadState();
const store = new owl.Store({ state, actions }); const store = new owl.Store({ state, actions });
store.on("update", null, () => saveState(store.state)); store.on("update", null, () => saveState(store.state));
const qweb = new owl.QWeb(TEMPLATES); return store;
return { qweb, store };
} }
const env = makeEnv(); owl.config.env.store = makeStore();
const app = new TodoApp(env); const app = new TodoApp();
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1040,12 +1032,9 @@ function setupResponsivePlugin(env) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application Startup // Application Startup
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const env = { setupResponsivePlugin(owl.config.env);
qweb: new owl.QWeb(TEMPLATES),
};
setupResponsivePlugin(env);
const app = new App(env); const app = new App();
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1187,8 +1176,7 @@ class App extends Component {
App.components = {Card, Counter}; App.components = {Card, Counter};
// Application setup // Application setup
const qweb = new owl.QWeb(TEMPLATES); const app = new App();
const app = new App({ qweb });
app.mount(document.body);`; app.mount(document.body);`;
const SLOTS_XML = `<templates> const SLOTS_XML = `<templates>
@@ -1301,10 +1289,9 @@ class App extends Component {
}, 3000); }, 3000);
} }
} }
App.components = {SlowComponent, NotificationList}; App.components = {SlowComponent, NotificationList, AsyncRoot};
const qweb = new owl.QWeb(TEMPLATES); const app = new App();
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
`; `;
@@ -1373,8 +1360,7 @@ class Form extends Component {
} }
// Application setup // Application setup
const qweb = new owl.QWeb(TEMPLATES); const form = new Form();
const form = new Form({ qweb });
form.mount(document.body); form.mount(document.body);
`; `;
@@ -1540,7 +1526,6 @@ class App extends Component {
} }
App.components = { WindowManager }; App.components = { WindowManager };
const qweb = new owl.QWeb(TEMPLATES);
const windows = [ const windows = [
{ {
name: "Hello", name: "Hello",
@@ -1558,8 +1543,8 @@ const windows = [
} }
]; ];
const env = { qweb, windows }; owl.config.env.windows = windows;
const app = new App(env); const app = new App();
app.mount(document.body); app.mount(document.body);
`; `;
+1 -1
View File
@@ -31,7 +31,7 @@
t-att-style="topEditorStyle"/> t-att-style="topEditorStyle"/>
<t t-if="state.splitLayout"> <t t-if="state.splitLayout">
<div class="separator horizontal"/> <div class="separator horizontal"/>
<TabbedEditor t-keepalive="1" <TabbedEditor
js="false" js="false"
css="state.css" css="state.css"
xml="state.xml" xml="state.xml"