From 361ecbe90b1802bb9cb7bc2e2872c6e76628662c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 14 May 2019 23:34:28 +0200 Subject: [PATCH] [IMP] component: deduce template name from component name closes #102 --- doc/component.md | 18 +++++++------ src/component.ts | 57 +++++++++++++++++++++++++++++++++++------ src/qweb_core.ts | 7 +++++ tests/component.test.ts | 54 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 16 deletions(-) diff --git a/doc/component.md b/doc/component.md index 9604a39b..2f65e86d 100644 --- a/doc/component.md +++ b/doc/component.md @@ -43,7 +43,6 @@ Let us have a look at a simple component: ```javascript class ClickCounter extends owl.Component { - template = "click-counter"; state = { value: 0 }; increment() { @@ -53,7 +52,7 @@ class ClickCounter extends owl.Component { ``` ```xml - ``` @@ -62,7 +61,8 @@ Note that this code is written in ESNext style, so it will only run on the latest browsers without a transpilation step. This example show how a component should be defined: it simply subclasses the -Component class. It needs to define a template (`click-counter` here). Also, +Component class. If no `template` key (or `inlineTemplate`), is defined, then +Owl will use the component's name as template name. Here, a state object is defined. It is not mandatory to use the state object, but it is certainly encouraged. The state object is [observed](observer.md), and any change to it will cause a rerendering. @@ -75,7 +75,7 @@ templates are standard [QWeb](qweb.md) templates, but with an extra directive: widgets: ```xml -
+
some text
@@ -83,7 +83,6 @@ widgets: ```js class ParentWidget extends owl.Component { - template = 'parent'; widgets = { MyWidget: MyWidget}; ... } @@ -101,7 +100,7 @@ additional css classes or style for the sub widget: css declared in `class`, `st root widget element. ```xml -
+
``` @@ -124,6 +123,10 @@ It exists in the context of an environment (`env`), which is propagated from a parent to its children. The environment needs to have a QWeb instance, which will be used to render the component template. +Be aware that the name of the component may be significant: if a component does +not define a `template` or `inlineTemplate` key, then Owl will lookup in QWeb to +find a template with the component name (or one of its ancestor). + ### Properties - **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the @@ -131,7 +134,7 @@ will be used to render the component template. - **`env`** (Object): the component environment, which contains a QWeb instance. -- **`template`** (string): a string, which is the name of the QWeb template that will render +- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. - **`inlineTemplate`** (string, optional): a string that represents a xml template. If set, @@ -236,7 +239,6 @@ implemented in most cases: ```javascript class ClickCounter extends owl.Component { - template = "click-counter"; state = { value: 0 }; ... diff --git a/src/component.ts b/src/component.ts index db66796d..ab2d3b8a 100644 --- a/src/component.ts +++ b/src/component.ts @@ -43,6 +43,12 @@ export interface Meta { mountedHandlers: { [key: number]: Function }; } +// If a component does not define explicitely a template (or inlineTemplate) +// 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: { [key: number]: { [name: string]: string } } = {}; + //------------------------------------------------------------------------------ // Widget //------------------------------------------------------------------------------ @@ -54,8 +60,8 @@ export class Component< State extends {} > extends EventBus { readonly __owl__: Meta; - template: string = "default"; - inlineTemplate: string | null = null; + template?: string; + inlineTemplate?: string; get el(): HTMLElement | null { return this.__owl__.vnode ? (this).__owl__.vnode.elm : null; @@ -379,13 +385,48 @@ export class Component< return Promise.resolve(h("div")); } this.__owl__.isStarted = true; - if (this.inlineTemplate) { - this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true); + + const qweb = this.env.qweb; + if (!this.template) { + if (this.inlineTemplate) { + this.env.qweb.addTemplate( + this.inlineTemplate, + this.inlineTemplate, + true + ); + + // we write on the proto, so any new component of this class will get + // automatically the template key properly setup. + (this).__proto__.template = this.inlineTemplate; + } else { + let tmap = TEMPLATE_MAP[qweb.id]; + if (!tmap) { + tmap = {}; + TEMPLATE_MAP[qweb.id] = tmap; + } + let p = (this).constructor; + let name: string = p.name; + let template = tmap[name]; + if (template) { + this.template = template; + } else { + while ( + (template = p.name) && + !(template in qweb.templates) && + p !== Component + ) { + p = p.__proto__; + } + if (p === Component) { + this.template = "default"; + } else { + tmap[name] = template; + this.template = template; + } + } + } } - this.__owl__.render = this.env.qweb.render.bind( - this.env.qweb, - this.inlineTemplate || this.template - ); + this.__owl__.render = qweb.render.bind(qweb, this.template); this._observeState(); return this._render(); } diff --git a/src/qweb_core.ts b/src/qweb_core.ts index f0e0cba7..64ed9edd 100644 --- a/src/qweb_core.ts +++ b/src/qweb_core.ts @@ -118,6 +118,8 @@ function parseXML(xml: string): Document { return doc; } +let nextID = 1; + //------------------------------------------------------------------------------ // QWeb rendering engine //------------------------------------------------------------------------------ @@ -125,6 +127,11 @@ export class QWeb { templates: { [name: string]: Template } = {}; utils = UTILS; + // 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. + id = nextID++; + constructor(data?: string) { if (data) { this.loadTemplates(data); diff --git a/tests/component.test.ts b/tests/component.test.ts index 7d07e578..f619897b 100644 --- a/tests/component.test.ts +++ b/tests/component.test.ts @@ -2117,3 +2117,57 @@ describe("t-mounted directive", () => { expect(widget.f).toHaveBeenCalledTimes(1); }); }); + +describe("can deduce template from name", () => { + test("can find template if name of component", async () => { + class ABC extends Widget {} + env.qweb.addTemplate("ABC", "Orval"); + const abc = new ABC(env); + await abc.mount(fixture); + expect(fixture.innerHTML).toBe("Orval"); + }); + + test("can find template of parent component", async () => { + class ABC extends Widget {} + class DEF extends ABC {} + env.qweb.addTemplate("ABC", "Orval"); + const def = new DEF(env); + await def.mount(fixture); + expect(fixture.innerHTML).toBe("Orval"); + }); + + test("can find template of parent component, defined by template key", async () => { + class ABC extends Widget { + template = "Achel"; + } + class DEF extends ABC {} + env.qweb.addTemplate("Achel", "Orval"); + const def = new DEF(env); + await def.mount(fixture); + expect(fixture.innerHTML).toBe("Orval"); + }); + + test("can find template of parent component, defined by inlinetemplate key", async () => { + class ABC extends Widget { + inlineTemplate = "Orval"; + } + class DEF extends ABC {} + const def = new DEF(env); + await def.mount(fixture); + expect(fixture.innerHTML).toBe("Orval"); + }); + + test("templates are found in proper qweb instance", async () => { + const env2 = makeTestWEnv(); + env.qweb.addTemplate("ABC", "Rochefort 8"); + env2.qweb.addTemplate("ABC", "Rochefort 10"); + class ABC extends Widget {} + const abc = new ABC(env); + await abc.mount(fixture); + expect(fixture.innerHTML).toBe("Rochefort 8"); + abc.destroy(); + const abc2 = new ABC(env2); + await abc2.mount(fixture); + expect(fixture.innerHTML).toBe("Rochefort 10"); + }); +});