From b9591cfecdc372f0845a4f68315b9011d89e1cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 6 May 2019 10:36:40 +0200 Subject: [PATCH] [IMP] component: add support for dynamic css on t-widget closes #80 --- doc/component.md | 11 +++ src/qweb.ts | 39 +++++++-- tests/__snapshots__/component.test.ts.snap | 98 ++++++++++++++++++++++ tests/component.test.ts | 62 +++++++++++++- 4 files changed, 200 insertions(+), 10 deletions(-) diff --git a/doc/component.md b/doc/component.md index caf29ce2..124b25a9 100644 --- a/doc/component.md +++ b/doc/component.md @@ -104,6 +104,17 @@ root widget element. ``` +Warning: there is a small caveat with dynamic class attributes: since Owl needs +to be able to add/remove proper classes whenever necessary, it needs to be aware +of the possible classes. Otherwise, it will not be able to make the difference +between a valid css class added by the component, or some custom code, and a +class that need to be removed. This is why we only support the explicit syntax +with a class object: + +```js + + +``` ## Reference diff --git a/src/qweb.ts b/src/qweb.ts index 4b5b3c71..ecb55215 100644 --- a/src/qweb.ts +++ b/src/qweb.ts @@ -1211,20 +1211,41 @@ const widgetDirective: Directive = { if (ref) { finalizeWidgetCode += `;delete context.refs[${refKey}]`; } - ctx.addIf(`isNew${widgetID}`); let createHook = ""; let classAttr = node.getAttribute("class"); + let tattClass = node.getAttribute("t-att-class"); let styleAttr = node.getAttribute("style"); - if (classAttr || styleAttr) { - const classCode = classAttr - ? classAttr + let tattStyle = node.getAttribute("t-att-style"); + if (tattStyle) { + const attVar = `_${ctx.generateID()}`; + ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`); + tattStyle = attVar; + } + let updateClassCode = ""; + if (classAttr || tattClass || styleAttr || tattStyle) { + let classCode = ""; + if (classAttr) { + classCode = + classAttr .split(" ") .map(c => `vn.elm.classList.add('${c}')`) - .join(";") + ";" - : ""; - const styleCode = styleAttr ? `vn.elm.style = '${styleAttr}'` : ""; + .join(";") + ";"; + } + if (tattClass) { + const attVar = `_${ctx.generateID()}`; + ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`); + classCode = `for (let k in ${attVar}) { + if (${attVar}[k]) { + vn.elm.classList.add(k); + } + }`; + updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`; + } + const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); + const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : ""; createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`; } + ctx.addIf(`isNew${widgetID}`); ctx.addLine( `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}});c${ ctx.parentNode @@ -1232,7 +1253,9 @@ const widgetDirective: Directive = { ); ctx.addElse(); ctx.addLine( - `def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){${finalizeWidgetCode}}, destroy() {${finalizeWidgetCode}}}}c${ + `def${defID} = def${defID}.then(()=>{if (w${widgetID}.__owl__.isDestroyed) {return};${ + tattStyle ? `w${widgetID}.el.style=${tattStyle};` : "" + }${updateClassCode}let vnode;if (!w${widgetID}.__owl__.vnode){vnode=w${widgetID}.__owl__.pvnode} else { vnode=h(w${widgetID}.__owl__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){${finalizeWidgetCode}}, destroy() {${finalizeWidgetCode}}}}c${ ctx.parentNode }[_${dummyID}_index]=vnode;});` ); diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap index 03ea5d46..c9a8877f 100644 --- a/tests/__snapshots__/component.test.ts.snap +++ b/tests/__snapshots__/component.test.ts.snap @@ -1,5 +1,103 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = ` +"function anonymous(context,extra +) { + let owner = context; + var h = this.utils.h; + var c1 = [], p1 = {key:1}; + var vn1 = h('div', p1, c1); + //WIDGET + let _2_index = c1.length; + c1.push(null); + let def3; + let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; + let props4 = {}; + let isNew4 = !w4; + if (w4 && w4.__owl__.renderPromise) { + if (w4.__owl__.isStarted) { + def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + } else { + isNew4 = true + if (props4 === w4.__owl__.renderProps) { + def3 = w4.__owl__.renderPromise; + } else { + w4.destroy(); + w4 = false + } + } + } + if (!def3) { + if (w4) { + def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + } else { + w4 = new context.widgets['child'](owner, props4); + context.__owl__.cmap[4] = w4.__owl__.id; + def3 = w4._prepare(); + } + } + const _5 = context['state'].style; + if (isNew4) { + def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;}); + } else { + def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;}); + } + extra.promises.push(def3); + return vn1; +}" +`; + +exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = ` +"function anonymous(context,extra +) { + let owner = context; + var h = this.utils.h; + var c1 = [], p1 = {key:1}; + var vn1 = h('div', p1, c1); + //WIDGET + let _2_index = c1.length; + c1.push(null); + let def3; + let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; + let props4 = {}; + let isNew4 = !w4; + if (w4 && w4.__owl__.renderPromise) { + if (w4.__owl__.isStarted) { + def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + } else { + isNew4 = true + if (props4 === w4.__owl__.renderProps) { + def3 = w4.__owl__.renderPromise; + } else { + w4.destroy(); + w4 = false + } + } + } + if (!def3) { + if (w4) { + def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); + } else { + w4 = new context.widgets['child'](owner, props4); + context.__owl__.cmap[4] = w4.__owl__.id; + def3 = w4._prepare(); + } + } + const _5 = {a: context['state'].a,b: context['state'].b}; + if (isNew4) { + def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){for (let k in _5) { + if (_5[k]) { + vn.elm.classList.add(k); + } + }}};let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__owl__.pvnode = pvnode;}); + } else { + def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let vnode;if (!w4.__owl__.vnode){vnode=w4.__owl__.pvnode} else { vnode=h(w4.__owl__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}, destroy() {w4.destroy()}}}c1[_2_index]=vnode;}); + } + extra.promises.push(def3); + return vn1; +}" +`; + exports[`composition sub widgets with some state rendered in a loop 1`] = ` "function anonymous(context,extra ) { diff --git a/tests/component.test.ts b/tests/component.test.ts index 0af094cc..82901c1d 100644 --- a/tests/component.test.ts +++ b/tests/component.test.ts @@ -1234,13 +1234,39 @@ describe("class and style attributes with t-widget", () => { widgets = { child: Child }; } class Child extends Widget { - inlineTemplate = `
` + inlineTemplate = `
`; } const widget = new ParentWidget(env); await widget.mount(fixture); expect(fixture.innerHTML).toBe(`
`); }); + test("t-att-class is properly added/removed on widget root el", async () => { + env.qweb.addTemplate( + "parent", + `
+ +
` + ); + class ParentWidget extends Widget { + template = "parent"; + widgets = { child: Child }; + state = { a: true, b: false }; + } + class Child extends Widget { + inlineTemplate = `
`; + } + const widget = new ParentWidget(env); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe(`
`); + expect(env.qweb.templates.parent.fn.toString()).toMatchSnapshot(); + + widget.state.a = false; + widget.state.b = true; + await nextTick(); + expect(fixture.innerHTML).toBe(`
`); + }); + test("style is properly added on widget root el", async () => { class ParentWidget extends Widget { inlineTemplate = ` @@ -1251,7 +1277,39 @@ describe("class and style attributes with t-widget", () => { } const widget = new ParentWidget(env); await widget.mount(fixture); - expect(fixture.innerHTML).toBe(`
`); + expect(fixture.innerHTML).toBe( + `
` + ); + }); + + test("dynamic t-att-style is properly added and updated on widget root el", async () => { + env.qweb.addTemplate( + "parent", + ` +
+ +
` + ); + class ParentWidget extends Widget { + template = "parent"; + widgets = { child: Widget }; + state = { style: "font-size: 20px" }; + } + const widget = new ParentWidget(env); + await widget.mount(fixture); + + expect(env.qweb.templates.parent.fn.toString()).toMatchSnapshot(); + + expect(fixture.innerHTML).toBe( + `
` + ); + + widget.state.style = "font-size: 30px"; + await nextTick(); + + expect(fixture.innerHTML).toBe( + `
` + ); }); });