[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
This commit is contained in:
Géry Debongnie
2019-09-12 09:45:32 +02:00
parent cfc2fb2ba2
commit 9846b2e997
20 changed files with 381 additions and 310 deletions
+19 -18
View File
@@ -18,20 +18,16 @@ related projects. OWL's main features are:
Here is a short example to illustrate interactive components: Here is a short example to illustrate interactive components:
```xml
<templates>
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
<div t-name="App">
<span>Hello Owl</span>
<Counter />
</div>
</templates>
```
```javascript ```javascript
class Counter extends owl.Component { import { Component, QWeb } from 'owl'
import { xml } from 'owl/tags'
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 }; state = { value: 0 };
increment() { increment() {
@@ -39,17 +35,22 @@ class Counter extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
static template = xml`
<div>
<span>Hello Owl</span>
<Counter />
</div>`;
static components = { Counter }; static components = { Counter };
} }
const qweb = new owl.QWeb(TEMPLATES); const app = new App({ qweb: new QWeb() });
const app = new App({ qweb });
app.mount(document.body); app.mount(document.body);
``` ```
Note that we assume here that the xml templates are available in the `TEMPLATES` More interesting examples can be found on the
string. More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application. [playground](https://odoo.github.io/owl/playground) application.
## OWL's Design Principles ## OWL's Design Principles
+7 -7
View File
@@ -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. latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the 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, 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 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 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. - **`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 - **`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 any. After the willStart method, the `state` property is observed, and each
change will cause the component to rerender itself. 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 ### 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 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. 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 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 `dev`, this will be used to validate the props each time the component is
created/updated. See [Props Validation](#props-validation) for more information. 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 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 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. change the initial object, a new object will be created instead.
+1 -1
View File
@@ -73,9 +73,9 @@ Let us now add the javascript to make it work, in `app.js`:
```javascript ```javascript
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
static template = "clickcounter";
constructor() { constructor() {
super(...arguments); super(...arguments);
this.template = "clickcounter";
this.state = { value: 0 }; this.state = { value: 0 };
} }
+1 -1
View File
@@ -135,7 +135,7 @@ It's API is quite simple:
having a reference to the actual QWeb instance. having a reference to the actual QWeb instance.
```js ```js
QWeb.registerTemplate('mytemplate', `<div>some template`); QWeb.registerTemplate("mytemplate", `<div>some template`);
``` ```
- **`registerComponent(name, Component)`**: static function to register an OWL Component - **`registerComponent(name, Component)`**: static function to register an OWL Component
+3
View File
@@ -19,6 +19,8 @@ owl
store store
Store Store
ConnectedComponent ConnectedComponent
tags
xml
utils utils
debounce debounce
escape escape
@@ -36,6 +38,7 @@ owl
- [QWeb](qweb.md) - [QWeb](qweb.md)
- [Router](router.md) - [Router](router.md)
- [Store](store.md) - [Store](store.md)
- [Tags](tags.md)
- [Utils](utils.md) - [Utils](utils.md)
- [Virtual DOM](vdom.md) - [Virtual DOM](vdom.md)
+47
View File
@@ -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 = `
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
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`
<div>
<span t-if="somecondition">text</span>
<button t-on-click="someMethod">Click</button>
</div>
`;
...
}
```
+3 -3
View File
@@ -419,7 +419,7 @@ QWeb.addDirective({
const clone = <Element>node.cloneNode(true); const clone = <Element>node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]"); const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++; const slotId = QWeb.nextSlotId++;
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`); ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
if (slotNodes.length) { if (slotNodes.length) {
for (let i = 0, length = slotNodes.length; i < length; i++) { for (let i = 0, length = slotNodes.length; i < length; i++) {
@@ -428,7 +428,7 @@ QWeb.addDirective({
const key = slotNode.getAttribute("t-set")!; const key = slotNode.getAttribute("t-set")!;
slotNode.removeAttribute("t-set"); slotNode.removeAttribute("t-set");
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx);
qweb.slots[`${slotId}_${key}`] = slotFn; QWeb.slots[`${slotId}_${key}`] = slotFn;
} }
} }
if (clone.childNodes.length) { if (clone.childNodes.length) {
@@ -437,7 +437,7 @@ QWeb.addDirective({
t.appendChild(child); t.appendChild(child);
} }
const slotFn = qweb._compile(`slot_default_template`, t, ctx); const slotFn = qweb._compile(`slot_default_template`, t, ctx);
qweb.slots[`${slotId}_default`] = slotFn; QWeb.slots[`${slotId}_default`] = slotFn;
} }
} }
+2 -1
View File
@@ -10,6 +10,7 @@ import { QWeb } from "./qweb/index";
import { ConnectedComponent } from "./store/connected_component"; import { ConnectedComponent } from "./store/connected_component";
import { Store } from "./store/store"; import { Store } from "./store/store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import * as _tags from "./tags";
import { Link } from "./router/Link"; import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent"; import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router"; import { Router } from "./router/Router";
@@ -20,7 +21,7 @@ export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link }; export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent }; export const store = { Store, ConnectedComponent };
export const utils = _utils; export const utils = _utils;
export const tags = _tags;
export const __info__ = {}; export const __info__ = {};
Object.defineProperty(__info__, "mode", { Object.defineProperty(__info__, "mode", {
+3 -1
View File
@@ -227,7 +227,9 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value }): boolean { atNodeEncounter({ ctx, value }): boolean {
const slotKey = ctx.generateID(); const slotKey = ctx.generateID();
ctx.rootContext.shouldDefineOwner = true; 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.addIf(`slot${slotKey}`);
ctx.addLine( ctx.addLine(
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));` `slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: extra.vars, parent: owner}));`
+4 -3
View File
@@ -143,15 +143,16 @@ export class QWeb extends EventBus {
static TEMPLATES: { [name: string]: Template } = {}; static TEMPLATES: { [name: string]: Template } = {};
static nextId: number = 1;
h = h; h = h;
// dev mode enables better error messages or more costly validations // dev mode enables better error messages or more costly validations
static dev: boolean = false; static dev: boolean = false;
// slots contains sub templates defined with t-set inside t-component nodes, and // slots contains sub templates defined with t-set inside t-component nodes, and
// are meant to be used by the t-slot directive. // are meant to be used by the t-slot directive.
slots = {}; static slots = {};
nextSlotId = 1; static nextSlotId = 1;
// recursiveTemplates contains sub templates called with t-call, but which // recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation, // ends up in recursive situations. This is very similar to the slot situation,
+9 -9
View File
@@ -1,18 +1,18 @@
import { Component } from "../component/component"; import { Component } from "../component/component";
import { xml } from "../tags";
import { Destination, RouterEnv } from "./Router"; import { Destination, RouterEnv } from "./Router";
export const LINK_TEMPLATE_NAME = "__owl__-router-link";
export const LINK_TEMPLATE = `
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>`;
type Props = Destination; type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> { export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
static template = LINK_TEMPLATE_NAME; static template = xml`
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>
`;
href: string = this.env.router.destToPath(this.props); href: string = this.env.router.destToPath(this.props);
async willUpdateProps(nextProps) { async willUpdateProps(nextProps) {
+5 -5
View File
@@ -1,15 +1,15 @@
import { Component } from "../component/component"; import { Component } from "../component/component";
import { xml } from "../tags";
export const ROUTE_COMPONENT_TEMPLATE_NAME = "__owl__-router-component"; export class RouteComponent extends Component<any, {}, {}> {
export const ROUTE_COMPONENT_TEMPLATE = ` static template = xml`
<t t-foreach="routes" t-as="route"> <t t-foreach="routes" t-as="route">
<t t-if="env.router.currentRouteName === route.name"> <t t-if="env.router.currentRouteName === route.name">
<t t-component="{{route.component}}" t-props="env.router.currentParams"/> <t t-component="{{route.component}}" t-props="env.router.currentParams"/>
</t> </t>
</t>`; </t>
`;
export class RouteComponent extends Component<any, {}, {}> {
static template = ROUTE_COMPONENT_TEMPLATE_NAME;
routes: any[] = []; routes: any[] = [];
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
-6
View File
@@ -1,7 +1,5 @@
import { Env } from "../component/component"; import { Env } from "../component/component";
import { QWeb } from "../qweb/index"; 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"; import { shallowEqual } from "../utils";
type NavigationGuard = (info: { type NavigationGuard = (info: {
@@ -84,10 +82,6 @@ export class Router {
this.routes[partialRoute.name] = partialRoute as Route; this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name); 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);
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
+28
View File
@@ -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`<div>some template</div>`;
* }
* ```
*/
export function xml(strings) {
const name = `__template__${QWeb.nextId++}`;
QWeb.registerTemplate(name, strings[0]);
return name;
}
@@ -1406,14 +1406,14 @@ exports[`t-slot directive can define and call slots 2`] = `
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
c1.push(vn2); c1.push(vn2);
const slot3 = this.slots[context.__owl__.slotId + '_' + 'header']; const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot3) { if (slot3) {
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner})); slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
} }
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
var vn4 = h('div', p4, c4); var vn4 = h('div', p4, c4);
c1.push(vn4); c1.push(vn4);
const slot5 = this.slots[context.__owl__.slotId + '_' + 'footer']; const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot5) { if (slot5) {
slot5.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c4, vars: extra.vars, parent: owner})); 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}}; let c2 = [], p2 = {key:2,attrs:{href: _1}};
var vn2 = h('a', p2, c2); var vn2 = h('a', p2, c2);
result = vn2; result = vn2;
const slot3 = this.slots[context.__owl__.slotId + '_' + 'default']; const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot3) { if (slot3) {
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner})); 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}}; let c2 = [], p2 = {key:2,attrs:{href: _1}};
var vn2 = h('a', p2, c2); var vn2 = h('a', p2, c2);
result = vn2; result = vn2;
const slot3 = this.slots[context.__owl__.slotId + '_' + 'default']; const slot3 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot3) { if (slot3) {
slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner})); slot3.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c2, vars: extra.vars, parent: owner}));
} }
+184 -211
View File
@@ -1,5 +1,6 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { EventBus } from "../../src/core/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { import {
makeDeferred, makeDeferred,
@@ -119,11 +120,11 @@ describe("basic widget properties", () => {
}); });
test("widget style and classname", async () => { test("widget style and classname", async () => {
env.qweb.addTemplate( class StyledWidget extends Widget {
"StyledWidget", static template = xml`
`<div style="font-weight:bold;" class="some-class">world</div>` <div style="font-weight:bold;" class="some-class">world</div>
); `;
class StyledWidget extends Widget {} }
const widget = new StyledWidget(env); const widget = new StyledWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`); expect(fixture.innerHTML).toBe(`<div style="font-weight:bold;" class="some-class">world</div>`);
@@ -164,19 +165,17 @@ describe("basic widget properties", () => {
test("reconciliation alg is not confused in some specific situation", async () => { 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 // in this test, we set t-key to 4 because it was in conflict with the
// template id corresponding to the first child. // template id corresponding to the first child.
env.qweb.addTemplates(` class Child extends Component<any, any, any> {
<templates> static template = xml`<span>child</span>`;
<div t-name="Parent"> }
class Parent extends Component<any, any, any> {
static template = xml`
<div>
<Child /> <Child />
<Child t-key="4"/> <Child t-key="4"/>
</div> </div>
<span t-name="Child">child</span> `;
</templates>
`);
class Child extends Component<any, any, any> {}
class Parent extends Component<any, any, any> {
static components = { Child }; static components = { Child };
} }
@@ -226,7 +225,6 @@ describe("lifecycle hooks", () => {
test("willStart hook is called on subwidget", async () => { test("willStart hook is called on subwidget", async () => {
let ok = false; let ok = false;
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
class ChildWidget extends Widget { class ChildWidget extends Widget {
async willStart() { async willStart() {
ok = true; ok = true;
@@ -234,6 +232,7 @@ describe("lifecycle hooks", () => {
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`<div><t t-component="child"/></div>`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -244,8 +243,6 @@ describe("lifecycle hooks", () => {
test("mounted hook is called on subcomponents, in proper order", async () => { test("mounted hook is called on subcomponents, in proper order", async () => {
const steps: any[] = []; const steps: any[] = [];
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
class ChildWidget extends Widget { class ChildWidget extends Widget {
mounted() { mounted() {
expect(document.body.contains(this.el)).toBe(true); expect(document.body.contains(this.el)).toBe(true);
@@ -254,7 +251,8 @@ describe("lifecycle hooks", () => {
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: ChildWidget }; static template = xml`<div><ChildWidget /></div>`
static components = { ChildWidget };
mounted() { mounted() {
steps.push("parent:mounted"); steps.push("parent:mounted");
} }
@@ -267,12 +265,6 @@ describe("lifecycle hooks", () => {
test("mounted hook is called on subsubcomponents, in proper order", async () => { test("mounted hook is called on subsubcomponents, in proper order", async () => {
const steps: any[] = []; const steps: any[] = [];
env.qweb.addTemplate(
"ParentWidget",
`<div><t t-if="state.flag"><t t-component="child"/></t></div>`
);
env.qweb.addTemplate("ChildWidget", `<div><t t-component="childchild"/></div>`);
class ChildChildWidget extends Widget { class ChildChildWidget extends Widget {
mounted() { mounted() {
steps.push("childchild:mounted"); steps.push("childchild:mounted");
@@ -283,6 +275,7 @@ describe("lifecycle hooks", () => {
} }
class ChildWidget extends Widget { class ChildWidget extends Widget {
static template = xml`<div><t t-component="childchild"/></div>`;
static components = { childchild: ChildChildWidget }; static components = { childchild: ChildChildWidget };
mounted() { mounted() {
steps.push("child:mounted"); steps.push("child:mounted");
@@ -293,6 +286,7 @@ describe("lifecycle hooks", () => {
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`<div><t t-if="state.flag"><t t-component="child"/></t></div>`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { flag: false }; state = { flag: false };
mounted() { mounted() {
@@ -435,12 +429,7 @@ describe("lifecycle hooks", () => {
test("components are unmounted and destroyed if no longer in DOM", async () => { test("components are unmounted and destroyed if no longer in DOM", async () => {
let steps: string[] = []; let steps: string[] = [];
env.qweb.addTemplate(
"ParentWidget",
`<div>
<t t-if="state.ok"><t t-component="child"/></t>
</div>`
);
class ChildWidget extends Widget { class ChildWidget extends Widget {
constructor(parent) { constructor(parent) {
super(parent); super(parent);
@@ -457,8 +446,13 @@ describe("lifecycle hooks", () => {
} }
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`
<div>
<t t-if="state.ok"><ChildWidget /></t>
</div>
`;
static components = { ChildWidget };
state = { ok: true }; state = { ok: true };
static components = { child: ChildWidget };
} }
const widget = new ParentWidget(env); 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 () => { test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
let childUnmounted = false; let childUnmounted = false;
env.qweb.addTemplate("ChildWidget", `<span><t t-esc="props.n"/></span>`);
class ChildWidget extends Widget { class ChildWidget extends Widget {
static template = xml`<span><t t-esc="props.n"/></span>`;
willUnmount() { willUnmount() {
childUnmounted = true; childUnmounted = true;
} }
@@ -481,16 +476,14 @@ describe("lifecycle hooks", () => {
} }
} }
env.qweb.addTemplate( class ParentWidget extends Widget {
"ParentWidget", static template = xml`
`
<div> <div>
<div t-if="state.flag"> <div t-if="state.flag">
<ChildWidget n="state.n"/> <ChildWidget n="state.n"/>
</div> </div>
</div>` </div>
); `;
class ParentWidget extends Widget {
static components = { ChildWidget }; static components = { ChildWidget };
state = { n: 0, flag: true }; state = { n: 0, flag: true };
increment() { increment() {
@@ -515,7 +508,7 @@ describe("lifecycle hooks", () => {
test("hooks are called in proper order in widget creation/destruction", async () => { test("hooks are called in proper order in widget creation/destruction", async () => {
let steps: string[] = []; let steps: string[] = [];
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child"/></div>`);
class ChildWidget extends Widget { class ChildWidget extends Widget {
constructor(parent) { constructor(parent) {
super(parent); super(parent);
@@ -532,6 +525,7 @@ describe("lifecycle hooks", () => {
} }
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`<div><t t-component="child"/></div>`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
constructor(parent) { constructor(parent) {
super(parent); super(parent);
@@ -614,13 +608,13 @@ describe("lifecycle hooks", () => {
test("patched hook is called after updateProps", async () => { test("patched hook is called after updateProps", async () => {
let n = 0; let n = 0;
env.qweb.addTemplate("Parent", '<div><Child a="state.a"/></div>');
class TestWidget extends Widget { class TestWidget extends Widget {
patched() { patched() {
n++; n++;
} }
} }
class Parent extends Widget { class Parent extends Widget {
static template = xml`<div><Child a="state.a"/></div>`;
state = { a: 1 }; state = { a: 1 };
static components = { Child: TestWidget }; static components = { Child: TestWidget };
} }
@@ -654,17 +648,18 @@ describe("lifecycle hooks", () => {
test("shouldUpdate hook prevent rerendering", async () => { test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false; let shouldUpdate = false;
env.qweb.addTemplate("Parent", `<div><Child val="state.val"/></div>`);
class TestWidget extends Widget { class TestWidget extends Widget {
static template = xml`<div><t t-esc="props.val"/></div>`;
shouldUpdate() { shouldUpdate() {
return shouldUpdate; return shouldUpdate;
} }
} }
class Parent extends Widget { class Parent extends Widget {
state = { val: 42 }; static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child: TestWidget }; static components = { Child: TestWidget };
state = { val: 42 };
} }
env.qweb.addTemplate("TestWidget", `<div><t t-esc="props.val"/></div>`);
const widget = new Parent(env); const widget = new Parent(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>42</div></div>"); expect(fixture.innerHTML).toBe("<div><div>42</div></div>");
@@ -680,15 +675,6 @@ describe("lifecycle hooks", () => {
test("sub widget (inside sub node): hooks are correctly called", async () => { test("sub widget (inside sub node): hooks are correctly called", async () => {
let created = false; let created = false;
let mounted = false; let mounted = false;
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-if="state.flag">
<div><t t-component="child"/></div>
</t>
</div>`
);
class ChildWidget extends Widget { class ChildWidget extends Widget {
constructor(parent, props) { constructor(parent, props) {
@@ -700,6 +686,13 @@ describe("lifecycle hooks", () => {
} }
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`
<div>
<t t-if="state.flag">
<div><t t-component="child"/></div>
</t>
</div>
`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { flag: false }; state = { flag: false };
} }
@@ -716,13 +709,7 @@ describe("lifecycle hooks", () => {
test("willPatch/patched hook", async () => { test("willPatch/patched hook", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" v="state.n"/>
</div>`
);
class ChildWidget extends Widget { class ChildWidget extends Widget {
willPatch() { willPatch() {
steps.push("child:willPatch"); steps.push("child:willPatch");
@@ -732,6 +719,11 @@ describe("lifecycle hooks", () => {
} }
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`
<div>
<t t-component="child" v="state.n"/>
</div>
`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { n: 1 }; state = { n: 1 };
willPatch() { willPatch() {
@@ -763,13 +755,6 @@ describe("lifecycle hooks", () => {
// we make sure here that willPatch/patched is only called if widget is in // we make sure here that willPatch/patched is only called if widget is in
// dom, mounted // dom, mounted
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<ChildWidget t-if="state.flag" v="state.n" t-keepalive="1"/>
</div>
</templates>
`);
class ChildWidget extends Widget { class ChildWidget extends Widget {
willPatch() { willPatch() {
@@ -786,6 +771,11 @@ describe("lifecycle hooks", () => {
} }
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`
<div>
<ChildWidget t-if="state.flag" v="state.n" t-keepalive="1"/>
</div>
`;
static components = { ChildWidget }; static components = { ChildWidget };
state = { n: 1, flag: true }; state = { n: 1, flag: true };
} }
@@ -793,7 +783,7 @@ describe("lifecycle hooks", () => {
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); 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"]); expect(steps).toEqual(["child:mounted"]);
widget.state.flag = false; widget.state.flag = false;
await nextTick(); await nextTick();
@@ -865,21 +855,20 @@ describe("destroy method", () => {
}); });
test("destroying a widget before being mounted", async () => { test("destroying a widget before being mounted", async () => {
env.qweb.addTemplates(` class GrandChild extends Component<any, any, any> {
<templates> static template = xml`
<div t-name="Parent" t-on-some-event="doStuff"> <span>
<Child /> <t t-esc="props.val.val"/>
</div> </span>
<span t-name="Child"> `;
}
class Child extends Component<any, any, any> {
static template = xml`
<span>
<GrandChild t-if="state.flag" val="something"/> <GrandChild t-if="state.flag" val="something"/>
<button t-on-click="doSomething">click</button> <button t-on-click="doSomething">click</button>
</span> </span>
<span t-name="GrandChild"> `;
<t t-esc="props.val.val"/>
</span>
</templates>`);
class GrandChild extends Component<any, any, any> {}
class Child extends Component<any, any, any> {
static components = { GrandChild }; static components = { GrandChild };
state = { val: 33, flag: false }; state = { val: 33, flag: false };
doSomething() { doSomething() {
@@ -892,6 +881,11 @@ describe("destroy method", () => {
} }
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any, any> {
static template = xml`
<div t-on-some-event="doStuff">
<Child />
</div>
`;
static components = { Child }; static components = { Child };
state = { p: 1 }; state = { p: 1 };
doStuff() { doStuff() {
@@ -985,14 +979,12 @@ describe("composition", () => {
test("t-refs are bound at proper timing", async () => { test("t-refs are bound at proper timing", async () => {
expect.assertions(2); expect.assertions(2);
env.qweb.addTemplate( class ParentWidget extends Widget {
"ParentWidget", static template = xml`
`
<div> <div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/> <t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
</div>` </div>
); `;
class ParentWidget extends Widget {
static components = { Widget }; static components = { Widget };
state = { list: <any>[] }; state = { list: <any>[] };
willPatch() { willPatch() {
@@ -3055,8 +3047,8 @@ describe("t-slot directive", () => {
); );
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.Dialog.fn.toString()).toMatchSnapshot();
expect(env.qweb.slots["1_header"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_header"].toString()).toMatchSnapshot();
expect(env.qweb.slots["1_footer"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
}); });
test("slots are rendered with proper context", async () => { test("slots are rendered with proper context", async () => {
@@ -3092,7 +3084,7 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>' '<div><span class="counter">1</span><span><button>do something</button></span></div>'
); );
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 () => { test("slots are rendered with proper context, part 2", async () => {
@@ -3130,7 +3122,7 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>' '<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
); );
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 () => { test("slots are rendered with proper context, part 3", async () => {
@@ -3169,7 +3161,7 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>' '<div><u><li><a href="/user/1">User Aaron</a></li><li><a href="/user/2">User Mathieu</a></li></u></div>'
); );
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 () => { test("slots are rendered with proper context, part 4", async () => {
@@ -3202,7 +3194,7 @@ describe("t-slot directive", () => {
app.state.user.name = "David"; app.state.user.name = "David";
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>'); expect(fixture.innerHTML).toBe('<div><a href="/user/1">User David</a></div>');
expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
}); });
test("refs are properly bound in slots", async () => { test("refs are properly bound in slots", async () => {
@@ -3238,7 +3230,7 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
'<div><span class="counter">1</span><span><button>do something</button></span></div>' '<div><span class="counter">1</span><span><button>do something</button></span></div>'
); );
expect(env.qweb.slots["1_footer"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_footer"].toString()).toMatchSnapshot();
}); });
test("content is the default slot", async () => { test("content is the default slot", async () => {
@@ -3260,7 +3252,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts rocks</span></div></div>"); expect(fixture.innerHTML).toBe("<div><div><span>sts rocks</span></div></div>");
expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
}); });
test("default slot work with text nodes", async () => { test("default slot work with text nodes", async () => {
@@ -3280,7 +3272,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>sts rocks</div></div>"); expect(fixture.innerHTML).toBe("<div><div>sts rocks</div></div>");
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 () => { test("multiple roots are allowed in a named slot", async () => {
@@ -3305,7 +3297,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>"); expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
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 () => { test("multiple roots are allowed in a default slot", async () => {
@@ -3328,7 +3320,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>"); expect(fixture.innerHTML).toBe("<div><div><span>sts</span><span>rocks</span></div></div>");
expect(env.qweb.slots["1_default"].toString()).toMatchSnapshot(); expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
}); });
test("missing slots are ignored", async () => { test("missing slots are ignored", async () => {
@@ -3599,17 +3591,16 @@ describe("t-model directive", () => {
}); });
test("on a select, initial state", async () => { test("on a select, initial state", async () => {
env.qweb.addTemplates(` class SomeComponent extends Widget {
<templates> static template = xml`
<div t-name="SomeComponent"> <div>
<select t-model="color"> <select t-model="color">
<option value="">Please select one</option> <option value="">Please select one</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
</select> </select>
</div> </div>
</templates>`); `;
class SomeComponent extends Widget {
state = { color: "red" }; state = { color: "red" };
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
@@ -3619,15 +3610,13 @@ describe("t-model directive", () => {
}); });
test("on a sub state key", async () => { test("on a sub state key", async () => {
env.qweb.addTemplates(` class SomeComponent extends Widget {
<templates> static template = xml`
<div t-name="SomeComponent"> <div>
<input t-model="something.text"/> <input t-model="something.text"/>
<span><t t-esc="state.something.text"/></span> <span><t t-esc="state.something.text"/></span>
</div> </div>
</templates>`); `;
class SomeComponent extends Widget {
state = { something: { text: "" } }; state = { something: { text: "" } };
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
@@ -3639,18 +3628,17 @@ describe("t-model directive", () => {
await editInput(input, "test"); await editInput(input, "test");
expect(comp.state.something.text).toBe("test"); expect(comp.state.something.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>"); expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
}); });
test(".lazy modifier", async () => { test(".lazy modifier", async () => {
env.qweb.addTemplates(` class SomeComponent extends Widget {
<templates> static template = xml`
<div t-name="SomeComponent"> <div>
<input t-model.lazy="text"/> <input t-model.lazy="text"/>
<span><t t-esc="state.text"/></span> <span><t t-esc="state.text"/></span>
</div> </div>
</templates>`); `;
class SomeComponent extends Widget {
state = { text: "" }; state = { text: "" };
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
@@ -3668,18 +3656,17 @@ describe("t-model directive", () => {
await nextTick(); await nextTick();
expect(comp.state.text).toBe("test"); expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>"); expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
}); });
test(".trim modifier", async () => { test(".trim modifier", async () => {
env.qweb.addTemplates(` class SomeComponent extends Widget {
<templates> static template = xml`
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<input t-model.trim="text"/> <input t-model.trim="text"/>
<span><t t-esc="state.text"/></span> <span><t t-esc="state.text"/></span>
</div> </div>
</templates>`); `;
class SomeComponent extends Widget {
state = { text: "" }; state = { text: "" };
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
@@ -3692,14 +3679,13 @@ describe("t-model directive", () => {
}); });
test(".number modifier", async () => { test(".number modifier", async () => {
env.qweb.addTemplates(` class SomeComponent extends Widget {
<templates> static template = xml`
<div t-name="SomeComponent"> <div>
<input t-model.number="number"/> <input t-model.number="number"/>
<span><t t-esc="state.number"/></span> <span><t t-esc="state.number"/></span>
</div> </div>
</templates>`); `;
class SomeComponent extends Widget {
state = { number: 0 }; state = { number: 0 };
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
@@ -3732,15 +3718,14 @@ describe("environment and plugins", () => {
test("plugin works as expected", async () => { test("plugin works as expected", async () => {
somePlugin(env); somePlugin(env);
env.qweb.addTemplates(` class App extends Widget {
<templates> static template=xml`
<div t-name="App"> <div>
<t t-if="env.someFlag">Red</t> <t t-if="env.someFlag">Red</t>
<t t-else="1">Blue</t> <t t-else="1">Blue</t>
</div> </div>
</templates> `;
`); }
class App extends Widget {}
const app = new App(env); const app = new App(env);
await app.mount(fixture); await app.mount(fixture);
@@ -4086,18 +4071,19 @@ describe("top level sub widgets", () => {
}); });
test("can select a sub widget ", async () => { test("can select a sub widget ", async () => {
env.qweb.addTemplates(` class Child extends Widget {
<templates> static template = xml`<span>CHILD 1</span>`;
<t t-name="Parent"> }
class OtherChild extends Widget {
static template = xml`<div>CHILD 2</div>`;
}
class Parent extends Widget {
static template = xml`
<t>
<t t-if="env.flag"><Child /></t> <t t-if="env.flag"><Child /></t>
<t t-if="!env.flag"><OtherChild /></t> <t t-if="!env.flag"><OtherChild /></t>
</t> </t>
<span t-name="Child">CHILD 1</span> `;
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
static components = { Child, OtherChild }; static components = { Child, OtherChild };
} }
(<any>env).flag = true; (<any>env).flag = true;
@@ -4110,22 +4096,23 @@ describe("top level sub widgets", () => {
await parent.mount(fixture); await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>"); expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
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 () => { test("can select a sub widget, part 2", async () => {
env.qweb.addTemplates(` class Child extends Widget {
<templates> static template = xml`<span>CHILD 1</span>`;
<t t-name="Parent"> }
class OtherChild extends Widget {
static template = xml`<div>CHILD 2</div>`;
}
class Parent extends Widget {
static template = xml`
<t>
<t t-if="state.flag"><Child /></t> <t t-if="state.flag"><Child /></t>
<t t-if="!state.flag"><OtherChild /></t> <t t-if="!state.flag"><OtherChild /></t>
</t> </t>
<span t-name="Child">CHILD 1</span> `;
<div t-name="OtherChild">CHILD 2</div>
</templates>`);
class Child extends Widget {}
class OtherChild extends Widget {}
class Parent extends Widget {
state = { flag: true }; state = { flag: true };
static components = { Child, OtherChild }; static components = { Child, OtherChild };
} }
@@ -4140,13 +4127,9 @@ describe("top level sub widgets", () => {
describe("unmounting and remounting", () => { describe("unmounting and remounting", () => {
test("widget can be unmounted and remounted", async () => { test("widget can be unmounted and remounted", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="MyWidget">Hey</div>
</templates>`);
const steps: string[] = []; const steps: string[] = [];
class MyWidget extends Widget { class MyWidget extends Widget {
static template = xml`<div>Hey</div>`;
async willStart() { async willStart() {
steps.push("willstart"); steps.push("willstart");
} }
@@ -4173,13 +4156,9 @@ describe("unmounting and remounting", () => {
}); });
test("widget can be mounted twice without ill effect", async () => { test("widget can be mounted twice without ill effect", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="MyWidget">Hey</div>
</templates>`);
const steps: string[] = []; const steps: string[] = [];
class MyWidget extends Widget { class MyWidget extends Widget {
static template = xml`<div>Hey</div>`;
async willStart() { async willStart() {
steps.push("willstart"); steps.push("willstart");
} }
@@ -4200,16 +4179,11 @@ describe("unmounting and remounting", () => {
test("state changes in willUnmount do not trigger rerender", async () => { test("state changes in willUnmount do not trigger rerender", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<Child t-if="state.flag" val="state.val"/>
</div>
<span t-name="Child"><t t-esc="props.val"/><t t-esc="state.n"/></span>
</templates>
`);
class Child extends Widget { class Child extends Widget {
static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`;
state = { n: 2 }; state = { n: 2 };
__render(a, b, c, d) { __render(a, b, c, d) {
steps.push("render"); steps.push("render");
@@ -4228,6 +4202,11 @@ describe("unmounting and remounting", () => {
} }
} }
class Parent extends Widget { class Parent extends Widget {
static template = xml`
<div>
<Child t-if="state.flag" val="state.val"/>
</div>
`;
static components = { Child }; static components = { Child };
state = { val: 1, flag: true }; state = { val: 1, flag: true };
} }
@@ -4243,13 +4222,10 @@ describe("unmounting and remounting", () => {
}); });
test("state changes in willUnmount will be applied on remount", async () => { test("state changes in willUnmount will be applied on remount", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="TestWidget"><t t-esc="state.val"/></div>
</templates>
`);
class TestWidget extends Widget { class TestWidget extends Widget {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = { val: 1 }; state = { val: 1 };
willUnmount() { willUnmount() {
this.state.val = 3; this.state.val = 3;
@@ -4271,15 +4247,14 @@ describe("unmounting and remounting", () => {
describe("dynamic root nodes", () => { describe("dynamic root nodes", () => {
test("template with t-if, part 1", async () => { test("template with t-if, part 1", async () => {
env.qweb.addTemplates(` class TestWidget extends Widget {
<templates> static template = xml`
<t t-name="TestWidget"> <t>
<t t-if="true"><span>hey</span></t> <t t-if="true"><span>hey</span></t>
<t t-if="false"><div>abc</div></t> <t t-if="false"><div>abc</div></t>
</t> </t>
</templates> `;
`); }
class TestWidget extends Widget {}
const widget = new TestWidget(env); const widget = new TestWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -4288,15 +4263,14 @@ describe("dynamic root nodes", () => {
}); });
test("template with t-if, part 2", async () => { test("template with t-if, part 2", async () => {
env.qweb.addTemplates(` class TestWidget extends Widget {
<templates> static template = xml`
<t t-name="TestWidget"> <t>
<t t-if="false"><span>hey</span></t> <t t-if="false"><span>hey</span></t>
<t t-if="true"><div>abc</div></t> <t t-if="true"><div>abc</div></t>
</t> </t>
</templates> `;
`); }
class TestWidget extends Widget {}
const widget = new TestWidget(env); const widget = new TestWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -4305,15 +4279,13 @@ describe("dynamic root nodes", () => {
}); });
test("switching between sub branches dynamically", async () => { test("switching between sub branches dynamically", async () => {
env.qweb.addTemplates(` class TestWidget extends Widget {
<templates> static template = xml`
<t t-name="TestWidget"> <t>
<t t-if="state.flag"><span>hey</span></t> <t t-if="state.flag"><span>hey</span></t>
<t t-if="!state.flag"><div>abc</div></t> <t t-if="!state.flag"><div>abc</div></t>
</t> </t>
</templates> `;
`);
class TestWidget extends Widget {
state = { flag: true }; state = { flag: true };
} }
@@ -4328,19 +4300,19 @@ describe("dynamic root nodes", () => {
}); });
test("switching between sub components dynamically", async () => { test("switching between sub components dynamically", async () => {
env.qweb.addTemplates(` class ChildA extends Widget {
<templates> static template = xml`<span>hey</span>`;
<t t-name="ChildA"><span>hey</span></t> }
<t t-name="ChildB"><div>abc</div></t> class ChildB extends Widget {
<t t-name="TestWidget"> static template = xml`<div>abc</div>`;
}
class TestWidget extends Widget {
static template = xml`
<t>
<t t-if="state.flag"><ChildA/></t> <t t-if="state.flag"><ChildA/></t>
<t t-if="!state.flag"><ChildB/></t> <t t-if="!state.flag"><ChildB/></t>
</t> </t>
</templates> `;
`);
class ChildA extends Widget {}
class ChildB extends Widget {}
class TestWidget extends Widget {
static components = { ChildA, ChildB }; static components = { ChildA, ChildB };
state = { flag: true }; state = { flag: true };
} }
@@ -4359,17 +4331,13 @@ describe("dynamic root nodes", () => {
describe("dynamic t-props", () => { describe("dynamic t-props", () => {
test("basic use", async () => { test("basic use", async () => {
expect.assertions(4); expect.assertions(4);
env.qweb.addTemplates(`
<templates> class Child extends Widget {
<span t-name="Child"> static template = xml`
<span>
<t t-esc="props.a + props.b"/> <t t-esc="props.a + props.b"/>
</span> </span>
<div t-name="Parent"> `;
<Child t-props="some.obj"/>
</div>
</templates>
`);
class Child extends Widget {
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
expect(props).toEqual({ a: 1, b: 2 }); expect(props).toEqual({ a: 1, b: 2 });
@@ -4377,6 +4345,11 @@ describe("dynamic t-props", () => {
} }
} }
class Parent extends Widget { class Parent extends Widget {
static template = xml`
<div>
<Child t-props="some.obj"/>
</div>
`;
static components = { Child }; static components = { Child };
some = { obj: { a: 1, b: 2 } }; some = { obj: { a: 1, b: 2 } };
@@ -4386,6 +4359,6 @@ describe("dynamic t-props", () => {
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>"); expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
}); });
}); });
+21
View File
@@ -5,6 +5,27 @@ import "../src/qweb/base_directives";
import "../src/qweb/extensions"; import "../src/qweb/extensions";
import "../src/component/directive"; 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<void> { export function nextMicroTick(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { Component } from "../../src/component/component"; 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 { RouterEnv } from "../../src/router/Router";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter"; import { TestRouter } from "./TestRouter";
@@ -50,7 +50,7 @@ describe("Link component", () => {
'<div><a href="/about" class="router-link-active">About</a></div>' '<div><a href="/about" class="router-link-active">About</a></div>'
); );
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 () => { test("do not redirect if right clicking", async () => {
+2 -2
View File
@@ -1,6 +1,6 @@
import { Component } from "../../src/component/component"; import { Component } from "../../src/component/component";
import { RouterEnv } from "../../src/router/Router"; 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 { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
import { TestRouter } from "./TestRouter"; import { TestRouter } from "./TestRouter";
@@ -52,7 +52,7 @@ describe("RouteComponent", () => {
await router.navigate({ to: "users" }); await router.navigate({ to: "users" });
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>Users</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Users</span></div>");
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 () => { test("can render parameterized route", async () => {
+2 -2
View File
@@ -12,11 +12,11 @@ exports[`Link component can render simple cases 1`] = `
var vn3 = h('a', p3, c3); var vn3 = h('a', p3, c3);
result = vn3; result = vn3;
if (!context['navigate']) { 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); extra.handlers['click' + 3] = extra.handlers['click' + 3] || context['navigate'].bind(owner);
p3.on['click'] = extra.handlers['click' + 3]; 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) { if (slot4) {
slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner})); slot4.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: c3, vars: extra.vars, parent: owner}));
} }