diff --git a/src/blockdom/events.ts b/src/blockdom/events.ts index d2488bc0..47d31745 100644 --- a/src/blockdom/events.ts +++ b/src/blockdom/events.ts @@ -18,8 +18,9 @@ export function createEventHandler(rawEvent: string): EventHandlerCreator { } // Native listener +let nextNativeEventId = 1; function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator { - let eventKey = `__event__${evName}`; + let eventKey = `__event__${evName}_${nextNativeEventId++}`; if (capture) { eventKey = `${eventKey}_capture`; } diff --git a/src/qweb/parser.ts b/src/qweb/parser.ts index 34fb80c4..c185d0d9 100644 --- a/src/qweb/parser.ts +++ b/src/qweb/parser.ts @@ -350,13 +350,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { const hasNumberMod = attr.includes(".number"); const hasTrimMod = attr.includes(".trim"); const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input"; - const similarTOnEvent = nodeAttrsNames.find((a) => a.startsWith(`t-on-${eventType}`)); - - if (similarTOnEvent) { - throw new Error( - `Conflicting t-model and ${similarTOnEvent} directives on event type: ${eventType}` - ); - } model = { baseExpr, diff --git a/tests/components/__snapshots__/t_model.test.ts.snap b/tests/components/__snapshots__/t_model.test.ts.snap index 5c3aa8a8..975f9d35 100644 --- a/tests/components/__snapshots__/t_model.test.ts.snap +++ b/tests/components/__snapshots__/t_model.test.ts.snap @@ -108,6 +108,50 @@ exports[`t-model directive basic use, on another key in component 1`] = ` }" `; +exports[`t-model directive can also define t-on directive on same event, part 1 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [ctx, 'onInput']; + const bExpr1 = ctx['state']; + let d2 = ctx['state']['text']; + let d3 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + return block1([d1, d2, d3]); + } +}" +`; + +exports[`t-model directive can also define t-on directive on same event, part 2 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [ctx, 'onClick']; + const bExpr1 = ctx['state']; + let d2 = ctx['state']['choice'] === 'One'; + let d3 = [(ev) => { bExpr1['choice'] = ev.target.value; }]; + let d4 = [ctx, 'onClick']; + const bExpr2 = ctx['state']; + let d5 = ctx['state']['choice'] === 'Two'; + let d6 = [(ev) => { bExpr2['choice'] = ev.target.value; }]; + let d7 = [ctx, 'onClick']; + const bExpr3 = ctx['state']; + let d8 = ctx['state']['choice'] === 'Three'; + let d9 = [(ev) => { bExpr3['choice'] = ev.target.value; }]; + return block1([d1, d2, d3, d4, d5, d6, d7, d8, d9]); + } +}" +`; + exports[`t-model directive following a scope protecting directive (e.g. t-set) 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/t_model.test.ts b/tests/components/t_model.test.ts index 0e1d8014..c8be3d3a 100644 --- a/tests/components/t_model.test.ts +++ b/tests/components/t_model.test.ts @@ -406,51 +406,50 @@ describe("t-model directive", () => { expect(comp.state.text).toBe("Commander Data"); }); - test("throws when conflicting with t-on directive, part 1", async () => { + test("can also define t-on directive on same event, part 1", async () => { class SomeComponent extends Component { - static template = xml`
`; - state = useState({ text: "" }); + static template = xml` +
+ +
+ `; + state = useState({ text: "", other: "" }); + onInput(ev: InputEvent) { + this.state.other = (ev.target as HTMLInputElement).value; + } } - let error; - try { - await mount(SomeComponent, fixture); - } catch (e) { - error = e; - } - expect(error.message).toBe( - "Conflicting t-model and t-on-input directives on event type: input" - ); + 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("throws when conflicting with t-on directive, part 2", async () => { + test("can also define t-on directive on same event, part 2", async () => { class SomeComponent extends Component { - static template = xml`
`; - state = useState({ text: "" }); + static template = xml` +
+ + + +
+ `; + state = useState({ choice: "", lastClicked: "" }); + onClick(ev: MouseEvent) { + this.state.lastClicked = (ev.target as HTMLInputElement).value; + } } - let error; - try { - await mount(SomeComponent, fixture); - } catch (e) { - error = e; - } - expect(error.message).toBe( - "Conflicting t-model and t-on-change directives on event type: change" - ); - }); - test("throws when conflicting with t-on directive, part 3", async () => { - class SomeComponent extends Component { - static template = xml`
`; - state = useState({ text: "" }); - } - let error; - try { - await mount(SomeComponent, fixture); - } catch (e) { - error = e; - } - expect(error.message).toBe( - "Conflicting t-model and t-on-click directives on event type: click" - ); + 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"); }); });