Compare commits

...

7 Commits

Author SHA1 Message Date
Géry Debongnie d12115554c [REL] v1.3.1
# v1.3.1

Fixes

- components: fix issue with t-call in nested t-slot (parent component was incorrect)
2021-06-10 10:36:33 +02:00
Géry Debongnie 1a6b26c8c6 [REF] cleanup: prettier 2021-06-10 10:23:48 +02:00
Lucas Perais (lpe) d67d295eaa [FIX] qweb, t-call: t-call nested in t-slot
Have a t-call within a t-set-slot of a component.
The called template has a t-component directive.

It should be like:

```xml
<t t-name="Zero">
  <Slotted>
    <t t-call="someTemplate" />
  </Slotted>
</t>

<t t-name="someTemplate">
  <SomeComponent />
</t>
```

Before this commit, the parent of SomeComponent was the Zero component

After this commit, the parent of SomeComponent is the Slotted Component as it should be

closes #862
2021-06-10 10:00:22 +02:00
Géry Debongnie 9cbcf20b33 [REL] v1.3.0
# v1.3.0

## Improvements

- qweb: add support for  directive
2021-06-04 14:19:07 +02:00
Géry Debongnie 3a461e1dd1 [IMP] qweb: add support for t-tag directive
Very useful in some cases, when one needs to define a generic component.

closes #721
2021-06-04 14:11:31 +02:00
Géry Debongnie e8387810e6 [REL] v1.2.6
# v1.2.6

## Fixes

- router: add support for arbitrary param placements in paths
2021-05-19 12:28:13 +02:00
Samuel Degueldre acac9d1741 [FIX] router: add support for more arbitrary param placements in paths
Before this commit, params in paths had to be be between slashes and
comprise the entirety of the contents between those slashes (eg:
`/books/{{id}}/{{name}}`)

This is unnecessarily restrictive. This commit removes this restriction,
which allows for paths such as:
- `/books/{{id}}-{{name}}`
- `#books&id={{id}}&name={{name}}`

among others

