[IMP] qweb: t-ref: dyn values with string interpolation

Closes #100
This commit is contained in:
Aaron Bohy
2019-05-28 13:17:38 +02:00
committed by Géry Debongnie
parent bced9f3d04
commit 26f14180e2
10 changed files with 71 additions and 41 deletions
+20 -2
View File
@@ -294,14 +294,14 @@ There is another way to format a string attribute: the `t-attf-` directive. With
it, you get string interpolation: it, you get string interpolation:
```xml ```xml
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/> <div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> --> <!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
``` ```
For historical reason, there is an alternate form of string interpolation: For historical reason, there is an alternate form of string interpolation:
```xml ```xml
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/> <div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> --> <!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
``` ```
@@ -480,6 +480,24 @@ some method on a sub widget.
Note: if used on a component, the reference will be set in the `refs` Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`. variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the `t-attf-` directive). For example, if we have `id` set to 44 in the
rendering context,
```xml
<div t-ref="widget_#{id}"/>
```
```js
this.refs.widget_44;
```
Similarly to `t-attf-`, there is an alternate form of string interpolation:
```xml
<div t-ref="widget_{{id}}"/>
```
### `t-key` directive ### `t-key` directive
Even though Owl tries to be as declarative as possible, some DOM state is still Even though Owl tries to be as declarative as possible, some DOM state is still
+1 -1
View File
@@ -551,7 +551,7 @@ const TODO_APP_STORE_XML = `<templates>
</label> </label>
<button class="destroy" t-on-click="removeTodo"></button> <button class="destroy" t-on-click="removeTodo"></button>
</div> </div>
<input class="edit" t-ref="'input'" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-mounted="focusInput" t-on-blur="handleBlur"/> <input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-mounted="focusInput" t-on-blur="handleBlur"/>
</li> </li>
</templates>`; </templates>`;
+3 -3
View File
@@ -2,12 +2,12 @@
<div t-name="TabbedEditor" class="tabbed-editor"> <div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown"> <div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab"> <t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="tab" t-if="props[tab]" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)"> <a t-ref="{{tab}}" t-if="props[tab]" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-esc="tab"/> <t t-esc="tab"/>
</a> </a>
</t> </t>
</div> </div>
<div class="code-editor" t-ref="'editor'"></div> <div class="code-editor" t-ref="editor"></div>
</div> </div>
<div t-name="App" class="playground"> <div t-name="App" class="playground">
@@ -48,7 +48,7 @@
<div t-if="state.error" class="error"> <div t-if="state.error" class="error">
<t t-esc="state.error"/> <t t-esc="state.error"/>
</div> </div>
<div class="content" t-ref="'content'"/> <div class="content" t-ref="content"/>
</div> </div>
</div> </div>
</templates> </templates>
+27 -10
View File
@@ -483,9 +483,6 @@ export class QWeb {
props.push(`${key}: _${val}`); props.push(`${key}: _${val}`);
} }
} }
function formatter(expr) {
return "${" + ctx.formatExpression(expr) + "}";
}
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name; let name = attributes[i].name;
@@ -544,17 +541,13 @@ export class QWeb {
// attribute contains 'non letters' => we want to quote it // attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"'; attName = '"' + attName + '"';
} }
const formattedExpr = value! const formattedExpr = ctx.interpolate(value);
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
const attID = ctx.generateID(); const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName); let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) { if (staticVal) {
ctx.addLine( ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
);
} else { } else {
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`); ctx.addLine(`var _${attID} = ${formattedExpr};`);
} }
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
} }
@@ -758,4 +751,28 @@ export class Context {
const result = r.slice(0, -1); const result = r.slice(0, -1);
return result; return result;
} }
/**
* Perform string interpolation on the given string. Note that if the whole
* string is an expression, it simply returns it (formatted).
* For instance:
* 'Hello {{x}}!' -> `Hello ${x}`
* '{{x}}' -> x
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
if (matches && matches[0].length === s.length) {
return this.formatExpression(s.slice(2, -2));
}
matches = s.match(/\#\{.*?\}/g);
if (matches && matches[0].length === s.length) {
return this.formatExpression(s.slice(2, -1));
}
let formatter = expr => "${" + this.formatExpression(expr) + "}";
let r = s
.replace(/\{\{.*?\}\}/g, s => formatter(s.slice(2, -2)))
.replace(/\#\{.*?\}/g, s => formatter(s.slice(2, -1)));
return "`" + r + "`";
}
} }
+2 -2
View File
@@ -82,7 +82,7 @@ QWeb.addDirective({
priority: 95, priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) { atNodeCreation({ ctx, value, addNodeHook }) {
const refKey = `ref${ctx.generateID()}`; const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(value)}`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`); addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
} }
}); });
@@ -391,7 +391,7 @@ QWeb.addDirective({
let refKey: string = ""; let refKey: string = "";
if (ref) { if (ref) {
refKey = `ref${ctx.generateID()}`; refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${widgetID};`; refExpr = `context.refs[${refKey}] = w${widgetID};`;
} }
let transitionsInsertCode = ""; let transitionsInsertCode = "";
+5 -5
View File
@@ -59,7 +59,7 @@ exports[`attributes format expression 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = \`\${context['value'] + 37}\`; var _1 = context['value'] + 37;
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -70,7 +70,7 @@ exports[`attributes format expression, other format 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = \`\${context['value'] + 37}\`; var _1 = context['value'] + 37;
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -1494,7 +1494,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2); var vn2 = h('span', p2, c2);
c1.push(vn2); c1.push(vn2);
const ref3 = 'myspan' + 3 const ref3 = \`myspan\${context['id']}\`;
p2.hook = { p2.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref3] = n.elm; context.refs[ref3] = n.elm;
@@ -1513,7 +1513,7 @@ exports[`t-ref can get a ref on a node 1`] = `
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2); var vn2 = h('span', p2, c2);
c1.push(vn2); c1.push(vn2);
const ref3 = 'myspan' const ref3 = \`myspan\`;
p2.hook = { p2.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref3] = n.elm; context.refs[ref3] = n.elm;
@@ -1546,7 +1546,7 @@ exports[`t-ref refs in a loop 1`] = `
let c5 = [], p5 = {key:context['item']}; let c5 = [], p5 = {key:context['item']};
var vn5 = h('div', p5, c5); var vn5 = h('div', p5, c5);
c1.push(vn5); c1.push(vn5);
const ref6 = context['item'] const ref6 = context['item'];
p5.hook = { p5.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref6] = n.elm; context.refs[ref6] = n.elm;
+1 -1
View File
@@ -145,7 +145,7 @@ describe("animations", () => {
env.qweb.addTemplate( env.qweb.addTemplate(
"TestWidget", "TestWidget",
`<div><span t-ref="'span'" t-transition="chimay">blue</span></div>` `<div><span t-ref="span" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { hide: false }; state = { hide: false };
+6 -6
View File
@@ -896,7 +896,7 @@ describe("composition", () => {
test("t-refs on widget are widgets", async () => { test("t-refs on widget are widgets", async () => {
env.qweb.addTemplate( env.qweb.addTemplate(
"WidgetC", "WidgetC",
`<div>Hello<t t-ref="'mywidgetb'" t-widget="b"/></div>` `<div>Hello<t t-ref="mywidgetb" t-widget="b"/></div>`
); );
class WidgetC extends Widget { class WidgetC extends Widget {
widgets = { b: WidgetB }; widgets = { b: WidgetB };
@@ -912,7 +912,7 @@ describe("composition", () => {
"ParentWidget", "ParentWidget",
` `
<div> <div>
<t t-foreach="state.list" t-as="elem" t-ref="'child'" t-key="elem" t-widget="Widget"/> <t t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem" t-widget="Widget"/>
</div>` </div>`
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
@@ -938,8 +938,8 @@ describe("composition", () => {
"ParentWidget", "ParentWidget",
` `
<div> <div>
<t t-if="state.child1" t-ref="'child1'" t-widget="Widget"/> <t t-if="state.child1" t-ref="child1" t-widget="Widget"/>
<t t-if="state.child2" t-ref="'child2'" t-widget="Widget"/> <t t-if="state.child2" t-ref="child2" t-widget="Widget"/>
</div>` </div>`
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
@@ -1004,7 +1004,7 @@ describe("composition", () => {
"ParentWidget", "ParentWidget",
`<div> `<div>
<t t-foreach="state.items" t-as="item"> <t t-foreach="state.items" t-as="item">
<t t-widget="Child" t-ref="item" t-key="item"/> <t t-widget="Child" t-ref="{{item}}" t-key="item"/>
</t> </t>
</div>` </div>`
); );
@@ -2277,7 +2277,7 @@ describe("t-mounted directive", () => {
test("combined with a t-ref", async () => { test("combined with a t-ref", async () => {
env.qweb.addTemplate( env.qweb.addTemplate(
"TestWidget", "TestWidget",
`<div><input t-ref="'input'" t-mounted="f"/></div>` `<div><input t-ref="input" t-mounted="f"/></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
f() {} f() {}
+5 -10
View File
@@ -1,10 +1,5 @@
import { QWeb } from "../src/qweb_core"; import { QWeb } from "../src/qweb_core";
import { import { normalize, renderToDOM, renderToString, trim } from "./helpers";
normalize,
renderToDOM,
renderToString,
trim
} from "./helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -1056,16 +1051,16 @@ describe("t-on", () => {
describe("t-ref", () => { describe("t-ref", () => {
test("can get a ref on a node", () => { test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="'myspan'"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs }); renderToDOM(qweb, "test", { refs });
expect(refs.myspan.tagName).toBe("SPAN"); expect(refs.myspan.tagName).toBe("SPAN");
}); });
test("can get a dynamic ref on a node", () => { test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="'myspan' + 3"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs }); renderToDOM(qweb, "test", { refs, id: 3 });
expect(refs.myspan3.tagName).toBe("SPAN"); expect(refs.myspan3.tagName).toBe("SPAN");
}); });
@@ -1075,7 +1070,7 @@ describe("t-ref", () => {
` `
<div> <div>
<t t-foreach="items" t-as="item"> <t t-foreach="items" t-as="item">
<div t-ref="item" t-key="item"><t t-esc="item"/></div> <div t-ref="{{item}}" t-key="item"><t t-esc="item"/></div>
</t> </t>
</div>` </div>`
); );
+1 -1
View File
@@ -876,7 +876,7 @@ describe("connecting a component to store", () => {
"Parent", "Parent",
` `
<div> <div>
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/> <t t-widget="Child" t-props="{key: props.current}"/>
</div> </div>
` `
); );