Skip to content

Commit c368f8b

Browse files
authored
Merge pull request #4530 from easyops-cn/steve/v3-tpl-slot
feat(): support slots in template
2 parents 4a012c1 + 079cd04 commit c368f8b

File tree

9 files changed

+445
-22
lines changed

9 files changed

+445
-22
lines changed

packages/runtime/src/internal/CustomTemplates/expandCustomTemplate.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type {
22
BrickConf,
33
BrickConfInTemplate,
4+
SlotConfOfBricks,
45
SlotsConfInTemplate,
56
SlotsConfOfBricks,
67
UseSingleBrickConf,
78
} from "@next-core/types";
89
import { uniqueId } from "lodash";
910
import { customTemplates } from "../../CustomTemplates.js";
1011
import { DataStore } from "../data/DataStore.js";
11-
import { RuntimeBrickConfWithTplSymbols } from "./constants.js";
1212
import { setupTemplateProxy } from "./setupTemplateProxy.js";
1313
import type {
1414
AsyncPropertyEntry,
@@ -20,6 +20,7 @@ import { setupUseBrickInTemplate } from "./setupUseBrickInTemplate.js";
2020
import { childrenToSlots } from "../Renderer.js";
2121
import { hooks } from "../Runtime.js";
2222
import type { RendererContext } from "../RendererContext.js";
23+
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks.js";
2324

2425
export function expandCustomTemplate<T extends BrickConf | UseSingleBrickConf>(
2526
tplTagName: string,
@@ -115,45 +116,63 @@ export function expandCustomTemplate<T extends BrickConf | UseSingleBrickConf>(
115116
| undefined,
116117
tplStateStoreId,
117118
hostBrick: hostBrick as TemplateHostBrick,
119+
usedSlots: new Set(),
118120
};
119121

120122
newBrickConf.slots = {
121123
"": {
122124
type: "bricks",
123-
bricks: bricks.map((item) => expandBrickInTemplate(item, hostContext)),
125+
bricks: bricks.flatMap((item) =>
126+
expandBrickInTemplate(item, hostContext)
127+
),
124128
},
125129
};
126130

127131
return newBrickConf;
128132
}
129133

130134
function expandBrickInTemplate(
131-
brickConfInTemplate: BrickConfInTemplate,
132-
hostContext: TemplateHostContext
133-
): RuntimeBrickConfWithTplSymbols {
135+
brickConfInTemplate: BrickConf,
136+
hostContext: TemplateHostContext,
137+
markSlotted?: () => void
138+
): BrickConf | BrickConf[] {
134139
// Ignore `if: null` to make `looseCheckIf` working.
135140
if (brickConfInTemplate.if === null) {
136141
delete brickConfInTemplate.if;
137142
}
143+
144+
if (brickConfInTemplate.brick === "slot") {
145+
markSlotted?.();
146+
return replaceSlotWithSlottedBricks(
147+
brickConfInTemplate,
148+
hostContext,
149+
expandBrickInTemplate
150+
);
151+
}
152+
138153
const {
139154
properties,
140155
slots: slotsInTemplate,
141156
children: childrenInTemplate,
142157
...restBrickConfInTemplate
143-
} = brickConfInTemplate;
158+
} = brickConfInTemplate as BrickConfInTemplate;
144159

145160
const transpiledSlots = childrenToSlots(
146161
childrenInTemplate,
147162
slotsInTemplate
148163
) as SlotsConfInTemplate | undefined;
149164

150-
const slots: SlotsConfOfBricks = Object.fromEntries(
165+
let slotted = false;
166+
const markChild = () => {
167+
slotted = true;
168+
};
169+
const slots = Object.fromEntries<SlotConfOfBricks>(
151170
Object.entries(transpiledSlots ?? {}).map(([slotName, slotConf]) => [
152171
slotName,
153172
{
154173
type: "bricks",
155-
bricks: (slotConf.bricks ?? []).map((item) =>
156-
expandBrickInTemplate(item, hostContext)
174+
bricks: (slotConf.bricks ?? []).flatMap((item) =>
175+
expandBrickInTemplate(item, hostContext, markChild)
157176
),
158177
},
159178
])
@@ -163,6 +182,11 @@ function expandBrickInTemplate(
163182
...restBrickConfInTemplate,
164183
properties: setupUseBrickInTemplate(properties, hostContext),
165184
slots,
166-
...setupTemplateProxy(hostContext, restBrickConfInTemplate.ref, slots),
185+
...setupTemplateProxy(
186+
hostContext,
187+
restBrickConfInTemplate.ref,
188+
slots,
189+
slotted
190+
),
167191
};
168192
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { BrickConf } from "@next-core/types";
2+
import { identity } from "lodash";
3+
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks.js";
4+
import type { TemplateHostContext } from "../interfaces.js";
5+
6+
describe("replaceSlotWithSlottedBricks", () => {
7+
let mockHostContext: TemplateHostContext;
8+
9+
beforeEach(() => {
10+
mockHostContext = {
11+
hostBrick: {
12+
type: "tpl-test",
13+
runtimeContext: {},
14+
},
15+
usedSlots: new Set(),
16+
} as unknown as TemplateHostContext;
17+
});
18+
19+
it("should throw an error if 'if' is used in a slot", () => {
20+
const brickConf = { brick: "slot", if: false };
21+
expect(() =>
22+
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
23+
).toThrow(
24+
`Can not use "if" in a slot currently, check your template "tpl-test"`
25+
);
26+
const brickConf2 = { brick: "slot", if: "<% true %>" };
27+
expect(() =>
28+
replaceSlotWithSlottedBricks(brickConf2, mockHostContext, identity)
29+
).toThrow(
30+
`Can not use "if" in a slot currently, check your template "tpl-test"`
31+
);
32+
});
33+
34+
it("should throw an error if slot name is an expression", () => {
35+
const brickConf = {
36+
brick: "slot",
37+
properties: { name: "<% 'abc' %>" },
38+
} as BrickConf;
39+
expect(() =>
40+
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
41+
).toThrow(
42+
`Can not use an expression as slot name "<% 'abc' %>" currently, check your template "tpl-test"`
43+
);
44+
});
45+
46+
it("should throw an error if slot name is repeated", () => {
47+
const brickConf = {
48+
brick: "div",
49+
properties: { name: "repeated-slot" },
50+
} as BrickConf;
51+
mockHostContext.usedSlots.add("repeated-slot");
52+
expect(() =>
53+
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
54+
).toThrow(
55+
`Can not have multiple slots with the same name "repeated-slot", check your template "tpl-test"`
56+
);
57+
});
58+
59+
it("should return external bricks if available", () => {
60+
const brickConf = {
61+
brick: "slot",
62+
properties: { name: "slot1" },
63+
} as BrickConf;
64+
mockHostContext.externalSlots = {
65+
slot1: { bricks: [{ brick: "h1" }, { brick: "h2" }] },
66+
};
67+
const result = replaceSlotWithSlottedBricks(
68+
brickConf,
69+
mockHostContext,
70+
identity
71+
);
72+
expect(result).toEqual([{ brick: "h1" }, { brick: "h2" }]);
73+
});
74+
75+
it("should return expanded default slots if no external bricks", () => {
76+
const brickConf = {
77+
brick: "slot",
78+
properties: { name: "slot1" },
79+
slots: {
80+
"": {
81+
bricks: [
82+
{
83+
brick: "p",
84+
},
85+
],
86+
},
87+
oops: {
88+
bricks: [
89+
{
90+
brick: "hr",
91+
},
92+
],
93+
},
94+
},
95+
} as BrickConf;
96+
const expandMock = jest.fn().mockImplementation((item) => [item]);
97+
const result = replaceSlotWithSlottedBricks(
98+
brickConf,
99+
mockHostContext,
100+
expandMock
101+
);
102+
expect(result).toEqual([{ brick: "p" }]);
103+
expect(expandMock).toHaveBeenCalledTimes(1);
104+
});
105+
106+
it("no default bricks nor external bricks", () => {
107+
const brickConf = {
108+
brick: "slot",
109+
properties: { name: "slot1" },
110+
} as BrickConf;
111+
const result = replaceSlotWithSlottedBricks(
112+
brickConf,
113+
mockHostContext,
114+
identity
115+
);
116+
expect(result).toEqual([]);
117+
});
118+
119+
it("should handle external bricks with forEach", () => {
120+
const brickConf = { brick: "slot" };
121+
mockHostContext.externalSlots = {
122+
"": { bricks: [{ brick: "h1" }, { brick: "h2" }] },
123+
};
124+
mockHostContext.hostBrick.runtimeContext = {
125+
forEachItem: "item",
126+
forEachIndex: 0,
127+
forEachSize: 2,
128+
} as any;
129+
const result = replaceSlotWithSlottedBricks(
130+
brickConf,
131+
mockHostContext,
132+
identity
133+
);
134+
expect(result).toEqual([
135+
{
136+
brick: "h1",
137+
slots: {},
138+
[Symbol.for("tpl.externalForEachItem")]: "item",
139+
[Symbol.for("tpl.externalForEachIndex")]: 0,
140+
[Symbol.for("tpl.externalForEachSize")]: 2,
141+
},
142+
{
143+
brick: "h2",
144+
slots: {},
145+
[Symbol.for("tpl.externalForEachItem")]: "item",
146+
[Symbol.for("tpl.externalForEachIndex")]: 0,
147+
[Symbol.for("tpl.externalForEachSize")]: 2,
148+
},
149+
]);
150+
});
151+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type {
2+
BrickConf,
3+
SlotsConfInTemplate,
4+
UseSingleBrickConf,
5+
} from "@next-core/types";
6+
import { hasOwnProperty } from "@next-core/utils/general";
7+
import { isEvaluable } from "@next-core/cook";
8+
import type { TemplateHostContext } from "../interfaces.js";
9+
import { setupTemplateExternalBricksWithForEach } from "./setupTemplateProxy.js";
10+
import { childrenToSlots } from "../Renderer.js";
11+
12+
export function replaceSlotWithSlottedBricks<
13+
T extends BrickConf | UseSingleBrickConf,
14+
>(
15+
brickConf: T,
16+
hostContext: TemplateHostContext,
17+
expand: (item: T, hostContext: TemplateHostContext) => T | T[]
18+
): T[] {
19+
// Currently, no support for `if` in a slot.
20+
if (
21+
(brickConf.if != null && !brickConf.if) ||
22+
typeof brickConf.if === "string"
23+
) {
24+
throw new Error(
25+
`Can not use "if" in a slot currently, check your template "${hostContext.hostBrick.type}"`
26+
);
27+
}
28+
29+
const slot = String(brickConf.properties?.name ?? "");
30+
31+
// Currently, no support for expression as slot name.
32+
if (isEvaluable(slot)) {
33+
throw new Error(
34+
`Can not use an expression as slot name "${slot}" currently, check your template "${hostContext.hostBrick.type}"`
35+
);
36+
}
37+
38+
// Do not repeat the same slot name in a template.
39+
if (hostContext.usedSlots.has(slot)) {
40+
throw new Error(
41+
`Can not have multiple slots with the same name "${slot}", check your template "${hostContext.hostBrick.type}"`
42+
);
43+
}
44+
hostContext.usedSlots.add(slot);
45+
46+
if (
47+
hostContext.externalSlots &&
48+
hasOwnProperty(hostContext.externalSlots, slot)
49+
) {
50+
const insertBricks = hostContext.externalSlots[slot].bricks ?? [];
51+
if (insertBricks.length > 0) {
52+
const hostCtx = hostContext.hostBrick.runtimeContext;
53+
// External bricks of a template, should not access the template internal forEach `ITEM`.
54+
// For some existing templates who is *USING* this bug, we keep the old behavior.
55+
const hostHasForEach = hasOwnProperty(hostCtx, "forEachItem");
56+
return (
57+
hostHasForEach
58+
? setupTemplateExternalBricksWithForEach(
59+
insertBricks,
60+
hostCtx.forEachItem,
61+
hostCtx.forEachIndex!,
62+
hostCtx.forEachSize!
63+
)
64+
: insertBricks
65+
) as T[];
66+
}
67+
}
68+
69+
const defaultSlots = childrenToSlots(brickConf.children, brickConf.slots) as
70+
| SlotsConfInTemplate
71+
| undefined;
72+
return (defaultSlots?.[""]?.bricks ?? []).flatMap((item) =>
73+
expand(item as T, hostContext)
74+
);
75+
}

packages/runtime/src/internal/CustomTemplates/setupTemplateProxy.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { childrenToSlots } from "../Renderer.js";
1616
export function setupTemplateProxy(
1717
hostContext: TemplateHostContext,
1818
ref: string | undefined,
19-
slots: SlotsConfOfBricks
19+
slots: SlotsConfOfBricks,
20+
slotted: boolean
2021
) {
2122
const {
2223
reversedProxies,
@@ -54,6 +55,13 @@ export function setupTemplateProxy(
5455
}
5556

5657
const slotProxies = reversedProxies.slots.get(ref);
58+
59+
if (slotProxies && slotted) {
60+
throw new Error(
61+
`Can not have proxied slot ref when the parent has a slot element child, check your template "${hostBrick.type}" and ref "${ref}"`
62+
);
63+
}
64+
5765
if (slotProxies && externalSlots) {
5866
// Use an approach like template-literal's quasis:
5967
// `quasi0${0}quais1${1}quasi2...`
@@ -131,7 +139,7 @@ export function setupTemplateProxy(
131139
}
132140

133141
// External bricks of a template, have the same forEachItem context as their host.
134-
function setupTemplateExternalBricksWithForEach(
142+
export function setupTemplateExternalBricksWithForEach(
135143
bricks: BrickConf[],
136144
forEachItem: unknown,
137145
forEachIndex: number,

0 commit comments

Comments
 (0)