Files
FrancoisGe 70101e4c66 [IMP] parser: .trim modifier implies .lazy modifier
Before to this commit, the .trim modifier did not work correctly. The value
in the model is always trim at each input event but not the visual value
in the input. This difference between the visual and the model value cause
few strange bug.

After reflection, we think that we always want to be in .lazy when we use .trim.
Because we want to trim the final value during the onchange event and not
at each input event. If we do it at each input event, we can't write more then
one word easily.

So this commit change the behavior of the .trim modifier to always be in .lazy
2024-01-04 11:07:37 +01:00

738 lines
24 KiB
TypeScript

import { Component } from "../../src/runtime/component";
import { mount, useState, xml } from "../../src/index";
import { editInput, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
snapshotEverything();
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
describe("t-model directive", () => {
test("basic use, on an input", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state.text"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, 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>");
});
test("t-model on an input with an undefined value", async () => {
class SomeComponent extends Component {
static template = xml`<input t-model="state.text"/>`;
state = useState({ text: undefined });
}
await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<input>");
const input = fixture.querySelector("input")!;
expect(input.value).toBe("");
});
test("basic use, on an input with bracket expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, 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>");
});
test("throws if invalid expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state"/>
</div>`;
state = useState({ text: "" });
}
let error: Error;
try {
await mount(SomeComponent, fixture);
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
});
test("basic use, on another key in component", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<input t-model="some.text"/>
<span><t t-esc="some.text"/></span>
</div>`;
some = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.some.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test("on an input, type=checkbox", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<input type="checkbox" t-model="state.flag"/>
<span>
<t t-if="state.flag">yes</t>
<t t-else="">no</t>
</span>
</div>`;
state = useState({ flag: false });
}
const comp = await mount(SomeComponent, 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);
input.click();
await nextTick();
expect(comp.state.flag).toBe(false);
});
test("on an textarea", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<textarea t-model="state.text"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, 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 () => {
class SomeComponent extends Component {
static template = xml`<div>
<input type="radio" id="one" value="One" t-model="state.choice"/>
<input type="radio" id="two" value="Two" t-model="state.choice"/>
<span>Choice: <t t-esc="state.choice"/></span>
</div>`;
state = useState({ choice: "" });
}
const comp = await mount(SomeComponent, 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>'
);
});
test("on an input type=radio, with initial value", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<input type="radio" id="one" value="One" t-model="state.choice"/>
<input type="radio" id="two" value="Two" t-model="state.choice"/>
</div>`;
state = useState({ choice: "Two" });
}
await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"></div>'
);
const secondInput = fixture.querySelectorAll("input")[1];
expect(secondInput.checked).toBe(true);
});
test("on a select", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<select t-model="state.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>`;
state = useState({ color: "" });
}
const comp = await mount(SomeComponent, 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>'
);
});
test("on a select, initial state", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<select t-model="state.color">
<option value="">Please select one</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
`;
state = useState({ color: "red" });
}
await mount(SomeComponent, fixture);
const select = fixture.querySelector("select")!;
expect(select.value).toBe("red");
});
test("on a sub state key", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state.something.text"/>
<span><t t-esc="state.something.text"/></span>
</div>
`;
state = useState({ something: { text: "" } });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.something.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test("with expression having a changing key", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state.something[text.key]"/>
<span><t t-esc="state.something[text.key]"/></span>
</div>
`;
state: { something: { [key: string]: string } } = useState({ something: {} });
text = useState({ key: "foo" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
let input = fixture.querySelector("input")!;
await editInput(input, "footest");
expect(comp.state.something[comp.text.key]).toBe("footest");
expect(fixture.innerHTML).toBe("<div><input><span>footest</span></div>");
comp.text.key = "bar";
await nextTick();
input = fixture.querySelector("input")!;
await editInput(input, "test bar");
expect(comp.state.something[comp.text.key]).toBe("test bar");
expect(fixture.innerHTML).toBe("<div><input><span>test bar</span></div>");
});
test(".lazy modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model.lazy="state.text"/>
<span><t t-esc="state.text"/></span>
</div>
`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, 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>");
});
test(".trim modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model.trim="state.text"/>
<span><t t-esc="state.text"/></span>
</div>
`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, 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(".trim modifier implies .lazy modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model.trim="state.text"/>
<span><t t-esc="state.text"/></span>
</div>
`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, 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>");
});
test(".number modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model.number="state.number"/>
<span><t t-esc="state.number"/></span>
</div>
`;
state = useState({ number: 0 });
}
const comp = await mount(SomeComponent, 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>");
});
test("in a t-foreach", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing.id">
<input type="checkbox" t-model="thing.f"/>
</t>
</div>
`;
state = useState([
{ f: false, id: 1 },
{ f: false, id: 2 },
{ f: false, id: 3 },
]);
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><input type="checkbox"><input type="checkbox"></div>'
);
const input = fixture.querySelectorAll("input")[1]!;
input.click();
expect(comp.state[1].f).toBe(true);
expect(comp.state[0].f).toBe(false);
expect(comp.state[2].f).toBe(false);
});
test("in a t-foreach, part 2", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing_index" >
<input t-model="state[thing_index]"/>
</t>
</div>
`;
state = useState(["zuko", "iroh"]);
}
const comp = await mount(SomeComponent, fixture);
expect(comp.state).toEqual(["zuko", "iroh"]);
const input = fixture.querySelectorAll("input")[1]!;
await editInput(input, "uncle iroh");
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
});
test("in a t-foreach, part 3", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="names" t-as="name" t-key="name_index">
<input t-model="state.values[name]"/>
</t>
</div>
`;
names = ["Crusher", "Data", "Riker", "Worf"];
state = useState({ values: {} });
}
const comp = await mount(SomeComponent, fixture);
expect(comp.state).toEqual({ values: {} });
const input = fixture.querySelectorAll("input")[1]!;
await editInput(input, "Commander");
expect(comp.state).toEqual({ values: { Data: "Commander" } });
});
test("two inputs in a div alternating with a t-if", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input class="a" t-if="state.flag" t-model="state.text1"/>
<input class="b" t-if="!state.flag" t-model="state.text2"/>
</div>
`;
state = useState({ flag: true, text1: "", text2: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe('<div><input class="a"></div>');
let input = fixture.querySelector("input")!;
expect(input.value).toBe("");
await editInput(input, "Jean-Luc");
expect(comp.state.text1).toBe("Jean-Luc");
comp.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe('<div><input class="b"></div>');
input = fixture.querySelector("input")!;
expect(input.value).toBe("");
await editInput(input, "Picard");
expect(comp.state.text2).toBe("Picard");
});
test("following a scope protecting directive (e.g. t-set)", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-set="admiral" t-value="'Bruno'"/>
<input t-model="state['text']"/>
</div>
`;
state = useState({ text: "Jean-Luc Picard" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
const input = fixture.querySelector("input")!;
expect(input.value).toBe("Jean-Luc Picard");
await editInput(input, "Commander Data");
expect(comp.state.text).toBe("Commander Data");
});
test("can also define t-on directive on same event, part 1", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']" t-on-input="onInput"/>
</div>
`;
state = useState({ text: "", other: "" });
onInput(ev: InputEvent) {
this.state.other = (ev.target as HTMLInputElement).value;
}
}
const comp = await mount(SomeComponent, fixture);
expect(comp.state.text).toBe("");
expect(comp.state.other).toBe("");
const input = fixture.querySelector("input")!;
await editInput(input, "Beam me up, Scotty");
expect(comp.state.text).toBe("Beam me up, Scotty");
expect(comp.state.other).toBe("Beam me up, Scotty");
});
test("can also define t-on directive on same event, part 2", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input type="radio" id="one" value="One" t-model="state.choice" t-on-click="onClick"/>
<input type="radio" id="two" value="Two" t-model="state.choice" t-on-click="onClick"/>
<input type="radio" id="three" value="Three" t-model="state.choice" t-on-click="onClick"/>
</div>
`;
state = useState({ choice: "", lastClicked: "" });
onClick(ev: MouseEvent) {
this.state.lastClicked = (ev.target as HTMLInputElement).value;
}
}
const comp = await mount(SomeComponent, fixture);
expect(comp.state.choice).toBe("");
expect(comp.state.lastClicked).toBe("");
const lastInput = fixture.querySelectorAll("input")[2];
lastInput.click();
await nextTick();
expect(comp.state.choice).toBe("Three");
expect(comp.state.lastClicked).toBe("Three");
});
test("t-model on select with static options", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option value="a" t-esc="'a'"/>
<option value="b" t-esc="'b'"/>
<option value="c" t-esc="'c'"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b", "c"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option t-att-value="options[0]" t-esc="options[0]"/>
<option t-att-value="options[1]" t-esc="options[1]"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options -- 2", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option t-att-value="options[0]" t-esc="options[0]"/>
<option t-attf-value="{{ options[1] }}" t-esc="options[1]"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options -- 3", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<option t-att-value="options[0]" t-esc="options[0]"/>
<option value="b" t-esc="options[1]"/>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic values on select options in foreach", async () => {
class Test extends Component {
static template = xml`
<div>
<select t-model="state.model">
<t t-foreach="options" t-as="v" t-key="v">
<option t-att-value="v" t-esc="v"/>
</t>
</select>
</div>
`;
state: any;
options: any;
setup() {
this.state = useState({ model: "b" });
this.options = ["a", "b", "c"];
}
}
await mount(Test, fixture);
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic number values on select options in foreach", async () => {
class Test extends Component {
static template = xml`
<select t-model.number="state.value">
<t t-foreach="state.options" t-as="o" t-key="o.value">
<option t-att-value="o.value" t-esc="o.value"/>
</t>
</select>
`;
state: any;
setup() {
this.state = useState({
value: 2,
options: [{ value: 1 }, { value: 2 }, { value: 3 }],
});
}
}
const comp = await mount(Test, fixture);
// check that we have a value of 2 selected
expect(fixture.querySelector("select")!.value).toEqual("2");
expect(comp.state.value).toBe(2);
// emulate a click on the option=3 element
fixture.querySelectorAll("option")[2].selected = true;
fixture.querySelector("select")!.dispatchEvent(new Event("change"));
await nextTick();
// check that we have now selected the number 3 (and not the string)
expect(fixture.querySelector("select")!.value).toEqual("3");
expect(comp.state.value).toBe(3);
});
test("t-model is applied before t-on-input", async () => {
expect.assertions(3);
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']" t-on-input="onInput"/>
</div>
`;
state = useState({ text: "", other: "" });
onInput(ev: InputEvent) {
expect(this.state.text).toBe("Beam me up, Scotty");
expect((ev.target as HTMLInputElement).value).toBe("Beam me up, Scotty");
}
}
await mount(SomeComponent, fixture);
const input = fixture.querySelector("input")!;
await editInput(input, "Beam me up, Scotty");
});
test("t-model with radio button group in t-foreach", async () => {
expect.assertions(6);
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`
<div t-on-click="getData" id="get_data">
<t t-foreach="options" t-as="opt" t-key="opt">
<input type="radio" name="radio_group" t-model="state.group" t-att-value="opt" t-att-id="opt"/>
</t>
</div>
`;
state = useState({ group: "scotty" });
options = ["beam", "scotty"];
getData() {
steps.push(`group: ${this.state.group}`);
}
}
await mount(SomeComponent, fixture);
const divEl = fixture.querySelector("#get_data") as HTMLElement;
expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("scotty");
divEl.click();
expect(steps).toEqual(["group: scotty"]);
fixture.querySelector("input")!.click();
expect(steps).toEqual(["group: scotty", "group: beam"]);
await nextTick();
expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("beam");
divEl.click();
expect(steps).toEqual(["group: scotty", "group: beam", "group: beam"]);
});
});