Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(SliceZone): restore resolver prop #181

Merged
merged 1 commit into from
May 9, 2023
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
123 changes: 121 additions & 2 deletions src/SliceZone.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import * as React from "react";
import * as prismic from "@prismicio/client";

import { pascalCase, PascalCase } from "./lib/pascalCase";

/**
* Returns the type of a `SliceLike` type.
*
* @typeParam Slice - The Slice from which the type will be extracted.
*/
type ExtractSliceType<Slice extends SliceLike> = Slice extends SliceLikeRestV2
? Slice["slice_type"]
: Slice extends SliceLikeGraphQL
? Slice["type"]
: never;

/**
* The minimum required properties to represent a Prismic Slice from the Prismic
* Rest API V2 for the `<SliceZone>` component.
Expand Down Expand Up @@ -101,6 +114,37 @@ export type SliceComponentType<
TContext = unknown,
> = React.ComponentType<SliceComponentProps<TSlice, TContext>>;

/**
* A record of Slice types mapped to a React component. The component will be
* rendered for each instance of its Slice.
*
* @deprecated This type is no longer used by `@prismicio/react`. Prefer using
* `Record<string, SliceComponentType<any>>` instead.
* @typeParam TSlice - The type(s) of a Slice in the Slice Zone.
* @typeParam TContext - Arbitrary data made available to all Slice components.
*/
export type SliceZoneComponents<
TSlice extends SliceLike = SliceLike,
TContext = unknown,
> =
// This is purposely not wrapped in Partial to ensure a component is provided
// for all Slice types. <SliceZone> will render a default component if one is
// not provided, but it *should* be a type error if an explicit component is
// missing.
//
// If a developer purposely does not want to provide a component, they can
// assign it to the TODOSliceComponent exported from this package. This
// signals to future developers that it is a placeholder and should be
// implemented.
{
[SliceType in ExtractSliceType<TSlice>]: SliceComponentType<
Extract<TSlice, SliceLike<SliceType>> extends never
? SliceLike
: Extract<TSlice, SliceLike<SliceType>>,
TContext
>;
};

/**
* This Slice component can be used as a reminder to provide a proper
* implementation.
Expand Down Expand Up @@ -130,6 +174,54 @@ export const TODOSliceComponent = <TSlice extends SliceLike, TContext>({
}
};

/**
* Arguments for a `<SliceZone>` `resolver` function.
*/
type SliceZoneResolverArgs<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TSlice extends SliceLike = any,
> = {
/**
* The Slice to resolve to a React component.
*/
slice: TSlice;

/**
* The name of the Slice.
*/
sliceName: PascalCase<ExtractSliceType<TSlice>>;

/**
* The index of the Slice in the Slice Zone.
*/
i: number;
};

/**
* A function that determines the rendered React component for each Slice in the
* Slice Zone. If a nullish value is returned, the component will fallback to
* the `components` or `defaultComponent` props to determine the rendered
* component.
*
* @deprecated Use the `components` prop instead.
*
* @param args - Arguments for the resolver function.
*
* @returns The React component to render for a Slice.
*/
export type SliceZoneResolver<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TSlice extends SliceLike = any,
TContext = unknown,
> = (args: SliceZoneResolverArgs<TSlice>) =>
| SliceComponentType<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
TContext
>
| undefined
| null;

/**
* React props for the `<SliceZone>` component.
*
Expand All @@ -148,6 +240,19 @@ export type SliceZoneProps<TContext = unknown> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
components?: Record<string, SliceComponentType<any, TContext>>;

/**
* A function that determines the rendered React component for each Slice in
* the Slice Zone.
*
* @deprecated Use the `components` prop instead.
*
* @param args - Arguments for the resolver function.
*
* @returns The React component to render for a Slice.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolver?: SliceZoneResolver<any, TContext>;

/**
* The React component rendered if a component mapping from the `components`
* prop cannot be found.
Expand Down Expand Up @@ -179,16 +284,30 @@ export type SliceZoneProps<TContext = unknown> = {
export const SliceZone = <TContext,>({
slices = [],
components = {},
resolver,
defaultComponent = TODOSliceComponent,
context = {} as TContext,
}: SliceZoneProps<TContext>): JSX.Element => {
const renderedSlices = React.useMemo(() => {
return slices.map((slice, index) => {
const type = "slice_type" in slice ? slice.slice_type : slice.type;

const Comp =
let Comp =
components[type as keyof typeof components] || defaultComponent;

// TODO: Remove `resolver` in v3 in favor of `components`.
if (resolver) {
const resolvedComp = resolver({
slice,
sliceName: pascalCase(type),
i: index,
});

if (resolvedComp) {
Comp = resolvedComp as typeof Comp;
}
}

const key =
"id" in slice && slice.id
? slice.id
Expand All @@ -204,7 +323,7 @@ export const SliceZone = <TContext,>({
/>
);
});
}, [components, context, defaultComponent, slices]);
}, [components, context, defaultComponent, slices, resolver]);

return <>{renderedSlices}</>;
};
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ export { SliceZone, TODOSliceComponent } from "./SliceZone";
export type {
SliceComponentProps,
SliceComponentType,
SliceLikeRestV2,
SliceLikeGraphQL,
SliceLike,
SliceLikeGraphQL,
SliceLikeRestV2,
SliceZoneComponents,
SliceZoneLike,
SliceZoneProps,
} from "./SliceZone";
Expand Down
62 changes: 62 additions & 0 deletions test/SliceZone.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { it, expect, vi } from "vitest";
import { renderJSON } from "./__testutils__/renderJSON";

import { SliceZone, TODOSliceComponent, SliceComponentProps } from "../src";
import { SliceZoneResolver } from "../src/SliceZone";

type StringifySliceComponentProps = {
/**
Expand Down Expand Up @@ -201,6 +202,67 @@ it("TODO component renders null in production", () => {
process.env.NODE_ENV = originalNodeEnv;
});

it("renders components from a resolver function for backwards compatibility with next-slicezone", async () => {
const slices = [
{
slice_type: "foo_bar",
},
{
slice_type: "barFoo",
},
{
slice_type: "baz-qux",
},
] as const;

const resolver: SliceZoneResolver<(typeof slices)[number]> = ({
sliceName,
}) => {
switch (sliceName) {
case "FooBar": {
return (props) => <StringifySliceComponent id="foo_bar" {...props} />;
}

case "BarFoo": {
return (props) => <StringifySliceComponent id="barFoo" {...props} />;
}

case "BazQux": {
return (props) => <StringifySliceComponent id="baz-qux" {...props} />;
}
}
};

const actual = renderJSON(<SliceZone slices={slices} resolver={resolver} />);
const expected = renderJSON(
<>
<StringifySliceComponent
id="foo_bar"
slice={slices[0]}
index={0}
slices={slices}
context={{}}
/>
<StringifySliceComponent
id="barFoo"
slice={slices[1]}
index={1}
slices={slices}
context={{}}
/>
<StringifySliceComponent
id="baz-qux"
slice={slices[2]}
index={2}
slices={slices}
context={{}}
/>
</>,
);

expect(actual).toStrictEqual(expected);
});

it("supports the GraphQL API", () => {
const slices = [{ type: "foo" }, { type: "bar" }] as const;

Expand Down