From a1afcd21e80765e0fb665f80d80cbb6c3e2fc2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 12 Sep 2019 14:42:32 +0200 Subject: [PATCH] [IMP] owl: update to v0.21.0 --- owl.js | 197 ++++++++++++++++++++++++++---------------- playground/app.js | 183 ++++++++++++++++++++------------------- playground/samples.js | 57 ++++++------ 3 files changed, 241 insertions(+), 196 deletions(-) diff --git a/owl.js b/owl.js index 04699f5f..de8c4692 100644 --- a/owl.js +++ b/owl.js @@ -402,7 +402,7 @@ this.shouldDefineParent = false; this.shouldDefineQWeb = false; this.shouldDefineUtils = false; - this.shouldDefineResult = false; + this.shouldDefineResult = true; this.shouldProtectContext = false; this.shouldTrackScope = false; this.inLoop = false; @@ -469,6 +469,9 @@ if (!this.rootContext.rootNode) { this.rootContext.rootNode = node; } + if (!this.parentNode) { + this.addLine(`result = vn${node};`); + } return this.subContext("parentNode", node); } subContext(key, value) { @@ -1349,28 +1352,19 @@ } return doc; } - let nextID = 1; //------------------------------------------------------------------------------ // QWeb rendering engine //------------------------------------------------------------------------------ class QWeb extends EventBus { constructor(data) { super(); - this.templates = {}; this.h = h; - // the id field is useful to be able to hash qweb instances. The current - // use case is that component's templates are qweb dependant, and need to be - // able to map a qweb instance to a template name. - this.id = nextID++; - // slots contains sub templates defined with t-set inside t-component nodes, and - // are meant to be used by the t-slot directive. - this.slots = {}; - this.nextSlotId = 1; // recursiveTemplates contains sub templates called with t-call, but which // ends up in recursive situations. This is very similar to the slot situation, // as in we need to propagate the scope. this.recursiveFns = {}; this.isUpdating = false; + this.templates = Object.create(QWeb.TEMPLATES); if (data) { this.addTemplates(data); } @@ -1386,12 +1380,25 @@ directive.extraNames.forEach(n => (QWeb.DIRECTIVE_NAMES[n] = 1)); } } - static register(name, Component) { + static registerComponent(name, Component) { if (QWeb.components[name]) { throw new Error(`Component '${name}' has already been registered`); } QWeb.components[name] = Component; } + /** + * Register globally a template. All QWeb instances will obtain their + * templates from their own template map, and then, from the global static + * TEMPLATES property. + */ + static registerTemplate(name, template) { + if (QWeb.TEMPLATES[name]) { + throw new Error(`Template '${name}' has already been registered`); + } + const qweb = new QWeb(); + qweb.addTemplate(name, template); + QWeb.TEMPLATES[name] = qweb.templates[name]; + } /** * Add a template to the internal template map. Note that it is not * immediately compiled. @@ -1517,6 +1524,9 @@ _compile(name, elem, parentContext) { const isDebug = elem.attributes.hasOwnProperty("t-debug"); const ctx = new Context(name); + if (elem.tagName !== "t") { + ctx.shouldDefineResult = false; + } if (parentContext) { ctx.templates = Object.create(parentContext.templates); ctx.variables = Object.create(parentContext.variables); @@ -1524,6 +1534,7 @@ ctx.parentNode = parentContext.parentNode; ctx.allowMultipleRoots = true; ctx.hasParentWidget = true; + ctx.shouldDefineResult = false; ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); for (let v in parentContext.variables) { let variable = parentContext.variables[v]; @@ -1593,6 +1604,7 @@ // template rendering. let nodeID = ctx.generateID(); ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); + ctx.addLine(`result = vn${nodeID};`); ctx.rootContext.rootNode = nodeID; ctx.rootContext.parentTextNode = nodeID; } @@ -1871,8 +1883,14 @@ key: 1 }; QWeb.DIRECTIVES = []; + QWeb.TEMPLATES = {}; + QWeb.nextId = 1; // dev mode enables better error messages or more costly validations QWeb.dev = false; + // slots contains sub templates defined with t-set inside t-component nodes, and + // are meant to be used by the t-slot directive. + QWeb.slots = {}; + QWeb.nextSlotId = 1; /** * Owl QWeb Directives @@ -1927,6 +1945,7 @@ ctx.rootContext.rootNode = nodeID; ctx.rootContext.parentTextNode = nodeID; ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`); + ctx.addLine(`result = vn${nodeID}`); } } else { @@ -2066,6 +2085,7 @@ // extract variables from nodecopy const tempCtx = new Context(); tempCtx.nextID = ctx.rootContext.nextID; + tempCtx.allowMultipleRoots = true; qweb._compileNode(nodeCopy, tempCtx); const vars = Object.assign({}, ctx.variables, tempCtx.variables); ctx.rootContext.nextID = tempCtx.nextID; @@ -2404,7 +2424,7 @@ atNodeEncounter({ ctx, value }) { const slotKey = ctx.generateID(); ctx.rootContext.shouldDefineOwner = true; - ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); + ctx.addLine(`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`); ctx.addIf(`slot${slotKey}`); ctx.addLine(`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`); ctx.closeIf(); @@ -2646,6 +2666,7 @@ ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineUtils = true; let keepAlive = node.getAttribute("t-keepalive") ? true : false; + let hasDynamicProps = node.getAttribute("t-props") ? true : false; let async = node.getAttribute("t-asyncroot") ? true : false; // t-on- events and t-transition const events = []; @@ -2821,7 +2842,13 @@ ctx.addLine(`result = vn${id};`); } } - ctx.addLine(`let props${componentID} = {${propStr}};`); + if (hasDynamicProps) { + const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")); + ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`); + } + 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;`); @@ -2833,7 +2860,7 @@ ctx.addIf(`!w${componentID}`); // new component ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`); - ctx.addLine(`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`); + ctx.addLine(`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`); // 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) { @@ -2853,7 +2880,7 @@ } const clone = node.cloneNode(true); const slotNodes = clone.querySelectorAll("[t-set]"); - const slotId = qweb.nextSlotId++; + const slotId = QWeb.nextSlotId++; ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`); if (slotNodes.length) { for (let i = 0, length = slotNodes.length; i < length; i++) { @@ -2862,7 +2889,7 @@ const key = slotNode.getAttribute("t-set"); slotNode.removeAttribute("t-set"); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); - qweb.slots[`${slotId}_${key}`] = slotFn; + QWeb.slots[`${slotId}_${key}`] = slotFn; } } if (clone.childNodes.length) { @@ -2871,7 +2898,7 @@ t.appendChild(child); } const slotFn = qweb._compile(`slot_default_template`, t, ctx); - qweb.slots[`${slotId}_default`] = slotFn; + QWeb.slots[`${slotId}_default`] = slotFn; } } let scopeVars = ""; @@ -3020,11 +3047,6 @@ return result; } - // If a component does not define explicitely a template - // key, it needs to find a template with its name (or a parent's). This is - // qweb dependant, so we need a place to store this information indexed by - // qweb instances. - const TEMPLATE_MAP = {}; //------------------------------------------------------------------------------ // Component //------------------------------------------------------------------------------ @@ -3430,19 +3452,17 @@ return Promise.resolve(h("div")); } const qweb = this.env.qweb; - if (!this.template) { - let tmap = TEMPLATE_MAP[qweb.id]; - if (!tmap) { - tmap = {}; - TEMPLATE_MAP[qweb.id] = tmap; - } - let p = this.constructor; - let name = p.name; - let template = tmap[name]; - if (template) { - this.template = template; + let p = this.constructor; + // console.warn(p, p.template, p._template, 'template' in p, p.hasOwnProperty('template')) + if (!p.hasOwnProperty("_template")) { + if (p.template) { + p._template = p.template; } else { + // here, the component and none of its superclasses defines a static `template` + // key. So we fall back on looking for a template matching its name (or + // one of its subclass). + let template; while ((template = p.name) && !(template in qweb.templates) && p !== Component) { p = p.__proto__; } @@ -3450,12 +3470,11 @@ throw new Error(`Could not find template for component "${this.constructor.name}"`); } else { - tmap[name] = template; - this.template = template; + p._template = template; } } } - __owl__.render = qweb.render.bind(qweb, this.template); + __owl__.render = qweb.render.bind(qweb, p._template); this.__observeState(); return this.__render(false, [], scope, vars); } @@ -3583,6 +3602,9 @@ } } } + Component.template = null; + Component._template = null; + Component.components = {}; //------------------------------------------------------------------------------ // Error handling //------------------------------------------------------------------------------ @@ -3800,17 +3822,37 @@ } } - const LINK_TEMPLATE_NAME = "__owl__-router-link"; - const LINK_TEMPLATE = ` - - - `; + /** + * Owl Tags + * + * We have here a (very) small collection of tag functions: + * + * - xml + * + * The plan is to add a few other tags such as css, globalcss. + */ + /** + * XML tag helper for defining templates. With this, one can simply define + * an inline template with just the template xml: + * ```js + * class A extends Component { + * static template = xml`
some template
`; + * } + * ``` + */ + function xml(strings) { + const name = `__template__${QWeb.nextId++}`; + QWeb.registerTemplate(name, strings[0]); + return name; + } + + var _tags = /*#__PURE__*/Object.freeze({ + xml: xml + }); + class Link extends Component { constructor() { super(...arguments); - this.template = LINK_TEMPLATE_NAME; this.href = this.env.router.destToPath(this.props); } async willUpdateProps(nextProps) { @@ -3842,33 +3884,37 @@ this.env.router.navigate(this.props); } } + Link.template = xml ` + + + + `; - function makeDirective(env) { - return { - name: "routecomponent", - priority: 13, - atNodeEncounter({ node }) { - let first = true; - const router = env.router; - for (let name of router.routeIds) { - const route = router.routes[name]; - if (route.component) { - // make new t t-component element - const comp = node.ownerDocument.createElement("t"); - comp.setAttribute("t-component", "__component__" + route.name); - comp.setAttribute(first ? "t-if" : "t-elif", `env.router.currentRouteName === '${route.name}'`); - first = false; - for (let param of route.params) { - comp.setAttribute(param, `env.router.currentParams.${param}`); - } - node.appendChild(comp); - } + 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 + }); } - node.removeAttribute("t-routecomponent"); - return false; } - }; + } } + RouteComponent.template = xml ` + + + + + + `; const paramRegexp = /\{\{(.*?)\}\}/; class Router { @@ -3886,7 +3932,7 @@ partialRoute.name = "__route__" + nextId++; } if (partialRoute.component) { - QWeb.register("__component__" + partialRoute.name, partialRoute.component); + QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component); } if (partialRoute.redirect) { this.validateDestination(partialRoute.redirect); @@ -3895,9 +3941,6 @@ this.routes[partialRoute.name] = partialRoute; this.routeIds.push(partialRoute.name); } - // setup link and directive - env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE); - QWeb.addDirective(makeDirective(env)); } //-------------------------------------------------------------------------- // Public API @@ -4083,9 +4126,10 @@ * Note that dynamic values, such as a date or a commit hash are added by rollup */ const core = { EventBus, Observer }; - const router = { Router, Link }; + const router = { Router, RouteComponent, Link }; const store = { Store, ConnectedComponent }; const utils = _utils; + const tags = _tags; const __info__ = {}; Object.defineProperty(__info__, "mode", { get() { @@ -4109,11 +4153,12 @@ exports.core = core; exports.router = router; exports.store = store; + exports.tags = tags; exports.utils = utils; - exports.__info__.version = '0.20.0'; - exports.__info__.date = '2019-09-05T12:42:15.744Z'; - exports.__info__.hash = '9fad164'; + exports.__info__.version = '0.21.0'; + exports.__info__.date = '2019-09-12T12:21:59.533Z'; + exports.__info__.hash = '14ceb38'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/playground/app.js b/playground/app.js index 629c7051..974141c8 100644 --- a/playground/app.js +++ b/playground/app.js @@ -150,6 +150,97 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start); return zip.generateAsync({ type: "blob" }); } +//------------------------------------------------------------------------------ +// Tabbed editor +//------------------------------------------------------------------------------ +class TabbedEditor extends owl.Component { + constructor(parent, props) { + super(parent, props); + this.state = { + 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; + } + + mounted() { + this.editor = this.editor || ace.edit(this.refs.editor); + + this.editor.setValue(this.props[this.state.currentTab], -1); + this.editor.setFontSize("12px"); + this.editor.setTheme("ace/theme/monokai"); + this.editor.setSession(this.sessions[this.state.currentTab]); + const tabSize = this.state.currentTab === "xml" ? 2 : 4; + this.editor.session.setOption("tabSize", tabSize); + this.editor.on("blur", () => { + const editorValue = this.editor.getValue(); + const propsValue = this.props[this.state.currentTab]; + if (editorValue !== propsValue) { + this.trigger("updateCode", { + type: this.state.currentTab, + value: editorValue + }); + } + }); + } + + willUpdateProps(nextProps) { + this._setupSessions(nextProps); + } + + patched() { + const session = this.sessions[this.state.currentTab]; + let content = this.props[this.state.currentTab]; + if (content === false) { + const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css"; + content = this.props[tab]; + this.state.currentTab = tab; + } + session.setValue(content, -1); + this.editor.setSession(session); + this.editor.resize(); + } + + setTab(tab) { + if (this.state.currentTab !== tab) { + this.state.currentTab = tab; + const session = this.sessions[this.state.currentTab]; + session.doc.setValue(this.props[tab], -1); + this.editor.setSession(session); + } + } + + onMouseDown(ev) { + if (ev.target.tagName === "DIV") { + let y = ev.clientY; + const resizer = ev => { + const delta = ev.clientY - y; + y = ev.clientY; + this.trigger("updatePanelHeight", { delta }); + }; + document.body.addEventListener("mousemove", resizer); + document.body.addEventListener("mouseup", () => { + document.body.removeEventListener("mousemove", resizer); + }); + } + } + + _setupSessions(props) { + for (let tab of ["js", "xml", "css"]) { + if (props[tab] && !this.sessions[tab]) { + this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]); + this.sessions[tab].setOption("useWorker", false); + const tabSize = tab === "xml" ? 2 : 4; + this.sessions[tab].setOption("tabSize", tabSize); + this.sessions[tab].setUndoManager(new ace.UndoManager()); + } + } + } +} + //------------------------------------------------------------------------------ // MAIN APP //------------------------------------------------------------------------------ @@ -158,7 +249,6 @@ class App extends owl.Component { super(...args); this.version = owl.__info__.version; this.SAMPLES = SAMPLES; - this.components = { TabbedEditor }; this.state = { js: SAMPLES[0].code, @@ -270,97 +360,8 @@ class App extends owl.Component { saveAs(content, "app.zip"); } } +App.components = { TabbedEditor }; -//------------------------------------------------------------------------------ -// Tabbed editor -//------------------------------------------------------------------------------ -class TabbedEditor extends owl.Component { - constructor(parent, props) { - super(parent, props); - this.state = { - 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; - } - - mounted() { - this.editor = this.editor || ace.edit(this.refs.editor); - - this.editor.setValue(this.props[this.state.currentTab], -1); - this.editor.setFontSize("12px"); - this.editor.setTheme("ace/theme/monokai"); - this.editor.setSession(this.sessions[this.state.currentTab]); - const tabSize = this.state.currentTab === "xml" ? 2 : 4; - this.editor.session.setOption("tabSize", tabSize); - this.editor.on("blur", () => { - const editorValue = this.editor.getValue(); - const propsValue = this.props[this.state.currentTab]; - if (editorValue !== propsValue) { - this.trigger("updateCode", { - type: this.state.currentTab, - value: editorValue - }); - } - }); - } - - willUpdateProps(nextProps) { - this._setupSessions(nextProps); - } - - patched() { - const session = this.sessions[this.state.currentTab]; - let content = this.props[this.state.currentTab]; - if (content === false) { - const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css"; - content = this.props[tab]; - this.state.currentTab = tab; - } - session.setValue(content, -1); - this.editor.setSession(session); - this.editor.resize(); - } - - setTab(tab) { - if (this.state.currentTab !== tab) { - this.state.currentTab = tab; - const session = this.sessions[this.state.currentTab]; - session.doc.setValue(this.props[tab], -1); - this.editor.setSession(session); - } - } - - onMouseDown(ev) { - if (ev.target.tagName === "DIV") { - let y = ev.clientY; - const resizer = ev => { - const delta = ev.clientY - y; - y = ev.clientY; - this.trigger("updatePanelHeight", { delta }); - }; - document.body.addEventListener("mousemove", resizer); - document.body.addEventListener("mouseup", () => { - document.body.removeEventListener("mousemove", resizer); - }); - } - } - - _setupSessions(props) { - for (let tab of ["js", "xml", "css"]) { - if (props[tab] && !this.sessions[tab]) { - this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]); - this.sessions[tab].setOption("useWorker", false); - const tabSize = tab === "xml" ? 2 : 4; - this.sessions[tab].setOption("tabSize", tabSize); - this.sessions[tab].setUndoManager(new ace.UndoManager()); - } - } - } -} //------------------------------------------------------------------------------ // Application initialization diff --git a/playground/samples.js b/playground/samples.js index 2a20cd41..c9ed9c92 100644 --- a/playground/samples.js +++ b/playground/samples.js @@ -10,7 +10,7 @@ class Greeter extends owl.Component { // Main root component class App extends owl.Component { - components = { Greeter }; + static components = { Greeter }; state = { name: 'World'}; } @@ -56,7 +56,7 @@ class Counter extends owl.Component { class App extends owl.Component { state = { flag: false, componentFlag: false, numbers: [] }; - components = { Counter }; + static components = { Counter }; toggle(key) { this.state[key] = !this.state[key]; @@ -214,7 +214,7 @@ class DemoComponent extends owl.Component { } class App extends owl.Component { - components = { DemoComponent }; + static components = { DemoComponent }; state = { n: 0, flag: true }; increment() { @@ -388,7 +388,7 @@ class TodoItem extends owl.Component { // TodoApp //------------------------------------------------------------------------------ class TodoApp extends owl.store.ConnectedComponent { - components = { TodoItem }; + static components = { TodoItem }; state = { filter: "all" }; static mapStoreToProps(state) { @@ -896,25 +896,24 @@ const RESPONSIVE = `// In this example, we show how we can modify keys in the gl //------------------------------------------------------------------------------ class Navbar extends owl.Component {} -class ControlPanel extends owl.Component { - components = { MobileSearchView }; -} +class MobileSearchView extends owl.Component {} -class FormView extends owl.Component { - components = { AdvancedComponent }; +class ControlPanel extends owl.Component { + static components = { MobileSearchView }; } class AdvancedComponent extends owl.Component {} +class FormView extends owl.Component { + static components = { AdvancedComponent }; +} + class Chatter extends owl.Component { messages = Array.from(Array(100).keys()); } -class MobileSearchView extends owl.Component {} - - class App extends owl.Component { - components = { Navbar, ControlPanel, FormView, Chatter }; + static components = { Navbar, ControlPanel, FormView, Chatter }; } //------------------------------------------------------------------------------ @@ -1072,7 +1071,7 @@ class Counter extends owl.Component { // Main root component class App extends owl.Component { - components = {Card, Counter}; + static components = {Card, Counter}; state = {a: 1, b: 3}; inc(key, delta) { @@ -1170,8 +1169,18 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not // because of the slow component. We use the 't-asyncroot' directive for this // purpose. Try removing it to see the difference. +class SlowComponent extends owl.Component { + willUpdateProps() { + // simulate a component that needs to perform async stuff (e.g. an RPC) + // with the updated props before re-rendering itself + return new Promise(resolve => setTimeout(resolve, 1500)); + } +} + +class NotificationList extends owl.Component {} + class App extends owl.Component { - components = {SlowComponent, NotificationList}; + static components = {SlowComponent, NotificationList}; state = { value: 0, notifs: [] }; increment() { @@ -1185,16 +1194,6 @@ class App extends owl.Component { } } -class SlowComponent extends owl.Component { - willUpdateProps() { - // simulate a component that needs to perform async stuff (e.g. an RPC) - // with the updated props before re-rendering itself - return new Promise(resolve => setTimeout(resolve, 1500)); - } -} - -class NotificationList extends owl.Component {} - const qweb = new owl.QWeb(TEMPLATES); const app = new App({ qweb }); app.mount(document.body); @@ -1368,7 +1367,7 @@ class Window extends owl.Component { } class WindowManager extends owl.Component { - components = { Window }; + static components = { Window }; windows = []; nextId = 1; currentZindex = 1; @@ -1384,12 +1383,12 @@ class WindowManager extends owl.Component { left: 0, zindex: this.currentZindex++ }); - this.components[id] = info.component; + this.constructor.components[id] = info.component; this.render(); } closeWindow(ev) { const id = ev.detail.id; - delete this.components[id]; + delete this.constructor.components[id]; const index = this.windows.findIndex(w => w.id === id); this.windows.splice(index, 1); this.render(); @@ -1409,7 +1408,7 @@ class WindowManager extends owl.Component { } class App extends owl.Component { - components = { WindowManager }; + static components = { WindowManager }; addWindow(name) { this.refs.wm.addWindow(name);