Skip to content

Support field transforms within render - #1793

Open
FedericoBonel wants to merge 11 commits into
mainfrom
fede/puck-542-support-field-transforms-within-render
Open

Support field transforms within render#1793
FedericoBonel wants to merge 11 commits into
mainfrom
fede/puck-542-support-field-transforms-within-render

Conversation

@FedericoBonel

@FedericoBonel FedericoBonel commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #1780, Closes #1794

Description

This PR adds support for the fieldTransforms API to the Render component. Field transforms were previously only available inside Puck; now the same API can be applied at render time, after a page has been created.

This makes it possible to transform stored field values into components or computed values on every render, without polluting the saved data. Unlike resolveData, transforms can return React components (not just JSON) and re-run whenever the transforms or data change.

Transforms run through the existing useSlots walk (the same one that resolves slots), so props are transformed in a single pass at every depth: root props, component props, and slot children. Both render paths are supported: the client Render (which shares state through context) and the RSC Render from the rsc bundle (which prop-drills, so nothing pulls the page to the client).

Inside the editor, the interactive preview and the edit canvas run the same transforms, so the preview stays consistent with production output.

Changes made

  • Render and the RSC Render (ServerRender) accept an optional fieldTransforms prop and thread it through useSlots, SlotRender, and the dropzones, so it applies to root props, component props, and slot children at every depth.
    • useSlots now merges the built-in slot transform with the caller's transforms ({ ...slotTransform, ...fieldTransforms }), so user transforms compose with slots and can override them.
      • It also forces isReadOnly, since nothing is editable under Render.
      • This hook used to be used for edit mode root slots, but it doesn't use it any longer. So I'm defaulting this hook to be the render path for transforming slots.
  • The render rich text hook (useRichtextProps) is now transform-aware: it skips any field whose type has a user transform. Without this, the richtext pass would descend into a value a transform had already replaced, produce corrupt props, and crash React.
  • Fixed a pre-existing bug where a root with both a richtext field and a slot field dropped its resolved slot props, because the richtext pass was fed the raw props instead of the slot-resolved ones.
  • Split the editor preview into an interactive path (Render with transforms) and an edit path.
  • Fixed a pre-existing bug where field transforms were not applied to root fields.

How to test

  • Render a page with a text transform that converts every text value into a component with red text, and confirm it renders as that component instead of a raw string:
import { Render } from "@puckeditor/core";

export function Page() {
  return (
    <Render
      config={config}
      data={data}
      fieldTransforms={{
        text: ({ value }) => <div style={{ color: "red" }}>{ value }</div>,
      }}
    />
  );
}
  • Import Render from the rsc bundle and render it inside a Next.js Server Component with the same fieldTransforms; confirm the transforms run on the server and nothing is pulled to the client.
  • In the editor, pass fieldTransforms to Puck and toggle between edit and interactive preview; confirm root and component props render identically in both modes.

Summary by CodeRabbit

  • New Features

    • Field transforms now apply consistently during editing and rendered output.
    • Added optional fieldTransforms support to Render, including nested content, slots, drop zones, and rich-text fields.
    • Transforms can replace values with rendered components and receive read-only context.
  • Bug Fixes

    • Prevented rich-text processing from overriding values replaced by field transforms.
  • Documentation

    • Updated API and extension guides with configuration details, examples, and Render read-only behavior.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
puck-demo Ready Ready Preview Aug 12, 2026 3:15pm
puck-docs Ready Ready Preview Aug 12, 2026 3:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Field transforms now apply during <Render> and server rendering, including slots, drop zones, nested fields, and rich text. Preview routing separates edit and interactive pages. Root data replacement now handles root nodes directly. Documentation and regression tests cover the new behavior.

Changes

Render field transform support

