From b4eccb5149e121a8ce23d4deaa16060ee7a0e82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 1 Oct 2019 21:15:14 +0200 Subject: [PATCH] [IMP] owl: update to v0.22.0 --- owl.js | 367 +++++++++++++++++++++++++++--------------- playground/app.js | 21 +-- playground/samples.js | 189 ++++++++++++++++------ 3 files changed, 387 insertions(+), 190 deletions(-) diff --git a/owl.js b/owl.js index de8c4692..34808651 100644 --- a/owl.js +++ b/owl.js @@ -385,6 +385,7 @@ return result; } + const INTERP_REGEXP = /\{\{.*?\}\}/g; //------------------------------------------------------------------------------ // Compilation Context //------------------------------------------------------------------------------ @@ -402,6 +403,7 @@ this.shouldDefineParent = false; this.shouldDefineQWeb = false; this.shouldDefineUtils = false; + this.shouldDefineRefs = false; this.shouldDefineResult = true; this.shouldProtectContext = false; this.shouldTrackScope = false; @@ -439,6 +441,9 @@ if (this.shouldDefineResult) { this.code.unshift(" let result;"); } + if (this.shouldDefineRefs) { + this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); + } if (this.shouldDefineOwner) { // this is necessary to prevent some directives (t-forach for ex) to // pollute the rendering context by adding some keys in it. @@ -528,7 +533,7 @@ * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') */ interpolate(s) { - let matches = s.match(/\{\{.*?\}\}/g); + let matches = s.match(INTERP_REGEXP); if (matches && matches[0].length === s.length) { return `(${this.formatExpression(s.slice(2, -2))})`; } @@ -1133,9 +1138,13 @@ data.ns = "http://www.w3.org/2000/svg"; if (sel !== "foreignObject" && children !== undefined) { 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) { - 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); } } - 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); } @@ -1232,9 +1235,7 @@ if (!result.ok) { throw new Error("Error while fetching xml templates"); } - let templates = await result.text(); - templates = templates.replace(//g, ""); - return templates; + return await result.text(); } function escape(str) { if (str === undefined) { @@ -1322,10 +1323,15 @@ } return expr; }, - shallowEqual + shallowEqual, + addNameSpace(vnode) { + addNS(vnode.data, vnode.children, vnode.sel); + } }; function parseXML(xml) { const parser = new DOMParser(); + // we remove comments from the xml string + xml = xml.replace(//g, ""); const doc = parser.parseFromString(xml, "text/xml"); if (doc.getElementsByTagName("parsererror").length) { let msg = "Invalid XML in template."; @@ -1436,7 +1442,7 @@ this._processTemplate(elem); const template = { elem, - fn: (context, extra) => { + fn: function (context, extra) { const compiledFunction = this._compile(name, elem); template.fn = compiledFunction; return compiledFunction.call(this, context, extra); @@ -1497,8 +1503,8 @@ * to render a full component tree, since this is an asynchronous operation. * This method can only render templates without components. */ - renderToString(name, context = {}) { - const vnode = this.render(name, context); + renderToString(name, context = {}, extra) { + const vnode = this.render(name, context, extra); if (vnode.sel === undefined) { return vnode.text; } @@ -1530,8 +1536,8 @@ if (parentContext) { ctx.templates = Object.create(parentContext.templates); ctx.variables = Object.create(parentContext.variables); - ctx.nextID = parentContext.parentNode + 1; - ctx.parentNode = parentContext.parentNode; + ctx.nextID = parentContext.nextID + 1; + ctx.parentNode = parentContext.parentNode || ctx.nextID++; ctx.allowMultipleRoots = true; ctx.hasParentWidget = true; ctx.shouldDefineResult = false; @@ -1539,12 +1545,12 @@ for (let v in parentContext.variables) { let variable = parentContext.variables[v]; if (variable.id) { - ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`); + ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`); } } } if (parentContext) { - ctx.addLine(" Object.assign(context, extra.scope);"); + ctx.addLine(" Object.assign(context, extra.fiber.scope);"); } this._compileNode(elem, ctx); if (!parentContext) { @@ -1702,6 +1708,16 @@ ctx = ctx.subContext("inPreTag", true); } 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) { if (directive.finalize) { directive.finalize({ node, qweb: this, ctx, fullName, value }); @@ -2125,7 +2141,7 @@ .join(","); 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; } templateMap[subTemplate] = true; @@ -2298,9 +2314,10 @@ name: "ref", priority: 95, atNodeCreation({ ctx, value, addNodeHook }) { + ctx.rootContext.shouldDefineRefs = true; const refKey = `ref${ctx.generateID()}`; 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"); let handler; let event = fullName.includes(".lazy") ? "change" : "input"; - const expr = ctx.formatExpression(`state.${value}`); + const expr = ctx.formatExpression(value); if (node.tagName === "select") { ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); addNodeHook("create", `n.elm.value=${expr};`); @@ -2732,9 +2749,10 @@ let refExpr = ""; let refKey = ""; if (ref) { + ctx.rootContext.shouldDefineRefs = true; refKey = `ref${ctx.generateID()}`; ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); - refExpr = `context.refs[${refKey}] = w${componentID};`; + refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`; } let transitionsInsertCode = ""; if (transition) { @@ -2742,7 +2760,7 @@ } let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; if (ref && !keepAlive) { - finalizeComponentCode += `delete context.refs[${refKey}];`; + finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`; } if (transition) { finalizeComponentCode = `let finalize = () => { @@ -2825,8 +2843,10 @@ ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); } let shouldProxy = false; + if (async || keepAlive) { + ctx.addLine(`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`); + } if (async) { - ctx.addLine(`const patchQueue${componentID} = [];`); ctx.addLine(`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`); } else { @@ -2849,9 +2869,9 @@ else { ctx.addLine(`let props${componentID} = {${propStr}};`); } - ctx.addIf(`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`); - ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`); - ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`); + ctx.addIf(`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`); + ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`); + ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`); ctx.addElse(); ctx.addLine(`w${componentID}.destroy();`); ctx.addLine(`w${componentID} = false;`); @@ -2859,8 +2879,13 @@ ctx.closeIf(); ctx.addIf(`!w${componentID}`); // new component - ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`); - ctx.addLine(`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`); + let dynamicFallback = ""; + 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... ctx.addLine(`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`); if (QWeb.dev) { @@ -2901,14 +2926,16 @@ QWeb.slots[`${slotId}_default`] = slotFn; } } - let scopeVars = ""; + let scopeVars; if (hasSlots) { - scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : ""; - if (varDefs.length) { - scopeVars += `, {${varDefs.join(",")}}`; - } + let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`; + let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined"; + 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 let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`; 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.addElse(); // need to update component - let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue"; + let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber"; if (keepAlive) { // if we have t-keepalive="1", the component could be unmounted, but then // we __updateProps is called. This is ok, but we do not want to call // the willPatch/patched hooks of the component in this case, so we // disable the patch queue - patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`; + patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`; } if (QWeb.dev) { 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});`); let keepAliveCode = ""; if (keepAlive) { @@ -2940,7 +2967,7 @@ ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`); } 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 { ctx.addLine(`extra.promises.push(def${defID});`); @@ -3075,8 +3102,8 @@ * the t-component directive in a template) */ constructor(parent, props) { - this.refs = {}; const defaultProps = this.constructor.defaultProps; + Component._current = this; if (defaultProps) { props = this.__applyDefaultProps(props, defaultProps); } @@ -3121,14 +3148,14 @@ parent: p, children: {}, cmap: {}, - renderId: 1, - renderPromise: null, - renderProps: props || null, + currentFiber: null, boundHandlers: {}, mountedHandlers: {}, + willUnmountCB: null, observer: null, render: null, - classObj: null + classObj: null, + refs: null }; } /** @@ -3229,8 +3256,10 @@ if (__owl__.isMounted) { return; } + const fiber = this.__createFiber(false, undefined, undefined, undefined); if (!__owl__.vnode) { - const vnode = await this.__prepare(); + fiber.promise = this.__prepareAndRender(fiber); + const vnode = await fiber.promise; if (__owl__.isDestroyed) { // component was destroyed before we get here... return; @@ -3238,9 +3267,10 @@ this.__patch(vnode); } else if (renderBeforeRemount) { - const patchQueue = []; - await this.__render(false, patchQueue, undefined, undefined); - this.__applyPatchQueue(patchQueue); + fiber.patchQueue.push(fiber); + fiber.promise = this.__render(fiber); + await fiber.promise; + this.__applyPatchQueue(fiber); } target.appendChild(this.el); if (document.body.contains(target)) { @@ -3271,15 +3301,34 @@ if (!__owl__.isMounted) { return; } - const patchQueue = []; - const renderId = ++__owl__.renderId; - await this.__render(force, patchQueue, undefined, undefined); - if (__owl__.isMounted && renderId === __owl__.renderId) { + const fiber = this.__createFiber(force, undefined, undefined, undefined); + fiber.patchQueue.push(fiber); + fiber.promise = this.__render(fiber); + await fiber.promise; + if (__owl__.isMounted && fiber === __owl__.currentFiber) { // we only update the vnode and the actual DOM if no other rendering // occurred between now and when the render method was initially called. - this.__applyPatchQueue(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: * - it recursively destroy all children @@ -3386,19 +3435,22 @@ } __owl__.isMounted = true; const handlers = __owl__.mountedHandlers; - for (let key in handlers) { - handlers[key](); - } try { this.mounted(); + for (let key in handlers) { + handlers[key](); + } } catch (e) { errorHandler(e, this); } } __callWillUnmount() { - this.willUnmount(); const __owl__ = this.__owl__; + if (__owl__.willUnmountCB) { + __owl__.willUnmountCB(); + } + this.willUnmount(); __owl__.isMounted = false; const children = __owl__.children; for (let id in children) { @@ -3412,8 +3464,8 @@ * The __updateProps method is called by the t-component directive whenever * it updates a component (so, when the parent template is rerendered). */ - async __updateProps(nextProps, forceUpdate = false, patchQueue, scope, vars) { - const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps); + async __updateProps(nextProps, parentFiber, scope, vars) { + const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps); if (shouldUpdate) { const defaultProps = this.constructor.defaultProps; if (defaultProps) { @@ -3421,7 +3473,9 @@ } await this.willUpdateProps(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); __owl__.vnode = patch(target, vnode); } - __prepare(scope, vars) { - const __owl__ = this.__owl__; - __owl__.renderProps = this.props; - __owl__.renderPromise = this.__prepareAndRender(scope, vars); - return __owl__.renderPromise; + /** + * The __prepare method is only called by the t-component directive, when a + * subcomponent is created. It gets its scope and vars, if any, from the + * parent template. + */ + __prepare(parentFiber, scope, vars) { + const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber); + fiber.promise = this.__prepareAndRender(fiber); + return fiber.promise; } - async __prepareAndRender(scope, vars) { + async __prepareAndRender(fiber) { try { await this.willStart(); } @@ -3475,14 +3533,11 @@ } } __owl__.render = qweb.render.bind(qweb, p._template); - this.__observeState(); - return this.__render(false, [], scope, vars); + return this.__render(fiber); } - __render(force = false, patchQueue = [], scope, vars) { + __render(fiber) { const __owl__ = this.__owl__; const promises = []; - const patch = [this]; - patchQueue.push(patch); if (__owl__.observer) { __owl__.observer.allowMutations = false; } @@ -3492,17 +3547,14 @@ promises, handlers: __owl__.boundHandlers, mountedHandlers: __owl__.mountedHandlers, - forceUpdate: force, - patchQueue, - scope, - vars + fiber: fiber }); } catch (e) { vnode = __owl__.vnode || h("div"); errorHandler(e, this); } - patch.push(vnode); + fiber.vnode = vnode; if (__owl__.observer) { __owl__.observer.allowMutations = true; } @@ -3544,18 +3596,6 @@ 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). * @@ -3572,29 +3612,30 @@ return props; } /** - * Apply the given patch queue. A patch is a pair [c, vn], where c is a - * Component instance and vn a VNode. + * Apply the given patch queue from a fiber. * 1) Call 'willPatch' on the component of each patch * 2) Call '__patch' on the component of each patch - * 3) Call 'patched' on the component of each patch, in 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; try { const patchLen = patchQueue.length; for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - component = patch[0]; - patch.push(patch[0].willPatch()); + const fiber = patchQueue[i]; + component = fiber.component; + fiber.willPatchResult = component.willPatch(); } for (let i = 0; i < patchLen; i++) { - const patch = patchQueue[i]; - patch[0].__patch(patch[1]); + const fiber = patchQueue[i]; + component = fiber.component; + component.__patch(fiber.vnode); } for (let i = patchLen - 1; i >= 0; i--) { - const patch = patchQueue[i]; - component = patch[0]; - patch[0].patched(patch[2]); + const fiber = patchQueue[i]; + component = fiber.component; + component.patched(fiber.willPatchResult); } } catch (e) { @@ -3604,6 +3645,7 @@ } Component.template = null; Component._template = null; + Component._current = null; Component.components = {}; //------------------------------------------------------------------------------ // Error handling @@ -3675,7 +3717,7 @@ /** * 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 ownProps = this.props || {}; this.storeProps = this.constructor.mapStoreToProps(store.state, ownProps, store.getters); @@ -3688,7 +3730,7 @@ prevStoreProps: this.storeProps }); 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 @@ -3730,9 +3772,9 @@ this.__owl__.renderPromise = super.render(force); return this.__owl__.renderPromise; } - async __updateProps(nextProps, f, p, s, v) { + async __updateProps(nextProps, f, s, v) { this.__updateStoreProps(nextProps); - return super.__updateProps(nextProps, f, p, s, v); + return super.__updateProps(nextProps, f, s, v); } __updateStoreProps(nextProps) { const __owl__ = this.__owl__; @@ -3840,9 +3882,10 @@ * } * ``` */ - function xml(strings) { + function xml(strings, ...args) { const name = `__template__${QWeb.nextId++}`; - QWeb.registerTemplate(name, strings[0]); + const value = String.raw(strings, ...args); + QWeb.registerTemplate(name, value); return name; } @@ -3850,6 +3893,79 @@ 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 { constructor() { super(...arguments); @@ -3893,26 +4009,17 @@ `; class RouteComponent extends Component { - constructor(parent, props) { - super(parent, props); - 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 - }); - } - } + get routeComponent() { + return this.env.router.currentRoute && this.env.router.currentRoute.component; } } RouteComponent.template = xml ` - - - - + + `; @@ -4125,11 +4232,13 @@ * * 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 router = { Router, RouteComponent, Link }; const store = { Store, ConnectedComponent }; const utils = _utils; const tags = _tags; + const hooks$1 = _hooks; const __info__ = {}; Object.defineProperty(__info__, "mode", { get() { @@ -4151,14 +4260,16 @@ exports.QWeb = QWeb; exports.__info__ = __info__; exports.core = core; + exports.hooks = hooks$1; exports.router = router; exports.store = store; exports.tags = tags; + exports.useState = useState$1; exports.utils = utils; - exports.__info__.version = '0.21.0'; - exports.__info__.date = '2019-09-12T12:21:59.533Z'; - exports.__info__.hash = '14ceb38'; + exports.__info__.version = '0.22.0'; + exports.__info__.date = '2019-10-01T19:05:23.486Z'; + exports.__info__.hash = 'b859fcb'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/playground/app.js b/playground/app.js index 974141c8..dab8a424 100644 --- a/playground/app.js +++ b/playground/app.js @@ -1,5 +1,5 @@ import { SAMPLES } from "./samples.js"; - +const {useState, useRef} = owl.hooks; //------------------------------------------------------------------------------ // Constants, helpers, utils //------------------------------------------------------------------------------ @@ -156,18 +156,18 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start); class TabbedEditor extends owl.Component { constructor(parent, props) { super(parent, props); - this.state = { + this.state = useState({ currentTab: props.js ? "js" : props.xml ? "xml" : "css" - }; + }); this.setTab = owl.utils.debounce(this.setTab, 250, true); this.sessions = {}; this._setupSessions(props); - this.editor = null; + this.editorNode = useRef("editor"); } 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.setFontSize("12px"); @@ -250,7 +250,7 @@ class App extends owl.Component { this.version = owl.__info__.version; this.SAMPLES = SAMPLES; - this.state = { + this.state = useState({ js: SAMPLES[0].code, css: SAMPLES[0].css || "", xml: SAMPLES[0].xml || DEFAULT_XML, @@ -259,18 +259,19 @@ class App extends owl.Component { splitLayout: true, leftPaneWidth: Math.ceil(window.innerWidth / 2), topPanelHeight: null - }; + }); this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); this.runCode = owl.utils.debounce(this.runCode, 250, true); this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true); + this.content = useRef("content"); } displayError(error) { this.state.error = error; if (error) { setTimeout(() => { - this.refs.content.innerHTML = ""; + this.content.el.innerHTML = ""; }); return; } @@ -296,8 +297,8 @@ class App extends owl.Component { } else { this.state.error = false; } - this.refs.content.innerHTML = ""; - this.refs.content.appendChild(subiframe); + this.content.el.innerHTML = ""; + this.content.el.appendChild(subiframe); } setSample(ev) { diff --git a/playground/samples.js b/playground/samples.js index c9ed9c92..f7ba4b57 100644 --- a/playground/samples.js +++ b/playground/samples.js @@ -1,7 +1,8 @@ const COMPONENTS = `// In this example, we show how components can be defined and created. +const { Component, useState } = owl; -class Greeter extends owl.Component { - state = { word: 'Hello' }; +class Greeter extends Component { + state = useState({ word: 'Hello' }); toggle() { this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi' @@ -9,9 +10,9 @@ class Greeter extends owl.Component { } // Main root component -class App extends owl.Component { +class App extends Component { static components = { Greeter }; - state = { name: 'World'}; + state = useState({ name: 'World'}); } // 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 // used to generate simple transition effects. +const { Component, useState } = owl; -class Counter extends owl.Component { - state = { value: 0 }; +class Counter extends Component { + state = useState({ value: 0 }); increment() { this.state.value++; } } -class App extends owl.Component { - state = { flag: false, componentFlag: false, numbers: [] }; +class App extends Component { + state = useState({ flag: false, componentFlag: false, numbers: [] }); static components = { Counter }; 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 // clicking on the two main buttons, and look into the console to see what // happens. +const { Component, useState } = owl; -class DemoComponent extends owl.Component { +class DemoComponent extends Component { constructor() { super(...arguments); - this.state = { n: 0 }; + this.state = useState({ n: 0 }); console.log("constructor"); } async willStart() { @@ -213,9 +216,9 @@ class DemoComponent extends owl.Component { } } -class App extends owl.Component { +class App extends Component { static components = { DemoComponent }; - state = { n: 0, flag: true }; + state = useState({ n: 0, flag: true }); increment() { this.state.n++; @@ -259,12 +262,70 @@ const LIFECYCLE_CSS = `button { 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 = ` +
+ +
Mouse: ,
+
+
+`; + +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 // www.todomvc.com project. This is a non trivial application with some // interesting user interactions. It uses the local storage for persistence. // // In this implementation, we use the owl Store class to manage the state. It // is very similar to the VueX store. +const { Component, useState } = owl; +const { useRef } = owl.hooks; const ENTER_KEY = 13; const ESC_KEY = 27; @@ -335,8 +396,9 @@ function makeStore() { //------------------------------------------------------------------------------ // TodoItem //------------------------------------------------------------------------------ -class TodoItem extends owl.Component { - state = { isEditing: false }; +class TodoItem extends Component { + state = useState({ isEditing: false }); + inputRef = useRef("input"); removeTodo() { this.env.store.dispatch("removeTodo", this.props.id); @@ -351,9 +413,9 @@ class TodoItem extends owl.Component { } focusInput() { - this.refs.input.value = ""; - this.refs.input.focus(); - this.refs.input.value = this.props.title; + this.inputRef.el.value = ""; + this.inputRef.el.focus(); + this.inputRef.el.value = this.props.title; } handleKeyup(ev) { @@ -389,7 +451,7 @@ class TodoItem extends owl.Component { //------------------------------------------------------------------------------ class TodoApp extends owl.store.ConnectedComponent { static components = { TodoItem }; - state = { filter: "all" }; + state = useState({ filter: "all" }); static mapStoreToProps(state) { 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 // the context of the App component, even though it is inside the Card component +const { Component, useState } = owl; -class Card extends owl.Component { - state = { showContent: true }; +class Card extends Component { + state = useState({ showContent: true }); toggleDisplay() { this.state.showContent = !this.state.showContent; } } -class Counter extends owl.Component { - state = {val: 1}; +class Counter extends Component { + state = useState({val: 1}); inc() { this.state.val++; @@ -1070,9 +1133,9 @@ class Counter extends owl.Component { } // Main root component -class App extends owl.Component { +class App extends Component { static components = {Card, Counter}; - state = {a: 1, b: 3}; + state = useState({a: 1, b: 3}); inc(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 // because of the slow component. We use the 't-asyncroot' directive for this // purpose. Try removing it to see the difference. +const { Component, useState } = owl; -class SlowComponent extends owl.Component { +class SlowComponent extends Component { willUpdateProps() { // simulate a component that needs to perform async stuff (e.g. an RPC) // 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}; - state = { value: 0, notifs: [] }; + state = useState({ value: 0, notifs: [] }); increment() { 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. // Note that there are two controls with t-model="color": they are totally // synchronized. +const { Component, useState } = owl; -class Form extends owl.Component { - state = { +class Form extends Component { + state = useState({ text: "", othertext: "", number: 11, color: "", bool: false - }; + }); } // Application setup @@ -1271,20 +1336,20 @@ const FORM_XML = `

