Skip to content

Commit

Permalink
feat: refine SliceZone types and TODOSliceComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
angeloashmore committed Jul 16, 2021
1 parent 75d70c7 commit 0728187
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 28 deletions.
85 changes: 63 additions & 22 deletions src/SliceZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,44 +40,87 @@ export type SliceComponentProps<
/** The index of the Slice in the Slice Zone. */
index: number;

/** The Slice Zone to which the Slice belongs. */
slices: SliceZoneLike<TSlice>;
/** All Slices from the Slice Zone to which the Slice belongs. */
// TODO: We have to keep this list of Slices general due to circular
// reference limtiations. If we had another generic to determine the full
// union of Slice types, it would include TSlice. This causes TypeScript to
// throw a compilation error.
slices: SliceZoneLike<SliceLike>;

/** Arbitrary data passed to `<SliceZone>` and made available to all Slice components. */
context?: TContext;
context: TContext;
};

/**
* A React component to be rendered for each instance of its Slice.
*
* @typeParam TSlice - The type(s) of a Slice in the Slice Zone.
* @typeParam TContext - Arbitrary data made available to all Slice components.
*/
export type SliceComponentType<
TSlice extends SliceLike = SliceLike,
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.
*
* @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 keyof Record<
TSlice["slice_type"],
never
>]: SliceComponentType<Extract<TSlice, SliceLike<SliceType>>, TContext>;
};

/**
* React props for the `<SliceZone>` component.
*
* @typeParam TSlice - The type(s) of a Slice in the Slice Zone.
* @typeParam TContext - Arbitrary data made available to all Slice components.
*/
export type SliceZoneProps<TSlice extends SliceLike, TContext> = {
export type SliceZoneProps<
TSlice extends SliceLike = SliceLike,
TContext = unknown,
> = {
/** List of Slice data from the Slice Zone. */
slices: SliceZoneLike<TSlice>;

/** A record mapping Slice types to React components. */
components: Partial<
Record<
TSlice["slice_type"],
React.ComponentType<SliceComponentProps<TSlice, TContext>>
>
>;
components: SliceZoneComponents<TSlice, TContext>;

/** The React component rendered if a component mapping from the `components` prop cannot be found. */
defaultComponent?: React.ComponentType<SliceComponentProps>;
defaultComponent?: SliceComponentType<TSlice, TContext>;

/** Arbitrary data made available to all Slice components. */
context?: TContext;
};

/**
* The default React component rendered when a component mapping cannot be found in `<SliceZone>`.
* This Slice component can be used as a reminder to provide a proper implementation.
*
* This is also the default React component rendered when a component mapping cannot be found in `<SliceZone>`.
*/
export const MissingSliceComponent = __PRODUCTION__
export const TODOSliceComponent = __PRODUCTION__
? () => null
: ({ slice }: SliceComponentProps): JSX.Element | null => {
: <TSlice extends SliceLike, TContext>({
slice,
}: SliceComponentProps<TSlice, TContext>): JSX.Element | null => {
React.useEffect(() => {
console.warn(
`[SliceZone] Could not find a component Slice type "${slice.slice_type}"`,
Expand All @@ -87,7 +130,7 @@ export const MissingSliceComponent = __PRODUCTION__

return (
<section
data-slice-zone-mising-component=""
data-slice-zone-todo-component=""
data-slice-type={slice.slice_type}
>
Could not find a component for Slice type &ldquo;{slice.slice_type}
Expand All @@ -103,17 +146,15 @@ export const MissingSliceComponent = __PRODUCTION__
* @typeParam TContext - Arbitrary data made available to all Slice components.
*/
export const SliceZone = <TSlice extends SliceLike, TContext>({
slices,
components,
defaultComponent = MissingSliceComponent,
context,
slices = [],
components = {} as SliceZoneComponents<TSlice, TContext>,
defaultComponent = TODOSliceComponent,
context = {} as TContext,
}: SliceZoneProps<TSlice, TContext>): JSX.Element => {
const renderedSlices = React.useMemo(() => {
return slices.map((slice, index) => {
const Comp = (components[slice.slice_type as keyof typeof components] ||
defaultComponent) as React.ComponentType<
SliceComponentProps<TSlice, TContext>
>;
defaultComponent) as SliceComponentType<TSlice, TContext>;
const key = JSON.stringify(slice);

return (
Expand Down
20 changes: 14 additions & 6 deletions test/SliceZone.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as sinon from "sinon";

import { renderJSON } from "./__testutils__/renderJSON";

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

type StringifySliceComponentProps = {
/** A unique identifier for the component to differentiate this component from other instances. */
Expand Down Expand Up @@ -53,12 +53,14 @@ test("renders components for each Slice with correct component mapping", (t) =>
slice={slices[0]}
index={0}
slices={slices}
context={{}}
/>
<StringifySliceComponent
id="bar"
slice={slices[1]}
index={1}
slices={slices}
context={{}}
/>
</>,
);
Expand Down Expand Up @@ -102,17 +104,17 @@ test("passes context to each component if provided", (t) => {
t.deepEqual(actual, expected);
});

test("renders default component if component mapping is missing", (t) => {
test("renders TODO component if component mapping is missing", (t) => {
const consoleWarnStub = sinon.stub(console, "warn");

const slices = [{ slice_type: "foo" }, { slice_type: "bar" }] as const;

const actual = renderJSON(
<SliceZone
slices={slices}
// @ts-expect-error - We are leaving `bar` out of the test on purpose.
components={{
foo: (props) => <StringifySliceComponent id="foo" {...props} />,
// We are leaving `bar` out of the test on purpose.
// bar: (props) => <StringifySliceComponent id="bar" {...props} />,
}}
/>,
Expand All @@ -124,8 +126,14 @@ test("renders default component if component mapping is missing", (t) => {
slice={slices[0]}
index={0}
slices={slices}
context={{}}
/>
<TODOSliceComponent
slice={slices[1]}
index={0}
slices={slices}
context={{}}
/>
<MissingSliceComponent slice={slices[1]} index={0} slices={slices} />
</>,
);

Expand All @@ -137,13 +145,13 @@ test("renders default component if component mapping is missing", (t) => {
consoleWarnStub.restore();
});

test.skip("default component renders null in production", () => {
test.skip("TODO component renders null in production", () => {
// ts-eager does not allow esbuild configuration.
// We cannot override the `process.env.NODE_ENV` inline replacement.
// As a result, we cannot test for production currently.
});

test.skip("default component does not warn in production", () => {
test.skip("TODO component does not warn in production", () => {
// ts-eager does not allow esbuild configuration.
// We cannot override the `process.env.NODE_ENV` inline replacement.
// As a result, we cannot test for production currently.
Expand Down

0 comments on commit 0728187

Please sign in to comment.