Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type {
BrickConf,
BrickConfInTemplate,
SlotConfOfBricks,
SlotsConfInTemplate,
SlotsConfOfBricks,
UseSingleBrickConf,
} from "@next-core/types";
import { uniqueId } from "lodash";
import { customTemplates } from "../../CustomTemplates.js";
import { DataStore } from "../data/DataStore.js";
import { RuntimeBrickConfWithTplSymbols } from "./constants.js";
import { setupTemplateProxy } from "./setupTemplateProxy.js";
import type {
AsyncPropertyEntry,
Expand All @@ -20,6 +20,7 @@ import { setupUseBrickInTemplate } from "./setupUseBrickInTemplate.js";
import { childrenToSlots } from "../Renderer.js";
import { hooks } from "../Runtime.js";
import type { RendererContext } from "../RendererContext.js";
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks.js";

export function expandCustomTemplate<T extends BrickConf | UseSingleBrickConf>(
tplTagName: string,
Expand Down Expand Up @@ -115,45 +116,63 @@ export function expandCustomTemplate<T extends BrickConf | UseSingleBrickConf>(
| undefined,
tplStateStoreId,
hostBrick: hostBrick as TemplateHostBrick,
usedSlots: new Set(),
};

newBrickConf.slots = {
"": {
type: "bricks",
bricks: bricks.map((item) => expandBrickInTemplate(item, hostContext)),
bricks: bricks.flatMap((item) =>
expandBrickInTemplate(item, hostContext)
),
},
};

return newBrickConf;
}

function expandBrickInTemplate(
brickConfInTemplate: BrickConfInTemplate,
hostContext: TemplateHostContext
): RuntimeBrickConfWithTplSymbols {
brickConfInTemplate: BrickConf,
hostContext: TemplateHostContext,
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,
hostContext,
expandBrickInTemplate
);
}

const {
properties,
slots: slotsInTemplate,
children: childrenInTemplate,
...restBrickConfInTemplate
} = brickConfInTemplate;
} = brickConfInTemplate as BrickConfInTemplate;

const transpiledSlots = childrenToSlots(
childrenInTemplate,
slotsInTemplate
) as SlotsConfInTemplate | undefined;

