Skip to content

Commit

Permalink
Add component render to custom pages (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-lee authored Oct 16, 2024
1 parent f2a195f commit 95cefaa
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 20 deletions.
8 changes: 5 additions & 3 deletions packages/zudoku/src/config/validators/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import z, {
ZodUnion,
} from "zod";
import { fromError } from "zod-validation-error";
import type { SlotletComponentProps } from "../../lib/components/SlotletProvider.js";
import type { ExposedComponentProps } from "../../lib/components/SlotletProvider.js";
import { DevPortalContext } from "../../lib/core/DevPortalContext.js";
import type { ApiKey } from "../../lib/plugins/api-keys/index.js";
import type { MdxComponentsType } from "../../lib/util/MdxComponents.js";
Expand Down Expand Up @@ -201,7 +201,7 @@ const ConfigSchema = z
// slotlets are a concept we are working on and not yet finalized
UNSAFE_slotlets: z.record(
z.string(),
z.custom<ReactNode | ComponentType<SlotletComponentProps>>(),
z.custom<ReactNode | ComponentType<ExposedComponentProps>>(),
),
theme: z
.object({
Expand Down Expand Up @@ -265,7 +265,9 @@ const ConfigSchema = z
customPages: z.array(
z.object({
path: z.string(),
element: z.custom<NonNullable<ReactNode>>(),
element: z.custom<NonNullable<ReactNode>>().optional(),
render: z.custom<ComponentType<ExposedComponentProps>>().optional(),
prose: z.boolean().optional(),
}),
),
plugins: z.array(z.custom<DevPortalPlugin>()),
Expand Down
2 changes: 1 addition & 1 deletion packages/zudoku/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { ZudokuConfig } from "./config/config.js";
export type { ConfigSidebar as Sidebar } from "./config/validators/InputSidebarSchema.js";
export type { SlotletComponentProps } from "./lib/components/SlotletProvider.js";
export type { ExposedComponentProps } from "./lib/components/SlotletProvider.js";
export type { MDXImport } from "./lib/plugins/markdown/index.js";
21 changes: 14 additions & 7 deletions packages/zudoku/src/lib/components/SlotletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import React, {
useContext,
} from "react";
import { isValidElementType } from "react-is";
import { useLocation } from "react-router-dom";
import {
type Location,
type NavigateFunction,
type SetURLSearchParams,
} from "react-router-dom";
import { useExposedProps } from "../util/useExposedProps.js";

export type Slotlets = Record<
string,
ReactNode | ReactElement | ComponentType<SlotletComponentProps>
ReactNode | ReactElement | ComponentType<ExposedComponentProps>
>;

const SlotletContext = React.createContext<Slotlets | undefined>({});
Expand All @@ -27,19 +33,20 @@ export const SlotletProvider = ({
);
};

export type SlotletComponentProps = {
export type ExposedComponentProps = {
location: Location;
navigate: NavigateFunction;
searchParams: URLSearchParams;
setSearchParams: SetURLSearchParams;
};

export const Slotlet = ({ name }: { name: string }) => {
const context = useContext(SlotletContext);
const componentOrElement = context?.[name];
const location = useLocation();
const slotletProps = useExposedProps();

if (isValidElementType(componentOrElement)) {
return React.createElement(componentOrElement, {
location,
});
return React.createElement(componentOrElement, slotletProps);
}

return componentOrElement as ReactNode;
Expand Down
15 changes: 15 additions & 0 deletions packages/zudoku/src/lib/plugins/custom-pages/CustomPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";
import { cn } from "../../util/cn.js";
import { useExposedProps } from "../../util/useExposedProps.js";
import type { CustomPageConfig } from "./index.js";

export const CustomPage = ({
element,
render,
prose = true,
}: Omit<CustomPageConfig, "path">) => {
const slotletProps = useExposedProps();
const content = render ? React.createElement(render, slotletProps) : element;

return <div className={cn(prose && "prose max-w-full")}>{content}</div>;
};
20 changes: 11 additions & 9 deletions packages/zudoku/src/lib/plugins/custom-pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import type { ReactNode } from "react";
import { type ComponentType, type ReactNode } from "react";
import type { RouteObject } from "react-router-dom";
import { ProseClasses } from "../../components/Markdown.js";
import { type ExposedComponentProps } from "../../components/SlotletProvider.js";
import type { DevPortalPlugin, NavigationPlugin } from "../../core/plugins.js";
import { CustomPage } from "./CustomPage.js";

type CustomPagesConfig = Array<{
export type CustomPageConfig = {
path: string;
element: ReactNode;
}>;
prose?: boolean;
element?: ReactNode;
render?: ComponentType<ExposedComponentProps>;
};

export const customPagesPlugin = (
config: CustomPagesConfig,
config: CustomPageConfig[],
): DevPortalPlugin & NavigationPlugin => {
return {
getRoutes: (): RouteObject[] =>
config.map(({ path, element }) => ({
config.map(({ path, ...props }) => ({
path,
// TODO: we should componentize prose pages
element: <div className={ProseClasses + " max-w-full"}>{element}</div>,
element: <CustomPage {...props} />,
})),
};
};
10 changes: 10 additions & 0 deletions packages/zudoku/src/lib/util/useExposedProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import type { ExposedComponentProps } from "../components/SlotletProvider.js";

export const useExposedProps = (): ExposedComponentProps => {
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();

return { location, navigate, searchParams, setSearchParams };
};

0 comments on commit 95cefaa

Please sign in to comment.