closes #858
2021-05-19 12:25:58 +02:00
16 changed files with 303 additions and 65 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.2.5](https://github.com/odoo/owl/releases/tag/v1.2.5) - [owl-1.3.1](https://github.com/odoo/owl/releases/tag/v1.3.1)
## License ## License
+18
View File
@@ -13,6 +13,7 @@
- [Setting Variables](#setting-variables) - [Setting Variables](#setting-variables)
- [Conditionals](#conditionals) - [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes) - [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops) - [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates) - [Rendering Sub Templates](#rendering-sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates) - [Dynamic Sub Templates](#dynamic-sub-templates)
@@ -76,6 +77,7 @@ needs. Here is a list of all Owl specific directives:
| `t-transition` | [Defining an animation](animations.md#css-transitions) | | `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-slot` | [Rendering a slot](slots.md) | | `t-slot` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) | | `t-model` | [Form input bindings](component.md#form-input-bindings) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
## Reference ## Reference
@@ -324,6 +326,22 @@ values) or a pair `[key, value]`. For example:
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> --> <div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
``` ```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
HTML element is not known yet. In those situations, the `t-tag` directive is
useful. It simply evaluates dynamically an expression to use as a tag name. The
template:
```xml
<t t-tag="tag">
<span>content</span>
</t>
```
will be rendered as `<div><span>content</span></div>` if the `tag` context key
is set to `div`.
### Loops ### Loops
QWeb has an iteration directive `t-foreach` which take an expression returning the QWeb has an iteration directive `t-foreach` which take an expression returning the
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.2.5", "version": "1.3.1",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.2.5 - Current version: 1.3.1
- Status: stable - Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
+3 -1
View File
@@ -281,7 +281,9 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component // Step 4: add the appropriate function call to current component
// ------------------------------------------------ // ------------------------------------------------
const parentComponent = `utils.getComponent(context)`; const parentComponent = ctx.rootContext.shouldDefineParent
? `parent`
: `utils.getComponent(context)`;
const key = ctx.generateTemplateKey(); const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result"; const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`; const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
+11 -2
View File
@@ -203,6 +203,7 @@ export class QWeb extends EventBus {
att: 1, att: 1,
attf: 1, attf: 1,
translation: 1, translation: 1,
tag: 1,
}; };
static DIRECTIVES: Directive[] = []; static DIRECTIVES: Directive[] = [];
@@ -435,6 +436,7 @@ export class QWeb extends EventBus {
ctx.variables = Object.create(null); ctx.variables = Object.create(null);
ctx.parentNode = ctx.generateID(); ctx.parentNode = ctx.generateID();
ctx.allowMultipleRoots = true; ctx.allowMultipleRoots = true;
ctx.shouldDefineParent = true;
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
@@ -614,7 +616,7 @@ export class QWeb extends EventBus {
} }
} }
if (node.nodeName !== "t") { if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
let nodeID = this._compileGenericNode(node, ctx, withHandlers); let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID); ctx = ctx.withParent(nodeID);
let nodeHooks = {}; let nodeHooks = {};
@@ -841,7 +843,14 @@ export class QWeb extends EventBus {
ctx.addLine(`}`); ctx.addLine(`}`);
ctx.closeIf(); ctx.closeIf();
} }
ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); let nodeName = `'${node.nodeName}'`;
if ((<Element>node).hasAttribute("t-tag")) {
const tagExpr = (<Element>node).getAttribute("t-tag");
(<Element>node).removeAttribute("t-tag");
nodeName = `tag${ctx.generateID()}`;
ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
}
ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
if (ctx.parentNode) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) { } else if (ctx.loopNumber || ctx.hasKey0) {
+36 -31
View File
@@ -11,6 +11,7 @@ type NavigationGuard = (info: {
export interface Route { export interface Route {
name: string; name: string;
path: string; path: string;
extractionRegExp: RegExp;
component?: any; component?: any;
redirect?: Destination; redirect?: Destination;
params: string[]; params: string[];
@@ -54,6 +55,7 @@ export interface EnvWithRouter extends Env {
} }
const paramRegexp = /\{\{(.*?)\}\}/; const paramRegexp = /\{\{(.*?)\}\}/;
const globalParamRegexp = new RegExp(paramRegexp.source, "g");
export class Router { export class Router {
currentRoute: Route | null = null; currentRoute: Route | null = null;
@@ -87,6 +89,7 @@ export class Router {
this.validateDestination(partialRoute.redirect); this.validateDestination(partialRoute.redirect);
} }
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : []; partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
partialRoute.extractionRegExp = makeExtractionRegExp(partialRoute.path);
this.routes[partialRoute.name] = partialRoute as Route; this.routes[partialRoute.name] = partialRoute as Route;
this.routeIds.push(partialRoute.name); this.routeIds.push(partialRoute.name);
} }
@@ -170,19 +173,14 @@ export class Router {
} }
private routeToPath(route: Route, params: RouteParams): string { private routeToPath(route: Route, params: RouteParams): string {
const path = route.path;
const parts = path.split("/");
const l = parts.length;
for (let i = 0; i < l; i++) {
const part = parts[i];
const match = part.match(paramRegexp);
if (match) {
const key = match[1].split(".")[0];
parts[i] = <string>params[key];
}
}
const prefix = this.mode === "hash" ? "#" : ""; const prefix = this.mode === "hash" ? "#" : "";
return prefix + parts.join("/"); return (
prefix +
route.path.replace(globalParamRegexp, (match, param) => {
const [key] = param.split(".");
return <string>params[key];
})
);
} }
private currentPath(): string { private currentPath(): string {
@@ -244,40 +242,47 @@ export class Router {
if (path.startsWith("#")) { if (path.startsWith("#")) {
path = path.slice(1); path = path.slice(1);
} }
const descrParts = route.path.split("/"); const paramsMatch = path.match(route.extractionRegExp);
const targetParts = path.split("/"); if (!paramsMatch) {
const l = descrParts.length;
if (l !== targetParts.length) {
return false; return false;
} }
const result = {}; const result = {};
for (let i = 0; i < l; i++) { route.params.forEach((param, index) => {
const descr = descrParts[i]; const [key, suffix] = param.split(".");
let target: string | number = targetParts[i]; const paramValue = paramsMatch[index + 1];
const match = descr.match(paramRegexp); if (suffix === "number") {
if (match) { return (result[key] = parseInt(paramValue, 10));
const [key, suffix] = match[1].split(".");
if (suffix === "number") {
target = parseInt(target, 10);
}
result[key] = target;
} else if (descr !== target) {
return false;
} }
} return (result[key] = paramValue);
});
return result; return result;
} }
} }
function findParams(str: string): string[] { function findParams(str: string): string[] {
const globalParamRegexp = /\{\{(.*?)\}\}/g;
const result: string[] = []; const result: string[] = [];
let m; let m;
do { do {
m = globalParamRegexp.exec(str); m = globalParamRegexp.exec(str);
if (m) { if (m) {
result.push(m[1].split(".")[0]); result.push(m[1]);
} }
} while (m); } while (m);
return result; return result;
} }
function escapeRegExp(str: string) {
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function makeExtractionRegExp(path: string) {
// replace param strings with capture groups so that we can build a regex to match over the path
const extractionString = path
.split(paramRegexp)
.map((part, index) => {
return index % 2 ? "(.*)" : escapeRegExp(part);
})
.join("");
// Example: /home/{{param1}}/{{param2}} => ^\/home\/(.*)\/(.*)$
return new RegExp(`^${extractionString}$`);
}
@@ -1573,6 +1573,7 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
) { ) {
// Template name: \\"sub\\" // Template name: \\"sub\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
let key0 = extra.key || \\"\\"; let key0 = extra.key || \\"\\";
@@ -1591,6 +1592,7 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
) { ) {
// Template name: \\"sub\\" // Template name: \\"sub\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -68,6 +68,7 @@ exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -81,6 +82,7 @@ exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c6 = extra.parentNode; let c6 = extra.parentNode;
let c7 = [], p7 = {key:7}; let c7 = [], p7 = {key:7};
@@ -158,6 +160,7 @@ exports[`t-slot directive can define and call slots using old t-set keyword 3`]
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -171,6 +174,7 @@ exports[`t-slot directive can define and call slots using old t-set keyword 4`]
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c6 = extra.parentNode; let c6 = extra.parentNode;
let c7 = [], p7 = {key:7}; let c7 = [], p7 = {key:7};
@@ -184,6 +188,7 @@ exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -214,6 +219,7 @@ exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
c4.push({text: \`sts rocks\`}); c4.push({text: \`sts rocks\`});
@@ -243,6 +249,7 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -260,6 +267,7 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_content_template\\" // Template name: \\"slot_content_template\\"
let parent = extra.parent;
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
@@ -295,6 +303,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
context.__owl__.refs = context.__owl__.refs || {}; context.__owl__.refs = context.__owl__.refs || {};
let h = this.h; let h = this.h;
let c8 = extra.parentNode; let c8 = extra.parentNode;
@@ -321,6 +330,7 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h; let h = this.h;
let c8 = extra.parentNode; let c8 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}}; let c9 = [], p9 = {key:9,on:{}};
@@ -418,6 +428,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c10 = extra.parentNode; let c10 = extra.parentNode;
@@ -516,6 +527,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c10 = extra.parentNode; let c10 = extra.parentNode;
@@ -570,6 +582,7 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c4 = extra.parentNode; let c4 = extra.parentNode;
+33
View File
@@ -1118,4 +1118,37 @@ describe("t-slot directive", () => {
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot(); expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
}); });
test("t-slot within dynamic t-call", async () => {
let child;
class Child extends Component {
static template = xml`<div class="child"/>`;
constructor(...args) {
super(...args);
child = this;
}
}
class Slotted extends Component {
static template = xml`<div class="slotted"><t t-slot="default" /></div>`;
}
class UsingTcallInSlotted extends Component {
tcallTemplate = xml`<div class="slot"><Child/></div>`;
static template = xml`
<div>
<Slotted>
<t t-call="{{ tcallTemplate }}"/>
</Slotted>
</div>`;
static components = { Slotted, Child };
}
await mount(UsingTcallInSlotted, { target: fixture });
expect(child.__owl__.parent).toBeInstanceOf(Slotted);
expect(fixture.innerHTML).toBe(
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
);
});
}); });
+8 -4
View File
@@ -1514,6 +1514,7 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
) { ) {
// Template name: \\"recursive\\" // Template name: \\"recursive\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c3 = extra.parentNode; let c3 = extra.parentNode;
@@ -1530,7 +1531,7 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
scope = Object.create(scope); scope = Object.create(scope);
scope.__access_mode__ = 'ro'; scope.__access_mode__ = 'ro';
let k7 = \`__7__\${key0}__\`; let k7 = \`__7__\${key0}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c4, parent: utils.getComponent(context), key: k7})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c4, parent: parent, key: k7}));
scope = _origScope6; scope = _origScope6;
} }
}" }"
@@ -1566,6 +1567,7 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
) { ) {
// Template name: \\"nodeTemplate\\" // Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1607,7 +1609,7 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k11 = \`__11__\${key0}__\${key1}__\`; let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11}));
} }
scope = _origScope10; scope = _origScope10;
} }
@@ -1645,6 +1647,7 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
) { ) {
// Template name: \\"nodeTemplate\\" // Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1686,7 +1689,7 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k11 = \`__11__\${key0}__\${key1}__\`; let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11}));
} }
scope = _origScope10; scope = _origScope10;
} }
@@ -1725,6 +1728,7 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
) { ) {
// Template name: \\"nodeTemplate\\" // Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c2 = extra.parentNode; let c2 = extra.parentNode;
@@ -1771,7 +1775,7 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
scope[utils.zero] = c__0; scope[utils.zero] = c__0;
} }
let k11 = \`__11__\${key0}__\${key1}__\`; let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11})); this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11}));
} }
scope = _origScope10; scope = _origScope10;
} }
@@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-tag simple usecases 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c1 = [], p1 = {key:1};
let tag2 = 'div';
let vn1 = h(tag2, p1, c1);
result = vn1;
return result;
}"
`;
exports[`qweb t-tag simple usecases 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c3 = [], p3 = {key:3};
let tag4 = scope['tag'];
let vn3 = h(tag4, p3, c3);
result = vn3;
c3.push({text: \`text\`});
return result;
}"
`;
exports[`qweb t-tag with multiple attributes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _2 = {'blueberry':true};
let _3 = 'raspberry';
let c4 = [], p4 = {key:4,attrs:{taste: _3},class:_2};
let tag5 = scope['tag'];
let vn4 = h(tag5, p4, c4);
result = vn4;
c4.push({text: \`gooseberry\`});
return result;
}"
`;
exports[`qweb t-tag with multiple child nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let c1 = [], p1 = {key:1};
let tag2 = scope['tag'];
let vn1 = h(tag2, p1, c1);
result = vn1;
c1.push({text: \` pear \`});
let c3 = [], p3 = {key:3};
let vn3 = h('span', p3, c3);
c1.push(vn3);
c3.push({text: \`apple\`});
c1.push({text: \` strawberry \`});
return result;
}"
`;
+42
View File
@@ -0,0 +1,42 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
function render(template, context = {}) {
const qweb = new QWeb();
qweb.addTemplate("test", template);
return renderToString(qweb, "test", context);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("qweb t-tag", () => {
test("simple usecases", () => {
expect(render(`<t t-tag="'div'"></t>`)).toBe("<div></div>");
expect(render(`<t t-tag="tag">text</t>`, { tag: "span" })).toBe("<span>text</span>");
});
test("with multiple child nodes", () => {
const template = `
<t t-tag="tag">
pear
<span>apple</span>
strawberry
</t>`;
expect(render(template, { tag: "div" })).toBe(
"<div> pear <span>apple</span> strawberry </div>"
);
});
test("with multiple attributes", () => {
const template = `
<t t-tag="tag" class="blueberry" taste="raspberry">gooseberry</t>`;
const expected = `<div taste=\"raspberry\" class=\"blueberry\">gooseberry</div>`;
expect(render(template, { tag: "div" })).toBe(expected);
});
});
+24
View File
@@ -103,4 +103,28 @@ describe("RouteComponent", () => {
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Book 1984|124</span></div>");
}); });
test("can render parameterized route where params are not separated by slashes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App">
<RouteComponent />
</div>
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="props.val"/></span>
</templates>
`);
class Book extends Component {}
class App extends Component {
static components = { RouteComponent };
}
const routes = [
{ name: "book", path: "/#title={{title}}&val={{val.number}}", component: Book },
];
router = new TestRouter(env, routes, { mode: "hash" });
await router.navigate({ to: "book", params: { title: "1984", val: "123" } });
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>Book 1984|123</span></div>");
});
}); });
+27 -23
View File
@@ -1,6 +1,6 @@
import { Destination, RouterEnv, Route } from "../../src/router/router"; import { Destination, RouterEnv, Route } from "../../src/router/router";
import { makeTestEnv, nextTick } from "../helpers"; import { makeTestEnv, nextTick } from "../helpers";
import { TestRouter } from "./test_router"; import { TestRouter, getRouteParams } from "./test_router";
let env: RouterEnv; let env: RouterEnv;
let router: TestRouter | null = null; let router: TestRouter | null = null;
@@ -107,60 +107,64 @@ describe("destToPath", () => {
describe("getRouteParams", () => { describe("getRouteParams", () => {
test("properly match simple routes", () => { test("properly match simple routes", () => {
router = new TestRouter(env, []);
// simple route // simple route
expect(router["getRouteParams"]({ path: "/home" } as Route, "/home")).toEqual({}); expect(getRouteParams({ path: "/home" }, "/home")).toEqual({});
// no match // no match
expect(router["getRouteParams"]({ path: "/home" } as Route, "/otherpath")).toEqual(false); expect(getRouteParams({ path: "/home" }, "/otherpath")).toEqual(false);
// fallback route // fallback route
expect(router["getRouteParams"]({ path: "*" } as Route, "somepath")).toEqual({}); expect(getRouteParams({ path: "*" }, "somepath")).toEqual({});
}); });
test("properly match simple routes, mode hash", () => { test("properly match simple routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" });
// simple route // simple route
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/home")).toEqual({}); expect(getRouteParams({ path: "/home" }, "#/home")).toEqual({});
// no match // no match
expect(router["getRouteParams"]({ path: "/home" } as Route, "#/otherpath")).toEqual(false); expect(getRouteParams({ path: "/home" }, "#/otherpath")).toEqual(false);
// fallback route // fallback route
expect(router["getRouteParams"]({ path: "*" } as Route, "#/somepath")).toEqual({}); expect(getRouteParams({ path: "*" }, "#/somepath")).toEqual({});
}); });
test("match some parameterized routes", () => { test("match some parameterized routes", () => {
router = new TestRouter(env, []); expect(getRouteParams({ path: "/invoices/{{id}}" }, "/invoices/3")).toEqual({
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "/invoices/3")).toEqual({
id: "3", id: "3",
}); });
}); });
test("match some parameterized routes, mode hash", () => { test("match some parameterized routes, mode hash", () => {
router = new TestRouter(env, [], { mode: "hash" }); expect(getRouteParams({ path: "/invoices/{{id}}" }, "#/invoices/3")).toEqual({
expect(router["getRouteParams"]({ path: "/invoices/{{id}}" } as Route, "#/invoices/3")).toEqual( id: "3",
{ });
id: "3",
}
);
}); });
test("can convert to number if needed", () => { test("can convert to number if needed", () => {
router = new TestRouter(env, []); expect(getRouteParams({ path: "/invoices/{{id.number}}" }, "/invoices/3")).toEqual({
expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "/invoices/3")
).toEqual({
id: 3, id: 3,
}); });
}); });
test("can convert to number if needed, mode: hash", () => { test("can convert to number if needed, mode: hash", () => {
router = new TestRouter(env, [], { mode: "hash" }); expect(getRouteParams({ path: "/invoices/{{id.number}}" }, "#/invoices/3")).toEqual({
id: 3,
});
});
test("can extract params not separated by slashes", () => {
expect(getRouteParams({ path: "/books/{{id.number}}-{{name}}" }, "/books/3-1984")).toEqual({
id: 3,
name: "1984",
});
});
test("can extract params not separated by slashes, mode: hash", () => {
expect( expect(
router["getRouteParams"]({ path: "/invoices/{{id.number}}" } as Route, "#/invoices/3") getRouteParams({ path: "books&id={{id.number}}&name={{name}}" }, "#books&id=3&name=1984")
).toEqual({ ).toEqual({
id: 3, id: 3,
name: "1984",
}); });
}); });
}); });
+12 -1
View File
@@ -1,4 +1,5 @@
import { Router } from "../../src/router/router"; import { Router, Route, RouterEnv } from "../../src/router/router";
import { makeTestEnv } from "../helpers";
import { QWeb } from "../../src/qweb/index"; import { QWeb } from "../../src/qweb/index";
export class TestRouter extends Router { export class TestRouter extends Router {
@@ -13,3 +14,13 @@ export class TestRouter extends Router {
} }
} }
} }
export function getRouteParams(route: Partial<Route>, path: string) {
const env = <RouterEnv>makeTestEnv();
const router = new TestRouter(env, [route]);
const {
routeIds: [routeId],
routes,
} = router;
return router["getRouteParams"](routes[routeId], path);
}