[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:
```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
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 };
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 };
}
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
+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.
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.
+1 -1
View File
@@ -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 };
}
+1 -1
View File
@@ -135,7 +135,7 @@ It's API is quite simple:
having a reference to the actual QWeb instance.
```js
QWeb.registerTemplate('mytemplate', `<div>some template`);
QWeb.registerTemplate("mytemplate", `<div>some template`);
```
- **`registerComponent(name, Component)`**: static function to register an OWL Component
+3
View File
@@ -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)
+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 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;
}
}
+2 -1
View File
@@ -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", {
+3 -1
View File
@@ -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}));`
+4 -3
View File
@@ -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,
+9 -9
View File
@@ -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 = `
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
t-on-click="navigate">
<t t-slot="default"/>
</a>`;
type Props = Destination;
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);
async willUpdateProps(nextProps) {
+5 -5
View File
@@ -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<any, {}, {}> {
static template = xml`
<t t-foreach="routes" t-as="route">
<t t-if="env.router.currentRouteName === route.name">
<t t-component="{{route.component}}" t-props="env.router.currentParams"/>
</t>
</t>`;
</t>
`;
export class RouteComponent extends Component<any, {}, {}> {
static template = ROUTE_COMPONENT_TEMPLATE_NAME;
routes: any[] = [];
constructor(parent, props) {
super(parent, props);
-6
View File
@@ -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);
}
//--------------------------------------------------------------------------
+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};
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}));
}
+217 -244
View File
@@ -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",
`<div style="font-weight:bold;" class="some-class">world</div>`
);
class StyledWidget extends Widget {}
class StyledWidget extends Widget {
static template = xml`
<div style="font-weight:bold;" class="some-class">world</div>
`;
}
const widget = new StyledWidget(env);
await widget.mount(fixture);
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 () => {
// 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(`
<templates>
<div t-name="Parent">
<Child />
<Child t-key="4"/>
</div>
<span t-name="Child">child</span>
</templates>
`);
class Child extends Component<any, any, any> {}
class Child extends Component<any, any, any> {
static template = xml`<span>child</span>`;
}
class Parent extends Component<any, any, any> {
static template = xml`
<div>
<Child />
<Child t-key="4"/>
</div>
`;
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", `<div><t t-component="child"/></div>`);
class ChildWidget extends Widget {
async willStart() {
ok = true;
@@ -234,6 +232,7 @@ describe("lifecycle hooks", () => {
}
class ParentWidget extends Widget {
static template = xml`<div><t t-component="child"/></div>`;
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", `<div><t t-component="child"/></div>`);
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`<div><ChildWidget /></div>`
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",
`<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 {
mounted() {
steps.push("childchild:mounted");
@@ -283,6 +275,7 @@ describe("lifecycle hooks", () => {
}
class ChildWidget extends Widget {
static template = xml`<div><t t-component="childchild"/></div>`;
static components = { childchild: ChildChildWidget };
mounted() {
steps.push("child:mounted");
@@ -293,6 +286,7 @@ describe("lifecycle hooks", () => {
}
class ParentWidget extends Widget {
static template = xml`<div><t t-if="state.flag"><t t-component="child"/></t></div>`;
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",
`<div>
<t t-if="state.ok"><t t-component="child"/></t>
</div>`
);
class ChildWidget extends Widget {
constructor(parent) {
super(parent);
@@ -457,8 +446,13 @@ describe("lifecycle hooks", () => {
}
}
class ParentWidget extends Widget {
static template = xml`
<div>
<t t-if="state.ok"><ChildWidget /></t>
</div>
`;
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", `<span><t t-esc="props.n"/></span>`);
class ChildWidget extends Widget {
static template = xml`<span><t t-esc="props.n"/></span>`;
willUnmount() {
childUnmounted = true;
}
@@ -481,16 +476,14 @@ describe("lifecycle hooks", () => {
}
}
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<div t-if="state.flag">
<ChildWidget n="state.n"/>
</div>
</div>`
);
class ParentWidget extends Widget {
static template = xml`
<div>
<div t-if="state.flag">
<ChildWidget n="state.n"/>
</div>
</div>
`;
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", `<div><t t-component="child"/></div>`);
class ChildWidget extends Widget {
constructor(parent) {
super(parent);
@@ -532,6 +525,7 @@ describe("lifecycle hooks", () => {
}
}
class ParentWidget extends Widget {
static template = xml`<div><t t-component="child"/></div>`;
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", '<div><Child a="state.a"/></div>');
class TestWidget extends Widget {
patched() {
n++;
}
}
class Parent extends Widget {
static template = xml`<div><Child a="state.a"/></div>`;
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", `<div><Child val="state.val"/></div>`);
class TestWidget extends Widget {
static template = xml`<div><t t-esc="props.val"/></div>`;
shouldUpdate() {
return shouldUpdate;
}
}
class Parent extends Widget {
state = { val: 42 };
static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child: TestWidget };
state = { val: 42 };
}
env.qweb.addTemplate("TestWidget", `<div><t t-esc="props.val"/></div>`);
const widget = new Parent(env);
await widget.mount(fixture);
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 () => {
let created = 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 {
constructor(parent, props) {
@@ -700,6 +686,13 @@ describe("lifecycle hooks", () => {
}
}
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 };
state = { flag: false };
}
@@ -716,13 +709,7 @@ describe("lifecycle hooks", () => {
test("willPatch/patched hook", async () => {
const steps: string[] = [];
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" v="state.n"/>
</div>`
);
class ChildWidget extends Widget {
willPatch() {
steps.push("child:willPatch");
@@ -732,6 +719,11 @@ describe("lifecycle hooks", () => {
}
}
class ParentWidget extends Widget {
static template = xml`
<div>
<t t-component="child" v="state.n"/>
</div>
`;
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(`
<templates>
<div t-name="ParentWidget">
<ChildWidget t-if="state.flag" v="state.n" t-keepalive="1"/>
</div>
</templates>
`);
class ChildWidget extends Widget {
willPatch() {
@@ -786,6 +771,11 @@ describe("lifecycle hooks", () => {
}
}
class ParentWidget extends Widget {
static template = xml`
<div>
<ChildWidget t-if="state.flag" v="state.n" t-keepalive="1"/>
</div>
`;
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(`
<templates>
<div t-name="Parent" t-on-some-event="doStuff">
<Child />
</div>
<span t-name="Child">
<GrandChild t-if="state.flag" val="something"/>
<button t-on-click="doSomething">click</button>
</span>
<span t-name="GrandChild">
<t t-esc="props.val.val"/>
</span>
</templates>`);
class GrandChild extends Component<any, any, any> {}
class GrandChild extends Component<any, any, any> {
static template = xml`
<span>
<t t-esc="props.val.val"/>
</span>
`;
}
class Child extends Component<any, any, any> {
static template = xml`
<span>
<GrandChild t-if="state.flag" val="something"/>
<button t-on-click="doSomething">click</button>
</span>
`;
static components = { GrandChild };
state = { val: 33, flag: false };
doSomething() {
@@ -892,6 +881,11 @@ describe("destroy method", () => {
}
}
class Parent extends Component<any, any, any> {
static template = xml`
<div t-on-some-event="doStuff">
<Child />
</div>
`;
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`
<div>
<t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-component="Widget"/>
</div>`
);
class ParentWidget extends Widget {
</div>
`;
static components = { Widget };
state = { list: <any>[] };
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(
'<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 () => {
@@ -3130,7 +3122,7 @@ describe("t-slot directive", () => {
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>'
);
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(
'<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 () => {
@@ -3202,7 +3194,7 @@ describe("t-slot directive", () => {
app.state.user.name = "David";
await nextTick();
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 () => {
@@ -3238,7 +3230,7 @@ describe("t-slot directive", () => {
expect(fixture.innerHTML).toBe(
'<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 () => {
@@ -3260,7 +3252,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture);
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 () => {
@@ -3280,7 +3272,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture);
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 () => {
@@ -3305,7 +3297,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture);
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 () => {
@@ -3328,7 +3320,7 @@ describe("t-slot directive", () => {
await parent.mount(fixture);
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 () => {
@@ -3599,17 +3591,16 @@ describe("t-model directive", () => {
});
test("on a select, initial state", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<select t-model="color">
<option value="">Please select one</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
</templates>`);
class SomeComponent extends Widget {
static template = xml`
<div>
<select t-model="color">
<option value="">Please select one</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
`;
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(`
<templates>
<div t-name="SomeComponent">
<input t-model="something.text"/>
<span><t t-esc="state.something.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
static template = xml`
<div>
<input t-model="something.text"/>
<span><t t-esc="state.something.text"/></span>
</div>
`;
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("<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 () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
class SomeComponent extends Widget {
static template = xml`
<div>
<input t-model.lazy="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
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("<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 () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model.trim="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
static template = xml`
<div t-name="SomeComponent">
<input t-model.trim="text"/>
<span><t t-esc="state.text"/></span>
</div>
`;
state = { text: "" };
}
const comp = new SomeComponent(env);
@@ -3692,14 +3679,13 @@ describe("t-model directive", () => {
});
test(".number modifier", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model.number="number"/>
<span><t t-esc="state.number"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
static template = xml`
<div>
<input t-model.number="number"/>
<span><t t-esc="state.number"/></span>
</div>
`;
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(`
<templates>
<div t-name="App">
class App extends Widget {
static template=xml`
<div>
<t t-if="env.someFlag">Red</t>
<t t-else="1">Blue</t>
</div>
</templates>
`);
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(`
<templates>
<t t-name="Parent">
<t t-if="env.flag"><Child /></t>
<t t-if="!env.flag"><OtherChild /></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 Child extends Widget {
static template = xml`<span>CHILD 1</span>`;
}
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"><OtherChild /></t>
</t>
`;
static components = { Child, OtherChild };
}
(<any>env).flag = true;
@@ -4110,22 +4096,23 @@ describe("top level sub widgets", () => {
await parent.mount(fixture);
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 () => {
env.qweb.addTemplates(`
<templates>
<t t-name="Parent">
<t t-if="state.flag"><Child /></t>
<t t-if="!state.flag"><OtherChild /></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 Child extends Widget {
static template = xml`<span>CHILD 1</span>`;
}
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"><OtherChild /></t>
</t>
`;
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(`
<templates>
<div t-name="MyWidget">Hey</div>
</templates>`);
const steps: string[] = [];
class MyWidget extends Widget {
static template = xml`<div>Hey</div>`;
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(`
<templates>
<div t-name="MyWidget">Hey</div>
</templates>`);
const steps: string[] = [];
class MyWidget extends Widget {
static template = xml`<div>Hey</div>`;
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(`
<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 {
static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`;
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`
<div>
<Child t-if="state.flag" val="state.val"/>
</div>
`;
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(`
<templates>
<div t-name="TestWidget"><t t-esc="state.val"/></div>
</templates>
`);
class TestWidget extends Widget {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
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(`
<templates>
<t t-name="TestWidget">
<t t-if="true"><span>hey</span></t>
<t t-if="false"><div>abc</div></t>
</t>
</templates>
`);
class TestWidget extends Widget {}
class TestWidget extends Widget {
static template = xml`
<t>
<t t-if="true"><span>hey</span></t>
<t t-if="false"><div>abc</div></t>
</t>
`;
}
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(`
<templates>
<t t-name="TestWidget">
<t t-if="false"><span>hey</span></t>
<t t-if="true"><div>abc</div></t>
</t>
</templates>
`);
class TestWidget extends Widget {}
class TestWidget extends Widget {
static template = xml`
<t>
<t t-if="false"><span>hey</span></t>
<t t-if="true"><div>abc</div></t>
</t>
`;
}
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(`
<templates>
<t t-name="TestWidget">
<t t-if="state.flag"><span>hey</span></t>
<t t-if="!state.flag"><div>abc</div></t>
</t>
</templates>
`);
class TestWidget extends Widget {
static template = xml`
<t>
<t t-if="state.flag"><span>hey</span></t>
<t t-if="!state.flag"><div>abc</div></t>
</t>
`;
state = { flag: true };
}
@@ -4328,19 +4300,19 @@ describe("dynamic root nodes", () => {
});
test("switching between sub components dynamically", async () => {
env.qweb.addTemplates(`
<templates>
<t t-name="ChildA"><span>hey</span></t>
<t t-name="ChildB"><div>abc</div></t>
<t t-name="TestWidget">
<t t-if="state.flag"><ChildA/></t>
<t t-if="!state.flag"><ChildB/></t>
</t>
</templates>
`);
class ChildA extends Widget {}
class ChildB extends Widget {}
class ChildA extends Widget {
static template = xml`<span>hey</span>`;
}
class ChildB extends Widget {
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"><ChildB/></t>
</t>
`;
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(`
<templates>
<span t-name="Child">
<t t-esc="props.a + props.b"/>
</span>
<div t-name="Parent">
<Child t-props="some.obj"/>
</div>
</templates>
`);
class Child extends Widget {
static template = xml`
<span>
<t t-esc="props.a + props.b"/>
</span>
`;
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`
<div>
<Child t-props="some.obj"/>
</div>
`;
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("<div><span>3</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
});
+22 -1
View File
@@ -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<void> {
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;
}
+2 -2
View File
@@ -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", () => {
'<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 () => {
+2 -2
View File
@@ -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("<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 () => {
+2 -2
View File
@@ -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}));
}