From b4d3757302675a0e1eab5cb1b44b99651b0b150a Mon Sep 17 00:00:00 2001 From: weareoutman Date: Wed, 23 Oct 2024 19:36:19 +0800 Subject: [PATCH] feat(): supports slots in template --- .../CustomTemplates/expandCustomTemplate.ts | 45 +++++-- .../replaceSlotWithSlottedBricks.spec.ts | 119 ++++++++++++++++++ .../replaceSlotWithSlottedBricks.ts | 57 +++++++++ .../CustomTemplates/setupTemplateProxy.ts | 9 +- 4 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.spec.ts create mode 100644 packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.ts diff --git a/packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts b/packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts index f24d848349..bde2361c62 100644 --- a/packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts +++ b/packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts @@ -6,6 +6,8 @@ import { RuntimeBrickConf, SlotsConfOfBricks, CustomTemplate, + type SlotConfOfBricks, + type BrickConf, } from "@next-core/brick-types"; import { RuntimeBrick } from "../BrickNode"; import { @@ -16,16 +18,14 @@ import { } from "./internalInterfaces"; import { isMergeableProperty, isVariableProperty } from "./assertions"; import { collectRefsInTemplate } from "./collectRefsInTemplate"; -import { - customTemplateRegistry, - RuntimeBrickConfWithTplSymbols, -} from "./constants"; +import { customTemplateRegistry } from "./constants"; import { collectMergeBases } from "./collectMergeBases"; import { CustomTemplateContext } from "./CustomTemplateContext"; import { setupUseBrickInTemplate } from "./setupUseBrickInTemplate"; import { setupTemplateProxy } from "./setupTemplateProxy"; import { collectWidgetContract } from "../CollectContracts"; import type { LocationContext } from "../LocationContext"; +import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks"; export interface ProxyContext { reversedProxies: ReversedProxies; @@ -33,6 +33,7 @@ export interface ProxyContext { externalSlots: SlotsConfOfBricks; templateContextId: string; proxyBrick: RuntimeBrick; + usedSlots: Set; } interface ReversedProxies { @@ -219,12 +220,15 @@ function lowLevelExpandCustomTemplate( externalSlots: externalSlots as SlotsConfOfBricks, templateContextId: tplContext.id, proxyBrick, + usedSlots: new Set(), }; newBrickConf.slots = { "": { type: "bricks", - bricks: bricks.map((item) => expandBrickInTemplate(item, proxyContext)), + bricks: bricks.flatMap((item) => + expandBrickInTemplate(item, proxyContext) + ), }, }; @@ -232,26 +236,41 @@ function lowLevelExpandCustomTemplate( } function expandBrickInTemplate( - brickConfInTemplate: BrickConfInTemplate, - proxyContext: ProxyContext -): RuntimeBrickConfWithTplSymbols { + brickConfInTemplate: BrickConf, + proxyContext: ProxyContext, + markSlotted?: () => void +): BrickConf | BrickConf[] { // Ignore `if: null` to make `looseCheckIf` working. if (brickConfInTemplate.if === null) { delete brickConfInTemplate.if; } + + if (brickConfInTemplate.brick === "slot") { + markSlotted?.(); + return replaceSlotWithSlottedBricks( + brickConfInTemplate, + proxyContext, + expandBrickInTemplate + ); + } + const { ref, slots: slotsInTemplate, ...restBrickConfInTemplate - } = brickConfInTemplate; + } = brickConfInTemplate as BrickConfInTemplate; - const slots: SlotsConfOfBricks = Object.fromEntries( + let slotted = false; + const markChild = (): void => { + slotted = true; + }; + const slots = Object.fromEntries( Object.entries(slotsInTemplate ?? {}).map(([slotName, slotConf]) => [ slotName, { type: "bricks", - bricks: (slotConf.bricks ?? []).map((item) => - expandBrickInTemplate(item, proxyContext) + bricks: (slotConf.bricks ?? []).flatMap((item) => + expandBrickInTemplate(item, proxyContext, markChild) ), }, ]) @@ -264,6 +283,6 @@ function expandBrickInTemplate( proxyContext ), slots, - ...setupTemplateProxy(proxyContext, ref, slots), + ...setupTemplateProxy(proxyContext, ref, slots, slotted), }; } diff --git a/packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.spec.ts b/packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.spec.ts new file mode 100644 index 0000000000..4313f13709 --- /dev/null +++ b/packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.spec.ts @@ -0,0 +1,119 @@ +import type { BrickConf } from "@next-core/brick-types"; +import { identity } from "lodash"; +import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks"; +import type { ProxyContext } from "./expandCustomTemplate"; + +describe("replaceSlotWithSlottedBricks", () => { + let mockHostContext: ProxyContext; + + beforeEach(() => { + mockHostContext = { + proxyBrick: { + type: "tpl-test", + }, + usedSlots: new Set(), + } as unknown as ProxyContext; + }); + + it("should throw an error if 'if' is used in a slot", () => { + const brickConf = { brick: "slot", if: false }; + expect(() => + replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity) + ).toThrow( + `Can not use "if" in a slot currently, check your template "tpl-test"` + ); + const brickConf2 = { brick: "slot", if: "<% true %>" }; + expect(() => + replaceSlotWithSlottedBricks(brickConf2, mockHostContext, identity) + ).toThrow( + `Can not use "if" in a slot currently, check your template "tpl-test"` + ); + }); + + it("should throw an error if slot name is an expression", () => { + const brickConf = { + brick: "slot", + properties: { name: "<% 'abc' %>" }, + } as BrickConf; + expect(() => + replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity) + ).toThrow( + `Can not use an expression as slot name "<% 'abc' %>" currently, check your template "tpl-test"` + ); + }); + + it("should throw an error if slot name is repeated", () => { + const brickConf = { + brick: "div", + properties: { name: "repeated-slot" }, + } as BrickConf; + mockHostContext.usedSlots.add("repeated-slot"); + expect(() => + replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity) + ).toThrow( + `Can not have multiple slots with the same name "repeated-slot", check your template "tpl-test"` + ); + }); + + it("should return external bricks if available", () => { + const brickConf = { + brick: "slot", + properties: { name: "slot1" }, + } as BrickConf; + mockHostContext.externalSlots = { + slot1: { type: "bricks", bricks: [{ brick: "h1" }, { brick: "h2" }] }, + }; + const result = replaceSlotWithSlottedBricks( + brickConf, + mockHostContext, + identity + ); + expect(result).toEqual([{ brick: "h1" }, { brick: "h2" }]); + }); + + it("should return expanded default slots if no external bricks", () => { + const brickConf = { + brick: "slot", + properties: { name: "slot1" }, + slots: { + "": { + type: "bricks", + bricks: [ + { + brick: "p", + }, + ], + }, + oops: { + type: "bricks", + bricks: [ + { + brick: "hr", + }, + ], + }, + }, + } as BrickConf; + const expandMock = jest.fn().mockImplementation((item) => [item]); + const result = replaceSlotWithSlottedBricks( + brickConf, + mockHostContext, + expandMock + ); + expect(result).toEqual([{ brick: "p" }]); + expect(expandMock).toHaveBeenCalledTimes(1); + }); + + it("no default bricks nor external bricks", () => { + const brickConf = { + brick: "slot", + properties: { name: "slot1" }, + } as BrickConf; + const result = replaceSlotWithSlottedBricks( + brickConf, + mockHostContext, + identity + ); + expect(result).toEqual([]); + }); +}); diff --git a/packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.ts b/packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.ts new file mode 100644 index 0000000000..8ae7b29a30 --- /dev/null +++ b/packages/brick-kit/src/core/CustomTemplates/replaceSlotWithSlottedBricks.ts @@ -0,0 +1,57 @@ +import type { + BrickConf, + BrickConfInTemplate, + UseSingleBrickConf, +} from "@next-core/brick-types"; +import { hasOwnProperty } from "@next-core/brick-utils"; +import { isEvaluable } from "@next-core/cook"; +import type { ProxyContext } from "./expandCustomTemplate"; + +export function replaceSlotWithSlottedBricks< + T extends BrickConf | UseSingleBrickConf +>( + brickConf: T, + proxyContext: ProxyContext, + expand: (item: T, proxyContext: ProxyContext) => T | T[] +): T[] { + // Currently, no support for `if` in a slot. + if ( + (brickConf.if != null && !brickConf.if) || + typeof brickConf.if === "string" + ) { + throw new Error( + `Can not use "if" in a slot currently, check your template "${proxyContext.proxyBrick.type}"` + ); + } + + const slot = String(brickConf.properties?.name ?? ""); + + // Currently, no support for expression as slot name. + if (isEvaluable(slot)) { + throw new Error( + `Can not use an expression as slot name "${slot}" currently, check your template "${proxyContext.proxyBrick.type}"` + ); + } + + // Do not repeat the same slot name in a template. + if (proxyContext.usedSlots.has(slot)) { + throw new Error( + `Can not have multiple slots with the same name "${slot}", check your template "${proxyContext.proxyBrick.type}"` + ); + } + proxyContext.usedSlots.add(slot); + + if ( + proxyContext.externalSlots && + hasOwnProperty(proxyContext.externalSlots, slot) + ) { + const insertBricks = proxyContext.externalSlots[slot].bricks ?? []; + if (insertBricks.length > 0) { + return insertBricks as T[]; + } + } + + return ((brickConf as BrickConfInTemplate).slots?.[""]?.bricks ?? []).flatMap( + (item) => expand(item as T, proxyContext) + ); +} diff --git a/packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts b/packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts index 007876f777..8444168dd3 100644 --- a/packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts +++ b/packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts @@ -19,7 +19,8 @@ import { propertyMergeAll } from "./propertyMerge"; export function setupTemplateProxy( proxyContext: Partial, ref: string, - slots: SlotsConfOfBricks + slots: SlotsConfOfBricks, + slotted?: boolean ): RuntimeBrickConfOfTplSymbols { const computedPropsFromProxy: Record = {}; let refForProxy: RefForProxy; @@ -89,6 +90,12 @@ export function setupTemplateProxy( const quasisMap = new Map(); if (reversedProxies.slots.has(ref)) { + if (slotted) { + throw new Error( + `Can not have proxied slot ref when the ref target has a slot element child, check your template "${proxyBrick.type}" and ref "${ref}"` + ); + } + for (const item of reversedProxies.slots.get(ref)) { if (!quasisMap.has(item.refSlot)) { const quasis: BrickConf[][] = [];