Skip to content

Commit

Permalink
feat(): supports slots in template
Browse files Browse the repository at this point in the history
  • Loading branch information
weareoutman committed Oct 23, 2024
1 parent ff5cf0b commit c9a9bde
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = [
},
{
path: "packages/brick-kit/dist/index.esm.js",
limit: "131 KB",
limit: "135 KB",
},
{
path: "packages/brick-types/dist/index.esm.js",
Expand Down
45 changes: 32 additions & 13 deletions packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
RuntimeBrickConf,
SlotsConfOfBricks,
CustomTemplate,
type SlotConfOfBricks,
type BrickConf,
} from "@next-core/brick-types";
import { RuntimeBrick } from "../BrickNode";
import {
Expand All @@ -16,23 +18,22 @@ 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;
templateProperties: Record<string, unknown>;
externalSlots: SlotsConfOfBricks;
templateContextId: string;
proxyBrick: RuntimeBrick;
usedSlots: Set<string>;
}

interface ReversedProxies {
Expand Down Expand Up @@ -219,39 +220,57 @@ 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)
),
},
};

return newBrickConf;
}

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(

Check warning on line 250 in packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts

View check run for this annotation

Codecov / codecov/patch

packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts#L249-L250

Added lines #L249 - L250 were not covered by tests
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;

Check warning on line 265 in packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L265 was not covered by tests
};
const slots = Object.fromEntries<SlotConfOfBricks>(
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)
),
},
])
Expand All @@ -264,6 +283,6 @@ function expandBrickInTemplate(
proxyContext
),
slots,
...setupTemplateProxy(proxyContext, ref, slots),
...setupTemplateProxy(proxyContext, ref, slots, slotted),
};
}
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Original file line number Diff line number Diff line change
@@ -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)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { propertyMergeAll } from "./propertyMerge";
export function setupTemplateProxy(
proxyContext: Partial<ProxyContext>,
ref: string,
slots: SlotsConfOfBricks
slots: SlotsConfOfBricks,
slotted?: boolean
): RuntimeBrickConfOfTplSymbols {
const computedPropsFromProxy: Record<string, unknown> = {};
let refForProxy: RefForProxy;
Expand Down Expand Up @@ -89,6 +90,12 @@ export function setupTemplateProxy(
const quasisMap = new Map<string, BrickConf[][]>();

if (reversedProxies.slots.has(ref)) {
if (slotted) {
throw new Error(

Check warning on line 94 in packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L94 was not covered by tests
`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[][] = [];
Expand Down

0 comments on commit c9a9bde

Please sign in to comment.