From 3196b585fd542a49c079674cac24c3e8bfe1aa83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 8 Feb 2022 09:57:49 +0100 Subject: [PATCH] [FIX] slots: process slot params/values like normal props --- doc/reference/slots.md | 6 ++ src/compiler/code_generator.ts | 71 +++++++++------- .../compiler/__snapshots__/misc.test.ts.snap | 6 +- .../__snapshots__/basics.test.ts.snap | 2 +- .../__snapshots__/concurrency.test.ts.snap | 18 ++-- .../__snapshots__/props.test.ts.snap | 2 +- .../props_validation.test.ts.snap | 4 +- .../__snapshots__/slots.test.ts.snap | 85 +++++++++++++++++++ .../__snapshots__/style_class.test.ts.snap | 2 +- .../__snapshots__/t_foreach.test.ts.snap | 2 +- .../__snapshots__/t_props.test.ts.snap | 2 +- tests/components/slots.test.ts | 59 +++++++++++++ tests/misc/__snapshots__/memo.test.ts.snap | 2 +- 13 files changed, 211 insertions(+), 50 deletions(-) diff --git a/doc/reference/slots.md b/doc/reference/slots.md index 7e12e450..4e6e1872 100644 --- a/doc/reference/slots.md +++ b/doc/reference/slots.md @@ -210,6 +210,9 @@ use this `Notebook` component: ``` +Slot params works like normal props, so one can use the `.bind` suffix to +bind a function if needed. + ## Slot scopes For other kinds of advanced use cases, the content of a slot may depends on some @@ -246,3 +249,6 @@ component itself: ``` + +Slot values works like normal props, so one can use the `.bind` suffix to +bind a function if needed. diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index e5dba03e..5d28f5c9 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -1050,29 +1050,50 @@ export class CodeGenerator { return parts.join("__"); } + /** + * Formats a prop name and value into a string suitable to be inserted in the + * generated code. For example: + * + * Name Value Result + * --------------------------------------------------------- + * "number" "state" "number: ctx['state']" + * "something" "" "something: undefined" + * "some-prop" "state" "'some-prop': ctx['state']" + * "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])" + */ + formatProp(name: string, value: string): string { + value = this.captureExpression(value); + if (name.includes(".")) { + let [_name, suffix] = name.split("."); + if (suffix === "bind") { + this.helpers.add("bind"); + name = _name; + value = `bind(ctx, ${value || undefined})`; + } else { + throw new Error("Invalid prop suffix"); + } + } + name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`; + return `${name}: ${value || undefined}`; + } + + formatPropObject(obj: { [prop: string]: any }): string { + const params = []; + for (const [n, v] of Object.entries(obj)) { + params.push(this.formatProp(n, v)); + } + return params.join(", "); + } + compileComponent(ast: ASTComponent, ctx: Context) { let { block } = ctx; // props + const hasSlotsProp = "slots" in ast.props; const props: string[] = []; - let hasSlotsProp = false; - 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})`; - } else { - throw new Error("Invalid prop suffix"); - } - } - propName = /^[a-z_]+$/i.test(propName) ? propName : `'${propName}'`; - props.push(`${propName}: ${propValue}`); - if (propName === "slots") { - hasSlotsProp = true; - } + const propExpr = this.formatPropObject(ast.props); + if (propExpr) { + props.push(propExpr); } // slots @@ -1095,9 +1116,7 @@ export class CodeGenerator { params.push(`__scope: "${scope}"`); } if (ast.slots[slotName].attrs) { - for (const [n, v] of Object.entries(ast.slots[slotName].attrs!)) { - params.push(`${n}: ${compileExpr(v) || undefined}`); - } + params.push(this.formatPropObject(ast.slots[slotName].attrs!)); } const slotInfo = `{${params.join(", ")}}`; slotStr.push(`'${slotName}': ${slotInfo}`); @@ -1176,15 +1195,7 @@ export class CodeGenerator { slotName = "'" + ast.name + "'"; } - let scope = null; - if (ast.attrs) { - const params = []; - for (const [n, v] of Object.entries(ast.attrs!)) { - params.push(`${n}: ${compileExpr(v) || undefined}`); - } - scope = `{${params.join(", ")}}`; - } - + const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null; if (ast.defaultContent) { const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx); blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`; diff --git a/tests/compiler/__snapshots__/misc.test.ts.snap b/tests/compiler/__snapshots__/misc.test.ts.snap index 6e4ff912..57fe3adc 100644 --- a/tests/compiler/__snapshots__/misc.test.ts.snap +++ b/tests/compiler/__snapshots__/misc.test.ts.snap @@ -34,7 +34,7 @@ exports[`misc complex template 1`] = ` for (let i1 = 0; i1 < l_block4; i1++) { ctx[\`slot\`] = v_block4[i1]; let key1 = ctx['slot'].id; - c_block4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'],slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1); + c_block4[i1] = withKey(component(\`SlotButton\`, {class: ctx['slot_container'], slot: ctx['slot']}, key + \`__1__\${key1}\`, node, ctx), key1); } ctx = ctx.__proto__; b4 = list(c_block4); @@ -316,8 +316,8 @@ exports[`misc other complex template 1`] = ` if (!ctx['project']) { b24 = block24(); } else { - let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__2\`, node, ctx); - let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__3\`, node, ctx); + let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky, category_custom_views: ctx['category_custom_views'], search: ctx['search']}, key + \`__2\`, node, ctx); + let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev, search: ctx['search']}, key + \`__3\`, node, ctx); b25 = block25([], [b26, b27]); } return block1([attr1, txt1, hdlr2, hdlr3, attr8, hdlr4, hdlr5, ref1, hdlr6, ref2], [b2, b4, b14, b17, b22, b23, b24, b25]); diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap index 5d05da30..6d2bc63f 100644 --- a/tests/components/__snapshots__/basics.test.ts.snap +++ b/tests/components/__snapshots__/basics.test.ts.snap @@ -994,7 +994,7 @@ exports[`basics update props of component without concrete own node 2`] = ` return function template(ctx, node, key = \\"\\") { const tKey_1 = ctx['props'].subKey; - return toggler(tKey_1, component(\`Custom\`, {key: ctx['props'].key,subKey: ctx['props'].subKey}, tKey_1 + key + \`__1\`, node, ctx)); + return toggler(tKey_1, component(\`Custom\`, {key: ctx['props'].key, subKey: ctx['props'].subKey}, tKey_1 + key + \`__1\`, node, ctx)); } }" `; diff --git a/tests/components/__snapshots__/concurrency.test.ts.snap b/tests/components/__snapshots__/concurrency.test.ts.snap index 92623c8a..c47d6ade 100644 --- a/tests/components/__snapshots__/concurrency.test.ts.snap +++ b/tests/components/__snapshots__/concurrency.test.ts.snap @@ -183,7 +183,7 @@ exports[`concurrent renderings scenario 1 2`] = ` let block1 = createBlock(\`