Layer / File(s) Summary
Transform contracts and resolution
packages/core/components/Render/index.tsx, packages/core/lib/use-slots.tsx, packages/core/components/RichTextEditor/lib/use-richtext-props.tsx
Render and its context accept fieldTransforms. Slot and rich-text resolution forward transforms and skip replaced values.
Editor and preview integration
packages/core/components/Puck/components/Preview/..., packages/core/components/Puck/__tests__/field-transforms.spec.tsx
Edit mode combines configured transforms. Other preview modes render through InteractivePage. The edit canvas test verifies transformed root output.
Nested render propagation
packages/core/components/DropZone/index.tsx, packages/core/components/SlotRender/server.tsx, packages/core/components/ServerRender/..., packages/core/components/Render/__tests__/field-transforms.spec.tsx, packages/core/components/ServerRender/__tests__/field-transforms.spec.tsx
Client and server paths forward transforms through slots, drop zones, nested content, and root rendering. Tests cover text, object, array, slot, rich-text, metadata, and read-only behavior.
Root data resolution
packages/core/lib/data/resolve-and-replace-data.ts, packages/core/lib/get-selector-for-id.ts, packages/core/components/ServerRender/__tests__/field-transforms.spec.tsx
Root nodes use replaceRoot and do not generate component selectors. Root rendering regression coverage preserves transformed props and slots.
Render API documentation
apps/docs/pages/docs/api-reference/components/render.mdx, apps/docs/pages/docs/api-reference/field-transforms.mdx, apps/docs/pages/docs/extending-puck/field-transforms.mdx, apps/docs/pages/docs/api-reference/components/puck.mdx
Documentation describes Render.fieldTransforms, component-returning transforms, and isReadOnly: true under <Render>.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Preview
  participant InteractivePage
  participant Render
  participant SlotRender
  participant DropZoneRender
  Preview->>InteractivePage: render non-edit preview
  InteractivePage->>Render: provide fieldTransforms
  Render->>SlotRender: forward fieldTransforms
  Render->>DropZoneRender: provide transform context
  DropZoneRender->>SlotRender: forward fieldTransforms
Loading

Suggested reviewers: chrisvxd

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation satisfies issue #1780 by adding fieldTransforms to client and RSC Render with component and computed-value support.
Out of Scope Changes check ✅ Passed The editor refactoring and root handling fixes directly support consistent field transform behavior and are within the stated objectives.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding field transform support to rendering.
Description check ✅ Passed The description includes issue links, a clear summary, detailed changes, and concrete testing instructions relevant to the implementation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fede/puck-542-support-field-transforms-within-render

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/core/components/ServerRender/__tests__/field-transforms.spec.tsx (1)

266-292: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the ServerRender path with no root render.

Render in packages/core/components/ServerRender/index.tsx returns from two branches: one when config.root?.render exists (line 195) and one fallback (line 209). Both branches pass fieldTransforms to DropZoneRender, but only the first branch is exercised here. The client spec covers this case explicitly at packages/core/components/Render/__tests__/field-transforms.spec.tsx line 105.

