[IMP] owl: update to v0.22.0

This commit is contained in:
Géry Debongnie
2019-10-01 21:15:14 +02:00
parent 65ec12532f
commit b4eccb5149
3 changed files with 387 additions and 190 deletions
+239 -128
View File
@@ -385,6 +385,7 @@
return result; return result;
} }
const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Compilation Context // Compilation Context
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -402,6 +403,7 @@
this.shouldDefineParent = false; this.shouldDefineParent = false;
this.shouldDefineQWeb = false; this.shouldDefineQWeb = false;
this.shouldDefineUtils = false; this.shouldDefineUtils = false;
this.shouldDefineRefs = false;
this.shouldDefineResult = true; this.shouldDefineResult = true;
this.shouldProtectContext = false; this.shouldProtectContext = false;
this.shouldTrackScope = false; this.shouldTrackScope = false;
@@ -439,6 +441,9 @@
if (this.shouldDefineResult) { if (this.shouldDefineResult) {
this.code.unshift(" let result;"); this.code.unshift(" let result;");
} }
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) { if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to // this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it. // pollute the rendering context by adding some keys in it.
@@ -528,7 +533,7 @@
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/ */
interpolate(s) { interpolate(s) {
let matches = s.match(/\{\{.*?\}\}/g); let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) { if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`; return `(${this.formatExpression(s.slice(2, -2))})`;
} }
@@ -1133,9 +1138,13 @@
data.ns = "http://www.w3.org/2000/svg"; data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) { if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0, iLen = children.length; i < iLen; ++i) { for (let i = 0, iLen = children.length; i < iLen; ++i) {
let childData = children[i].data; const child = children[i];
if (child === null) {
continue;
}
let childData = child.data;
if (childData !== undefined) { if (childData !== undefined) {
addNS(childData, children[i].children, children[i].sel); addNS(childData, child.children, child.sel);
} }
} }
} }
@@ -1174,12 +1183,6 @@
children[i] = vnode(undefined, undefined, undefined, children[i], undefined); children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
} }
} }
if (sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined); return vnode(sel, data, children, text, undefined);
} }
@@ -1232,9 +1235,7 @@
if (!result.ok) { if (!result.ok) {
throw new Error("Error while fetching xml templates"); throw new Error("Error while fetching xml templates");
} }
let templates = await result.text(); return await result.text();
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
return templates;
} }
function escape(str) { function escape(str) {
if (str === undefined) { if (str === undefined) {
@@ -1322,10 +1323,15 @@
} }
return expr; return expr;
}, },
shallowEqual shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
}
}; };
function parseXML(xml) { function parseXML(xml) {
const parser = new DOMParser(); const parser = new DOMParser();
// we remove comments from the xml string
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
const doc = parser.parseFromString(xml, "text/xml"); const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) { if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template."; let msg = "Invalid XML in template.";
@@ -1436,7 +1442,7 @@
this._processTemplate(elem); this._processTemplate(elem);
const template = { const template = {
elem, elem,
fn: (context, extra) => { fn: function (context, extra) {
const compiledFunction = this._compile(name, elem); const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction; template.fn = compiledFunction;
return compiledFunction.call(this, context, extra); return compiledFunction.call(this, context, extra);
@@ -1497,8 +1503,8 @@
* to render a full component tree, since this is an asynchronous operation. * to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components. * This method can only render templates without components.
*/ */
renderToString(name, context = {}) { renderToString(name, context = {}, extra) {
const vnode = this.render(name, context); const vnode = this.render(name, context, extra);
if (vnode.sel === undefined) { if (vnode.sel === undefined) {
return vnode.text; return vnode.text;
} }
@@ -1530,8 +1536,8 @@
if (parentContext) { if (parentContext) {
ctx.templates = Object.create(parentContext.templates); ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables); ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode + 1; ctx.nextID = parentContext.nextID + 1;
ctx.parentNode = parentContext.parentNode; ctx.parentNode = parentContext.parentNode || ctx.nextID++;
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
@@ -1539,12 +1545,12 @@
for (let v in parentContext.variables) { for (let v in parentContext.variables) {
let variable = parentContext.variables[v]; let variable = parentContext.variables[v];
if (variable.id) { if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`); ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
} }
} }
} }
if (parentContext) { if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);"); ctx.addLine(" Object.assign(context, extra.fiber.scope);");
} }
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
if (!parentContext) { if (!parentContext) {
@@ -1702,6 +1708,16 @@
ctx = ctx.subContext("inPreTag", true); ctx = ctx.subContext("inPreTag", true);
} }
this._compileChildren(node, ctx); this._compileChildren(node, ctx);
// svg support
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
// the root node. This is the easiest way to support svg sub components:
// they need to have a g tag as root. Otherwise, we would need a complete
// list of allowed svg tags.
const shouldAddNS = node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
if (shouldAddNS) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of validDirectives) { for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) { if (directive.finalize) {
directive.finalize({ node, qweb: this, ctx, fullName, value }); directive.finalize({ node, qweb: this, ctx, fullName, value });
@@ -2125,7 +2141,7 @@
.join(","); .join(",");
varCode = `{${content}}`; varCode = `{${content}}`;
} }
ctx.addLine(`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: ${varCode}, scope}));`); ctx.addLine(`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`);
return true; return true;
} }
templateMap[subTemplate] = true; templateMap[subTemplate] = true;
@@ -2298,9 +2314,10 @@
name: "ref", name: "ref",
priority: 95, priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) { atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`; const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`); addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
} }
}); });
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -2445,7 +2462,7 @@
const type = node.getAttribute("type"); const type = node.getAttribute("type");
let handler; let handler;
let event = fullName.includes(".lazy") ? "change" : "input"; let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(`state.${value}`); const expr = ctx.formatExpression(value);
if (node.tagName === "select") { if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`); addNodeHook("create", `n.elm.value=${expr};`);
@@ -2732,9 +2749,10 @@
let refExpr = ""; let refExpr = "";
let refKey = ""; let refKey = "";
if (ref) { if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`; refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`; refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
} }
let transitionsInsertCode = ""; let transitionsInsertCode = "";
if (transition) { if (transition) {
@@ -2742,7 +2760,7 @@
} }
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) { if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`; finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
} }
if (transition) { if (transition) {
finalizeComponentCode = `let finalize = () => { finalizeComponentCode = `let finalize = () => {
@@ -2825,8 +2843,10 @@
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
} }
let shouldProxy = false; let shouldProxy = false;
if (async || keepAlive) {
ctx.addLine(`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`);
}
if (async) { if (async) {
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine(`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`); ctx.addLine(`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`);
} }
else { else {
@@ -2849,9 +2869,9 @@
else { else {
ctx.addLine(`let props${componentID} = {${propStr}};`); ctx.addLine(`let props${componentID} = {${propStr}};`);
} }
ctx.addIf(`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`); ctx.addIf(`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`);
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`); ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`);
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`); ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
ctx.addElse(); ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`); ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`); ctx.addLine(`w${componentID} = false;`);
@@ -2859,8 +2879,13 @@
ctx.closeIf(); ctx.closeIf();
ctx.addIf(`!w${componentID}`); ctx.addIf(`!w${componentID}`);
// new component // new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`); let dynamicFallback = "";
ctx.addLine(`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`); if (!value.match(INTERP_REGEXP)) {
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
}
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine(`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`);
// maybe only do this in dev mode... // 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(`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`);
if (QWeb.dev) { if (QWeb.dev) {
@@ -2901,14 +2926,16 @@
QWeb.slots[`${slotId}_default`] = slotFn; QWeb.slots[`${slotId}_default`] = slotFn;
} }
} }
let scopeVars = ""; let scopeVars;
if (hasSlots) { if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : ""; let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
if (varDefs.length) { let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars += `, {${varDefs.join(",")}}`; scopeVars = `${scope}, ${vars}`;
}
} }
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`); else {
scopeVars = "undefined, undefined";
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM // hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`; let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) { if (shouldProxy) {
@@ -2917,18 +2944,18 @@
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.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(); ctx.addElse();
// need to update component // need to update component
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) { if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then // 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 // 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 // the willPatch/patched hooks of the component in this case, so we
// disable the patch queue // disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`; patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
} }
if (QWeb.dev) { if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`); ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
} }
ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars && ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`); ", " + scopeVars});`);
let keepAliveCode = ""; let keepAliveCode = "";
if (keepAlive) { if (keepAlive) {
@@ -2940,7 +2967,7 @@
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`); ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
} }
if (async) { if (async) {
ctx.addLine(`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`); ctx.addLine(`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`);
} }
else { else {
ctx.addLine(`extra.promises.push(def${defID});`); ctx.addLine(`extra.promises.push(def${defID});`);
@@ -3075,8 +3102,8 @@
* the t-component directive in a template) * the t-component directive in a template)
*/ */
constructor(parent, props) { constructor(parent, props) {
this.refs = {};
const defaultProps = this.constructor.defaultProps; const defaultProps = this.constructor.defaultProps;
Component._current = this;
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
} }
@@ -3121,14 +3148,14 @@
parent: p, parent: p,
children: {}, children: {},
cmap: {}, cmap: {},
renderId: 1, currentFiber: null,
renderPromise: null,
renderProps: props || null,
boundHandlers: {}, boundHandlers: {},
mountedHandlers: {}, mountedHandlers: {},
willUnmountCB: null,
observer: null, observer: null,
render: null, render: null,
classObj: null classObj: null,
refs: null
}; };
} }
/** /**
@@ -3229,8 +3256,10 @@
if (__owl__.isMounted) { if (__owl__.isMounted) {
return; return;
} }
const fiber = this.__createFiber(false, undefined, undefined, undefined);
if (!__owl__.vnode) { if (!__owl__.vnode) {
const vnode = await this.__prepare(); fiber.promise = this.__prepareAndRender(fiber);
const vnode = await fiber.promise;
if (__owl__.isDestroyed) { if (__owl__.isDestroyed) {
// component was destroyed before we get here... // component was destroyed before we get here...
return; return;
@@ -3238,9 +3267,10 @@
this.__patch(vnode); this.__patch(vnode);
} }
else if (renderBeforeRemount) { else if (renderBeforeRemount) {
const patchQueue = []; fiber.patchQueue.push(fiber);
await this.__render(false, patchQueue, undefined, undefined); fiber.promise = this.__render(fiber);
this.__applyPatchQueue(patchQueue); await fiber.promise;
this.__applyPatchQueue(fiber);
} }
target.appendChild(this.el); target.appendChild(this.el);
if (document.body.contains(target)) { if (document.body.contains(target)) {
@@ -3271,15 +3301,34 @@
if (!__owl__.isMounted) { if (!__owl__.isMounted) {
return; return;
} }
const patchQueue = []; const fiber = this.__createFiber(force, undefined, undefined, undefined);
const renderId = ++__owl__.renderId; fiber.patchQueue.push(fiber);
await this.__render(force, patchQueue, undefined, undefined); fiber.promise = this.__render(fiber);
if (__owl__.isMounted && renderId === __owl__.renderId) { await fiber.promise;
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
// we only update the vnode and the actual DOM if no other rendering // we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called. // occurred between now and when the render method was initially called.
this.__applyPatchQueue(patchQueue); this.__applyPatchQueue(fiber);
} }
} }
__createFiber(force, scope, vars, parent) {
const fiber = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
willPatchResult: null,
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/** /**
* Destroy the component. This operation is quite complex: * Destroy the component. This operation is quite complex:
* - it recursively destroy all children * - it recursively destroy all children
@@ -3386,19 +3435,22 @@
} }
__owl__.isMounted = true; __owl__.isMounted = true;
const handlers = __owl__.mountedHandlers; const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
try { try {
this.mounted(); this.mounted();
for (let key in handlers) {
handlers[key]();
}
} }
catch (e) { catch (e) {
errorHandler(e, this); errorHandler(e, this);
} }
} }
__callWillUnmount() { __callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false; __owl__.isMounted = false;
const children = __owl__.children; const children = __owl__.children;
for (let id in children) { for (let id in children) {
@@ -3412,8 +3464,8 @@
* The __updateProps method is called by the t-component directive whenever * The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered). * it updates a component (so, when the parent template is rerendered).
*/ */
async __updateProps(nextProps, forceUpdate = false, patchQueue, scope, vars) { async __updateProps(nextProps, parentFiber, scope, vars) {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) { if (shouldUpdate) {
const defaultProps = this.constructor.defaultProps; const defaultProps = this.constructor.defaultProps;
if (defaultProps) { if (defaultProps) {
@@ -3421,7 +3473,9 @@
} }
await this.willUpdateProps(nextProps); await this.willUpdateProps(nextProps);
this.props = nextProps; this.props = nextProps;
await this.__render(forceUpdate, patchQueue, scope, vars); const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.patchQueue.push(fiber);
await this.__render(fiber);
} }
} }
/** /**
@@ -3433,13 +3487,17 @@
const target = __owl__.vnode || document.createElement(vnode.sel); const target = __owl__.vnode || document.createElement(vnode.sel);
__owl__.vnode = patch(target, vnode); __owl__.vnode = patch(target, vnode);
} }
__prepare(scope, vars) { /**
const __owl__ = this.__owl__; * The __prepare method is only called by the t-component directive, when a
__owl__.renderProps = this.props; * subcomponent is created. It gets its scope and vars, if any, from the
__owl__.renderPromise = this.__prepareAndRender(scope, vars); * parent template.
return __owl__.renderPromise; */
__prepare(parentFiber, scope, vars) {
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.promise = this.__prepareAndRender(fiber);
return fiber.promise;
} }
async __prepareAndRender(scope, vars) { async __prepareAndRender(fiber) {
try { try {
await this.willStart(); await this.willStart();
} }
@@ -3475,14 +3533,11 @@
} }
} }
__owl__.render = qweb.render.bind(qweb, p._template); __owl__.render = qweb.render.bind(qweb, p._template);
this.__observeState(); return this.__render(fiber);
return this.__render(false, [], scope, vars);
} }
__render(force = false, patchQueue = [], scope, vars) { __render(fiber) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const promises = []; const promises = [];
const patch = [this];
patchQueue.push(patch);
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = false; __owl__.observer.allowMutations = false;
} }
@@ -3492,17 +3547,14 @@
promises, promises,
handlers: __owl__.boundHandlers, handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers, mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force, fiber: fiber
patchQueue,
scope,
vars
}); });
} }
catch (e) { catch (e) {
vnode = __owl__.vnode || h("div"); vnode = __owl__.vnode || h("div");
errorHandler(e, this); errorHandler(e, this);
} }
patch.push(vnode); fiber.vnode = vnode;
if (__owl__.observer) { if (__owl__.observer) {
__owl__.observer.allowMutations = true; __owl__.observer.allowMutations = true;
} }
@@ -3544,18 +3596,6 @@
this.mounted(); this.mounted();
} }
} }
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
this.state = __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/** /**
* Apply default props (only top level). * Apply default props (only top level).
* *
@@ -3572,29 +3612,30 @@
return props; return props;
} }
/** /**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a * Apply the given patch queue from a fiber.
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch * 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch * 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order * 3) Call 'patched' on the component of each patch, in reverse order
*/ */
__applyPatchQueue(patchQueue) { __applyPatchQueue(fiber) {
const patchQueue = fiber.patchQueue;
let component = this; let component = this;
try { try {
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i]; const fiber = patchQueue[i];
component = patch[0]; component = fiber.component;
patch.push(patch[0].willPatch()); fiber.willPatchResult = component.willPatch();
} }
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i]; const fiber = patchQueue[i];
patch[0].__patch(patch[1]); component = fiber.component;
component.__patch(fiber.vnode);
} }
for (let i = patchLen - 1; i >= 0; i--) { for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i]; const fiber = patchQueue[i];
component = patch[0]; component = fiber.component;
patch[0].patched(patch[2]); component.patched(fiber.willPatchResult);
} }
} }
catch (e) { catch (e) {
@@ -3604,6 +3645,7 @@
} }
Component.template = null; Component.template = null;
Component._template = null; Component._template = null;
Component._current = null;
Component.components = {}; Component.components = {};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Error handling // Error handling
@@ -3675,7 +3717,7 @@
/** /**
* Need to do this here so 'deep' can be overrided by subcomponent easily * Need to do this here so 'deep' can be overrided by subcomponent easily
*/ */
async __prepareAndRender(scope, vars) { async __prepareAndRender(fiber) {
const store = this.getStore(this.env); const store = this.getStore(this.env);
const ownProps = this.props || {}; const ownProps = this.props || {};
this.storeProps = this.constructor.mapStoreToProps(store.state, ownProps, store.getters); this.storeProps = this.constructor.mapStoreToProps(store.state, ownProps, store.getters);
@@ -3688,7 +3730,7 @@
prevStoreProps: this.storeProps prevStoreProps: this.storeProps
}); });
this.__owl__.rev = observer.rev; this.__owl__.rev = observer.rev;
return super.__prepareAndRender(scope, vars); return super.__prepareAndRender(fiber);
} }
/** /**
* We do not use the mounted hook here for a subtle reason: we want the * We do not use the mounted hook here for a subtle reason: we want the
@@ -3730,9 +3772,9 @@
this.__owl__.renderPromise = super.render(force); this.__owl__.renderPromise = super.render(force);
return this.__owl__.renderPromise; return this.__owl__.renderPromise;
} }
async __updateProps(nextProps, f, p, s, v) { async __updateProps(nextProps, f, s, v) {
this.__updateStoreProps(nextProps); this.__updateStoreProps(nextProps);
return super.__updateProps(nextProps, f, p, s, v); return super.__updateProps(nextProps, f, s, v);
} }
__updateStoreProps(nextProps) { __updateStoreProps(nextProps) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
@@ -3840,9 +3882,10 @@
* } * }
* ``` * ```
*/ */
function xml(strings) { function xml(strings, ...args) {
const name = `__template__${QWeb.nextId++}`; const name = `__template__${QWeb.nextId++}`;
QWeb.registerTemplate(name, strings[0]); const value = String.raw(strings, ...args);
QWeb.registerTemplate(name, value);
return name; return name;
} }
@@ -3850,6 +3893,79 @@
xml: xml xml: xml
}); });
/**
* Owl Hook System
*
* This file introduces the concept of hooks, similar to React or Vue hooks.
* We have currently an implementation of:
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
/**
* useState hook
*
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
function useState(state) {
const component = Component._current;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
return __owl__.observer.observe(state);
}
/**
* Mounted hook. The callback will be called when the current component is
* mounted. Note that the component mounted method is called first.
*/
let nextID = 1;
function onMounted(cb) {
const component = Component._current;
component.__owl__.mountedHandlers[`h${nextID++}`] = cb;
}
/**
* willUnmount hook. The callback will be called when the current component is
* willUnmounted. Note that the component mounted method is called last.
*/
function onWillUnmount(cb) {
const component = Component._current;
if (component.__owl__.willUnmountCB) {
const current = component.__owl__.willUnmountCB;
component.__owl__.willUnmountCB = function () {
cb.call(component);
current.call(component);
};
}
else {
component.__owl__.willUnmountCB = cb;
}
}
function useRef(name) {
const __owl__ = Component._current.__owl__;
return {
get el() {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
},
get comp() {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
};
}
var _hooks = /*#__PURE__*/Object.freeze({
useState: useState,
onMounted: onMounted,
onWillUnmount: onWillUnmount,
useRef: useRef
});
class Link extends Component { class Link extends Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
@@ -3893,26 +4009,17 @@
`; `;
class RouteComponent extends Component { class RouteComponent extends Component {
constructor(parent, props) { get routeComponent() {
super(parent, props); return this.env.router.currentRoute && this.env.router.currentRoute.component;
this.routes = [];
const router = this.env.router;
for (let name of router.routeIds) {
const route = router.routes[name];
if (route.component) {
this.routes.push({
name: route.name,
component: "__component__" + route.name
});
}
}
} }
} }
RouteComponent.template = xml ` RouteComponent.template = xml `
<t t-foreach="routes" t-as="route"> <t>
<t t-if="env.router.currentRouteName === route.name"> <t
<t t-component="{{route.component}}" t-props="env.router.currentParams"/> t-if="routeComponent"
</t> t-component="routeComponent"
t-key="env.router.currentRouteName"
t-props="env.router.currentParams" />
</t> </t>
`; `;
@@ -4125,11 +4232,13 @@
* *
* Note that dynamic values, such as a date or a commit hash are added by rollup * Note that dynamic values, such as a date or a commit hash are added by rollup
*/ */
const useState$1 = useState;
const core = { EventBus, Observer }; const core = { EventBus, Observer };
const router = { Router, RouteComponent, Link }; const router = { Router, RouteComponent, Link };
const store = { Store, ConnectedComponent }; const store = { Store, ConnectedComponent };
const utils = _utils; const utils = _utils;
const tags = _tags; const tags = _tags;
const hooks$1 = _hooks;
const __info__ = {}; const __info__ = {};
Object.defineProperty(__info__, "mode", { Object.defineProperty(__info__, "mode", {
get() { get() {
@@ -4151,14 +4260,16 @@
exports.QWeb = QWeb; exports.QWeb = QWeb;
exports.__info__ = __info__; exports.__info__ = __info__;
exports.core = core; exports.core = core;
exports.hooks = hooks$1;
exports.router = router; exports.router = router;
exports.store = store; exports.store = store;
exports.tags = tags; exports.tags = tags;
exports.useState = useState$1;
exports.utils = utils; exports.utils = utils;
exports.__info__.version = '0.21.0'; exports.__info__.version = '0.22.0';
exports.__info__.date = '2019-09-12T12:21:59.533Z'; exports.__info__.date = '2019-10-01T19:05:23.486Z';
exports.__info__.hash = '14ceb38'; exports.__info__.hash = 'b859fcb';
exports.__info__.url = 'https://github.com/odoo/owl'; exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {})); }(this.owl = this.owl || {}));
+11 -10
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const {useState, useRef} = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -156,18 +156,18 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
class TabbedEditor extends owl.Component { class TabbedEditor extends owl.Component {
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
this.state = { this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css" currentTab: props.js ? "js" : props.xml ? "xml" : "css"
}; });
this.setTab = owl.utils.debounce(this.setTab, 250, true); this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {}; this.sessions = {};
this._setupSessions(props); this._setupSessions(props);
this.editor = null; this.editorNode = useRef("editor");
} }
mounted() { mounted() {
this.editor = this.editor || ace.edit(this.refs.editor); this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1); this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px"); this.editor.setFontSize("12px");
@@ -250,7 +250,7 @@ class App extends owl.Component {
this.version = owl.__info__.version; this.version = owl.__info__.version;
this.SAMPLES = SAMPLES; this.SAMPLES = SAMPLES;
this.state = { this.state = useState({
js: SAMPLES[0].code, js: SAMPLES[0].code,
css: SAMPLES[0].css || "", css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML, xml: SAMPLES[0].xml || DEFAULT_XML,
@@ -259,18 +259,19 @@ class App extends owl.Component {
splitLayout: true, splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2), leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null topPanelHeight: null
}; });
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true); this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true); this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
} }
displayError(error) { displayError(error) {
this.state.error = error; this.state.error = error;
if (error) { if (error) {
setTimeout(() => { setTimeout(() => {
this.refs.content.innerHTML = ""; this.content.el.innerHTML = "";
}); });
return; return;
} }
@@ -296,8 +297,8 @@ class App extends owl.Component {
} else { } else {
this.state.error = false; this.state.error = false;
} }
this.refs.content.innerHTML = ""; this.content.el.innerHTML = "";
this.refs.content.appendChild(subiframe); this.content.el.appendChild(subiframe);
} }
setSample(ev) { setSample(ev) {
+137 -52
View File
@@ -1,7 +1,8 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState } = owl;
class Greeter extends owl.Component { class Greeter extends Component {
state = { word: 'Hello' }; state = useState({ word: 'Hello' });
toggle() { toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi' this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
@@ -9,9 +10,9 @@ class Greeter extends owl.Component {
} }
// Main root component // Main root component
class App extends owl.Component { class App extends Component {
static components = { Greeter }; static components = { Greeter };
state = { name: 'World'}; state = useState({ name: 'World'});
} }
// Application setup // Application setup
@@ -45,17 +46,18 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects. // used to generate simple transition effects.
const { Component, useState } = owl;
class Counter extends owl.Component { class Counter extends Component {
state = { value: 0 }; state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
} }
} }
class App extends owl.Component { class App extends Component {
state = { flag: false, componentFlag: false, numbers: [] }; state = useState({ flag: false, componentFlag: false, numbers: [] });
static components = { Counter }; static components = { Counter };
toggle(key) { toggle(key) {
@@ -183,11 +185,12 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// methods in the console. Try modifying its state by clicking on it, or by // methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what // clicking on the two main buttons, and look into the console to see what
// happens. // happens.
const { Component, useState } = owl;
class DemoComponent extends owl.Component { class DemoComponent extends Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.state = { n: 0 }; this.state = useState({ n: 0 });
console.log("constructor"); console.log("constructor");
} }
async willStart() { async willStart() {
@@ -213,9 +216,9 @@ class DemoComponent extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
static components = { DemoComponent }; static components = { DemoComponent };
state = { n: 0, flag: true }; state = useState({ n: 0, flag: true });
increment() { increment() {
this.state.n++; this.state.n++;
@@ -259,12 +262,70 @@ const LIFECYCLE_CSS = `button {
width: 250px; width: 250px;
}`; }`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onWillUnmount(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class App extends owl.Component {
// simple state hook (reactive object)
counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
increment() {
this.counter.value++;
}
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
`;
const HOOKS_DEMO_XML = `<templates>
<div t-name="App">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
`;
const HOOKS_CSS = `button {
width: 120px;
height: 35px;
font-size: 16px;
}`;
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some // www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence. // interesting user interactions. It uses the local storage for persistence.
// //
// In this implementation, we use the owl Store class to manage the state. It // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState } = owl;
const { useRef } = owl.hooks;
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
@@ -335,8 +396,9 @@ function makeStore() {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoItem // TodoItem
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends owl.Component { class TodoItem extends Component {
state = { isEditing: false }; state = useState({ isEditing: false });
inputRef = useRef("input");
removeTodo() { removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
@@ -351,9 +413,9 @@ class TodoItem extends owl.Component {
} }
focusInput() { focusInput() {
this.refs.input.value = ""; this.inputRef.el.value = "";
this.refs.input.focus(); this.inputRef.el.focus();
this.refs.input.value = this.props.title; this.inputRef.el.value = this.props.title;
} }
handleKeyup(ev) { handleKeyup(ev) {
@@ -389,7 +451,7 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoApp extends owl.store.ConnectedComponent { class TodoApp extends owl.store.ConnectedComponent {
static components = { TodoItem }; static components = { TodoItem };
state = { filter: "all" }; state = useState({ filter: "all" });
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { return {
@@ -1052,17 +1114,18 @@ const SLOTS = `// We show here how slots can be used to create generic component
// //
// Note that the t-on-click event, defined in the App template, is executed in // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
const { Component, useState } = owl;
class Card extends owl.Component { class Card extends Component {
state = { showContent: true }; state = useState({ showContent: true });
toggleDisplay() { toggleDisplay() {
this.state.showContent = !this.state.showContent; this.state.showContent = !this.state.showContent;
} }
} }
class Counter extends owl.Component { class Counter extends Component {
state = {val: 1}; state = useState({val: 1});
inc() { inc() {
this.state.val++; this.state.val++;
@@ -1070,9 +1133,9 @@ class Counter extends owl.Component {
} }
// Main root component // Main root component
class App extends owl.Component { class App extends Component {
static components = {Card, Counter}; static components = {Card, Counter};
state = {a: 1, b: 3}; state = useState({a: 1, b: 3});
inc(key, delta) { inc(key, delta) {
this.state[key] += delta; this.state[key] += delta;
@@ -1168,8 +1231,9 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// However, we don't want renderings of the other sub component to be delayed // However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the 't-asyncroot' directive for this // because of the slow component. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
const { Component, useState } = owl;
class SlowComponent extends owl.Component { class SlowComponent extends Component {
willUpdateProps() { willUpdateProps() {
// simulate a component that needs to perform async stuff (e.g. an RPC) // simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself // with the updated props before re-rendering itself
@@ -1177,11 +1241,11 @@ class SlowComponent extends owl.Component {
} }
} }
class NotificationList extends owl.Component {} class NotificationList extends Component {}
class App extends owl.Component { class App extends Component {
static components = {SlowComponent, NotificationList}; static components = {SlowComponent, NotificationList};
state = { value: 0, notifs: [] }; state = useState({ value: 0, notifs: [] });
increment() { increment() {
this.state.value++; this.state.value++;
@@ -1250,15 +1314,16 @@ const FORM = `// This example illustrate how the t-model directive can be used t
// data between html inputs (and select/textareas) and the state of a component. // data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally // Note that there are two controls with t-model="color": they are totally
// synchronized. // synchronized.
const { Component, useState } = owl;
class Form extends owl.Component { class Form extends Component {
state = { state = useState({
text: "", text: "",
othertext: "", othertext: "",
number: 11, number: 11,
color: "", color: "",
bool: false bool: false
}; });
} }
// Application setup // Application setup
@@ -1271,20 +1336,20 @@ const FORM_XML = `<templates>
<div t-name="Form"> <div t-name="Form">
<h1>Form</h1> <h1>Form</h1>
<div> <div>
Text (immediate): <input t-model="text"/> Text (immediate): <input t-model="state.text"/>
</div> </div>
<div> <div>
Other text (lazy): <input t-model.lazy="othertext"/> Other text (lazy): <input t-model.lazy="state.othertext"/>
</div> </div>
<div> <div>
Number: <input t-model.number="number"/> Number: <input t-model.number="state.number"/>
</div> </div>
<div> <div>
Boolean: <input type="checkbox" t-model="bool"/> Boolean: <input type="checkbox" t-model="state.bool"/>
</div> </div>
<div> <div>
Color, with a select: Color, with a select:
<select t-model="color"> <select t-model="state.color">
<option value="">Select a color</option> <option value="">Select a color</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
@@ -1292,8 +1357,8 @@ const FORM_XML = `<templates>
</div> </div>
<div> <div>
Color, with radio buttons: Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="color"/><label for="red">Red</label></span> <span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="color"/><label for="blue">Blue</label></span> <span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div> </div>
<hr/> <hr/>
<h1>State</h1> <h1>State</h1>
@@ -1316,26 +1381,31 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height // - minimal width/height
// - better heuristic for initial window position // - better heuristic for initial window position
// - ... // - ...
const { Component, useState } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends owl.Component {} class HelloWorld extends Component {}
class Counter extends owl.Component { class Counter extends Component {
state = { value: 0 }; state = useState({ value: 0 });
inc() { inc() {
this.state.value++; this.state.value++;
} }
} }
class Window extends owl.Component { class Window extends Component {
get style() { get style() {
let { width, height, top, left, zindex } = this.props.info; let { width, height, top, left, zindex } = this.props.info;
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`; return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
} }
close() { close() {
this.trigger("close-window", { id: this.props.info.id }); this.trigger("close-window", { id: this.props.info.id });
} }
startDragAndDrop(ev) { startDragAndDrop(ev) {
this.updateZIndex(); this.updateZIndex();
this.el.classList.add('dragging'); this.el.classList.add('dragging');
@@ -1361,31 +1431,37 @@ class Window extends owl.Component {
self.trigger("set-window-position", options); self.trigger("set-window-position", options);
} }
} }
updateZIndex() { updateZIndex() {
this.trigger("update-z-index", { id: this.props.info.id }); this.trigger("update-z-index", { id: this.props.info.id });
} }
} }
class WindowManager extends owl.Component { class WindowManager extends Component {
static components = { Window }; static components = { Window };
windows = []; windows = [];
nextId = 1; nextId = 1;
currentZindex = 1; currentZindex = 1;
nextLeft = 0;
nextTop = 0;
addWindow(name) { addWindow(name) {
const info = this.env.windows.find(w => w.name === name); const info = this.env.windows.find(w => w.name === name);
const id = \`w\${this.nextId++}\`; this.nextLeft = this.nextLeft + 30;
this.nextTop = this.nextTop + 30;
this.windows.push({ this.windows.push({
id: id, id: this.nextId++,
title: info.title, title: info.title,
width: info.defaultWidth, width: info.defaultWidth,
height: info.defaultHeight, height: info.defaultHeight,
top: 0, top: this.nextTop,
left: 0, left: this.nextLeft,
zindex: this.currentZindex++ zindex: this.currentZindex++,
component: info.component
}); });
this.constructor.components[id] = info.component;
this.render(); this.render();
} }
closeWindow(ev) { closeWindow(ev) {
const id = ev.detail.id; const id = ev.detail.id;
delete this.constructor.components[id]; delete this.constructor.components[id];
@@ -1393,12 +1469,14 @@ class WindowManager extends owl.Component {
this.windows.splice(index, 1); this.windows.splice(index, 1);
this.render(); this.render();
} }
setWindowPosition(ev) { setWindowPosition(ev) {
const id = ev.detail.id; const id = ev.detail.id;
const w = this.windows.find(w => w.id === id); const w = this.windows.find(w => w.id === id);
w.top = ev.detail.top; w.top = ev.detail.top;
w.left = ev.detail.left; w.left = ev.detail.left;
} }
updateZIndex(ev) { updateZIndex(ev) {
const id = ev.detail.id; const id = ev.detail.id;
const w = this.windows.find(w => w.id === id); const w = this.windows.find(w => w.id === id);
@@ -1407,11 +1485,12 @@ class WindowManager extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
static components = { WindowManager }; static components = { WindowManager };
wmRef = useRef("wm");
addWindow(name) { addWindow(name) {
this.refs.wm.addWindow(name); this.wmRef.comp.addWindow(name);
} }
} }
@@ -1452,7 +1531,7 @@ const WMS_XML = `<templates>
t-on-update-z-index="updateZIndex" t-on-update-z-index="updateZIndex"
t-on-set-window-position="setWindowPosition"> t-on-set-window-position="setWindowPosition">
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w"> <Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
<t t-component="{{w.id}}"/> <t t-component="w.component"/>
</Window> </Window>
</div> </div>
@@ -1570,6 +1649,12 @@ export const SAMPLES = [
xml: LIFECYCLE_DEMO_XML, xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS css: LIFECYCLE_CSS
}, },
{
description: "Hooks",
code: HOOKS_DEMO,
xml: HOOKS_DEMO_XML,
css: HOOKS_CSS
},
{ {
description: "Todo List App (with store)", description: "Todo List App (with store)",
code: TODO_APP_STORE, code: TODO_APP_STORE,