From 9846b2e997e6da6840d7ca2de201f5bf2b0e0b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 12 Sep 2019 09:45:32 +0200 Subject: [PATCH] [IMP] tags: introduce xml tag Big change! This commit introduces an xml function tag to easily define inline templates. This is a pretty big change toward single file owl components Part of #284 --- README.md | 37 +- doc/component.md | 14 +- doc/quick_start.md | 2 +- doc/qweb.md | 2 +- doc/readme.md | 3 + doc/tags.md | 47 ++ src/component/directive.ts | 6 +- src/index.ts | 3 +- src/qweb/extensions.ts | 4 +- src/qweb/qweb.ts | 7 +- src/router/Link.ts | 18 +- src/router/RouteComponent.ts | 10 +- src/router/Router.ts | 6 - src/tags.ts | 28 ++ .../__snapshots__/component.test.ts.snap | 8 +- tests/component/component.test.ts | 461 +++++++++--------- tests/helpers.ts | 23 +- tests/router/Link.test.ts | 4 +- tests/router/RouteComponent.test.ts | 4 +- tests/router/__snapshots__/Link.test.ts.snap | 4 +- 20 files changed, 381 insertions(+), 310 deletions(-) create mode 100644 doc/tags.md create mode 100644 src/tags.ts diff --git a/README.md b/README.md index ec8499c2..e7eceded 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,16 @@ related projects. OWL's main features are: Here is a short example to illustrate interactive components: -```xml - - -
- Hello Owl - -
-
-``` - ```javascript -class Counter extends owl.Component { +import { Component, QWeb } from 'owl' +import { xml } from 'owl/tags' + +class Counter extends Component { + static template = xml` + `; + state = { value: 0 }; increment() { @@ -39,17 +35,22 @@ class Counter extends owl.Component { } } -class App extends owl.Component { +class App extends Component { + static template = xml` +
+ Hello Owl + +
`; + static components = { Counter }; + } -const qweb = new owl.QWeb(TEMPLATES); -const app = new App({ qweb }); +const app = new App({ qweb: new QWeb() }); app.mount(document.body); ``` -Note that we assume here that the xml templates are available in the `TEMPLATES` -string. More interesting examples can be found on the +More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application. ## OWL's Design Principles diff --git a/doc/component.md b/doc/component.md index 1746b2dd..e916c396 100644 --- a/doc/component.md +++ b/doc/component.md @@ -72,7 +72,7 @@ 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. If no `template` key is defined, then +Component class. If no static `template` key 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 @@ -96,9 +96,6 @@ find a template with the component name (or one of its ancestor). - **`env`** (Object): the component environment, which contains a QWeb instance. -- **`template`** (string, optional): if given, this is the name of the QWeb template that will render - the component. - - **`state`** (Object): this is the location of the component's state, if there is any. After the willStart method, the `state` property is observed, and each change will cause the component to rerender itself. @@ -113,7 +110,10 @@ find a template with the component name (or one of its ancestor). ### Static Properties -- **`components`** (Object, optional): if given, this is an object that contains +- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to + make it easy to define an inline template. + +* **`components`** (Object, optional): if given, this is an object that contains the classes of any sub components needed by the template. This is the main way used by Owl to be able to create sub components. @@ -123,7 +123,7 @@ find a template with the component name (or one of its ancestor). } ``` -- **`props`** (Object, optional): if given, this is an object that describes the +* **`props`** (Object, optional): if given, this is an object that describes the type and shape of the (actual) props given to the component. If Owl mode is `dev`, this will be used to validate the props each time the component is created/updated. See [Props Validation](#props-validation) for more information. @@ -137,7 +137,7 @@ find a template with the component name (or one of its ancestor). } ``` -* **`defaultProps`** (Object, optional): if given, this object define default +- **`defaultProps`** (Object, optional): if given, this object define default values for (top-level) props. Whenever `props` are given to the object, they will be altered to add default value (if missing). Note that it does not change the initial object, a new object will be created instead. diff --git a/doc/quick_start.md b/doc/quick_start.md index 8c497e4f..f4abc8ab 100644 --- a/doc/quick_start.md +++ b/doc/quick_start.md @@ -73,9 +73,9 @@ Let us now add the javascript to make it work, in `app.js`: ```javascript class ClickCounter extends owl.Component { + static template = "clickcounter"; constructor() { super(...arguments); - this.template = "clickcounter"; this.state = { value: 0 }; } diff --git a/doc/qweb.md b/doc/qweb.md index c0e27025..e9c5c4b5 100644 --- a/doc/qweb.md +++ b/doc/qweb.md @@ -135,7 +135,7 @@ It's API is quite simple: having a reference to the actual QWeb instance. ```js - QWeb.registerTemplate('mytemplate', `
some template`); + QWeb.registerTemplate("mytemplate", `
some template`); ``` - **`registerComponent(name, Component)`**: static function to register an OWL Component diff --git a/doc/readme.md b/doc/readme.md index d3a3a4d3..9c5fd29c 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -19,6 +19,8 @@ owl store Store ConnectedComponent + tags + xml utils debounce escape @@ -36,6 +38,7 @@ owl - [QWeb](qweb.md) - [Router](router.md) - [Store](store.md) +- [Tags](tags.md) - [Utils](utils.md) - [Virtual DOM](vdom.md) diff --git a/doc/tags.md b/doc/tags.md new file mode 100644 index 00000000..ecf32c3a --- /dev/null +++ b/doc/tags.md @@ -0,0 +1,47 @@ +# 🦉 Tags 🦉 + +Tags are very small helper to make it easy to write inline templates. There is +only one currently available tag: `xml`, but we plan to add other tags later, +such as a `css` tag, which will be used to write single file components. + +## XML tag + +Without tags, creating a standalone component would look like this: + +```js +import { Component } from 'owl' + +const name = 'some-unique-name'; +const template = ` +
+ text + +
+`; +QWeb.registerTemplate(name, template); + +class MyComponent extends Component { + static template = name; + + ... +} +``` + +With tags, this process is slightly simplified. The name is uniquely generated, +and the template is automatically registered: + +```js +import { Component } from 'owl' +import { xml } from 'owl/tags' + +class MyComponent extends Component { + static template = xml` +
+ text + +
+ `; + + ... +} +``` diff --git a/src/component/directive.ts b/src/component/directive.ts index 43ffcc29..6115370f 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -419,7 +419,7 @@ QWeb.addDirective({ 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++) { @@ -428,7 +428,7 @@ QWeb.addDirective({ 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) { @@ -437,7 +437,7 @@ QWeb.addDirective({ t.appendChild(child); } const slotFn = qweb._compile(`slot_default_template`, t, ctx); - qweb.slots[`${slotId}_default`] = slotFn; + QWeb.slots[`${slotId}_default`] = slotFn; } } diff --git a/src/index.ts b/src/index.ts index 28907d81..01eccc00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { QWeb } from "./qweb/index"; import { ConnectedComponent } from "./store/connected_component"; import { Store } from "./store/store"; import * as _utils from "./utils"; +import * as _tags from "./tags"; import { Link } from "./router/Link"; import { RouteComponent } from "./router/RouteComponent"; import { Router } from "./router/Router"; @@ -20,7 +21,7 @@ export const core = { EventBus, Observer }; export const router = { Router, RouteComponent, Link }; export const store = { Store, ConnectedComponent }; export const utils = _utils; - +export const tags = _tags; export const __info__ = {}; Object.defineProperty(__info__, "mode", { diff --git a/src/qweb/extensions.ts b/src/qweb/extensions.ts index d3c083ef..e4f03491 100644 --- a/src/qweb/extensions.ts +++ b/src/qweb/extensions.ts @@ -227,7 +227,9 @@ QWeb.addDirective({ atNodeEncounter({ ctx, value }): boolean { 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}));` diff --git a/src/qweb/qweb.ts b/src/qweb/qweb.ts index 45c62e96..48a11528 100644 --- a/src/qweb/qweb.ts +++ b/src/qweb/qweb.ts @@ -143,15 +143,16 @@ export class QWeb extends EventBus { static TEMPLATES: { [name: string]: Template } = {}; + static nextId: number = 1; + h = h; // dev mode enables better error messages or more costly validations static dev: boolean = false; - // slots contains sub templates defined with t-set inside t-component nodes, and // are meant to be used by the t-slot directive. - slots = {}; - nextSlotId = 1; + static slots = {}; + static 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, diff --git a/src/router/Link.ts b/src/router/Link.ts index dcb9af30..c97c9d8d 100644 --- a/src/router/Link.ts +++ b/src/router/Link.ts @@ -1,18 +1,18 @@ import { Component } from "../component/component"; +import { xml } from "../tags"; import { Destination, RouterEnv } from "./Router"; -export const LINK_TEMPLATE_NAME = "__owl__-router-link"; -export const LINK_TEMPLATE = ` - - - `; - type Props = Destination; export class Link extends Component { - static template = LINK_TEMPLATE_NAME; + static template = xml` + + + + `; + href: string = this.env.router.destToPath(this.props); async willUpdateProps(nextProps) { diff --git a/src/router/RouteComponent.ts b/src/router/RouteComponent.ts index 162d047a..faef639c 100644 --- a/src/router/RouteComponent.ts +++ b/src/router/RouteComponent.ts @@ -1,15 +1,15 @@ import { Component } from "../component/component"; +import { xml } from "../tags"; -export const ROUTE_COMPONENT_TEMPLATE_NAME = "__owl__-router-component"; -export const ROUTE_COMPONENT_TEMPLATE = ` +export class RouteComponent extends Component { + static template = xml` - `; + + `; -export class RouteComponent extends Component { - static template = ROUTE_COMPONENT_TEMPLATE_NAME; routes: any[] = []; constructor(parent, props) { super(parent, props); diff --git a/src/router/Router.ts b/src/router/Router.ts index d039c4f1..7eedb6a1 100644 --- a/src/router/Router.ts +++ b/src/router/Router.ts @@ -1,7 +1,5 @@ import { Env } from "../component/component"; import { QWeb } from "../qweb/index"; -import { ROUTE_COMPONENT_TEMPLATE, ROUTE_COMPONENT_TEMPLATE_NAME } from "./RouteComponent"; -import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link"; import { shallowEqual } from "../utils"; type NavigationGuard = (info: { @@ -84,10 +82,6 @@ export class Router { this.routes[partialRoute.name] = partialRoute as Route; this.routeIds.push(partialRoute.name); } - - // setup link and directive - env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE); - env.qweb.addTemplate(ROUTE_COMPONENT_TEMPLATE_NAME, ROUTE_COMPONENT_TEMPLATE); } //-------------------------------------------------------------------------- diff --git a/src/tags.ts b/src/tags.ts new file mode 100644 index 00000000..3e5d2352 --- /dev/null +++ b/src/tags.ts @@ -0,0 +1,28 @@ +import { QWeb } from "./qweb/index"; + +/** + * 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
`; + * } + * ``` + */ +export function xml(strings) { + const name = `__template__${QWeb.nextId++}`; + QWeb.registerTemplate(name, strings[0]); + return name; +} diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index c8f2ef3e..3bd31515 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -1406,14 +1406,14 @@ exports[`t-slot directive can define and call slots 2`] = ` let c2 = [], p2 = {key:2}; var vn2 = h('div', p2, c2); c1.push(vn2); - const slot3 = this.slots[context.__owl__.slotId + '_' + 'header']; + const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'header']; if (slot3) { slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner})); } let c4 = [], p4 = {key:4}; var vn4 = h('div', p4, c4); c1.push(vn4); - const slot5 = this.slots[context.__owl__.slotId + '_' + 'footer']; + const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer']; if (slot5) { slot5.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c4, vars: extra.vars, parent: owner})); } @@ -1557,7 +1557,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = ` let c2 = [], p2 = {key:2,attrs:{href: _1}}; var vn2 = h('a', p2, c2); result = vn2; - const slot3 = this.slots[context.__owl__.slotId + '_' + 'default']; + const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; if (slot3) { slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner})); } @@ -1661,7 +1661,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = ` let c2 = [], p2 = {key:2,attrs:{href: _1}}; var vn2 = h('a', p2, c2); result = vn2; - const slot3 = this.slots[context.__owl__.slotId + '_' + 'default']; + const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; if (slot3) { slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner})); } diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index 4f7f0a2c..1077929a 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -1,5 +1,6 @@ import { Component, Env } from "../../src/component/component"; import { QWeb } from "../../src/qweb/qweb"; +import { xml } from "../../src/tags"; import { EventBus } from "../../src/core/event_bus"; import { makeDeferred, @@ -119,11 +120,11 @@ describe("basic widget properties", () => { }); test("widget style and classname", async () => { - env.qweb.addTemplate( - "StyledWidget", - `
world
` - ); - class StyledWidget extends Widget {} + class StyledWidget extends Widget { + static template = xml` +
world
+ `; + } const widget = new StyledWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
world
`); @@ -164,19 +165,17 @@ describe("basic widget properties", () => { test("reconciliation alg is not confused in some specific situation", async () => { // in this test, we set t-key to 4 because it was in conflict with the // template id corresponding to the first child. - env.qweb.addTemplates(` - -
- - -
- child -
- `); - - class Child extends Component {} + class Child extends Component { + static template = xml`child`; + } class Parent extends Component { + static template = xml` +
+ + +
+ `; static components = { Child }; } @@ -226,7 +225,6 @@ describe("lifecycle hooks", () => { test("willStart hook is called on subwidget", async () => { let ok = false; - env.qweb.addTemplate("ParentWidget", `
`); class ChildWidget extends Widget { async willStart() { ok = true; @@ -234,6 +232,7 @@ describe("lifecycle hooks", () => { } class ParentWidget extends Widget { + static template = xml`
`; static components = { child: ChildWidget }; } const widget = new ParentWidget(env); @@ -244,8 +243,6 @@ describe("lifecycle hooks", () => { test("mounted hook is called on subcomponents, in proper order", async () => { const steps: any[] = []; - env.qweb.addTemplate("ParentWidget", `
`); - class ChildWidget extends Widget { mounted() { expect(document.body.contains(this.el)).toBe(true); @@ -254,7 +251,8 @@ describe("lifecycle hooks", () => { } class ParentWidget extends Widget { - static components = { child: ChildWidget }; + static template = xml`
` + static components = { ChildWidget }; mounted() { steps.push("parent:mounted"); } @@ -267,12 +265,6 @@ describe("lifecycle hooks", () => { test("mounted hook is called on subsubcomponents, in proper order", async () => { const steps: any[] = []; - env.qweb.addTemplate( - "ParentWidget", - `
` - ); - env.qweb.addTemplate("ChildWidget", `
`); - class ChildChildWidget extends Widget { mounted() { steps.push("childchild:mounted"); @@ -283,6 +275,7 @@ describe("lifecycle hooks", () => { } class ChildWidget extends Widget { + static template = xml`
`; static components = { childchild: ChildChildWidget }; mounted() { steps.push("child:mounted"); @@ -293,6 +286,7 @@ describe("lifecycle hooks", () => { } class ParentWidget extends Widget { + static template = xml`
`; static components = { child: ChildWidget }; state = { flag: false }; mounted() { @@ -435,12 +429,7 @@ describe("lifecycle hooks", () => { test("components are unmounted and destroyed if no longer in DOM", async () => { let steps: string[] = []; - env.qweb.addTemplate( - "ParentWidget", - `
- -
` - ); + class ChildWidget extends Widget { constructor(parent) { super(parent); @@ -457,8 +446,13 @@ describe("lifecycle hooks", () => { } } class ParentWidget extends Widget { + static template = xml` +
+ +
+ `; + static components = { ChildWidget }; state = { ok: true }; - static components = { child: ChildWidget }; } const widget = new ParentWidget(env); @@ -471,8 +465,9 @@ describe("lifecycle hooks", () => { test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => { let childUnmounted = false; - env.qweb.addTemplate("ChildWidget", ``); + class ChildWidget extends Widget { + static template = xml``; willUnmount() { childUnmounted = true; } @@ -481,16 +476,14 @@ describe("lifecycle hooks", () => { } } - env.qweb.addTemplate( - "ParentWidget", - ` -
-
- -
-
` - ); class ParentWidget extends Widget { + static template = xml` +
+
+ +
+
+ `; static components = { ChildWidget }; state = { n: 0, flag: true }; increment() { @@ -515,7 +508,7 @@ describe("lifecycle hooks", () => { test("hooks are called in proper order in widget creation/destruction", async () => { let steps: string[] = []; - env.qweb.addTemplate("ParentWidget", `
`); + class ChildWidget extends Widget { constructor(parent) { super(parent); @@ -532,6 +525,7 @@ describe("lifecycle hooks", () => { } } class ParentWidget extends Widget { + static template = xml`
`; static components = { child: ChildWidget }; constructor(parent) { super(parent); @@ -614,13 +608,13 @@ describe("lifecycle hooks", () => { test("patched hook is called after updateProps", async () => { let n = 0; - env.qweb.addTemplate("Parent", '
'); class TestWidget extends Widget { patched() { n++; } } class Parent extends Widget { + static template = xml`
`; state = { a: 1 }; static components = { Child: TestWidget }; } @@ -654,17 +648,18 @@ describe("lifecycle hooks", () => { test("shouldUpdate hook prevent rerendering", async () => { let shouldUpdate = false; - env.qweb.addTemplate("Parent", `
`); class TestWidget extends Widget { + static template = xml`
`; shouldUpdate() { return shouldUpdate; } } class Parent extends Widget { - state = { val: 42 }; + static template = xml`
`; static components = { Child: TestWidget }; + state = { val: 42 }; } - env.qweb.addTemplate("TestWidget", `
`); + const widget = new Parent(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe("
42
"); @@ -680,15 +675,6 @@ describe("lifecycle hooks", () => { test("sub widget (inside sub node): hooks are correctly called", async () => { let created = false; let mounted = false; - env.qweb.addTemplate( - "ParentWidget", - ` -
- -
-
-
` - ); class ChildWidget extends Widget { constructor(parent, props) { @@ -700,6 +686,13 @@ describe("lifecycle hooks", () => { } } class ParentWidget extends Widget { + static template = xml` +
+ +
+
+
+ `; static components = { child: ChildWidget }; state = { flag: false }; } @@ -716,13 +709,7 @@ describe("lifecycle hooks", () => { test("willPatch/patched hook", async () => { const steps: string[] = []; - env.qweb.addTemplate( - "ParentWidget", - ` -
- -
` - ); + class ChildWidget extends Widget { willPatch() { steps.push("child:willPatch"); @@ -732,6 +719,11 @@ describe("lifecycle hooks", () => { } } class ParentWidget extends Widget { + static template = xml` +
+ +
+ `; static components = { child: ChildWidget }; state = { n: 1 }; willPatch() { @@ -763,13 +755,6 @@ describe("lifecycle hooks", () => { // we make sure here that willPatch/patched is only called if widget is in // dom, mounted const steps: string[] = []; - env.qweb.addTemplates(` - -
- -
-
- `); class ChildWidget extends Widget { willPatch() { @@ -786,6 +771,11 @@ describe("lifecycle hooks", () => { } } class ParentWidget extends Widget { + static template = xml` +
+ +
+ `; static components = { ChildWidget }; state = { n: 1, flag: true }; } @@ -793,7 +783,7 @@ describe("lifecycle hooks", () => { const widget = new ParentWidget(env); await widget.mount(fixture); - expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[ParentWidget.template].fn.toString()).toMatchSnapshot(); expect(steps).toEqual(["child:mounted"]); widget.state.flag = false; await nextTick(); @@ -865,21 +855,20 @@ describe("destroy method", () => { }); test("destroying a widget before being mounted", async () => { - env.qweb.addTemplates(` - -
- -
- - - - - - - -
`); - class GrandChild extends Component {} + class GrandChild extends Component { + static template = xml` + + + + `; + } class Child extends Component { + static template = xml` + + + + + `; static components = { GrandChild }; state = { val: 33, flag: false }; doSomething() { @@ -892,6 +881,11 @@ describe("destroy method", () => { } } class Parent extends Component { + static template = xml` +
+ +
+ `; static components = { Child }; state = { p: 1 }; doStuff() { @@ -985,14 +979,12 @@ describe("composition", () => { test("t-refs are bound at proper timing", async () => { expect.assertions(2); - env.qweb.addTemplate( - "ParentWidget", - ` + class ParentWidget extends Widget { + static template = xml`
-
` - ); - class ParentWidget extends Widget { +
+ `; static components = { Widget }; state = { list: [] }; willPatch() { @@ -3055,8 +3047,8 @@ describe("t-slot directive", () => { ); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot(); - expect(env.qweb.slots["1_header"].toString()).toMatchSnapshot(); - expect(env.qweb.slots["1_footer"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_header"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context", async () => { @@ -3092,7 +3084,7 @@ describe("t-slot directive", () => { expect(fixture.innerHTML).toBe( '
1
' ); - expect(env.qweb.slots["1_footer"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context, part 2", async () => { @@ -3130,7 +3122,7 @@ describe("t-slot directive", () => { expect(fixture.innerHTML).toBe( '
  • User Aaron
  • User Mathieu
  • ' ); - expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context, part 3", async () => { @@ -3169,7 +3161,7 @@ describe("t-slot directive", () => { expect(fixture.innerHTML).toBe( '
  • User Aaron
  • User Mathieu
  • ' ); - expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("slots are rendered with proper context, part 4", async () => { @@ -3202,7 +3194,7 @@ describe("t-slot directive", () => { app.state.user.name = "David"; await nextTick(); expect(fixture.innerHTML).toBe('
    User David
    '); - expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("refs are properly bound in slots", async () => { @@ -3238,7 +3230,7 @@ describe("t-slot directive", () => { expect(fixture.innerHTML).toBe( '
    1
    ' ); - expect(env.qweb.slots["1_footer"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot(); }); test("content is the default slot", async () => { @@ -3260,7 +3252,7 @@ describe("t-slot directive", () => { await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    sts rocks
    "); - expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("default slot work with text nodes", async () => { @@ -3280,7 +3272,7 @@ describe("t-slot directive", () => { await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    sts rocks
    "); - expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("multiple roots are allowed in a named slot", async () => { @@ -3305,7 +3297,7 @@ describe("t-slot directive", () => { await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    stsrocks
    "); - expect(env.qweb.slots["1_content"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_content"].toString()).toMatchSnapshot(); }); test("multiple roots are allowed in a default slot", async () => { @@ -3328,7 +3320,7 @@ describe("t-slot directive", () => { await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    stsrocks
    "); - expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); }); test("missing slots are ignored", async () => { @@ -3599,17 +3591,16 @@ describe("t-model directive", () => { }); test("on a select, initial state", async () => { - env.qweb.addTemplates(` - -
    - -
    -
    `); class SomeComponent extends Widget { + static template = xml` +
    + +
    + `; state = { color: "red" }; } const comp = new SomeComponent(env); @@ -3619,15 +3610,13 @@ describe("t-model directive", () => { }); test("on a sub state key", async () => { - env.qweb.addTemplates(` - -
    - - -
    -
    `); - class SomeComponent extends Widget { + static template = xml` +
    + + +
    + `; state = { something: { text: "" } }; } const comp = new SomeComponent(env); @@ -3639,18 +3628,17 @@ describe("t-model directive", () => { await editInput(input, "test"); expect(comp.state.something.text).toBe("test"); expect(fixture.innerHTML).toBe("
    test
    "); - expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); }); test(".lazy modifier", async () => { - env.qweb.addTemplates(` - -
    + class SomeComponent extends Widget { + static template = xml` +
    - `); - class SomeComponent extends Widget { + `; state = { text: "" }; } const comp = new SomeComponent(env); @@ -3668,18 +3656,17 @@ describe("t-model directive", () => { await nextTick(); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
    test
    "); - expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); }); test(".trim modifier", async () => { - env.qweb.addTemplates(` - -
    - - -
    -
    `); class SomeComponent extends Widget { + static template = xml` +
    + + +
    + `; state = { text: "" }; } const comp = new SomeComponent(env); @@ -3692,14 +3679,13 @@ describe("t-model directive", () => { }); test(".number modifier", async () => { - env.qweb.addTemplates(` - -
    - - -
    -
    `); class SomeComponent extends Widget { + static template = xml` +
    + + +
    + `; state = { number: 0 }; } const comp = new SomeComponent(env); @@ -3732,15 +3718,14 @@ describe("environment and plugins", () => { test("plugin works as expected", async () => { somePlugin(env); - env.qweb.addTemplates(` - -
    + class App extends Widget { + static template=xml` +
    Red Blue
    - - `); - class App extends Widget {} + `; + } const app = new App(env); await app.mount(fixture); @@ -4086,18 +4071,19 @@ describe("top level sub widgets", () => { }); test("can select a sub widget ", async () => { - env.qweb.addTemplates(` - - - - - - CHILD 1 -
    CHILD 2
    -
    `); - class Child extends Widget {} - class OtherChild extends Widget {} + class Child extends Widget { + static template = xml`CHILD 1`; + } + class OtherChild extends Widget { + static template = xml`
    CHILD 2
    `; + } class Parent extends Widget { + static template = xml` + + + + + `; static components = { Child, OtherChild }; } (env).flag = true; @@ -4110,22 +4096,23 @@ describe("top level sub widgets", () => { await parent.mount(fixture); expect(fixture.innerHTML).toBe("
    CHILD 2
    "); - expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); test("can select a sub widget, part 2", async () => { - env.qweb.addTemplates(` - - - - - - CHILD 1 -
    CHILD 2
    -
    `); - class Child extends Widget {} - class OtherChild extends Widget {} + class Child extends Widget { + static template = xml`CHILD 1`; + } + class OtherChild extends Widget { + static template = xml`
    CHILD 2
    `; + } class Parent extends Widget { + static template = xml` + + + + + `; state = { flag: true }; static components = { Child, OtherChild }; } @@ -4140,13 +4127,9 @@ describe("top level sub widgets", () => { describe("unmounting and remounting", () => { test("widget can be unmounted and remounted", async () => { - env.qweb.addTemplates(` - -
    Hey
    -
    `); - const steps: string[] = []; class MyWidget extends Widget { + static template = xml`
    Hey
    `; async willStart() { steps.push("willstart"); } @@ -4173,13 +4156,9 @@ describe("unmounting and remounting", () => { }); test("widget can be mounted twice without ill effect", async () => { - env.qweb.addTemplates(` - -
    Hey
    -
    `); - const steps: string[] = []; class MyWidget extends Widget { + static template = xml`
    Hey
    `; async willStart() { steps.push("willstart"); } @@ -4200,16 +4179,11 @@ describe("unmounting and remounting", () => { test("state changes in willUnmount do not trigger rerender", async () => { const steps: string[] = []; - env.qweb.addTemplates(` - -
    - -
    - -
    - `); class Child extends Widget { + static template = xml` + + `; state = { n: 2 }; __render(a, b, c, d) { steps.push("render"); @@ -4228,6 +4202,11 @@ describe("unmounting and remounting", () => { } } class Parent extends Widget { + static template = xml` +
    + +
    + `; static components = { Child }; state = { val: 1, flag: true }; } @@ -4243,13 +4222,10 @@ describe("unmounting and remounting", () => { }); test("state changes in willUnmount will be applied on remount", async () => { - env.qweb.addTemplates(` - -
    -
    - `); - class TestWidget extends Widget { + static template = xml` +
    + `; state = { val: 1 }; willUnmount() { this.state.val = 3; @@ -4271,15 +4247,14 @@ describe("unmounting and remounting", () => { describe("dynamic root nodes", () => { test("template with t-if, part 1", async () => { - env.qweb.addTemplates(` - - - hey -
    abc
    -
    -
    - `); - class TestWidget extends Widget {} + class TestWidget extends Widget { + static template = xml` + + hey +
    abc
    +
    + `; + } const widget = new TestWidget(env); await widget.mount(fixture); @@ -4288,15 +4263,14 @@ describe("dynamic root nodes", () => { }); test("template with t-if, part 2", async () => { - env.qweb.addTemplates(` - - - hey -
    abc
    -
    -
    - `); - class TestWidget extends Widget {} + class TestWidget extends Widget { + static template = xml` + + hey +
    abc
    +
    + `; + } const widget = new TestWidget(env); await widget.mount(fixture); @@ -4305,15 +4279,13 @@ describe("dynamic root nodes", () => { }); test("switching between sub branches dynamically", async () => { - env.qweb.addTemplates(` - - - hey -
    abc
    -
    -
    - `); class TestWidget extends Widget { + static template = xml` + + hey +
    abc
    +
    + `; state = { flag: true }; } @@ -4328,19 +4300,19 @@ describe("dynamic root nodes", () => { }); test("switching between sub components dynamically", async () => { - env.qweb.addTemplates(` - - hey -
    abc
    - - - - -
    - `); - class ChildA extends Widget {} - class ChildB extends Widget {} + class ChildA extends Widget { + static template = xml`hey`; + } + class ChildB extends Widget { + static template = xml`
    abc
    `; + } class TestWidget extends Widget { + static template = xml` + + + + + `; static components = { ChildA, ChildB }; state = { flag: true }; } @@ -4359,17 +4331,13 @@ describe("dynamic root nodes", () => { describe("dynamic t-props", () => { test("basic use", async () => { expect.assertions(4); - env.qweb.addTemplates(` - - - - -
    - -
    -
    - `); + class Child extends Widget { + static template = xml` + + + + `; constructor(parent, props) { super(parent, props); expect(props).toEqual({ a: 1, b: 2 }); @@ -4377,6 +4345,11 @@ describe("dynamic t-props", () => { } } class Parent extends Widget { + static template = xml` +
    + +
    + `; static components = { Child }; some = { obj: { a: 1, b: 2 } }; @@ -4386,6 +4359,6 @@ describe("dynamic t-props", () => { await widget.mount(fixture); expect(fixture.innerHTML).toBe("
    3
    "); - expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot(); }); }); diff --git a/tests/helpers.ts b/tests/helpers.ts index df6991e1..0896ec4d 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -5,6 +5,27 @@ import "../src/qweb/base_directives"; import "../src/qweb/extensions"; import "../src/component/directive"; +// Some static cleanup +let nextSlotId; +let slots; +let nextId; +let TEMPLATES; + +beforeEach(() => { + nextSlotId = QWeb.nextSlotId; + slots = Object.assign({}, QWeb.slots); + nextId = QWeb.nextId; + TEMPLATES = Object.assign({}, QWeb.TEMPLATES); +}); + +afterEach(() => { + QWeb.nextSlotId = nextSlotId; + QWeb.slots = slots; + QWeb.nextId = nextId; + QWeb.TEMPLATES = TEMPLATES; +}); + +// helpers export function nextMicroTick(): Promise { return Promise.resolve(); } @@ -82,7 +103,7 @@ export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}) const node = renderToDOM(qweb, t, context); const result = node instanceof Text ? node.textContent! : node.outerHTML; if (result !== qweb.renderToString(t, context)) { - throw new Error("HTML string returned by renderToString helper does not match QWeb render"); + throw new Error("HTML string returned by renderToString helper does not match QWeb render"); } return result; } diff --git a/tests/router/Link.test.ts b/tests/router/Link.test.ts index 1ddee7f9..3148c255 100644 --- a/tests/router/Link.test.ts +++ b/tests/router/Link.test.ts @@ -1,5 +1,5 @@ import { Component } from "../../src/component/component"; -import { Link, LINK_TEMPLATE_NAME } from "../../src/router/Link"; +import { Link } from "../../src/router/Link"; import { RouterEnv } from "../../src/router/Router"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; import { TestRouter } from "./TestRouter"; @@ -50,7 +50,7 @@ describe("Link component", () => { '' ); - expect(env.qweb.templates[LINK_TEMPLATE_NAME].fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[Link.template].fn.toString()).toMatchSnapshot(); }); test("do not redirect if right clicking", async () => { diff --git a/tests/router/RouteComponent.test.ts b/tests/router/RouteComponent.test.ts index 97b7ef5f..ddbeda89 100644 --- a/tests/router/RouteComponent.test.ts +++ b/tests/router/RouteComponent.test.ts @@ -1,6 +1,6 @@ import { Component } from "../../src/component/component"; import { RouterEnv } from "../../src/router/Router"; -import { RouteComponent, ROUTE_COMPONENT_TEMPLATE_NAME } from "../../src/router/RouteComponent"; +import { RouteComponent } from "../../src/router/RouteComponent"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; import { TestRouter } from "./TestRouter"; @@ -52,7 +52,7 @@ describe("RouteComponent", () => { await router.navigate({ to: "users" }); await nextTick(); expect(fixture.innerHTML).toBe("
    Users
    "); - expect(env.qweb.templates[ROUTE_COMPONENT_TEMPLATE_NAME].fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates[RouteComponent.template].fn.toString()).toMatchSnapshot(); }); test("can render parameterized route", async () => { diff --git a/tests/router/__snapshots__/Link.test.ts.snap b/tests/router/__snapshots__/Link.test.ts.snap index 2912d704..ef18326d 100644 --- a/tests/router/__snapshots__/Link.test.ts.snap +++ b/tests/router/__snapshots__/Link.test.ts.snap @@ -12,11 +12,11 @@ exports[`Link component can render simple cases 1`] = ` var vn3 = h('a', p3, c3); result = vn3; if (!context['navigate']) { - throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__owl__-router-link'\`) + throw new Error('Missing handler \\\\'' + 'navigate' + \`\\\\' when evaluating template '__template__1'\`) } extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner); p3.on['click'] = extra.handlers['click' + 3]; - const slot4 = this.slots[context.__owl__.slotId + '_' + 'default']; + const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; if (slot4) { slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner})); }