From 27629cedfa2f5ecefd6f0ec5f7a9e86d3f0d8505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sat, 3 Jul 2021 16:02:57 +0200 Subject: [PATCH] [IMP] qweb: allow multiclasses in t-att-class object form the low level method htmlelement.classList.add does not accept multiple classes in one string, which is why, in owl, the expression `
` did not work as one might expect. It is however very convenient in real life templates, so this commit improve owl by adding support for this feature. closes #813 --- doc/reference/qweb_templating_language.md | 19 ++++++++++++ src/component/directive.ts | 2 +- src/qweb/qweb.ts | 23 +++++++++++---- .../__snapshots__/class_style.test.ts.snap | 6 ++-- tests/qweb/__snapshots__/qweb.test.ts.snap | 14 ++++----- .../__snapshots__/qweb_t_att.test.ts.snap | 29 +++++++++++++++++++ tests/qweb/qweb_t_att.test.ts | 27 +++++++++++++++++ tests/router/__snapshots__/link.test.ts.snap | 2 +- 8 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 tests/qweb/__snapshots__/qweb_t_att.test.ts.snap create mode 100644 tests/qweb/qweb_t_att.test.ts diff --git a/doc/reference/qweb_templating_language.md b/doc/reference/qweb_templating_language.md index 5c6d17e8..50691c0a 100644 --- a/doc/reference/qweb_templating_language.md +++ b/doc/reference/qweb_templating_language.md @@ -13,6 +13,7 @@ - [Setting Variables](#setting-variables) - [Conditionals](#conditionals) - [Dynamic Attributes](#dynamic-attributes) + - [Dynamic Class Attribute](#dynamic-class-attribute) - [Dynamic Tag Names](#dynamic-tag-names) - [Loops](#loops) - [Rendering Sub Templates](#rendering-sub-templates) @@ -326,6 +327,24 @@ values) or a pair `[key, value]`. For example:
``` +### Dynamic class attribute + +For convenience, Owl supports a special case for the `t-att-class` case: one can +use an object with keys describing the classes, and values boolean value denoting +if the class is or is not present: + +```xml +
+ +
+``` + +Note that it can be combined with normal class attribute: + +```xml +
+``` + ### Dynamic tag names When writing generic components or templates, the specific concrete tag for an diff --git a/src/component/directive.ts b/src/component/directive.ts index b148a2a5..cff63928 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -291,7 +291,7 @@ QWeb.addDirective({ if (tattClass) { let tattExpr = ctx.formatExpression(tattClass); if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") { - tattExpr = `utils.toObj(${tattExpr})`; + tattExpr = `utils.toClassObj(${tattExpr})`; } if (classAttr) { ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`); diff --git a/src/qweb/qweb.ts b/src/qweb/qweb.ts index e4381b94..2edc3b0c 100644 --- a/src/qweb/qweb.ts +++ b/src/qweb/qweb.ts @@ -80,7 +80,7 @@ const NODE_HOOKS_PARAMS = { }; interface Utils { - toObj(expr: any): Object; + toClassObj(expr: any): Object; shallowEqual(p1: Object, p2: Object): boolean; [key: string]: any; } @@ -111,20 +111,31 @@ function vDomToString(vdom: VNode[]): string { const UTILS: Utils = { zero: Symbol("zero"), - toObj(expr) { + toClassObj(expr) { + const result = {}; if (typeof expr === "string") { + // we transform here a list of classes into an object: + // 'hey you' becomes {hey: true, you: true} expr = expr.trim(); if (!expr) { return {}; } let words = expr.split(/\s+/); - let result = {}; for (let i = 0; i < words.length; i++) { result[words[i]] = true; } return result; } - return expr; + // this is already an object, but we may need to split keys: + // {'a': true, 'b c': true} should become {a: true, b: true, c: true} + for (let key in expr) { + const value = expr[key]; + const words = key.split(/\s+/); + for (let word of words) { + result[word] = value; + } + } + return result; }, combine(context, scope) { const clone = Object.create(context); @@ -767,7 +778,7 @@ export class QWeb extends EventBus { name = '"' + name + '"'; } attrs.push(`${name}: _${attID}`); - handleProperties(name, "_" + attID); + handleProperties(name, `_${attID}`); } } @@ -779,7 +790,7 @@ export class QWeb extends EventBus { if (attName === "class") { ctx.rootContext.shouldDefineUtils = true; - formattedValue = `utils.toObj(${formattedValue})`; + formattedValue = `utils.toClassObj(${formattedValue})`; if (classObj) { ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); } else { diff --git a/tests/component/__snapshots__/class_style.test.ts.snap b/tests/component/__snapshots__/class_style.test.ts.snap index 32797b72..e66083a6 100644 --- a/tests/component/__snapshots__/class_style.test.ts.snap +++ b/tests/component/__snapshots__/class_style.test.ts.snap @@ -90,7 +90,7 @@ exports[`class and style attributes with t-component t-att-class is properly add let scope = Object.create(context); let h = this.h; let _7 = {'c':true}; - Object.assign(_7, utils.toObj({d:scope['state'].d})) + Object.assign(_7, utils.toClassObj({d:scope['state'].d})) let c8 = [], p8 = {key:8,class:_7}; let vn8 = h('span', p8, c8); return vn8; @@ -112,7 +112,7 @@ exports[`class and style attributes with t-component t-att-class is properly add // Component 'Child' const ref4 = \`child\`; let _5 = {'a':true}; - Object.assign(_5, utils.toObj(scope['state'].b?'b':'')) + Object.assign(_5, utils.toClassObj(scope['state'].b?'b':'')) let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let props2 = {}; if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { @@ -148,7 +148,7 @@ exports[`class and style attributes with t-component t-att-class is properly add let scope = Object.create(context); let h = this.h; let _7 = {'c':true}; - Object.assign(_7, utils.toObj(scope['state'].d?'d':'')) + Object.assign(_7, utils.toClassObj(scope['state'].d?'d':'')) let c8 = [], p8 = {key:8,class:_7}; let vn8 = h('span', p8, c8); return vn8; diff --git a/tests/qweb/__snapshots__/qweb.test.ts.snap b/tests/qweb/__snapshots__/qweb.test.ts.snap index 26dc0ced..e061f6d8 100644 --- a/tests/qweb/__snapshots__/qweb.test.ts.snap +++ b/tests/qweb/__snapshots__/qweb.test.ts.snap @@ -7,7 +7,7 @@ exports[`attributes class and t-att-class should combine together 1`] = ` let utils = this.constructor.utils; let scope = Object.create(context); let h = this.h; - let _1 = utils.toObj(scope['value']); + let _1 = utils.toClassObj(scope['value']); Object.assign(_1, {'hello':true}) let c3 = [], p3 = {key:3,class:_1}; let vn3 = h('div', p3, c3); @@ -74,7 +74,7 @@ exports[`attributes dynamic class attribute 1`] = ` let utils = this.constructor.utils; let scope = Object.create(context); let h = this.h; - let _1 = utils.toObj(scope['c']); + let _1 = utils.toClassObj(scope['c']); let c2 = [], p2 = {key:2,class:_1}; let vn2 = h('div', p2, c2); return vn2; @@ -88,7 +88,7 @@ exports[`attributes dynamic empty class attribute 1`] = ` let utils = this.constructor.utils; let scope = Object.create(context); let h = this.h; - let _1 = utils.toObj(scope['c']); + let _1 = utils.toClassObj(scope['c']); let c2 = [], p2 = {key:2,class:_1}; let vn2 = h('div', p2, c2); return vn2; @@ -195,7 +195,7 @@ exports[`attributes from object variables set previously 1`] = ` let c1 = [], p1 = {key:1}; let vn1 = h('div', p1, c1); scope.o = {a:'b'}; - let _2 = utils.toObj(scope.o.a); + let _2 = utils.toClassObj(scope.o.a); let c3 = [], p3 = {key:3,class:_2}; let vn3 = h('span', p3, c3); c1.push(vn3); @@ -213,7 +213,7 @@ exports[`attributes from variables set previously 1`] = ` let c1 = [], p1 = {key:1}; let vn1 = h('div', p1, c1); scope.abc = 'def'; - let _2 = utils.toObj(scope.abc); + let _2 = utils.toClassObj(scope.abc); let c3 = [], p3 = {key:3,class:_2}; let vn3 = h('span', p3, c3); c1.push(vn3); @@ -288,7 +288,7 @@ exports[`attributes t-att-class and class should combine together 1`] = ` let scope = Object.create(context); let h = this.h; let _2 = {'hello':true}; - Object.assign(_2, utils.toObj(scope['value'])) + Object.assign(_2, utils.toClassObj(scope['value'])) let c3 = [], p3 = {key:3,class:_2}; let vn3 = h('div', p3, c3); return vn3; @@ -303,7 +303,7 @@ exports[`attributes t-att-class with object 1`] = ` let scope = Object.create(context); let h = this.h; let _2 = {'static':true}; - Object.assign(_2, utils.toObj({a:scope['b'],c:scope['d'],e:scope['f']})) + Object.assign(_2, utils.toClassObj({a:scope['b'],c:scope['d'],e:scope['f']})) let c3 = [], p3 = {key:3,class:_2}; let vn3 = h('div', p3, c3); return vn3; diff --git a/tests/qweb/__snapshots__/qweb_t_att.test.ts.snap b/tests/qweb/__snapshots__/qweb_t_att.test.ts.snap new file mode 100644 index 00000000..2da7fc7a --- /dev/null +++ b/tests/qweb/__snapshots__/qweb_t_att.test.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`qweb t-att t-att-class with multiple classes 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"test\\" + let utils = this.constructor.utils; + let scope = Object.create(context); + let h = this.h; + let _1 = utils.toClassObj({'a b c':scope['value']}); + let c2 = [], p2 = {key:2,class:_1}; + let vn2 = h('div', p2, c2); + return vn2; +}" +`; + +exports[`qweb t-att t-att-class with multiple classes 2`] = ` +"function anonymous(context, extra +) { + // Template name: \\"test\\" + let utils = this.constructor.utils; + let scope = Object.create(context); + let h = this.h; + let _3 = utils.toClassObj({['a b c']:scope['value']}); + let c4 = [], p4 = {key:4,class:_3}; + let vn4 = h('div', p4, c4); + return vn4; +}" +`; diff --git a/tests/qweb/qweb_t_att.test.ts b/tests/qweb/qweb_t_att.test.ts new file mode 100644 index 00000000..4d10c3af --- /dev/null +++ b/tests/qweb/qweb_t_att.test.ts @@ -0,0 +1,27 @@ +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-att", () => { + test("t-att-class with multiple classes", () => { + expect(render(`
`, { value: true })).toBe( + '
' + ); + expect(render(`
`, { value: true })).toBe( + '
' + ); + }); +}); diff --git a/tests/router/__snapshots__/link.test.ts.snap b/tests/router/__snapshots__/link.test.ts.snap index fb09c0b7..aeba54b5 100644 --- a/tests/router/__snapshots__/link.test.ts.snap +++ b/tests/router/__snapshots__/link.test.ts.snap @@ -7,7 +7,7 @@ exports[`Link component can render simple cases 1`] = ` let utils = this.constructor.utils; let scope = Object.create(context); let h = this.h; - let _5 = utils.toObj({'router-link-active':scope['isActive']}); + let _5 = utils.toClassObj({'router-link-active':scope['isActive']}); let _6 = scope['href']; let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}}; let vn7 = h('a', p7, c7);