Skip to content

Commit 3f7b943

Browse files
authored
Merge pull request #4532 from easyops-cn/steve/v2-slots-in-tpl
feat(): supports slots in template
2 parents ff5cf0b + c9a9bde commit 3f7b943

File tree

5 files changed

+217
-15
lines changed

5 files changed

+217
-15
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ module.exports = [
4545
},
4646
{
4747
path: "packages/brick-kit/dist/index.esm.js",
48-
limit: "131 KB",
48+
limit: "135 KB",
4949
},
5050
{
5151
path: "packages/brick-types/dist/index.esm.js",

packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
RuntimeBrickConf,
77
SlotsConfOfBricks,
88
CustomTemplate,
9+
type SlotConfOfBricks,
10+
type BrickConf,
911
} from "@next-core/brick-types";
1012
import { RuntimeBrick } from "../BrickNode";
1113
import {
@@ -16,23 +18,22 @@ import {
1618
} from "./internalInterfaces";
1719
import { isMergeableProperty, isVariableProperty } from "./assertions";
1820
import { collectRefsInTemplate } from "./collectRefsInTemplate";
19-
import {
20-
customTemplateRegistry,
21-
RuntimeBrickConfWithTplSymbols,
22-
} from "./constants";
21+
import { customTemplateRegistry } from "./constants";
2322
import { collectMergeBases } from "./collectMergeBases";
2423
import { CustomTemplateContext } from "./CustomTemplateContext";
2524
import { setupUseBrickInTemplate } from "./setupUseBrickInTemplate";
2625
import { setupTemplateProxy } from "./setupTemplateProxy";
2726
import { collectWidgetContract } from "../CollectContracts";
2827
import type { LocationContext } from "../LocationContext";
28+
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks";
2929

3030
export interface ProxyContext {
3131
reversedProxies: ReversedProxies;
3232
templateProperties: Record<string, unknown>;
3333
externalSlots: SlotsConfOfBricks;
3434
templateContextId: string;
3535
proxyBrick: RuntimeBrick;
36+
usedSlots: Set<string>;
3637
}
3738

3839
interface ReversedProxies {
@@ -219,39 +220,57 @@ function lowLevelExpandCustomTemplate(
219220
externalSlots: externalSlots as SlotsConfOfBricks,
220221
templateContextId: tplContext.id,
221222
proxyBrick,
223+
usedSlots: new Set(),
222224
};
223225

224226
newBrickConf.slots = {
225227
"": {
226228
type: "bricks",
227-
bricks: bricks.map((item) => expandBrickInTemplate(item, proxyContext)),
229+
bricks: bricks.flatMap((item) =>
230+
expandBrickInTemplate(item, proxyContext)
231+
),
228232
},
229233
};
230234

231235
return newBrickConf;
232236
}
233237

234238
function expandBrickInTemplate(
235-
brickConfInTemplate: BrickConfInTemplate,
236-
proxyContext: ProxyContext
237-
): RuntimeBrickConfWithTplSymbols {
239+
brickConfInTemplate: BrickConf,
240+
proxyContext: ProxyContext,
241+
markSlotted?: () => void
242+
): BrickConf | BrickConf[] {
238243
// Ignore `if: null` to make `looseCheckIf` working.
239244
if (brickConfInTemplate.if === null) {
240245
delete brickConfInTemplate.if;
241246
}
247+
248+
if (brickConfInTemplate.brick === "slot") {
249+
markSlotted?.();
250+
return replaceSlotWithSlottedBricks(
251+
brickConfInTemplate,
252+
proxyContext,
253+
expandBrickInTemplate
254+
);
255+
}
256+
242257
const {
243258
ref,
244259
slots: slotsInTemplate,
245260
...restBrickConfInTemplate
246-
} = brickConfInTemplate;
261+
} = brickConfInTemplate as BrickConfInTemplate;
247262

248-
const slots: SlotsConfOfBricks = Object.fromEntries(
263+
let slotted = false;
264+
const markChild = (): void => {
265+
slotted = true;
266+
};
267+
const slots = Object.fromEntries<SlotConfOfBricks>(
249268
Object.entries(slotsInTemplate ?? {}).map(([slotName, slotConf]) => [
250269
slotName,
251270
{
252271
type: "bricks",
253-
bricks: (slotConf.bricks ?? []).map((item) =>
254-
expandBrickInTemplate(item, proxyContext)
272+
bricks: (slotConf.bricks ?? []).flatMap((item) =>
273+
expandBrickInTemplate(item, proxyContext, markChild)
255274
),
256275
},
257276
])
@@ -264,6 +283,6 @@ function expandBrickInTemplate(
264283
proxyContext
265284
),
266285
slots,
267-
...setupTemplateProxy(proxyContext, ref, slots),
286+
...setupTemplateProxy(proxyContext, ref, slots, slotted),
268287
};
269288
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { BrickConf } from "@next-core/brick-types";
2+
import { identity } from "lodash";
3+
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks";
4+
import type { ProxyContext } from "./expandCustomTemplate";
5+
6+
describe("replaceSlotWithSlottedBricks", () => {
7+
let mockHostContext: ProxyContext;
8+
9+
beforeEach(() => {
10+
mockHostContext = {
11+
proxyBrick: {
12+
type: "tpl-test",
13+
},
14+
usedSlots: new Set(),
15+
} as unknown as ProxyContext;
16+
});
17+
18+
it("should throw an error if 'if' is used in a slot", () => {
19+
const brickConf = { brick: "slot", if: false };
20+
expect(() =>
21+
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
22+
).toThrow(
23+
`Can not use "if" in a slot currently, check your template "tpl-test"`
24+
);
25+
const brickConf2 = { brick: "slot", if: "<% true %>" };
26+
expect(() =>
27+
replaceSlotWithSlottedBricks(brickConf2, mockHostContext, identity)
28+
).toThrow(
29+
`Can not use "if" in a slot currently, check your template "tpl-test"`
30+
);
31+
});
32+
33+
it("should throw an error if slot name is an expression", () => {
34+
const brickConf = {
35+
brick: "slot",
36+
properties: { name: "<% 'abc' %>" },
37+
} as BrickConf;
38+
expect(() =>
39+
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
40+
).toThrow(
41+
`Can not use an expression as slot name "<% 'abc' %>" currently, check your template "tpl-test"`
42+
);
43+
});
44+
45+
it("should throw an error if slot name is repeated", () => {
46+
const brickConf = {
47+
brick: "div",
48+
properties: { name: "repeated-slot" },
49+
} as BrickConf;
50+
mockHostContext.usedSlots.add("repeated-slot");
51+
expect(() =>
52+
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
53+
).toThrow(
54+
`Can not have multiple slots with the same name "repeated-slot", check your template "tpl-test"`
55+
);
56+
});
57+
58+
it("should return external bricks if available", () => {
59+
const brickConf = {
60+
brick: "slot",
61+
properties: { name: "slot1" },
62+
} as BrickConf;
63+
mockHostContext.externalSlots = {
64+
slot1: { type: "bricks", bricks: [{ brick: "h1" }, { brick: "h2" }] },
65+
};
66+
const result = replaceSlotWithSlottedBricks(
67+
brickConf,
68+
mockHostContext,
69+
identity
70+
);
71+
expect(result).toEqual([{ brick: "h1" }, { brick: "h2" }]);
72+
});
73+
74+
it("should return expanded default slots if no external bricks", () => {
75+
const brickConf = {
76+
brick: "slot",
77+
properties: { name: "slot1" },
78+
slots: {
79+
"": {
80+
type: "bricks",
81+
bricks: [
82+
{
83+
brick: "p",
84+
},
85+
],
86+
},
87+
oops: {
88+
type: "bricks",
89+
bricks: [
90+
{
91+
brick: "hr",
92+
},
93+
],
94+
},
95+
},
96+
} as BrickConf;
97+
const expandMock = jest.fn().mockImplementation((item) => [item]);
98+
const result = replaceSlotWithSlottedBricks(
99+
brickConf,
100+
mockHostContext,
101+
expandMock
102+
);
103+
expect(result).toEqual([{ brick: "p" }]);
104+
expect(expandMock).toHaveBeenCalledTimes(1);
105+
});
106+
107+
it("no default bricks nor external bricks", () => {
108+
const brickConf = {
109+
brick: "slot",
110+
properties: { name: "slot1" },
111+
} as BrickConf;
112+
const result = replaceSlotWithSlottedBricks(
113+
brickConf,
114+
mockHostContext,
115+
identity
116+
);
117+
expect(result).toEqual([]);
118+
});
119+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type {
2+
BrickConf,
3+
BrickConfInTemplate,
4+
UseSingleBrickConf,
5+
} from "@next-core/brick-types";
6+
import { hasOwnProperty } from "@next-core/brick-utils";
7+
import { isEvaluable } from "@next-core/cook";
8+
import type { ProxyContext } from "./expandCustomTemplate";
9+
10+
export function replaceSlotWithSlottedBricks<
11+
T extends BrickConf | UseSingleBrickConf
12+
>(
13+
brickConf: T,
14+
proxyContext: ProxyContext,
15+
expand: (item: T, proxyContext: ProxyContext) => T | T[]
16+
): T[] {
17+
// Currently, no support for `if` in a slot.
18+
if (
19+
(brickConf.if != null && !brickConf.if) ||
20+
typeof brickConf.if === "string"
21+
) {
22+
throw new Error(
23+
`Can not use "if" in a slot currently, check your template "${proxyContext.proxyBrick.type}"`
24+
);
25+
}
26+
27+
const slot = String(brickConf.properties?.name ?? "");
28+
29+
// Currently, no support for expression as slot name.
30+
if (isEvaluable(slot)) {
31+
throw new Error(
32+
`Can not use an expression as slot name "${slot}" currently, check your template "${proxyContext.proxyBrick.type}"`
33+
);
34+
}
35+
36+
// Do not repeat the same slot name in a template.
37+
if (proxyContext.usedSlots.has(slot)) {
38+
throw new Error(
39+
`Can not have multiple slots with the same name "${slot}", check your template "${proxyContext.proxyBrick.type}"`
40+
);
41+
}
42+
proxyContext.usedSlots.add(slot);
43+
44+
if (
45+
proxyContext.externalSlots &&
46+
hasOwnProperty(proxyContext.externalSlots, slot)
47+
) {
48+
const insertBricks = proxyContext.externalSlots[slot].bricks ?? [];
49+
if (insertBricks.length > 0) {
50+
return insertBricks as T[];
51+
}
52+
}
53+
54+
return ((brickConf as BrickConfInTemplate).slots?.[""]?.bricks ?? []).flatMap(
55+
(item) => expand(item as T, proxyContext)
56+
);
57+
}

packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { propertyMergeAll } from "./propertyMerge";
1919
export function setupTemplateProxy(
2020
proxyContext: Partial<ProxyContext>,
2121
ref: string,
22-
slots: SlotsConfOfBricks
22+
slots: SlotsConfOfBricks,
23+
slotted?: boolean
2324
): RuntimeBrickConfOfTplSymbols {
2425
const computedPropsFromProxy: Record<string, unknown> = {};
2526
let refForProxy: RefForProxy;
@@ -89,6 +90,12 @@ export function setupTemplateProxy(
8990
const quasisMap = new Map<string, BrickConf[][]>();
9091

9192
if (reversedProxies.slots.has(ref)) {
93+
if (slotted) {
94+
throw new Error(
95+
`Can not have proxied slot ref when the ref target has a slot element child, check your template "${proxyBrick.type}" and ref "${ref}"`
96+
);
97+
}
98+
9299
for (const item of reversedProxies.slots.get(ref)) {
93100
if (!quasisMap.has(item.refSlot)) {
94101
const quasis: BrickConf[][] = [];

0 commit comments

Comments
 (0)