Skip to content

Commit

Permalink
feat: add renderComponentList API
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Oct 26, 2023
1 parent 760daef commit ec985e3
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 26 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ The plugin API follows a React paradigm. Each plugin passed to the Puck editor c
- `renderRoot` (`Component`): Render the root node of the preview content
- `renderRootFields` (`Component`): Render the root fields
- `renderFields` (`Component`): Render the fields for the currently selected component
- `renderComponentList` (`Component`): Render the component list

Each render function receives three props:

- **children** (`ReactNode`): The normal contents of the root or field. You must render this.
- **children** (`ReactNode`): The normal contents of the root or field. You must render this if provided.
- **state** (`AppState`): The current application state, including data and UI state
- **dispatch** (`(action: PuckAction) => void`): The Puck dispatcher, used for making data changes or updating the UI. See the [action definitions](https://github.com/measuredco/puck/blob/main/packages/core/reducer/actions.tsx) for a full reference of available mutations.

Expand Down Expand Up @@ -241,6 +242,7 @@ The `<Puck>` component renders the Puck editor.
- **data** (`Data`): Initial data to render
- **onChange** (`(Data) => void` [optional]): Callback that triggers when the user makes a change
- **onPublish** (`(Data) => void` [optional]): Callback that triggers when the user hits the "Publish" button
- **renderComponentList** (`Component` [optional]): Render function for wrapping the component list
- **renderHeader** (`Component` [optional]): Render function for overriding the Puck header component
- **renderHeaderActions** (`Component` [optional]): Render function for overriding the Puck header actions. Use a fragment.
- **headerTitle** (`string` [optional]): Set the title shown in the header title
Expand Down Expand Up @@ -313,6 +315,11 @@ The `AppState` object stores the puck application state.
- **leftSideBarVisible** (boolean): Whether or not the left side bar is visible
- **itemSelector** (object): An object describing which item is selected
- **arrayState** (object): An object describing the internal state of array items
- **componentList** (object): An object describing the component list. Similar shape to `Config.categories`.
- **components** (`sting[]`, [optional]): Array containing the names of components in this category
- **title** (`sting`, [optional]): Title of the category
- **visible** (`boolean`, [optional]): Whether or not the category is visible in the side bar
- **expanded** (`boolean`, [optional]): Whether or not the category is expanded in the side bar

### `Data`

Expand Down
28 changes: 19 additions & 9 deletions packages/core/components/ComponentList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styles from "./styles.module.css";
import getClassNameFactory from "../../lib/get-class-name-factory";
import { Draggable } from "../Draggable";
import { DragIcon } from "../DragIcon";
import { ReactNode, useState } from "react";
import { ReactNode } from "react";
import { useAppContext } from "../Puck/context";
import { ChevronDown, ChevronUp } from "react-feather";

Expand Down Expand Up @@ -44,31 +44,41 @@ const ComponentListItem = ({
const ComponentList = ({
children,
title,
defaultExpanded = true,
id,
}: {
id: string;
children?: ReactNode;
title?: string;
defaultExpanded?: boolean;
}) => {
const { config } = useAppContext();
const { config, state, setUi } = useAppContext();

const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const { expanded = true } = state.ui.componentList[id] || {};

return (
<div className={getClassName({ isExpanded })}>
<div className={getClassName({ isExpanded: expanded })}>
{title && (
<div
className={getClassName("title")}
onClick={() => setIsExpanded(!isExpanded)}
onClick={() =>
setUi({
componentList: {
...state.ui.componentList,
[id]: {
...state.ui.componentList[id],
expanded: !expanded,
},
},
})
}
title={
isExpanded
expanded
? `Collapse${title ? ` ${title}` : ""}`
: `Expand${title ? ` ${title}` : ""}`
}
>
<div>{title}</div>
<div className={getClassName("titleIcon")}>
{isExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</div>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions packages/core/components/Puck/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const defaultAppState: AppState = {
leftSideBarVisible: true,
arrayState: {},
itemSelector: null,
componentList: {},
},
};

Expand Down
65 changes: 62 additions & 3 deletions packages/core/components/Puck/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ const PluginRenderer = ({
dispatch: (action: PuckAction) => void;
state: AppState;
plugins;
renderMethod: "renderRoot" | "renderRootFields" | "renderFields";
renderMethod:
| "renderRoot"
| "renderRootFields"
| "renderFields"
| "renderComponentList";
}) => {
return plugins
.filter((item) => item[renderMethod])
Expand All @@ -72,6 +76,7 @@ export function Puck({
onChange,
onPublish,
plugins = [],
renderComponentList,
renderHeader,
renderHeaderActions,
headerTitle,
Expand All @@ -82,6 +87,11 @@ export function Puck({
onChange?: (data: Data) => void;
onPublish: (data: Data) => void;
plugins?: Plugin[];
renderComponentList?: (props: {
children: ReactNode;
dispatch: (action: PuckAction) => void;
state: AppState;
}) => ReactElement;
renderHeader?: (props: {
children: ReactNode;
dispatch: (action: PuckAction) => void;
Expand All @@ -99,6 +109,27 @@ export function Puck({
const initialAppState: AppState = {
...defaultAppState,
data: initialData,
ui: {
...defaultAppState.ui,

// Store categories under componentList on state to allow render functions and plugins to modify
componentList: config.categories
? Object.entries(config.categories).reduce(
(acc, [categoryName, category]) => {
return {
...acc,
[categoryName]: {
title: category.title,
components: category.components,
expanded: category.defaultExpanded,
visible: category.visible,
},
};
},
{}
)
: {},
},
};

const [appState, dispatch] = useReducer<StateReducer>(
Expand Down Expand Up @@ -171,6 +202,28 @@ export function Puck({
[]
);

const ComponentListWrapper = useCallback((props) => {
const children = (
<PluginRenderer
plugins={plugins}
renderMethod="renderComponentList"
dispatch={props.dispatch}
state={props.state}
>
{props.children}
</PluginRenderer>
);

// User's render method wraps the plugin render methods
return renderComponentList
? renderComponentList({
children,
dispatch,
state: appState,
})
: children;
}, []);

const FieldWrapper = itemSelector ? ComponentFieldWrapper : PageFieldWrapper;

const rootFields = config.root?.fields || defaultPageFields;
Expand All @@ -192,7 +245,7 @@ export function Puck({
DragStart & Partial<DragUpdate>
>();

const componentList = useComponentList(config);
const componentList = useComponentList(config, appState.ui);

return (
<div className="puck">
Expand Down Expand Up @@ -428,7 +481,13 @@ export function Puck({
}}
>
<SidebarSection title="Components">
{componentList ? componentList : <ComponentList />}
<ComponentListWrapper>
{componentList ? (
componentList
) : (
<ComponentList id="all" />
)}
</ComponentListWrapper>
</SidebarSection>
<SidebarSection title="Outline">
{ctx?.activeZones &&
Expand Down
22 changes: 9 additions & 13 deletions packages/core/lib/use-component-list.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { ReactNode, useEffect, useState } from "react";
import { Config } from "../types/Config";
import { Config, UiState } from "../types/Config";
import { ComponentList } from "../components/ComponentList";

export const useComponentList = (config: Config) => {
export const useComponentList = (config: Config, ui: UiState) => {
const [componentList, setComponentList] = useState<ReactNode[]>();

useEffect(() => {
if (config.categories) {
if (ui.componentList) {
const matchedComponents: string[] = [];

let _componentList: ReactNode[];

_componentList = Object.entries(config.categories).map(
_componentList = Object.entries(ui.componentList).map(
([categoryKey, category]) => {
if (category.visible === false || !category.components) {
return null;
}

return (
<ComponentList
id={categoryKey}
key={categoryKey}
title={category.title || categoryKey}
defaultExpanded={category.defaultExpanded}
>
{category.components.map((componentName, i) => {
matchedComponents.push(componentName as string);
Expand All @@ -45,15 +45,11 @@ export const useComponentList = (config: Config) => {

if (
remainingComponents.length > 0 &&
!config.categories["other"]?.components &&
config.categories["other"]?.visible !== false
!ui.componentList.other?.components &&
ui.componentList.other?.visible !== false
) {
_componentList.push(
<ComponentList
key="other"
title={"Other"}
defaultExpanded={config.categories.other?.defaultExpanded}
>
<ComponentList id="other" key="other" title={"Other"}>
{remainingComponents.map((componentName, i) => {
return (
<ComponentList.Item
Expand All @@ -69,7 +65,7 @@ export const useComponentList = (config: Config) => {

setComponentList(_componentList);
}
}, [config.categories, config.components]);
}, [config.categories, ui.componentList]);

return componentList;
};
9 changes: 9 additions & 0 deletions packages/core/types/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ export type UiState = {
leftSideBarVisible: boolean;
itemSelector: ItemSelector | null;
arrayState: Record<string, ArrayState | undefined>;
componentList: Record<
string,
{
components?: string[];
title?: string;
visible?: boolean;
expanded?: boolean;
}
>;
};

export type AppState = { data: Data; ui: UiState };

0 comments on commit ec985e3

Please sign in to comment.