Lesson
7
Drag and drop in Visual Editing
Allow authors to re-order blocks on page, without editing the document.
Log in to mark your progress for each Lesson and Task
The same functionality you setup in Add drag-and-drop elements can be used here for your Page Builder array. This way authors can reorder array items on the page without needing to use the document editor.
You can setup drag-and-drop for any array type field. Consider adding it to the Features and FAQs blocks as well.
Drag-and-drop support in Presentation requires the outer DOM element of an array—and every DOM element for an item within the array—to contain additional data-sanity
attributes. These attributes are created with a createDataAttribute
function exported from next-sanity
and require the ID and Type of the source document.
Additionally, for fast on-page changes, a useOptimistic
hook is provided by next-sanity
. Using this hook will require changing to a client component.
The PageBuilder
component you created in a previous lesson is where we can create and set these attributes, for the content
array and its individual blocks.
Update your
PageBuilder
component to add attributes for drag-and-dropsrc/components/PageBuilder.tsx
"use client";
import { Hero } from "@/components/blocks/Hero";import { Features } from "@/components/blocks/Features";import { SplitImage } from "@/components/blocks/SplitImage";import { FAQs } from "@/components/blocks/FAQs";import { PAGE_QUERYResult } from "@/sanity/types";import { client } from "@/sanity/lib/client";import { createDataAttribute } from "next-sanity";import { useOptimistic } from "next-sanity/hooks";
type PageBuilderProps = { content: NonNullable<PAGE_QUERYResult>["content"]; documentId: string; documentType: string;};
const { projectId, dataset, stega } = client.config();export const createDataAttributeConfig = { projectId, dataset, baseUrl: typeof stega.studioUrl === "string" ? stega.studioUrl : "",};
export function PageBuilder({ content, documentId, documentType,}: PageBuilderProps) { const blocks = useOptimistic< NonNullable<PAGE_QUERYResult>["content"] | undefined, NonNullable<PAGE_QUERYResult> >(content, (state, action) => { if (action.id === documentId) { return action?.document?.content?.map( (block) => state?.find((s) => s._key === block?._key) || block ); } return state; });
if (!Array.isArray(blocks)) { return null; }
return ( <main data-sanity={createDataAttribute({ ...createDataAttributeConfig, id: documentId, type: documentType, path: "content", }).toString()} > {blocks.map((block) => { const DragHandle = ({ children }: { children: React.ReactNode }) => ( <div data-sanity={createDataAttribute({ ...createDataAttributeConfig, id: documentId, type: documentType, path: `content[_key=="${block._key}"]`, }).toString()} > {children} </div> );
switch (block._type) { case "hero": return ( <DragHandle key={block._key}> <Hero {...block} /> </DragHandle> ); case "features": return ( <DragHandle key={block._key}> <Features {...block} /> </DragHandle> ); case "splitImage": return ( <DragHandle key={block._key}> <SplitImage {...block} /> </DragHandle> ); case "faqs": return ( <DragHandle key={block._key}> <FAQs {...block} /> </DragHandle> ); default: // This is a fallback for when we don't have a block type return <div key={block._key}>Block not found: {block._type}</div>; } })} </main> );}
The PageBuilder
component now requires the source document ID and document type.
Update your routes that load this component to include these props.
src/app/(frontend)/[slug]/page.tsx
import { PageBuilder } from "@/components/PageBuilder";import { sanityFetch } from "@/sanity/lib/live";import { PAGE_QUERY } from "@/sanity/lib/queries";
export default async function Page({ params,}: { params: Promise<{ slug: string }>;}) { const { data: page } = await sanityFetch({ query: PAGE_QUERY, params: await params, });
return page?.content ? ( <PageBuilder documentId={page._id} documentType={page._type} content={page.content} /> ) : null;}
Don't forget the home page route as well
src/app/(frontend)/page.tsx
import { PageBuilder } from "@/components/PageBuilder";import { sanityFetch } from "@/sanity/lib/live";import { HOME_PAGE_QUERY } from "@/sanity/lib/queries";
export default async function Page() { const { data: page } = await sanityFetch({ query: HOME_PAGE_QUERY, });
return page?.homePage?.content ? ( <PageBuilder documentId={page?.homePage._id} documentType={page?.homePage._type} content={page?.homePage.content} /> ) : null;}
Within Presentation you should now see the "drag handle" icon (two columns of three dots) when hovering over the outer edge of each block.
Click and hold, to drag-and-drop. Additionally, hold shift while dragging to zoom the page out and see the entire array at once.
You've created your page builder, wired it up to work on the frontend, and used the visual editor to rearrange block order.
You have built the gold standard of editorial experience for your end users. Great job!
What remains is to learn about some of the pitfalls and challenges of using the visual editor at scale, which will be covered in the final lesson.
You have 2 uncompleted tasks in this lesson
0 of 2