diff --git a/package-lock.json b/package-lock.json index d0c8830..7f7d86d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@usewaypoint/document", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@usewaypoint/document", - "version": "0.0.5", + "version": "0.0.6", "license": "MIT", "dependencies": { "react": "^18.2.0", diff --git a/package.json b/package.json index 694a8cb..dc18575 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@usewaypoint/document", - "version": "0.0.5", + "version": "0.0.6", "description": "Tools to render waypoint-style documents.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/builders/buildBlockComponent.ts b/src/builders/buildBlockComponent.ts index d4b6ce9..3a5cb14 100644 --- a/src/builders/buildBlockComponent.ts +++ b/src/builders/buildBlockComponent.ts @@ -1,15 +1,13 @@ import React from 'react'; -import { z } from 'zod'; -import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils'; +import { BaseZodDictionary, BlockConfiguration, DocumentBlocksDictionary } from '../utils'; +/** + * @param blocks Main DocumentBlocksDictionary + * @returns React component that can render a BlockConfiguration that is compatible with blocks + */ export default function buildBlockComponent(blocks: DocumentBlocksDictionary) { - type BaseBlockComponentProps = { - type: TType; - data: z.infer; - }; - - return function BlockComponent({ type, data }: BaseBlockComponentProps): React.ReactNode { + return function BlockComponent({ type, data }: BlockConfiguration): React.ReactNode { return React.createElement(blocks[type].Component, data); }; } diff --git a/src/builders/buildBlockConfigurationByIdSchema.ts b/src/builders/buildBlockConfigurationByIdSchema.ts index fd6dfb3..40a4799 100644 --- a/src/builders/buildBlockConfigurationByIdSchema.ts +++ b/src/builders/buildBlockConfigurationByIdSchema.ts @@ -4,6 +4,10 @@ import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils'; import buildBlockConfigurationSchema from './buildBlockConfigurationSchema'; +/** + * @param blocks Main DocumentBlocksDictionary + * @returns zod schema that can parse arbitrary objects into { [id]: BlockConfiguration } pairs + */ export default function buildBlockConfigurationByIdSchema( blocks: DocumentBlocksDictionary ) { diff --git a/src/builders/buildBlockConfigurationSchema.ts b/src/builders/buildBlockConfigurationSchema.ts index b5d5229..a5a4932 100644 --- a/src/builders/buildBlockConfigurationSchema.ts +++ b/src/builders/buildBlockConfigurationSchema.ts @@ -1,24 +1,22 @@ import { z } from 'zod'; -import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils'; +import { BaseZodDictionary, BlockConfiguration, DocumentBlocksDictionary } from '../utils'; +/** + * + * @param blocks Main DocumentBlocksDictionary + * @returns zod schema that can parse arbitary objects into a single BlockConfiguration + */ export default function buildBlockConfigurationSchema( blocks: DocumentBlocksDictionary ) { - type BaseBlockComponentProps = { - id: string; - type: TType; - data: z.infer; - }; - const blockObjects = Object.keys(blocks).map((type: keyof T) => z.object({ - id: z.string(), type: z.literal(type), data: blocks[type].schema, }) ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return z.discriminatedUnion('type', blockObjects as any).transform((v) => v as BaseBlockComponentProps); + return z.discriminatedUnion('type', blockObjects as any).transform((v) => v as BlockConfiguration); } diff --git a/src/builders/buildDocumentEditor.ts b/src/builders/buildDocumentEditor.ts index 6c991b8..65b4d17 100644 --- a/src/builders/buildDocumentEditor.ts +++ b/src/builders/buildDocumentEditor.ts @@ -6,6 +6,19 @@ import { BaseZodDictionary, BlockNotFoundError, DocumentBlocksDictionary } from import buildBlockComponent from './buildBlockComponent'; import buildBlockConfigurationByIdSchema from './buildBlockConfigurationByIdSchema'; +/** + * @typedef {Object} DocumentEditor + * @property DocumentEditorProvider - Entry point to the DocumentEditor + * @property DocumentConfigurationSchema - zod schema compatible with the value that DocumentReaderProvider expects + * @property Block - Component to render a block given an id + * @property useDocumentState - Hook that returns the current DocumentState and a setter + * @property useBlockState - Hook that returns the Block value and setter given an id + */ + +/** + * @param {DocumentBlocksDictionary} blocks root configuration + * @returns {DocumentEditor} + */ export default function buildDocumentEditor(blocks: DocumentBlocksDictionary) { const schema = buildBlockConfigurationByIdSchema(blocks); const BlockComponent = buildBlockComponent(blocks); @@ -21,28 +34,30 @@ export default function buildDocumentEditor(blocks: }; const useDocumentState = () => useContext(Context); - const useBlockState = (id: string) => { + const useBlockState = (id: string | null | undefined) => { const [value, setValue] = useDocumentState(); - return useMemo( - () => - [ - value[id], - (block: TValue[string]) => { - setValue({ ...value, [id]: block }); - }, - ] as const, - [value, setValue, id] - ); + return useMemo(() => { + if (id === null || id === undefined) { + return null; + } + return [ + value[id], + (block: TValue[string]) => { + setValue({ ...value, [id]: block }); + }, + ] as const; + }, [value, setValue, id]); }; return { useDocumentState, useBlockState, + DocumentConfigurationSchema: schema, Block: ({ id }: { id: string }) => { - const [block] = useBlockState(id); - if (!block) { + const state = useBlockState(id); + if (state === null || !state[0]) { throw new BlockNotFoundError(id); } - const { type, data } = block; + const { type, data } = state[0]; return React.createElement(BlockComponent, { type, data }); }, DocumentEditorProvider: ({ value, children }: TProviderProps) => { diff --git a/src/builders/buildDocumentReader.ts b/src/builders/buildDocumentReader.ts index 3df76e8..7992d20 100644 --- a/src/builders/buildDocumentReader.ts +++ b/src/builders/buildDocumentReader.ts @@ -6,6 +6,19 @@ import { BaseZodDictionary, BlockNotFoundError, DocumentBlocksDictionary } from import buildBlockComponent from './buildBlockComponent'; import buildBlockConfigurationByIdSchema from './buildBlockConfigurationByIdSchema'; +/** + * @typedef {Object} DocumentReader + * @property DocumentReaderProvider - Entry point to the DocumentReader + * @property DocumentConfigurationSchema - zod schema compatible with the value that DocumentReaderProvider expects + * @property Block - Component to render a block given an id + * @property useDocument - Hook that returns the current Document + * @property useBlock - Hook that returns the block given an id + */ + +/** + * @param {DocumentBlocksDictionary} blocks root configuration + * @returns {DocumentReader} + */ export default function buildDocumentReader(blocks: DocumentBlocksDictionary) { const schema = buildBlockConfigurationByIdSchema(blocks); const BlockComponent = buildBlockComponent(blocks); @@ -26,6 +39,7 @@ export default function buildDocumentReader(blocks: return { useDocument, useBlock, + DocumentConfigurationSchema: schema, Block: ({ id }: { id: string }) => { const block = useBlock(id); if (!block) { diff --git a/src/index.tsx b/src/index.tsx index b30503a..3f73871 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,4 +3,4 @@ export { default as buildBlockConfigurationSchema } from './builders/buildBlockC export { default as buildBlockConfigurationByIdSchema } from './builders/buildBlockConfigurationByIdSchema'; export { default as buildDocumentReader } from './builders/buildDocumentReader'; export { default as buildDocumentEditor } from './builders/buildDocumentEditor'; -export { DocumentBlocksDictionary } from './utils'; +export { BlockConfiguration, DocumentBlocksDictionary } from './utils'; diff --git a/src/utils.ts b/src/utils.ts index ad80bc4..2233974 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,6 +9,13 @@ export type DocumentBlocksDictionary = { }; }; +export type BlockConfiguration = { + [TType in keyof T]: { + type: TType; + data: z.infer; + }; +}[keyof T]; + export class BlockNotFoundError extends Error { blockId: string; constructor(blockId: string) { diff --git a/tests/builder/buildBlockConfigurationByIdSchema.spec.tsx b/tests/builder/buildBlockConfigurationByIdSchema.spec.tsx index 26beb0e..1f2d8d7 100644 --- a/tests/builder/buildBlockConfigurationByIdSchema.spec.tsx +++ b/tests/builder/buildBlockConfigurationByIdSchema.spec.tsx @@ -13,7 +13,6 @@ describe('builders/buildBlockConfigurationByIdSchema', () => { }); const parsedData = schema.safeParse({ 'my id': { - id: 'my id', type: 'SampleBlock', data: { text: 'Test text!' }, }, @@ -22,7 +21,6 @@ describe('builders/buildBlockConfigurationByIdSchema', () => { success: true, data: { 'my id': { - id: 'my id', type: 'SampleBlock', data: { text: 'Test text!' }, }, diff --git a/tests/builder/buildBlockConfigurationSchema.spec.tsx b/tests/builder/buildBlockConfigurationSchema.spec.tsx index d72cf1b..8c2779c 100644 --- a/tests/builder/buildBlockConfigurationSchema.spec.tsx +++ b/tests/builder/buildBlockConfigurationSchema.spec.tsx @@ -12,14 +12,12 @@ describe('builders/buildBlockConfigurationSchema', () => { }, }); const parsedData = blockConfigurationSchema.safeParse({ - id: 'my id', type: 'SampleBlock', data: { text: 'Test text!' }, }); expect(parsedData).toEqual({ success: true, data: { - id: 'my id', type: 'SampleBlock', data: { text: 'Test text!' }, },