Skip to content

Commit

Permalink
feat: add transformProps lib to migrate component props
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Dec 11, 2023
1 parent 51a15fd commit 1ec2a78
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 17 deletions.
4 changes: 3 additions & 1 deletion apps/demo/config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { VerticalSpace, VerticalSpaceProps } from "./blocks/VerticalSpace";

import Root, { RootProps } from "./root";

type Props = {
export type { RootProps } from "./root";

export type Props = {
ButtonGroup: ButtonGroupProps;
Card: CardProps;
Columns: ColumnsProps;
Expand Down
1 change: 1 addition & 0 deletions apps/docs/pages/docs/api-reference/functions.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Functions

- [resolveAllData](functions/resolve-all-data) - Utility function to execute all [`resolveData` methods](/docs/api-reference/configuration/component-config#resolvedatadata-params) on a data payload.
- [transformProps](functions/transform-props) - Transform component props stored in the [data payload](/docs/api-reference/data). Use this for migrations, like prop renames.
50 changes: 50 additions & 0 deletions apps/docs/pages/docs/api-reference/functions/transform-props.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: transformProps
---

# transformProps

Transform component props stored in a [Data payload](/docs/api-reference/data). This convenience method can be used for [prop renames and other data migrations](/docs/integrating-puck/prop-migration).

This method will modify all data in [`content`](/docs/api-reference/data#content) and [`zones`](/docs/api-reference/data#zones).

```tsx copy showLineNumbers {7-10}
import { transformProps } from "@measured/puck";

const data = {
content: [{ type: "HeadingBlock", props: { title: "Hello, world" } }],
};

const updatedData = transformProps(data, {
// Rename `title` to `heading`
HeadingBlock: ({ title, ...props }) => ({ heading: title, ...props }),
});

console.log(updatedData);
// { content: [{ type: "HeadingBlock", props: { heading: "Hello, world" } }] };
```

## Args

| Param | Example | Type |
| ------------ | -------------------------------------- | -------------------------------- |
| `data` | `{ content: {}, root: {} }` | [Data](/docs/api-reference/data) |
| `transforms` | `{ HeadingBlock: (props) => (props) }` | Object |

### `data`

The [Data payload](/docs/api-reference/data) to be transformed.

### `transforms`

An object describing the transform functions for each component defined in your [`config`](/docs/api-reference/configuration/config).

- `root` is a reserved property, and can be used to update the [`root` component](/docs/api-reference/configuration/config#root) props.

## Returns

The updated [Data](/docs/api-reference/data) object.

## Notes

- It's important to consider that data may include both components with old data and new data, and write your transform accordingly.
68 changes: 68 additions & 0 deletions apps/docs/pages/docs/integrating-puck/prop-migration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Prop Migration

Renaming or removing the props passed to your components are considering breaking changes. Any existing [Data](/docs/api-reference/data) payloads that reference these props will be unable to render.

There are two strategies for dealing with this:

1. Retaining backwards-compatible props
2. Implementing a prop migration

## Retaining backwards-compatibility

The easiest way to avoid breaking changes is to implement your prop changes in a backwards compatible manor:

```tsx copy showLineNumbers {2}
const config = {
HeadingBlock: ({ title, heading }) => <h1>{heading || title}</h1>,
};
```

## Implementing a prop migration

It will often be preferrable to update the underlying [Data](/docs/api-reference/data) payload. Puck provides the [`transformProps`](/docs/api-reference/functions/transform-props) utility method to conveniently transform the props for a given component throughout the payload.

```tsx copy showLineNumbers {15-18}
import { transformProps } from "@measured/puck";

const config = {
// Renamed `title` prop to `heading`
HeadingBlock: ({ heading }) => <h1>{heading}</h1>,
};

const data = {
content: [
// HeadingBlock references the legacy `title` prop
{ type: "HeadingBlock", props: { title: "Hello, world" } },
],
};

const updatedData = transformProps(data, {
// Map `heading` to the legacy `title` prop
HeadingBlock: ({ title, ...props }) => ({ heading: title, ...props }),
});

console.log(updatedData);
// { content: [{ type: "HeadingBlock", props: { heading: "Hello, world" } }] };
```

You may choose to run this transform every time you render your content, or perform a batch operation against your database.

```tsx copy showLineNumbers filename="Example showing data being updated before rendering"
import { Puck, Render, transformProps } from "@measured/puck";

const transforms = {
HeadingBlock: ({ title, ...props }) => ({ heading: title, ...props }),
};

export const MyEditor = ({ data, config }) => (
<Puck data={transformProps(data, transforms)} config={config} />
);

export const MyPage = ({ data, config }) => (
<Render data={transformProps(data, transforms)} config={config} />
);
```

## Further reading

- [`transformProps` API reference](/docs/api-reference/functions/transform-props)
4 changes: 2 additions & 2 deletions packages/core/components/Puck/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { useComponentList } from "../../lib/use-component-list";
import { useResolvedData } from "../../lib/use-resolved-data";
import { MenuBar } from "../MenuBar";
import styles from "./styles.module.css";
import { runTransforms } from "../../transforms";
import { transformData } from "../../transforms";

const getClassName = getClassNameFactory("Puck", styles);

Expand Down Expand Up @@ -126,7 +126,7 @@ export function Puck({

const [initialAppState] = useState<AppState>(() => ({
...defaultAppState,
data: runTransforms(initialData as CurrentData, []),
data: transformData(initialData),
ui: {
...defaultAppState.ui,
// Store categories under componentList on state to allow render functions and plugins to modify
Expand Down
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export * from "./components/Puck";
export * from "./components/Render";

export * from "./lib/resolve-all-data";
export { transformProps } from "./transforms";

export { FieldLabel } from "./components/InputOrGroup";
69 changes: 55 additions & 14 deletions packages/core/transforms/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,65 @@
import { CurrentData, LegacyData } from "../types/Config";
import {
CurrentData,
Data,
DefaultComponentProps,
LegacyData,
} from "../types/Config";
import { dataTransforms } from "./data-transforms";

export type DataTransform = (
props: LegacyData & { [key: string]: any }
) => CurrentData;

type PropTransform<Components = any> = {
[ComponentName in keyof Components]: (
props: Components[ComponentName] & { [key: string]: any }
) => Components[ComponentName];
};

export const runTransforms = (
data: LegacyData,
propTransforms: PropTransform[]
): CurrentData => {
const afterDataTransforms = dataTransforms.reduce(
// type TransformFn =
type PropTransform<
Props extends DefaultComponentProps = DefaultComponentProps,
RootProps extends DefaultComponentProps = DefaultComponentProps
> = Partial<
{
[ComponentName in keyof Props]: (
props: Props[ComponentName] & { [key: string]: any }
) => Props[ComponentName];
} & { root: (props: RootProps & { [key: string]: any }) => RootProps }
>;

export function transformData(data: Data): CurrentData {
return dataTransforms?.reduce(
(acc, dataTransform) => dataTransform(acc),
data
) as CurrentData;
}

export function transformProps<
Props extends DefaultComponentProps = DefaultComponentProps,
RootProps extends DefaultComponentProps = DefaultComponentProps
>(data: Data, propTransforms: PropTransform<Props, RootProps>): CurrentData {
const afterDataTransform = transformData(data);

const mapItem = (item) => {
if (propTransforms[item.type]) {
return {
...item,
props: propTransforms[item.type]!(item.props as any),
};
}

return item;
};

const afterPropTransforms: CurrentData = {
...data,
root: propTransforms["root"]
? propTransforms["root"](afterDataTransform.root.props as any)
: afterDataTransform.root,
content: data.content.map(mapItem),
zones: Object.keys(data.zones || {}).reduce(
(acc, zoneKey) => ({
...acc,
[zoneKey]: data.zones![zoneKey].map(mapItem),
}),
{}
),
};

return afterDataTransforms;
};
return afterPropTransforms;
}

0 comments on commit 1ec2a78

Please sign in to comment.