Add an equivalent test with a config that omits root.render.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/components/ServerRender/__tests__/field-transforms.spec.tsx`
around lines 266 - 292, Add a ServerRender field-transform test covering the
fallback branch of Render when config.root.render is omitted. Reuse the existing
slot field configuration and custom slot transform, render with the same data,
and assert the transformed slot output appears while the built-in rendered
content does not, ensuring fieldTransforms reaches DropZoneRender in the
no-root-render path.
packages/core/components/Render/__tests__/field-transforms.spec.tsx (1)

1-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the ResizeObserver stub to Jest setup

This test uses ESM, so static imports run before line 9. The reachable ResizeObserver usage is inside useEffect, but this placement does not protect module-evaluation code. Use Jest setupFiles if the stub must exist during imports. beforeAll is sufficient only for render-time usage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/components/Render/__tests__/field-transforms.spec.tsx` around
lines 1 - 11, Move the global ResizeObserver stub from field-transforms.spec.tsx
into the Jest setupFiles configuration so it is initialized before ESM imports
and module evaluation; remove the local class and global assignment while
preserving the existing test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/pages/docs/api-reference/components/render.mdx`:
- Around line 89-90: Update the global text transform in the render example to
wrap values with a phrasing element such as span instead of div, preserving
valid content when used inside HeadingBlock’s h1.

In `@apps/docs/pages/docs/extending-puck/field-transforms.mdx`:
- Line 28: Update the Example component’s Render invocation to provide the
required config and data props defined by Render, or explicitly mark
intentionally omitted props with a // ... placeholder so copied TypeScript
compiles without missing-property errors.

In `@packages/core/components/Render/__tests__/field-transforms.spec.tsx`:
- Around line 310-312: Strengthen the negative assertion in the array transform
test by checking that the escaped raw rich-text string is absent, using the same
unescaped sentinel approach as the object test’s "hidden" assertion. Keep the
existing rows:2 positive assertion unchanged.

---

Nitpick comments:
In `@packages/core/components/Render/__tests__/field-transforms.spec.tsx`:
- Around line 1-11: Move the global ResizeObserver stub from
field-transforms.spec.tsx into the Jest setupFiles configuration so it is
initialized before ESM imports and module evaluation; remove the local class and
global assignment while preserving the existing test behavior.

In `@packages/core/components/ServerRender/__tests__/field-transforms.spec.tsx`:
- Around line 266-292: Add a ServerRender field-transform test covering the
fallback branch of Render when config.root.render is omitted. Reuse the existing
slot field configuration and custom slot transform, render with the same data,
and assert the transformed slot output appears while the built-in rendered
content does not, ensuring fieldTransforms reaches DropZoneRender in the
no-root-render path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28fe61cc-0abe-4454-a325-a5da256be995

📥 Commits

Reviewing files that changed from the base of the PR and between b4204f0 and 79f69e8.

📒 Files selected for processing (15)
  • apps/docs/pages/docs/api-reference/components/render.mdx
  • apps/docs/pages/docs/api-reference/field-transforms.mdx
  • apps/docs/pages/docs/extending-puck/field-transforms.mdx
  • packages/core/components/DropZone/index.tsx
  • packages/core/components/Puck/__tests__/field-transforms.spec.tsx
  • packages/core/components/Puck/components/Preview/components/editor-page.tsx
  • packages/core/components/Puck/components/Preview/components/interactive-page.tsx
  • packages/core/components/Puck/components/Preview/index.tsx
  • packages/core/components/Render/__tests__/field-transforms.spec.tsx
  • packages/core/components/Render/index.tsx
  • packages/core/components/RichTextEditor/lib/use-richtext-props.tsx
  • packages/core/components/ServerRender/__tests__/field-transforms.spec.tsx
  • packages/core/components/ServerRender/index.tsx
  • packages/core/components/SlotRender/server.tsx
  • packages/core/lib/use-slots.tsx

Comment thread apps/docs/pages/docs/api-reference/components/render.mdx Outdated
Comment thread apps/docs/pages/docs/extending-puck/field-transforms.mdx
Comment thread packages/core/components/Render/__tests__/field-transforms.spec.tsx

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds fieldTransforms support to the Render component (client) and the rsc/ServerRender entrypoint, so callers can transform stored field values at render time (including returning React elements) without mutating persisted page data. The change threads transforms through the existing slot-resolution walk and updates the richtext post-pass to avoid corrupting props when user transforms replace values.

Changes:

  • Add optional fieldTransforms prop to Render and ServerRender, threading it through slot rendering (useSlots, SlotRender, DropZone).
  • Update useSlots to merge built-in slot transforms with user transforms and force read-only behavior for render-time resolution.
  • Make useRichtextProps aware of transforms (skip types with user transforms) and add/adjust tests + documentation; editor preview paths updated so edit/interactive modes apply transforms consistently.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/core/lib/use-slots.tsx Updates slot/field transform application for render paths and forces read-only transform context.
packages/core/components/SlotRender/server.tsx Threads fieldTransforms through server slot rendering and richtext prop resolution.
packages/core/components/ServerRender/index.tsx Adds fieldTransforms support to the rsc/ServerRender render and dropzone paths.
packages/core/components/ServerRender/tests/field-transforms.spec.tsx Adds server render coverage for transform behavior across root/slots/zones and richtext interactions.
packages/core/components/RichTextEditor/lib/use-richtext-props.tsx Skips richtext traversal for field types handled by user transforms to prevent corrupt props.
packages/core/components/Render/index.tsx Adds fieldTransforms prop, stores it in context, and applies transforms to root/component props and slots.
packages/core/components/Render/tests/field-transforms.spec.tsx Adds client Render coverage for field transforms, root-with-richtext regression, and nested replacement cases.
packages/core/components/Puck/components/Preview/index.tsx Splits preview into edit vs interactive pages (interactive uses <Render> with transforms).
packages/core/components/Puck/components/Preview/components/interactive-page.tsx New interactive preview path that passes fieldTransforms through <Render>.
packages/core/components/Puck/components/Preview/components/editor-page.tsx Applies user transforms on the edit canvas root (consistent output vs interactive preview).
packages/core/components/Puck/tests/field-transforms.spec.tsx Adds regression coverage ensuring edit-mode canvas applies root transforms.
packages/core/components/DropZone/index.tsx Threads fieldTransforms from render context through dropzone item render + richtext props.
apps/docs/pages/docs/extending-puck/field-transforms.mdx Updates docs to reflect transforms can be applied at render time via <Render>.
apps/docs/pages/docs/api-reference/field-transforms.mdx Updates API reference to note support in both <Puck> and <Render> and isReadOnly behavior.
apps/docs/pages/docs/api-reference/components/render.mdx Documents the new fieldTransforms prop on <Render>.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/core/components/RichTextEditor/lib/use-richtext-props.tsx Outdated
Comment thread packages/core/components/Render/__tests__/field-transforms.spec.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/lib/data/resolve-and-replace-data.ts`:
- Around line 35-42: Update the replaceRoot reducer handling in the replace-root
action to merge action.ui into state.ui before invoking walkAppState, matching
the existing replace behavior while preserving unchanged UI state when no
updates are provided. Add a regression test covering root resolution through the
replaceRoot path and asserting that the requested UI changes are retained.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca91123a-3975-48e5-8de7-e883277701ae