Form

- Text (immediate): + Text (immediate):
- Other text (lazy): + Other text (lazy):
- Number: + Number:
- Boolean: + Boolean:
Color, with a select: - @@ -1292,8 +1357,8 @@ const FORM_XML = `
Color, with radio buttons: - - + +

State

@@ -1316,26 +1381,31 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate // - minimal width/height // - 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 { - state = { value: 0 }; +class Counter extends Component { + state = useState({ value: 0 }); inc() { this.state.value++; } } -class Window extends owl.Component { +class Window extends Component { + get style() { 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}\`; } + close() { this.trigger("close-window", { id: this.props.info.id }); } + startDragAndDrop(ev) { this.updateZIndex(); this.el.classList.add('dragging'); @@ -1361,31 +1431,37 @@ class Window extends owl.Component { self.trigger("set-window-position", options); } } + updateZIndex() { this.trigger("update-z-index", { id: this.props.info.id }); } } -class WindowManager extends owl.Component { +class WindowManager extends Component { static components = { Window }; windows = []; nextId = 1; currentZindex = 1; + nextLeft = 0; + nextTop = 0; + addWindow(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({ - id: id, + id: this.nextId++, title: info.title, width: info.defaultWidth, height: info.defaultHeight, - top: 0, - left: 0, - zindex: this.currentZindex++ + top: this.nextTop, + left: this.nextLeft, + zindex: this.currentZindex++, + component: info.component }); - this.constructor.components[id] = info.component; this.render(); } + closeWindow(ev) { const id = ev.detail.id; delete this.constructor.components[id]; @@ -1393,12 +1469,14 @@ class WindowManager extends owl.Component { this.windows.splice(index, 1); this.render(); } + setWindowPosition(ev) { const id = ev.detail.id; const w = this.windows.find(w => w.id === id); w.top = ev.detail.top; w.left = ev.detail.left; } + updateZIndex(ev) { const id = ev.detail.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 }; + wmRef = useRef("wm"); addWindow(name) { - this.refs.wm.addWindow(name); + this.wmRef.comp.addWindow(name); } } @@ -1452,7 +1531,7 @@ const WMS_XML = ` t-on-update-z-index="updateZIndex" t-on-set-window-position="setWindowPosition"> - +
@@ -1570,6 +1649,12 @@ export const SAMPLES = [ xml: LIFECYCLE_DEMO_XML, css: LIFECYCLE_CSS }, + { + description: "Hooks", + code: HOOKS_DEMO, + xml: HOOKS_DEMO_XML, + css: HOOKS_CSS + }, { description: "Todo List App (with store)", code: TODO_APP_STORE,