\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx); + let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -227,7 +227,7 @@ exports[`concurrent renderings scenario 2 2`] = ` let block1 = createBlock(\`

\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx); + let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -270,7 +270,7 @@ exports[`concurrent renderings scenario 2bis 2`] = ` let block1 = createBlock(\`

\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA,fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx); + let b2 = component(\`ComponentC\`, {fromA: ctx['props'].fromA, fromB: ctx['state'].fromB}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -327,7 +327,7 @@ exports[`concurrent renderings scenario 3 3`] = ` let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx); + let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -384,7 +384,7 @@ exports[`concurrent renderings scenario 4 3`] = ` let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx); + let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -557,7 +557,7 @@ exports[`concurrent renderings scenario 9 3`] = ` let block1 = createBlock(\`

\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA,fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx); + let b2 = component(\`ComponentD\`, {fromA: ctx['props'].fromA, fromC: ctx['state'].fromC}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -734,7 +734,7 @@ exports[`concurrent renderings scenario 14 2`] = ` let block1 = createBlock(\`

\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); + let b2 = component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -778,7 +778,7 @@ exports[`concurrent renderings scenario 15 2`] = ` let block1 = createBlock(\`

\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); + let b2 = component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); return block1([], [b2]); } }" @@ -817,7 +817,7 @@ exports[`concurrent renderings scenario 16 2`] = ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; return function template(ctx, node, key = \\"\\") { - return component(\`C\`, {fromB: ctx['state'].fromB,fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); + return component(\`C\`, {fromB: ctx['state'].fromB, fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx); } }" `; diff --git a/tests/components/__snapshots__/props.test.ts.snap b/tests/components/__snapshots__/props.test.ts.snap index fb057ad4..3d97985c 100644 --- a/tests/components/__snapshots__/props.test.ts.snap +++ b/tests/components/__snapshots__/props.test.ts.snap @@ -273,7 +273,7 @@ exports[`bound functions is referentially equal after update 1`] = ` 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); + return component(\`Child\`, {val: ctx['state'].val, fn: bind(ctx, ctx['someFunction'])}, key + \`__1\`, node, ctx); } }" `; diff --git a/tests/components/__snapshots__/props_validation.test.ts.snap b/tests/components/__snapshots__/props_validation.test.ts.snap index 805885f8..4a106a16 100644 --- a/tests/components/__snapshots__/props_validation.test.ts.snap +++ b/tests/components/__snapshots__/props_validation.test.ts.snap @@ -115,7 +115,7 @@ exports[`props validation can specify that additional props are allowed (array) let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; return function template(ctx, node, key = \\"\\") { - const props1 = {message: 'm',otherProp: 'o'}; + const props1 = {message: 'm', otherProp: 'o'}; helpers.validateProps(\`Child\`, props1, ctx); return component(\`Child\`, props1, key + \`__1\`, node, ctx); } @@ -141,7 +141,7 @@ exports[`props validation can specify that additional props are allowed (object) let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; return function template(ctx, node, key = \\"\\") { - const props1 = {message: 'm',otherProp: 'o'}; + const props1 = {message: 'm', otherProp: 'o'}; helpers.validateProps(\`Child\`, props1, ctx); return component(\`Child\`, props1, key + \`__1\`, node, ctx); } diff --git a/tests/components/__snapshots__/slots.test.ts.snap b/tests/components/__snapshots__/slots.test.ts.snap index de520935..d97cd184 100644 --- a/tests/components/__snapshots__/slots.test.ts.snap +++ b/tests/components/__snapshots__/slots.test.ts.snap @@ -75,6 +75,37 @@ exports[`slots can define and call slots 2`] = ` }" `; +exports[`slots can define and call slots with bound params 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, bind } = helpers; + + function slot1(ctx, node, key = \\"\\") { + return text(\`abc\`); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return component(\`Child\`, {slots: {'abc': {__render: slot1, __ctx: ctx1, getValue: bind(ctx, ctx['getValue'])}}}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`slots can define and call slots with bound params 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + let b2 = callSlot(ctx, node, key, 'abc', false, {}); + let b3 = text(ctx['props'].slots['abc'].getValue()); + return multi([b2, b3]); + } +}" +`; + exports[`slots can define and call slots with params 1`] = ` "function anonymous(bdom, helpers ) { @@ -424,6 +455,33 @@ exports[`slots default slot next to named slot, with default content 2`] = ` }" `; +exports[`slots default slot with params with - in it 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + function slot1(ctx, node, key = \\"\\") { + return text(ctx['slotScope']['some-value']); + } + + return function template(ctx, node, key = \\"\\") { + return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`slots default slot with params with - in it 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + return callSlot(ctx, node, key, 'default', false, {'some-value': ctx['state'].value}); + } +}" +`; + exports[`slots default slot with slot scope: shorthand syntax 1`] = ` "function anonymous(bdom, helpers ) { @@ -1207,6 +1265,33 @@ exports[`slots simple default slot with params 2`] = ` }" `; +exports[`slots simple default slot with params and bound function 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + function slot1(ctx, node, key = \\"\\") { + return text(ctx['slotScope'].fn()); + } + + return function template(ctx, node, key = \\"\\") { + return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx, __scope: \\"slotScope\\"}}}, key + \`__1\`, node, ctx); + } +}" +`; + +exports[`slots simple default slot with params and bound function 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot, bind } = helpers; + + return function template(ctx, node, key = \\"\\") { + return callSlot(ctx, node, key, 'default', false, {fn: bind(ctx, ctx['getValue'])}); + } +}" +`; + exports[`slots simple default slot, variation 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/__snapshots__/style_class.test.ts.snap b/tests/components/__snapshots__/style_class.test.ts.snap index 38743fe1..7273b3a7 100644 --- a/tests/components/__snapshots__/style_class.test.ts.snap +++ b/tests/components/__snapshots__/style_class.test.ts.snap @@ -147,7 +147,7 @@ exports[`style and class handling class on sub component, which is switched to a let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; return function template(ctx, node, key = \\"\\") { - return component(\`Child\`, {class: 'someclass',child: ctx['state'].child}, key + \`__1\`, node, ctx); + return component(\`Child\`, {class: 'someclass', child: ctx['state'].child}, key + \`__1\`, node, ctx); } }" `; diff --git a/tests/components/__snapshots__/t_foreach.test.ts.snap b/tests/components/__snapshots__/t_foreach.test.ts.snap index b874ce67..955f3375 100644 --- a/tests/components/__snapshots__/t_foreach.test.ts.snap +++ b/tests/components/__snapshots__/t_foreach.test.ts.snap @@ -177,7 +177,7 @@ exports[`list of components reconciliation alg works for t-foreach in t-foreach, for (let i2 = 0; i2 < l_block4; i2++) { ctx[\`col\`] = v_block4[i2]; let key2 = ctx['col']; - let b6 = component(\`Child\`, {row: ctx['row'],col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, ctx); + let b6 = component(\`Child\`, {row: ctx['row'], col: ctx['col']}, key + \`__1__\${key1}__\${key2}\`, node, ctx); c_block4[i2] = withKey(block5([], [b6]), key2); } ctx = ctx.__proto__; diff --git a/tests/components/__snapshots__/t_props.test.ts.snap b/tests/components/__snapshots__/t_props.test.ts.snap index 4d8d8d48..c588fe42 100644 --- a/tests/components/__snapshots__/t_props.test.ts.snap +++ b/tests/components/__snapshots__/t_props.test.ts.snap @@ -117,7 +117,7 @@ exports[`t-props t-props with props 1`] = ` let block1 = createBlock(\`
\`); return function template(ctx, node, key = \\"\\") { - let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1,b: 2}), key + \`__1\`, node, ctx); + let b2 = component(\`Child\`, Object.assign({}, ctx['childProps'], {a: 1, b: 2}), key + \`__1\`, node, ctx); return block1([], [b2]); } }" diff --git a/tests/components/slots.test.ts b/tests/components/slots.test.ts index c5324d72..7f22f53a 100644 --- a/tests/components/slots.test.ts +++ b/tests/components/slots.test.ts @@ -126,6 +126,41 @@ describe("slots", () => { expect(mockConsoleWarn).toBeCalledTimes(1); }); + test("simple default slot with params and bound function", async () => { + class Child extends Component { + static template = xml``; + state = useState({ value: 123 }); + getValue() { + return this.state.value; + } + } + + class Parent extends Component { + static template = xml` + `; + static components = { Child }; + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("123"); + }); + + test("default slot with params with - in it", async () => { + class Child extends Component { + static template = xml``; + state = useState({ value: 123 }); + } + + class Parent extends Component { + static template = xml` + `; + static components = { Child }; + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("123"); + }); + test("fun: two calls to the same slot", async () => { class Child extends Component { static template = xml``; @@ -242,6 +277,30 @@ describe("slots", () => { ); }); + test("can define and call slots with bound params", async () => { + class Child extends Component { + static template = xml` + + `; + } + + class Parent extends Component { + static components = { Child }; + static template = xml` + + abc + `; + state = useState({ value: 444 }); + getValue() { + return this.state.value; + } + } + + await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe("abc444"); + }); + test("no named slot content => just no children", async () => { class Dialog extends Component { static template = xml``; diff --git a/tests/misc/__snapshots__/memo.test.ts.snap b/tests/misc/__snapshots__/memo.test.ts.snap index a2c74175..9fc02e42 100644 --- a/tests/misc/__snapshots__/memo.test.ts.snap +++ b/tests/misc/__snapshots__/memo.test.ts.snap @@ -16,7 +16,7 @@ exports[`Memo if no prop change, prevent renderings from above 1`] = ` let b2 = text(ctx['state'].a); let b3 = text(ctx['state'].b); let b4 = text(ctx['state'].c); - let b9 = component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b,slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); + let b9 = component(\`Memo\`, {a: ctx['state'].a, b: ctx['state'].b,slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__1\`, node, ctx); return multi([b2, b3, b4, b9]); } }"