const slots: SlotsConfOfBricks = Object.fromEntries(
let slotted = false;
const markChild = () => {
slotted = true;
};
const slots = Object.fromEntries<SlotConfOfBricks>(
Object.entries(transpiledSlots ?? {}).map(([slotName, slotConf]) => [
slotName,
{
type: "bricks",
bricks: (slotConf.bricks ?? []).map((item) =>
expandBrickInTemplate(item, hostContext)
bricks: (slotConf.bricks ?? []).flatMap((item) =>
expandBrickInTemplate(item, hostContext, markChild)
),
},
])
Expand All @@ -163,6 +182,11 @@ function expandBrickInTemplate(
...restBrickConfInTemplate,
properties: setupUseBrickInTemplate(properties, hostContext),
slots,
...setupTemplateProxy(hostContext, restBrickConfInTemplate.ref, slots),
...setupTemplateProxy(
hostContext,
restBrickConfInTemplate.ref,
slots,
slotted
),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { BrickConf } from "@next-core/types";
import { identity } from "lodash";
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks.js";
import type { TemplateHostContext } from "../interfaces.js";

describe("replaceSlotWithSlottedBricks", () => {
let mockHostContext: TemplateHostContext;

beforeEach(() => {
mockHostContext = {
hostBrick: {
type: "tpl-test",
runtimeContext: {},
},
usedSlots: new Set(),
} as unknown as TemplateHostContext;
});

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: { 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: {
"": {
bricks: [
{
brick: "p",
},
],
},
oops: {
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([]);
});

it("should handle external bricks with forEach", () => {
const brickConf = { brick: "slot" };
mockHostContext.externalSlots = {
"": { bricks: [{ brick: "h1" }, { brick: "h2" }] },
};
mockHostContext.hostBrick.runtimeContext = {
forEachItem: "item",
forEachIndex: 0,
forEachSize: 2,
} as any;
const result = replaceSlotWithSlottedBricks(
brickConf,
mockHostContext,
identity
);
expect(result).toEqual([
{
brick: "h1",
slots: {},
[Symbol.for("tpl.externalForEachItem")]: "item",
[Symbol.for("tpl.externalForEachIndex")]: 0,
[Symbol.for("tpl.externalForEachSize")]: 2,
},
{
brick: "h2",
slots: {},
[Symbol.for("tpl.externalForEachItem")]: "item",
[Symbol.for("tpl.externalForEachIndex")]: 0,
[Symbol.for("tpl.externalForEachSize")]: 2,
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type {
BrickConf,
SlotsConfInTemplate,
UseSingleBrickConf,
} from "@next-core/types";
import { hasOwnProperty } from "@next-core/utils/general";
import { isEvaluable } from "@next-core/cook";
import type { TemplateHostContext } from "../interfaces.js";
import { setupTemplateExternalBricksWithForEach } from "./setupTemplateProxy.js";
import { childrenToSlots } from "../Renderer.js";

export function replaceSlotWithSlottedBricks<
T extends BrickConf | UseSingleBrickConf,
>(
brickConf: T,
hostContext: TemplateHostContext,
expand: (item: T, hostContext: TemplateHostContext) => 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 "${hostContext.hostBrick.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 "${hostContext.hostBrick.type}"`
);
}

// Do not repeat the same slot name in a template.
if (hostContext.usedSlots.has(slot)) {
throw new Error(
`Can not have multiple slots with the same name "${slot}", check your template "${hostContext.hostBrick.type}"`
);
}
hostContext.usedSlots.add(slot);

if (
hostContext.externalSlots &&
hasOwnProperty(hostContext.externalSlots, slot)
) {
const insertBricks = hostContext.externalSlots[slot].bricks ?? [];
if (insertBricks.length > 0) {
const hostCtx = hostContext.hostBrick.runtimeContext;
// External bricks of a template, should not access the template internal forEach `ITEM`.
// For some existing templates who is *USING* this bug, we keep the old behavior.
const hostHasForEach = hasOwnProperty(hostCtx, "forEachItem");
return (
hostHasForEach
? setupTemplateExternalBricksWithForEach(
insertBricks,
hostCtx.forEachItem,
hostCtx.forEachIndex!,
hostCtx.forEachSize!
)
: insertBricks
) as T[];
}
}

const defaultSlots = childrenToSlots(brickConf.children, brickConf.slots) as
| SlotsConfInTemplate
| undefined;
return (defaultSlots?.[""]?.bricks ?? []).flatMap((item) =>
expand(item as T, hostContext)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { childrenToSlots } from "../Renderer.js";
export function setupTemplateProxy(
hostContext: TemplateHostContext,
ref: string | undefined,
slots: SlotsConfOfBricks
slots: SlotsConfOfBricks,
slotted: boolean
) {
const {
reversedProxies,
Expand Down Expand Up @@ -54,6 +55,13 @@ export function setupTemplateProxy(
}

const slotProxies = reversedProxies.slots.get(ref);

if (slotProxies && slotted) {
throw new Error(
`Can not have proxied slot ref when the parent has a slot element child, check your template "${hostBrick.type}" and ref "${ref}"`
);
}

if (slotProxies && externalSlots) {
// Use an approach like template-literal's quasis:
// `quasi0${0}quais1${1}quasi2...`
Expand Down Expand Up @@ -131,7 +139,7 @@ export function setupTemplateProxy(
}

// External bricks of a template, have the same forEachItem context as their host.
function setupTemplateExternalBricksWithForEach(
export function setupTemplateExternalBricksWithForEach(
bricks: BrickConf[],
forEachItem: unknown,
forEachIndex: number,
Expand Down
Loading
Loading