feat: add data hook and component

This commit is contained in:
Daniel Lautzenheiser 2023-07-19 12:20:13 -04:00
parent 7f228ebbcb
commit 2834f3535b
11 changed files with 103 additions and 2 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as Data, type DataProps } from './Data';

View File

@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { currentBackend } from './backend'; import { currentBackend } from './backend';
import bootstrap from './bootstrap'; import bootstrap from './bootstrap';
import useData from './lib/hooks/useData';
import useEntries from './lib/hooks/useEntries'; import useEntries from './lib/hooks/useEntries';
import useFolderSupport from './lib/hooks/useFolderSupport'; import useFolderSupport from './lib/hooks/useFolderSupport';
import useHasChildErrors from './lib/hooks/useHasChildErrors'; import useHasChildErrors from './lib/hooks/useHasChildErrors';
@ -15,6 +16,7 @@ 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';
@ -41,6 +43,7 @@ if (typeof window !== 'undefined') {
window.useMediaFiles = window.useMediaFiles || useMediaFiles; window.useMediaFiles = window.useMediaFiles || useMediaFiles;
window.useMediaInsert = window.useMediaInsert || useMediaInsert; window.useMediaInsert = window.useMediaInsert || useMediaInsert;
window.useUUID = window.useUUID || useUUID; window.useUUID = window.useUUID || useUUID;
window.useData = window.useData || useData;
window.useNavigate = window.useNavigate || useNavigate; window.useNavigate = window.useNavigate || useNavigate;
} }

View File

@ -1,8 +1,9 @@
export { default as useData } from './useData';
export { default as useEntries } from './useEntries'; export { default as useEntries } from './useEntries';
export { default as useFolderSupport } from './useFolderSupport'; export { default as useFolderSupport } from './useFolderSupport';
export { default as useHasChildErrors } from './useHasChildErrors'; export { default as useHasChildErrors } from './useHasChildErrors';
export { default as useIsMediaAsset } from './useIsMediaAsset'; export { default as useIsMediaAsset } from './useIsMediaAsset';
export { default as useMediaAsset, useGetMediaAsset } from './useMediaAsset'; export { useGetMediaAsset, default as useMediaAsset } from './useMediaAsset';
export { default as useMediaFiles } from './useMediaFiles'; export { default as useMediaFiles } from './useMediaFiles';
export { default as useMediaInsert } from './useMediaInsert'; export { default as useMediaInsert } from './useMediaInsert';
export { default as useMediaPersist } from './useMediaPersist'; export { default as useMediaPersist } from './useMediaPersist';

View File

@ -0,0 +1,39 @@
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { ValueOrNestedValue } from '@staticcms/core/interface';
import type DataUpdateEvent from '../util/events/DataEvent';
export default function useData(value: ValueOrNestedValue, path: string) {
const [data, setData] = useState(value);
const [searchParams] = useSearchParams();
const isCms = searchParams.get('useCmsData') === 'true';
const onDataChange = useCallback(
(event: DataUpdateEvent) => {
if (!isCms) {
return;
}
if (event.detail.fieldPath === path) {
setData(event.detail.value);
}
},
[isCms, path],
) as EventListenerOrEventListenerObject;
useEffect(() => {
if (!isCms) {
return;
}
window.addEventListener('data:update', onDataChange);
return () => {
window.removeEventListener('data:update', onDataChange);
};
}, [isCms, onDataChange]);
return data ?? null;
}

View File

@ -9,6 +9,7 @@ import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
import { invokeEvent } from '../registry'; import { invokeEvent } from '../registry';
import { fileForEntry } from '../util/collection.util'; import { fileForEntry } from '../util/collection.util';
import useDebouncedCallback from './useDebouncedCallback'; import useDebouncedCallback from './useDebouncedCallback';
import DataUpdateEvent from '../util/events/DataEvent';
import type { Collection, EntryData, Field } from '@staticcms/core/interface'; import type { Collection, EntryData, Field } from '@staticcms/core/interface';
@ -25,7 +26,25 @@ async function handleChange(
let newEntry = cloneDeep(entry); let newEntry = cloneDeep(entry);
if (!isEqual(oldValue, newValue)) { if (!isEqual(oldValue, newValue)) {
newEntry = await invokeEvent({ name: 'change', collection, field: field.name, data: newEntry }); const fieldPath = path.join('.');
newEntry = await invokeEvent({
name: 'change',
collection,
field: field.name,
fieldPath,
data: newEntry,
});
const updatedValue = get(newEntry, path);
window.dispatchEvent(
new DataUpdateEvent({
field: field.name,
fieldPath,
value: updatedValue,
}),
);
} }
if ('fields' in field && field.fields) { if ('fields' in field && field.fields) {

View File

@ -507,6 +507,7 @@ export async function invokeEvent(event: {
collection: string; collection: string;
file?: string; file?: string;
field: string; field: string;
fieldPath: string;
}): Promise<EntryData>; }): Promise<EntryData>;
export async function invokeEvent(event: { export async function invokeEvent(event: {
name: AllowedEvent; name: AllowedEvent;

View File

@ -0,0 +1,13 @@
import type { ValueOrNestedValue } from '@staticcms/core/interface';
export interface DataUpdateEventProps {
field: string;
fieldPath: string;
value: ValueOrNestedValue;
}
export default class DataUpdateEvent extends CustomEvent<DataUpdateEventProps> {
constructor(detail: DataUpdateEventProps) {
super('data:update', { detail });
}
}

View File

@ -2,12 +2,14 @@ import { useEffect } from 'react';
import type AlertEvent from './events/AlertEvent'; import type AlertEvent from './events/AlertEvent';
import type ConfirmEvent from './events/ConfirmEvent'; import type ConfirmEvent from './events/ConfirmEvent';
import type DataUpdateEvent from './events/DataEvent';
import type MediaLibraryCloseEvent from './events/MediaLibraryCloseEvent'; import type MediaLibraryCloseEvent from './events/MediaLibraryCloseEvent';
interface EventMap { interface EventMap {
alert: AlertEvent; alert: AlertEvent;
confirm: ConfirmEvent; confirm: ConfirmEvent;
mediaLibraryClose: MediaLibraryCloseEvent; mediaLibraryClose: MediaLibraryCloseEvent;
'data:update': DataUpdateEvent;
} }
export function useWindowEvent<K extends keyof WindowEventMap>( export function useWindowEvent<K extends keyof WindowEventMap>(

View File

@ -13,6 +13,7 @@ import type {
useMediaFiles, useMediaFiles,
useMediaInsert, useMediaInsert,
useUUID, useUUID,
useData,
} from '../lib/hooks'; } from '../lib/hooks';
declare global { declare global {
@ -34,6 +35,7 @@ declare global {
useMediaFiles: useMediaFiles; useMediaFiles: useMediaFiles;
useMediaInsert: useMediaInsert; useMediaInsert: useMediaInsert;
useUUID: useUUID; useUUID: useUUID;
useData: useData;
useNavigate: useNavigate; useNavigate: useNavigate;
} }
} }