diff --git a/docs/live-preview/frontend.mdx b/docs/live-preview/frontend.mdx index 32475822e..601b89e7d 100644 --- a/docs/live-preview/frontend.mdx +++ b/docs/live-preview/frontend.mdx @@ -8,7 +8,7 @@ keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, us While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly. -Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information. +Wiring your front-end into Live Preview is easy. If your front-end application is built with React, Next.js, Vue or Nuxt.js, use the `useLivePreview` hook that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information. By default, all hooks accept the following args: @@ -36,6 +36,10 @@ And return the following values: For example, `data?.relatedPosts?.[0]?.title`. + + It is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information. + + ### React If your front-end application is built with React or Next.js, you can use the `useLivePreview` hook that Payload provides. @@ -71,11 +75,40 @@ export const PageClient: React.FC<{ } ``` - - If is important that the `depth` argument matches exactly with the depth of your initial page - request. The depth property is used to populated relationships and uploads beyond their IDs. See - [Depth](../getting-started/concepts#depth) for more information. - +### Vue + +If your front-end application is built with Vue 3 or Nuxt 3, you can use the `useLivePreview` composable that Payload provides. + +First, install the `@payloadcms/live-preview-vue` package: + +```bash +npm install @payloadcms/live-preview-vue +``` + +Then, use the `useLivePreview` hook in your Vue component: + +```vue + + + +``` ## Building your own hook diff --git a/packages/live-preview-vue/.eslintignore b/packages/live-preview-vue/.eslintignore new file mode 100644 index 000000000..247f3f12d --- /dev/null +++ b/packages/live-preview-vue/.eslintignore @@ -0,0 +1,10 @@ +.tmp +**/.git +**/.hg +**/.pnp.* +**/.svn +**/.yarn/** +**/build +**/dist/** +**/node_modules +**/temp diff --git a/packages/live-preview-vue/.eslintrc.js b/packages/live-preview-vue/.eslintrc.js new file mode 100644 index 000000000..c1b1716e7 --- /dev/null +++ b/packages/live-preview-vue/.eslintrc.js @@ -0,0 +1,37 @@ +/** @type {import('prettier').Config} */ +module.exports = { + extends: ['@payloadcms'], + overrides: [ + { + extends: ['plugin:@typescript-eslint/disable-type-checked'], + files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'], + }, + { + files: ['package.json', 'tsconfig.json'], + rules: { + 'perfectionist/sort-array-includes': 'off', + 'perfectionist/sort-astro-attributes': 'off', + 'perfectionist/sort-classes': 'off', + 'perfectionist/sort-enums': 'off', + 'perfectionist/sort-exports': 'off', + 'perfectionist/sort-imports': 'off', + 'perfectionist/sort-interfaces': 'off', + 'perfectionist/sort-jsx-props': 'off', + 'perfectionist/sort-keys': 'off', + 'perfectionist/sort-maps': 'off', + 'perfectionist/sort-named-exports': 'off', + 'perfectionist/sort-named-imports': 'off', + 'perfectionist/sort-object-types': 'off', + 'perfectionist/sort-objects': 'off', + 'perfectionist/sort-svelte-attributes': 'off', + 'perfectionist/sort-union-types': 'off', + 'perfectionist/sort-vue-attributes': 'off', + }, + }, + ], + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + root: true, +} diff --git a/packages/live-preview-vue/.prettierignore b/packages/live-preview-vue/.prettierignore new file mode 100644 index 000000000..247f3f12d --- /dev/null +++ b/packages/live-preview-vue/.prettierignore @@ -0,0 +1,10 @@ +.tmp +**/.git +**/.hg +**/.pnp.* +**/.svn +**/.yarn/** +**/build +**/dist/** +**/node_modules +**/temp diff --git a/packages/live-preview-vue/.swcrc b/packages/live-preview-vue/.swcrc new file mode 100644 index 000000000..d46b555fe --- /dev/null +++ b/packages/live-preview-vue/.swcrc @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "sourceMaps": "inline", + "jsc": { + "target": "esnext", + "parser": { + "syntax": "typescript", + "tsx": true, + "dts": true + } + }, + "module": { + "type": "commonjs" + } +} diff --git a/packages/live-preview-vue/package.json b/packages/live-preview-vue/package.json new file mode 100644 index 000000000..9c1b7c6b6 --- /dev/null +++ b/packages/live-preview-vue/package.json @@ -0,0 +1,56 @@ +{ + "name": "@payloadcms/live-preview-vue", + "version": "0.1.0", + "description": "The official live preview Vue SDK for Payload", + "repository": { + "type": "git", + "url": "https://github.com/payloadcms/payload.git", + "directory": "packages/live-preview-vue" + }, + "license": "MIT", + "homepage": "https://payloadcms.com", + "author": "Payload CMS, Inc.", + "main": "./src/index.ts", + "types": "./src/index.ts", + "type": "module", + "scripts": { + "build": "pnpm copyfiles && pnpm build:swc && pnpm build:types", + "build:swc": "swc ./src -d ./dist --config-file .swcrc", + "build:types": "tsc --emitDeclarationOnly --outDir dist", + "clean": "rimraf {dist,*.tsbuildinfo}", + "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/", + "prepublishOnly": "pnpm clean && pnpm turbo build" + }, + "dependencies": { + "@payloadcms/live-preview": "workspace:^0.x" + }, + "devDependencies": { + "@payloadcms/eslint-config": "workspace:*", + "vue": "^3.0.0", + "payload": "workspace:*" + }, + "peerDependencies": { + "vue": "^3.0.0" + }, + "exports": { + ".": { + "default": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "publishConfig": { + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "registry": "https://registry.npmjs.org/", + "types": "./dist/index.d.ts" + }, + "files": [ + "dist" + ] +} diff --git a/packages/live-preview-vue/src/index.ts b/packages/live-preview-vue/src/index.ts new file mode 100644 index 000000000..a4ba15728 --- /dev/null +++ b/packages/live-preview-vue/src/index.ts @@ -0,0 +1,58 @@ +import type { Ref } from 'vue' + +import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview' +import { onMounted, onUnmounted, ref } from 'vue' + +/** + * Vue composable to implement Payload CMS Live Preview. + * + * {@link https://payloadcms.com/docs/live-preview/frontend View the documentation} + */ +export const useLivePreview = (props: { + apiRoute?: string + depth?: number + initialData: T + serverURL: string +}): { + data: Ref + isLoading: Ref +} => { + const { apiRoute, depth, initialData, serverURL } = props + const data = ref(initialData) as Ref + const isLoading = ref(true) + const hasSentReadyMessage = ref(false) + + const onChange = (mergedData: T) => { + data.value = mergedData + isLoading.value = false + } + + let subscription: (event: MessageEvent) => void + + onMounted(() => { + subscription = subscribe({ + apiRoute, + callback: onChange, + depth, + initialData, + serverURL, + }) + + if (!hasSentReadyMessage.value) { + hasSentReadyMessage.value = true + + ready({ + serverURL, + }) + } + }) + + onUnmounted(() => { + unsubscribe(subscription) + }) + + return { + data, + isLoading, + } +} diff --git a/packages/live-preview-vue/tsconfig.json b/packages/live-preview-vue/tsconfig.json new file mode 100644 index 000000000..6972bb9fd --- /dev/null +++ b/packages/live-preview-vue/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, // Make sure typescript knows that this module depends on their references + "noEmit": false /* Do not emit outputs. */, + "emitDeclarationOnly": true, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + "jsx": "react" + }, + "exclude": [ + "dist", + "build", + "tests", + "test", + "node_modules", + ".eslintrc.js", + "src/**/*.spec.js", + "src/**/*.spec.jsx", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"], + "references": [{ "path": "../payload" }] // db-mongodb depends on payload +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ed24c3ba..846077308 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -595,6 +595,22 @@ importers: specifier: workspace:* version: link:../payload + packages/live-preview-vue: + dependencies: + '@payloadcms/live-preview': + specifier: workspace:^0.x + version: link:../live-preview + devDependencies: + '@payloadcms/eslint-config': + specifier: workspace:* + version: link:../eslint-config-payload + payload: + specifier: workspace:* + version: link:../payload + vue: + specifier: ^3.0.0 + version: 3.4.23(typescript@5.4.4) + packages/next: dependencies: '@dnd-kit/core': @@ -6446,6 +6462,79 @@ packages: undici: 5.28.4 dev: true + /@vue/compiler-core@3.4.23: + resolution: {integrity: sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==} + dependencies: + '@babel/parser': 7.24.4 + '@vue/shared': 3.4.23 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 + dev: true + + /@vue/compiler-dom@3.4.23: + resolution: {integrity: sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==} + dependencies: + '@vue/compiler-core': 3.4.23 + '@vue/shared': 3.4.23 + dev: true + + /@vue/compiler-sfc@3.4.23: + resolution: {integrity: sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==} + dependencies: + '@babel/parser': 7.24.4 + '@vue/compiler-core': 3.4.23 + '@vue/compiler-dom': 3.4.23 + '@vue/compiler-ssr': 3.4.23 + '@vue/shared': 3.4.23 + estree-walker: 2.0.2 + magic-string: 0.30.10 + postcss: 8.4.38 + source-map-js: 1.2.0 + dev: true + + /@vue/compiler-ssr@3.4.23: + resolution: {integrity: sha512-hb6Uj2cYs+tfqz71Wj6h3E5t6OKvb4MVcM2Nl5i/z1nv1gjEhw+zYaNOV+Xwn+SSN/VZM0DgANw5TuJfxfezPg==} + dependencies: + '@vue/compiler-dom': 3.4.23 + '@vue/shared': 3.4.23 + dev: true + + /@vue/reactivity@3.4.23: + resolution: {integrity: sha512-GlXR9PL+23fQ3IqnbSQ8OQKLodjqCyoCrmdLKZk3BP7jN6prWheAfU7a3mrltewTkoBm+N7qMEb372VHIkQRMQ==} + dependencies: + '@vue/shared': 3.4.23 + dev: true + + /@vue/runtime-core@3.4.23: + resolution: {integrity: sha512-FeQ9MZEXoFzFkFiw9MQQ/FWs3srvrP+SjDKSeRIiQHIhtkzoj0X4rWQlRNHbGuSwLra6pMyjAttwixNMjc/xLw==} + dependencies: + '@vue/reactivity': 3.4.23 + '@vue/shared': 3.4.23 + dev: true + + /@vue/runtime-dom@3.4.23: + resolution: {integrity: sha512-RXJFwwykZWBkMiTPSLEWU3kgVLNAfActBfWFlZd0y79FTUxexogd0PLG4HH2LfOktjRxV47Nulygh0JFXe5f9A==} + dependencies: + '@vue/runtime-core': 3.4.23 + '@vue/shared': 3.4.23 + csstype: 3.1.3 + dev: true + + /@vue/server-renderer@3.4.23(vue@3.4.23): + resolution: {integrity: sha512-LDwGHtnIzvKFNS8dPJ1SSU5Gvm36p2ck8wCZc52fc3k/IfjKcwCyrWEf0Yag/2wTFUBXrqizfhK9c/mC367dXQ==} + peerDependencies: + vue: 3.4.23 + dependencies: + '@vue/compiler-ssr': 3.4.23 + '@vue/shared': 3.4.23 + vue: 3.4.23(typescript@5.4.4) + dev: true + + /@vue/shared@3.4.23: + resolution: {integrity: sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg==} + dev: true + /@webassemblyjs/ast@1.12.1: resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} dependencies: @@ -9349,6 +9438,10 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -11870,6 +11963,12 @@ packages: hasBin: true dev: true + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -15969,6 +16068,22 @@ packages: engines: {node: '>= 0.8'} dev: false + /vue@3.4.23(typescript@5.4.4): + resolution: {integrity: sha512-X1y6yyGJ28LMUBJ0k/qIeKHstGd+BlWQEOT40x3auJFTmpIhpbKLgN7EFsqalnJXq1Km5ybDEsp6BhuWKciUDg==} + peerDependencies: + typescript: 5.4.4 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.4.23 + '@vue/compiler-sfc': 3.4.23 + '@vue/runtime-dom': 3.4.23 + '@vue/server-renderer': 3.4.23(vue@3.4.23) + '@vue/shared': 3.4.23 + typescript: 5.4.4 + dev: true + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'}