From 4cde06d68563acbda6a2c0aad58f09162c087015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 1 Dec 2021 15:51:00 +0100 Subject: [PATCH] [IMP] component: add .bind suffix to props for easy binding --- src/app/template_helpers.ts | 18 +++++++ src/compiler/code_generator.ts | 17 ++++-- .../__snapshots__/props.test.ts.snap | 46 ++++++++++++++++ tests/components/props.test.ts | 54 ++++++++++++++++++- 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/src/app/template_helpers.ts b/src/app/template_helpers.ts index e97d8707..dd92d492 100644 --- a/src/app/template_helpers.ts +++ b/src/app/template_helpers.ts @@ -124,6 +124,23 @@ export function safeOutput(value: any): ReturnType { return toggler(safeKey, block); } +let boundFunctions = new WeakMap(); + +function bind(ctx: any, fn: Function): Function { + let component = ctx.__owl__.component; + let boundFnMap = boundFunctions.get(component); + if (!boundFnMap) { + boundFnMap = new WeakMap(); + boundFunctions.set(component, boundFnMap); + } + let boundFn = boundFnMap.get(fn); + if (!boundFn) { + boundFn = fn.bind(component); + boundFnMap.set(fn, boundFn); + } + return boundFn; +} + export const UTILS = { withDefault, zero: Symbol("zero"), @@ -137,4 +154,5 @@ export const UTILS = { toNumber, validateProps, safeOutput, + bind, }; diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 1034d339..49ec62de 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -975,10 +975,19 @@ export class CodeGenerator { // props const props: string[] = []; let hasSlotsProp = false; - for (let p in ast.props) { - const propName = /^[a-z_]+$/i.test(p) ? p : `'${p}'`; - props.push(`${propName}: ${this.captureExpression(ast.props[p]) || undefined}`); - if (p === "slots") { + for (let propName in ast.props) { + let propValue = this.captureExpression(ast.props[propName]) || undefined; + if (propName.includes(".")) { + let [name, suffix] = propName.split("."); + if (suffix === "bind") { + this.helpers.add("bind"); + propName = name; + propValue = `bind(ctx, ${propValue})`; + } + } + propName = /^[a-z_]+$/i.test(propName) ? propName : `'${propName}'`; + props.push(`${propName}: ${propValue}`); + if (propName === "slots") { hasSlotsProp = true; } } diff --git a/tests/components/__snapshots__/props.test.ts.snap b/tests/components/__snapshots__/props.test.ts.snap index 8a51387e..9b039092 100644 --- a/tests/components/__snapshots__/props.test.ts.snap +++ b/tests/components/__snapshots__/props.test.ts.snap @@ -262,3 +262,49 @@ exports[`basics template string in prop 2`] = ` } }" `; + +exports[`bound functions is referentially equal after update 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(ctx['props'].val); + } +}" +`; + +exports[`bound functions is referentially equal after update 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { bind } = helpers; + + return function template(ctx, node, key = \\"\\") { + return component(\`Child\`, {val: ctx['state'].val,fn: bind(ctx, ctx['someFunction'])}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`can bind function prop with bind suffix 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`child\`); + } +}" +`; + +exports[`can bind function prop with bind suffix 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { bind } = helpers; + + return function template(ctx, node, key = \\"\\") { + return component(\`Child\`, {doSomething: bind(ctx, ctx['doSomething'])}, key + \`__1\`, node, ctx); + } +}" +`; diff --git a/tests/components/props.test.ts b/tests/components/props.test.ts index a9706f79..25ac11ab 100644 --- a/tests/components/props.test.ts +++ b/tests/components/props.test.ts @@ -1,6 +1,6 @@ -import { Component, mount, useState } from "../../src"; +import { Component, mount, onWillUpdateProps, useState } from "../../src"; import { xml } from "../../src/tags"; -import { makeTestFixture, snapshotEverything } from "../helpers"; +import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; let fixture: HTMLElement; @@ -176,3 +176,53 @@ describe("basics", () => { await mount(Parent, fixture); }); }); + +test("can bind function prop with bind suffix", async () => { + class Child extends Component { + static template = xml`child`; + setup() { + this.props.doSomething(123); + } + } + + let boundedThing: any = null; + + class Parent extends Component { + static template = xml``; + static components = { Child }; + + doSomething(val: number) { + expect(val).toBe(123); + boundedThing = this; + } + } + + const parent = await mount(Parent, fixture); + expect(boundedThing).toBe(parent); + expect(fixture.innerHTML).toBe("child"); +}); + +test("bound functions is referentially equal after update", async () => { + let isEqual = false; + class Child extends Component { + static template = xml``; + setup() { + onWillUpdateProps((nextProps: any) => { + isEqual = nextProps.fn === this.props.fn; + }); + } + } + + class Parent extends Component { + static template = xml``; + static components = { Child }; + state = useState({ val: 1 }); + someFunction() {} + } + + const parent = await mount(Parent, fixture); + parent.state.val = 3; + await nextTick(); + expect(fixture.innerHTML).toBe("3"); + expect(isEqual).toBe(true); +});