📥 Commits

Reviewing files that changed from the base of the PR and between 0620b57 and 0b72363.

📒 Files selected for processing (4)
  • packages/core/components/Puck/components/Preview/components/edit-page.tsx
  • packages/core/components/Puck/components/Preview/index.tsx
  • packages/core/lib/data/resolve-and-replace-data.ts
  • packages/core/lib/get-selector-for-id.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/components/Puck/components/Preview/index.tsx

Comment thread packages/core/lib/data/resolve-and-replace-data.ts

@chrisvxd chrisvxd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excellent work, only a few comments on language and phrasing

Comment thread apps/docs/pages/docs/api-reference/components/render.mdx Outdated
Comment thread apps/docs/pages/docs/extending-puck/field-transforms.mdx Outdated

## Transforming at render time

Transforms can also be provided to [`<Render>`](/docs/api-reference/components/render) to apply them to a finished page.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to apply them to a finished page.

I find this phrasing a bit awkward

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to an active voice!

@@ -296,7 +296,10 @@ describe("Render fieldTransforms", () => {
type: "List",
props: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is "scaped/unscaped HTML" in this commit message? Do you mean escaped?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, typo!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it!

@@ -1,4 +1,4 @@
import { memo, useMemo } from "react";
import { useMemo } from "react";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "fix: apply inline and plugin field transforms to root fields" should be simply "fix: apply field transforms to root fields"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it!

@FedericoBonel

Copy link
Copy Markdown
Collaborator Author

Addressed your comments @chrisvxd!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inline fields don't work within root Support field transforms within Render

3 participants