Skip to content

Commit

Permalink
fix(SliceZone): restore resolver prop (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
angeloashmore authored May 9, 2023
1 parent 8f30e6c commit e3604d9
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 4 deletions.
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

0 comments on commit e3604d9

Please sign in to comment.