puck
puck copied to clipboard
Expose utilities to Puck API to resolve component data on demand
Closes #1249
Description
This PR adds two new functions to the Puck API to run component resolve data on demand and handle all internal state (showing loaders, checking cache, injecting params, etc.).
Changes made
- Added a new utility function
resolveAndReplaceDatathat runs resolve data for a component and dispatches the required replace operation afterward, handling getting the latest item selector. - Added a new utility function
resolveDataByIdthat runs resolve data for a component by ID using the newresolveAndReplaceDatafunction. - Added a new utility function
resolveDataBySelectorthat runs resolve data for a component by selector using the newresolveAndReplaceDatafunction. - Added the new functions to the
usePuckhook.- Changed the
UsePuckDatatype to expect the two new functions. - Changed the
generateUsePuckutility implementation to create the two new functions. - Changed the
generateUsePuckutility to expectgetStatefrom theAppStoreApias the second parameter. This was done so it can be used in the new functions and access the latest state to get the latest item selector before dispatching the required replace action to update the component with the resolved data.
- Changed the
- Added tests for the new functions.
How to test
Test resolveDataById
- Create a config that defines two components: a
Headingwith a longresolveDataand debugging logs, and aFillercomponent that acts as filler for the page.
export const conf = {
components: {
Filler: {
render: () => <h1>Filler</h1>,
},
Heading: {
fields: { title: { type: "text" } },
defaultProps: { title: "Heading" },
resolveData: async (data) => {
const someResolveValue = await new Promise((res) =>
setTimeout(() => res(Math.floor(Math.random() * 100)), 10000)
);
console.log(`Resolved for ${data.props.id}: ${someResolveValue}`);
return {
...data,
props: { ...data.props, resolved: someResolveValue },
};
},
render: ({ id, title, resolved }) => (
<div>
<p>{id}</p>
<h1>{title}</h1>
<p>{resolved}</p>
</div>
),
},
},
};
- Render the
Puckcomponent with a Header override that adds a button to insert a new component and resolve its data, and another one to update theHeadingtitle by ID and resolve its data:
<Puck
config={config}
data={{}}
overrides={{
headerActions: ({ children }) => {
const resolveDataBySelector = usePuck(
(s) => s.resolveDataBySelector
);
const dispatch = usePuck((s) => s.dispatch);
const getSelectorForId = usePuck((s) => s.getSelectorForId);
return (
<>
<div style={{ display: "flex", gap: 16 }}>
<Button
onClick={() => {
const newItemSelector = {
index: 0,
zone: "root:default-zone",
};
dispatch({
type: "insert",
componentType: "Heading",
destinationIndex: newItemSelector.index,
destinationZone: newItemSelector.zone,
});
resolveDataBySelector(newItemSelector);
}}
>
Add component
</Button>
<form
onSubmit={(e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
const id = form.get("id")?.toString();
if (!id) return;
const selector = getSelectorForId(id);
if (!selector) return;
dispatch({
type: "replace",
destinationIndex: selector.index,
destinationZone: selector.zone,
data: {
type: "Heading",
props: { id, title: "Something new" },
},
});
resolveDataBySelector(selector);
}}
style={{ display: "flex", gap: 8 }}
>
<input name="id" type="text" placeholder="id" />
<Button type="submit">Update component</Button>
</form>
</div>
{children}
</>
);
},
}}
/>
- Navigate to the editor
- Open devtools
- Add two Filler components to the page
- Press the "Add component" button
- Move the new Heading component (while resolving) to the bottom
- Wait for the resolver to finish
- Observe that the resolved value in the console matches the one on the page
https://github.com/user-attachments/assets/3a6aaffc-1735-420e-a02d-9bcedb48e8d0
Test resolveDataBySelector
- Follow the steps from the previous test
- Copy the ID of the new Heading component from the devtools console
- Paste it into the field in the editor header and press "Update component"
- Move the new Heading component (while resolving) to the top
- Wait for the resolver to finish
- Observe that the resolved value in the console matches the one on the page
https://github.com/user-attachments/assets/101a424b-08c7-4e1b-b3e5-0944f2679ac3
Recommendations for next steps
- There are multiple places where we dispatch replace actions after resolving data that might benefit from the new
resolveAndReplaceDatautility. It might be worth refactoring them to use it. - Console and UI messages are currently hardcoded throughout the codebase. It might be good to move them to a constants module so they are all in one place, as some are repeated. This could also help prepare for future localization features.