diff --git a/docs/live-preview/overview.mdx b/docs/live-preview/overview.mdx index e795440f7..c0006226e 100644 --- a/docs/live-preview/overview.mdx +++ b/docs/live-preview/overview.mdx @@ -8,7 +8,7 @@ keywords: live preview, preview, live, iframe, iframe preview, visual editing, d **With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.** -Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin panel communicates with your app through `window.postMessage` events. These events are emitted every time a change is made to the document. Your app then listens for these events and re-renders itself with the data it receives. +Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin panel communicates with your app through [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events. These events are emitted every time a change is made to the document. Your app then listens for these events and re-renders itself with the data it receives. {/* IMAGE OF LIVE PREVIEW HERE */} @@ -84,7 +84,7 @@ Here is an example of using a function that returns a dynamic URL: documentInfo, locale }) => `${data.tenant.url}${ // Multi-tenant top-level domain - documentInfo.slug === 'posts' ? `/posts/${data.slug}` : `/${data.slug} + documentInfo.slug === 'posts' ? `/posts/${data.slug}` : `${data.slug !== 'home' : `/${data.slug}` : ''}` `}?locale=${locale}`, // Localization query param collections: ['pages'], }, diff --git a/examples/live-preview/payload/src/collections/Pages/index.ts b/examples/live-preview/payload/src/collections/Pages/index.ts index b1bee554b..5f92bcbe3 100644 --- a/examples/live-preview/payload/src/collections/Pages/index.ts +++ b/examples/live-preview/payload/src/collections/Pages/index.ts @@ -10,7 +10,8 @@ export const Pages: CollectionConfig = { useAsTitle: 'title', defaultColumns: ['title', 'slug', 'updatedAt'], livePreview: { - url: ({ data }) => `${process.env.PAYLOAD_PUBLIC_SITE_URL}/${data.slug}`, + url: ({ data }) => + `${process.env.PAYLOAD_PUBLIC_SITE_URL}${data.slug !== 'home' ? `/${data.slug}` : ''}`, }, }, access: { diff --git a/packages/payload/src/admin/components/views/LivePreview/Preview/index.tsx b/packages/payload/src/admin/components/views/LivePreview/Preview/index.tsx index 4479e3cde..fe80c0ea6 100644 --- a/packages/payload/src/admin/components/views/LivePreview/Preview/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/Preview/index.tsx @@ -58,13 +58,16 @@ const Preview: React.FC< const values = reduceFieldsToValues(fields, true) // TODO: only send `fieldSchemaToJSON` one time - const message = JSON.stringify({ data: values, fieldSchemaJSON, type: 'livePreview' }) + const message = JSON.stringify({ + data: values, + fieldSchemaJSON, + type: 'payload-live-preview', + }) // external window if (isPopupOpen) { setIframeHasLoaded(false) - - if (popupHasLoaded && popupRef.current) { + if (popupRef.current) { popupRef.current.postMessage(message, url) } } diff --git a/packages/payload/src/admin/components/views/LivePreview/index.tsx b/packages/payload/src/admin/components/views/LivePreview/index.tsx index 753e30c8f..81575da6e 100644 --- a/packages/payload/src/admin/components/views/LivePreview/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/index.tsx @@ -55,8 +55,8 @@ export const LivePreviewView: React.FC = (props) => { : livePreviewConfig?.url const popupState = usePopupWindow({ - eventType: 'livePreview', - href: url, + eventType: 'payload-live-preview', + url, }) const { apiURL, data, permissions } = props diff --git a/packages/payload/src/admin/components/views/LivePreview/usePopupWindow.ts b/packages/payload/src/admin/components/views/LivePreview/usePopupWindow.ts index 88dd335f6..fb27cf9fd 100644 --- a/packages/payload/src/admin/components/views/LivePreview/usePopupWindow.ts +++ b/packages/payload/src/admin/components/views/LivePreview/usePopupWindow.ts @@ -14,28 +14,29 @@ export interface PopupMessage { export const usePopupWindow = (props: { eventType?: string - href: string // eslint-disable-next-line @typescript-eslint/no-explicit-any onMessage?: (searchParams: PopupMessage['searchParams']) => Promise + url: string }): { isPopupOpen: boolean openPopupWindow: (e: React.MouseEvent) => void popupHasLoaded: boolean popupRef?: React.MutableRefObject } => { - const { eventType, href, onMessage } = props + const { eventType, onMessage, url } = props const isReceivingMessage = useRef(false) const [isOpen, setIsOpen] = useState(false) const [popupHasLoaded, setPopupHasLoaded] = useState(false) const { serverURL } = useConfig() const popupRef = useRef(null) + const hasAttachedMessageListener = useRef(false) // Optionally broadcast messages back out to the parent component useEffect(() => { const receiveMessage = async (event: MessageEvent): Promise => { if ( event.origin !== window.location.origin || - event.origin !== href || + event.origin !== url || event.origin !== serverURL ) { // console.warn(`Message received by ${event.origin}; IGNORED.`) // eslint-disable-line no-console @@ -53,12 +54,14 @@ export const usePopupWindow = (props: { } } - window.addEventListener('message', receiveMessage, false) + if (isOpen && popupRef.current) { + window.addEventListener('message', receiveMessage, false) + } return () => { window.removeEventListener('message', receiveMessage) } - }, [onMessage, eventType, href, serverURL]) + }, [onMessage, eventType, url, serverURL, isOpen]) // Customize the size, position, and style of the popup window const openPopupWindow = useCallback( @@ -93,23 +96,36 @@ export const usePopupWindow = (props: { return strCopy }, '') .slice(0, -1) // remove last ',' (comma) - const newWindow = window.open(href, '_blank', popupOptions) + + const newWindow = window.open(url, '_blank', popupOptions) + popupRef.current = newWindow + setIsOpen(true) }, - [href], + [url], ) // the only cross-origin way of detecting when a popup window has loaded // we catch a message event that the site rendered within the popup window fires // there is no way in js to add an event listener to a popup window across domains useEffect(() => { + if (hasAttachedMessageListener.current) return + hasAttachedMessageListener.current = true + window.addEventListener('message', (event) => { - if (event.origin === href && event.data === 'ready') { + const data = JSON.parse(event.data) + + if ( + url.startsWith(event.origin) && + data.type === eventType && + data.popupReady && + !popupHasLoaded + ) { setPopupHasLoaded(true) } }) - }, [href]) + }, [url, eventType, popupHasLoaded]) // this is the most stable and widely supported way to check if a popup window is no longer open // we poll its ref every x ms and use the popup window's `closed` property diff --git a/test/live-preview/config.ts b/test/live-preview/config.ts index a83b8e73c..2ff0f47ae 100644 --- a/test/live-preview/config.ts +++ b/test/live-preview/config.ts @@ -25,8 +25,8 @@ export default buildConfigWithDefaults({ // The Live Preview config is inherited from the top down url: ({ data, documentInfo }) => `http://localhost:3001${ - documentInfo.slug !== 'pages' ? `/${documentInfo.slug}` : '' - }/${data?.slug}`, + documentInfo?.slug && documentInfo.slug !== 'pages' ? `/${documentInfo.slug}` : '' + }${data?.slug && data.slug !== 'home' ? `/${data.slug}` : ''}`, breakpoints: [ { label: 'Mobile', diff --git a/test/live-preview/next-app/app/(pages)/posts/[slug]/page.client.tsx b/test/live-preview/next-app/app/(pages)/posts/[slug]/page.client.tsx new file mode 100644 index 000000000..de19a46c3 --- /dev/null +++ b/test/live-preview/next-app/app/(pages)/posts/[slug]/page.client.tsx @@ -0,0 +1,66 @@ +'use client' + +import { Post as PostType } from '@/payload-types' +import { useLivePreview } from '../../../../../../../packages/live-preview-react' +import React from 'react' +import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL' +import { Blocks } from '@/app/_components/Blocks' +import { PostHero } from '@/app/_heros/PostHero' + +export const PostClient: React.FC<{ + post: PostType +}> = ({ post: initialPost }) => { + const { data } = useLivePreview({ + initialData: initialPost, + serverURL: PAYLOAD_SERVER_URL, + depth: 2, + }) + + return ( + + + + + + ) +} diff --git a/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx b/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx index 250a43234..8a1f49898 100644 --- a/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx +++ b/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx @@ -4,18 +4,9 @@ import { notFound } from 'next/navigation' import { Post } from '../../../../payload-types' import { fetchDoc } from '../../../_api/fetchDoc' import { fetchDocs } from '../../../_api/fetchDocs' -import { Blocks } from '../../../_components/Blocks' -import { PostHero } from '../../../_heros/PostHero' - -export default async function Post(args: { - params: { - slug: string - } -}) { - const { - params: { slug = 'home' }, - } = args +import { PostClient } from './page.client' +export default async function Post({ params: { slug = '' } }) { let post: Post | null = null try { @@ -31,55 +22,7 @@ export default async function Post(args: { notFound() } - const { layout, relatedPosts } = post - - return ( - - - - - - ) + return } export async function generateStaticParams() { diff --git a/test/live-preview/next-app/app/_heros/PostHero/index.module.scss b/test/live-preview/next-app/app/_heros/PostHero/index.module.scss index e56b292d2..813094a3a 100644 --- a/test/live-preview/next-app/app/_heros/PostHero/index.module.scss +++ b/test/live-preview/next-app/app/_heros/PostHero/index.module.scss @@ -24,10 +24,6 @@ } } -.title { - margin: 0; -} - .warning { margin-bottom: calc(var(--base) * 1.5); } diff --git a/test/live-preview/next-app/app/_heros/PostHero/index.tsx b/test/live-preview/next-app/app/_heros/PostHero/index.tsx index 7268d49ff..c86b86879 100644 --- a/test/live-preview/next-app/app/_heros/PostHero/index.tsx +++ b/test/live-preview/next-app/app/_heros/PostHero/index.tsx @@ -13,13 +13,13 @@ import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL' export const PostHero: React.FC<{ post: Post }> = ({ post }) => { - const { id, title, meta: { image: metaImage, description } = {}, createdAt } = post + const { id, meta: { image: metaImage, description } = {}, createdAt } = post return (
-

{title}

+

{createdAt && (