feat: live updates

This commit is contained in:
Daniel Lautzenheiser
2023-07-19 16:15:46 -04:00
parent 7601111424
commit 5046dc1558
10 changed files with 58 additions and 39 deletions

View File

@ -25,7 +25,7 @@
"prepublishOnly": "yarn build ", "prepublishOnly": "yarn build ",
"prepack": "cp ../../README.md ./", "prepack": "cp ../../README.md ./",
"postpack": "rm ./README.md", "postpack": "rm ./README.md",
"serve": "webpack serve", "serve": "webpack serve --config-name configMain",
"test": "cross-env NODE_ENV=test jest", "test": "cross-env NODE_ENV=test jest",
"test:integration": "cross-env NODE_ENV=test jest -c jest.config.integration.js", "test:integration": "cross-env NODE_ENV=test jest -c jest.config.integration.js",
"test:ci": "cross-env NODE_ENV=test jest --maxWorkers=2 --coverage", "test:ci": "cross-env NODE_ENV=test jest --maxWorkers=2 --coverage",

View File

@ -212,7 +212,6 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
}, [collection, createBackup, entryDraft.entry, hasChanged]); }, [collection, createBackup, entryDraft.entry, hasChanged]);
useEntryCallback({ useEntryCallback({
hasChanged,
collection, collection,
slug, slug,
callback: () => { callback: () => {

View File

@ -186,7 +186,10 @@ const EditorPreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
const livePreviewIframe = useRef<HTMLIFrameElement>(null); const livePreviewIframe = useRef<HTMLIFrameElement>(null);
const passEventToIframe = useCallback((event: DataUpdateEvent) => { const passEventToIframe = useCallback((event: DataUpdateEvent) => {
if (livePreviewIframe.current) { if (livePreviewIframe.current) {
livePreviewIframe.current.contentWindow?.postMessage(event); livePreviewIframe.current.contentWindow?.postMessage({
message: 'data:update',
value: { fieldPath: event.detail.fieldPath, value: event.detail.value },
});
} }
}, []); }, []);

View File

@ -1 +0,0 @@
export * from './live';

View File

@ -16,7 +16,6 @@ import Registry from './lib/registry';
export * from './backends'; export * from './backends';
export * from './interface'; export * from './interface';
export * from './components';
export * from './lib'; export * from './lib';
export { default as locales } from './locales'; export { default as locales } from './locales';
export * from './widgets'; export * from './widgets';

View File

@ -1,23 +1,29 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { ValueOrNestedValue } from '@staticcms/core/interface'; import type { ValueOrNestedValue } from '@staticcms/core/interface';
import type DataUpdateEvent from '../util/events/DataEvent';
export default function useData(value: ValueOrNestedValue, path: string) { export default function useData(value: ValueOrNestedValue, path: string) {
const [data, setData] = useState(value); const [data, setData] = useState(value);
const [searchParams] = useSearchParams(); const isCms = useMemo(() => {
const isCms = searchParams.get('useCmsData') === 'true'; if (!window) {
return false;
}
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get('useCmsData') === 'true';
}, []);
const onDataChange = useCallback( const onDataChange = useCallback(
(event: DataUpdateEvent) => { (event: MessageEvent) => {
if (!isCms) { if (!isCms || event.data.message !== 'data:update') {
return; return;
} }
if (event.detail.fieldPath === path) { const { fieldPath, value } = event.data.value;
setData(event.detail.value);
if (fieldPath === path) {
setData(value);
} }
}, },
[isCms, path], [isCms, path],
@ -28,10 +34,10 @@ export default function useData(value: ValueOrNestedValue, path: string) {
return; return;
} }
window.addEventListener('data:update', onDataChange); window?.addEventListener('message', onDataChange);
return () => { return () => {
window.removeEventListener('data:update', onDataChange); window?.removeEventListener('message', onDataChange);
}; };
}, [isCms, onDataChange]); }, [isCms, onDataChange]);

View File

@ -63,18 +63,12 @@ async function handleChange(
} }
interface EntryCallbackProps { interface EntryCallbackProps {
hasChanged: boolean;
collection: Collection; collection: Collection;
slug: string | undefined; slug: string | undefined;
callback: () => void; callback: () => void;
} }
export default function useEntryCallback({ export default function useEntryCallback({ slug, collection, callback }: EntryCallbackProps) {
hasChanged,
slug,
collection,
callback,
}: EntryCallbackProps) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const entry = useAppSelector(selectEditingDraft); const entry = useAppSelector(selectEditingDraft);
@ -85,7 +79,7 @@ export default function useEntryCallback({
return; return;
} }
if (hasChanged && entry) { if (entry) {
const file = fileForEntry(collection, slug); const file = fileForEntry(collection, slug);
let updatedEntryData = entry.data; let updatedEntryData = entry.data;
@ -121,9 +115,9 @@ export default function useEntryCallback({
setLastEntryData(entry?.data); setLastEntryData(entry?.data);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [entry, hasChanged]); }, [entry]);
const debouncedRunUpdateCheck = useDebouncedCallback(runUpdateCheck, 500); const debouncedRunUpdateCheck = useDebouncedCallback(runUpdateCheck, 200);
useEffect(() => { useEffect(() => {
debouncedRunUpdateCheck(); debouncedRunUpdateCheck();

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useData } from '@staticcms/core/lib'; import useData from '@staticcms/core/lib/hooks/useData';
import type { ValueOrNestedValue } from '@staticcms/core/interface'; import type { ValueOrNestedValue } from '@staticcms/core/interface';
import type { FC } from 'react'; import type { FC } from 'react';
@ -12,7 +12,6 @@ export interface DataProps {
const Data: FC<DataProps> = ({ path, value }) => { const Data: FC<DataProps> = ({ path, value }) => {
const data = useData(value, path); const data = useData(value, path);
return <>{data}</>; return <>{data}</>;
}; };

View File

@ -12,8 +12,7 @@ function moduleNameToPath(libName) {
return path.resolve(__dirname, '..', '..', 'node_modules', libName); return path.resolve(__dirname, '..', '..', 'node_modules', libName);
} }
module.exports = { const config = {
entry: './src/index.ts',
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
devtool: 'source-map', devtool: 'source-map',
externals: isProduction externals: isProduction
@ -107,15 +106,6 @@ module.exports = {
STATIC_CMS_CORE_VERSION: JSON.stringify(`${pkg.version}${isProduction ? '' : '-dev'}`), STATIC_CMS_CORE_VERSION: JSON.stringify(`${pkg.version}${isProduction ? '' : '-dev'}`),
}), }),
].filter(Boolean), ].filter(Boolean),
output: {
publicPath: '',
path: path.resolve(__dirname, 'dist'),
filename: 'static-cms-core.js',
library: {
name: 'StaticCmsCore',
type: 'umd',
},
},
devServer: { devServer: {
static: { static: {
directory: './dev-test', directory: './dev-test',
@ -126,3 +116,33 @@ module.exports = {
hot: true, hot: true,
}, },
}; };
const configLive = Object.assign({}, config, {
name: "configLive",
entry: './src/live/index.ts',
output: {
publicPath: '',
path: path.resolve(__dirname, 'dist'),
filename: 'live.js',
library: {
name: 'StaticCmsCoreLive',
type: 'umd',
},
}
});
const configMain = Object.assign({}, config, {
name: "configMain",
entry: './src/index.ts',
output: {
publicPath: '',
path: path.resolve(__dirname, 'dist'),
filename: 'static-cms-core.js',
library: {
name: 'StaticCmsCore',
type: 'umd',
},
}
});
module.exports = [configMain, configLive]