[ADD] component: implement t-model directive

closes #170
This commit is contained in:
Géry Debongnie
2019-06-13 16:27:12 +02:00
parent e64e415b3b
commit c481a73a76
8 changed files with 602 additions and 9 deletions
+147
View File
@@ -827,6 +827,153 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
}"
`;
exports[`t-model directive .lazy modifier 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
p2.props = {value: context.state['text']};
extra.handlers['change' + 2] = extra.handlers['change' + 2] || ((ev) => {context.state['text'] = ev.target.value});
p2.on['change'] = extra.handlers['change' + 2];
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
var _4 = context['state'].text;
if (_4 || _4 === 0) {
c3.push({text: _4});
}
return vn1;
}"
`;
exports[`t-model directive basic use, on an input 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('input', p2, c2);
c1.push(vn2);
p2.props = {value: context.state['text']};
extra.handlers['input' + 2] = extra.handlers['input' + 2] || ((ev) => {context.state['text'] = ev.target.value});
p2.on['input'] = extra.handlers['input' + 2];
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c1.push(vn3);
var _4 = context['state'].text;
if (_4 || _4 === 0) {
c3.push({text: _4});
}
return vn1;
}"
`;
exports[`t-model directive on a select 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2,on:{}};
var vn2 = h('select', p2, c2);
c1.push(vn2);
p2.props = {value: context.state['color']};
extra.handlers['change' + 2] = extra.handlers['change' + 2] || ((ev) => {context.state['color'] = ev.target.value});
p2.on['change'] = extra.handlers['change' + 2];
var _3 = '';
let c4 = [], p4 = {key:4,attrs:{value: _3}};
var vn4 = h('option', p4, c4);
c2.push(vn4);
c4.push({text: \`Please select one\`});
var _5 = 'red';
let c6 = [], p6 = {key:6,attrs:{value: _5}};
var vn6 = h('option', p6, c6);
c2.push(vn6);
c6.push({text: \`Red\`});
var _7 = 'blue';
let c8 = [], p8 = {key:8,attrs:{value: _7}};
var vn8 = h('option', p8, c8);
c2.push(vn8);
c8.push({text: \`Blue\`});
let c9 = [], p9 = {key:9};
var vn9 = h('span', p9, c9);
c1.push(vn9);
c9.push({text: \`Choice: \`});
var _10 = context['state'].color;
if (_10 || _10 === 0) {
c9.push({text: _10});
}
return vn1;
}"
`;
exports[`t-model directive on an input type=radio 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'radio';
var _3 = 'one';
var _4 = 'One';
let c5 = [], p5 = {key:5,attrs:{type: _2,id: _3,value: _4},on:{}};
var vn5 = h('input', p5, c5);
c1.push(vn5);
p5.props = {checked:context.state['choice'] === 'One'};
extra.handlers['click' + 5] = extra.handlers['click' + 5] || ((ev) => {context.state['choice'] = ev.target.value});
p5.on['click'] = extra.handlers['click' + 5];
var _6 = 'radio';
var _7 = 'two';
var _8 = 'Two';
let c9 = [], p9 = {key:9,attrs:{type: _6,id: _7,value: _8},on:{}};
var vn9 = h('input', p9, c9);
c1.push(vn9);
p9.props = {checked:context.state['choice'] === 'Two'};
extra.handlers['click' + 9] = extra.handlers['click' + 9] || ((ev) => {context.state['choice'] = ev.target.value});
p9.on['click'] = extra.handlers['click' + 9];
let c10 = [], p10 = {key:10};
var vn10 = h('span', p10, c10);
c1.push(vn10);
c10.push({text: \`Choice: \`});
var _11 = context['state'].choice;
if (_11 || _11 === 0) {
c10.push({text: _11});
}
return vn1;
}"
`;
exports[`t-model directive on an input, type=checkbox 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = 'checkbox';
let c3 = [], p3 = {key:3,attrs:{type: _2},on:{}};
var vn3 = h('input', p3, c3);
c1.push(vn3);
p3.props = {checked: context.state['flag']};
extra.handlers['input' + 3] = extra.handlers['input' + 3] || ((ev) => {context.state['flag'] = ev.target.checked});
p3.on['input'] = extra.handlers['input' + 3];
let c4 = [], p4 = {key:4};
var vn4 = h('span', p4, c4);
c1.push(vn4);
if (context['state'].flag) {
c4.push({text: \`yes\`});
}
else {
c4.push({text: \`no\`});
}
return vn1;
}"
`;
exports[`t-slot directive can define and call slots 1`] = `
"function anonymous(context,extra
) {
+234 -1
View File
@@ -6,7 +6,8 @@ import {
makeTestEnv,
nextMicroTick,
nextTick,
normalize
normalize,
editInput
} from "./helpers";
//------------------------------------------------------------------------------
@@ -2902,3 +2903,235 @@ describe("t-slot directive", () => {
);
});
});
describe("t-model directive", () => {
test("basic use, on an input", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input t-model="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on an input, type=checkbox", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input type="checkbox" t-model="flag"/>
<span>
<t t-if="state.flag">yes</t>
<t t-else="1">no</t>
</span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { flag: false };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><span>no</span></div>'
);
let input = fixture.querySelector("input")!;
input.click();
await nextTick();
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><span>yes</span></div>'
);
expect(comp.state.flag).toBe(true);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
input.click();
await nextTick();
expect(comp.state.flag).toBe(false);
});
test("on an textarea", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<textarea t-model="text"/>
<span><t t-esc="state.text"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><textarea></textarea><span></span></div>"
);
const textarea = fixture.querySelector("textarea")!;
await editInput(textarea, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe(
"<div><textarea></textarea><span>test</span></div>"
);
});
test("on an input type=radio", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<input type="radio" id="one" value="One" t-model="choice"/>
<input type="radio" id="two" value="Two" t-model="choice"/>
<span>Choice: <t t-esc="state.choice"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { choice: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: </span></div>'
);
const firstInput = fixture.querySelector("input")!;
firstInput.click();
await nextTick();
expect(comp.state.choice).toBe("One");
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: One</span></div>'
);
const secondInput = fixture.querySelectorAll("input")[1];
secondInput.click();
await nextTick();
expect(comp.state.choice).toBe("Two");
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: Two</span></div>'
);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test("on a select", 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>
<span>Choice: <t t-esc="state.color"/></span>
</div>
</templates>`);
class SomeComponent extends Widget {
state = { color: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe(
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: </span></div>'
);
const select = fixture.querySelector("select")!;
select.value = "red";
select.dispatchEvent(new Event("change"));
await nextTick();
expect(comp.state.color).toBe("red");
expect(fixture.innerHTML).toBe(
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: red</span></div>'
);
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
});
test(".lazy modifier", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="SomeComponent">
<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);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(comp.state.text).toBe("");
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
input.dispatchEvent(new Event("change"));
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();
});
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 {
state = { text: "" };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
const input = fixture.querySelector("input")!;
await editInput(input, " test ");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
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 {
state = { number: 0 };
}
const comp = new SomeComponent(env);
await comp.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><span>0</span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "13");
expect(comp.state.number).toBe(13);
expect(fixture.innerHTML).toBe("<div><input><span>13</span></div>");
await editInput(input, "invalid");
expect(comp.state.number).toBe("invalid");
expect(fixture.innerHTML).toBe("<div><input><span>invalid</span></div>");
});
});
+10
View File
@@ -91,3 +91,13 @@ export function patchNextFrame(f: Function) {
export function unpatchNextFrame() {
UTILS.nextFrame = nextFrame;
}
export async function editInput(
input: HTMLInputElement | HTMLTextAreaElement,
value: string
) {
input.value = value;
input.dispatchEvent(new Event("input"));
input.dispatchEvent(new Event("change"));
return nextTick();
}