-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
382 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import React from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils'; | ||
|
||
export default function buildBlockComponent<T extends BaseZodDictionary>(blocks: DocumentBlocksDictionary<T>) { | ||
type BaseBlockComponentProps<TType extends keyof T> = { | ||
type: TType; | ||
data: z.infer<T[TType]>; | ||
}; | ||
|
||
return function BlockComponent({ type, data }: BaseBlockComponentProps<keyof T>): React.ReactNode { | ||
return React.createElement(blocks[type].Component, data); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { z } from 'zod'; | ||
|
||
import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils'; | ||
|
||
import buildBlockConfigurationSchema from './buildBlockConfigurationSchema'; | ||
|
||
export default function buildBlockConfigurationByIdSchema<T extends BaseZodDictionary>( | ||
blocks: DocumentBlocksDictionary<T> | ||
) { | ||
const schema = buildBlockConfigurationSchema(blocks); | ||
return z.record(z.string(), schema); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { z } from 'zod'; | ||
|
||
import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils'; | ||
|
||
export default function buildBlockConfigurationSchema<T extends BaseZodDictionary>( | ||
blocks: DocumentBlocksDictionary<T> | ||
) { | ||
type BaseBlockComponentProps<TType extends keyof T> = { | ||
id: string; | ||
type: TType; | ||
data: z.infer<T[TType]>; | ||
}; | ||
|
||
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<keyof T>); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React, { createContext, useContext, useMemo, useState } from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import { BaseZodDictionary, BlockNotFoundError, DocumentBlocksDictionary } from '../utils'; | ||
|
||
import buildBlockComponent from './buildBlockComponent'; | ||
import buildBlockConfigurationByIdSchema from './buildBlockConfigurationByIdSchema'; | ||
|
||
export default function buildDocumentEditor<T extends BaseZodDictionary>(blocks: DocumentBlocksDictionary<T>) { | ||
const schema = buildBlockConfigurationByIdSchema(blocks); | ||
const BlockComponent = buildBlockComponent(blocks); | ||
|
||
type TValue = z.infer<typeof schema>; | ||
type TDocumentContextState = [value: TValue, setValue: (v: TValue) => void]; | ||
|
||
const Context = createContext<TDocumentContextState>([{}, () => {}]); | ||
|
||
type TProviderProps = { | ||
value: z.infer<typeof schema>; | ||
children?: Parameters<typeof Context.Provider>[0]['children']; | ||
}; | ||
|
||
const useDocumentState = () => useContext(Context); | ||
const useBlockState = (id: string) => { | ||
const [value, setValue] = useDocumentState(); | ||
return useMemo( | ||
() => | ||
[ | ||
value[id], | ||
(block: TValue[string]) => { | ||
setValue({ ...value, [id]: block }); | ||
}, | ||
] as const, | ||
[value, setValue, id] | ||
); | ||
}; | ||
return { | ||
useDocumentState, | ||
useBlockState, | ||
Block: ({ id }: { id: string }) => { | ||
const [block] = useBlockState(id); | ||
if (!block) { | ||
throw new BlockNotFoundError(id); | ||
} | ||
const { type, data } = block; | ||
return React.createElement(BlockComponent, { type, data }); | ||
}, | ||
DocumentEditorProvider: ({ value, children }: TProviderProps) => { | ||
const state = useState<TValue>(value); | ||
return React.createElement(Context.Provider, { value: state, children }); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import React, { createContext, useContext, useMemo } from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import { BaseZodDictionary, BlockNotFoundError, DocumentBlocksDictionary } from '../utils'; | ||
|
||
import buildBlockComponent from './buildBlockComponent'; | ||
import buildBlockConfigurationByIdSchema from './buildBlockConfigurationByIdSchema'; | ||
|
||
export default function buildDocumentReader<T extends BaseZodDictionary>(blocks: DocumentBlocksDictionary<T>) { | ||
const schema = buildBlockConfigurationByIdSchema(blocks); | ||
const BlockComponent = buildBlockComponent(blocks); | ||
|
||
type TValue = z.infer<typeof schema>; | ||
type TDocumentContextState = { value: TValue }; | ||
|
||
const Context = createContext<TDocumentContextState>({ value: {} }); | ||
|
||
type TProviderProps = { | ||
value: z.infer<typeof schema>; | ||
children?: Parameters<typeof Context.Provider>[0]['children']; | ||
}; | ||
|
||
const useDocument = () => useContext(Context).value; | ||
const useBlock = (id: string) => useDocument()[id]; | ||
|
||
return { | ||
useBlock, | ||
Block: ({ id }: { id: string }) => { | ||
const block = useBlock(id); | ||
if (!block) { | ||
throw new BlockNotFoundError(id); | ||
} | ||
const { type, data } = block; | ||
return React.createElement(BlockComponent, { type, data }); | ||
}, | ||
DocumentReaderProvider: ({ value, children }: TProviderProps) => { | ||
const v = useMemo(() => ({ value }), [value]); | ||
return React.createElement(Context.Provider, { value: v, children }); | ||
}, | ||
}; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
export * from './builders'; | ||
export { default as buildBlockComponent } from './builders/buildBlockComponent'; | ||
export { default as buildBlockConfigurationSchema } from './builders/buildBlockConfigurationSchema'; | ||
export { default as buildBlockConfigurationByIdSchema } from './builders/buildBlockConfigurationByIdSchema'; | ||
export { default as buildDocumentDictionaryContext } from './builders/buildDocumentReaderContext'; | ||
export { DocumentBlocksDictionary } from './utils'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
import { z } from 'zod'; | ||
|
||
export type BaseZodDictionary = { [name: string]: z.AnyZodObject }; | ||
export type DocumentBlocksDictionary<T extends BaseZodDictionary> = { | ||
[K in keyof T]: { | ||
schema: T[K]; | ||
Component: (props: z.infer<T[K]>) => React.ReactNode; | ||
}; | ||
}; | ||
|
||
export class BlockNotFoundError extends Error { | ||
blockId: string; | ||
constructor(blockId: string) { | ||
super('Could not find a block with the given blockId'); | ||
this.blockId = blockId; | ||
} | ||
} |
2 changes: 1 addition & 1 deletion
2
...builder/__snapshots__/index.spec.tsx.snap → ...shots__/buildBlockComponent.spec.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import { render } from '@testing-library/react'; | ||
|
||
import buildBlockComponent from '../../src/builders/buildBlockComponent'; | ||
|
||
describe('builders/buildBlockComponent', () => { | ||
it('renders the specified component', () => { | ||
const BlockComponent = buildBlockComponent({ | ||
SampleBlock: { | ||
schema: z.object({ text: z.string() }), | ||
Component: ({ text }) => <div>{text.toUpperCase()}</div>, | ||
}, | ||
}); | ||
expect(render(<BlockComponent type="SampleBlock" data={{ text: 'Test text!' }} />).asFragment()).toMatchSnapshot(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import React from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import buildBlockConfigurationByIdSchema from '../../src/builders/buildBlockConfigurationByIdSchema'; | ||
|
||
describe('builders/buildBlockConfigurationByIdSchema', () => { | ||
it('parses an object with id as keys and BlockConfiguration as body', () => { | ||
const schema = buildBlockConfigurationByIdSchema({ | ||
SampleBlock: { | ||
schema: z.object({ text: z.string() }), | ||
Component: ({ text }) => <div>{text.toUpperCase()}</div>, | ||
}, | ||
}); | ||
const parsedData = schema.safeParse({ | ||
'my id': { | ||
id: 'my id', | ||
type: 'SampleBlock', | ||
data: { text: 'Test text!' }, | ||
}, | ||
}); | ||
expect(parsedData).toEqual({ | ||
success: true, | ||
data: { | ||
'my id': { | ||
id: 'my id', | ||
type: 'SampleBlock', | ||
data: { text: 'Test text!' }, | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import React from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import buildBlockConfigurationSchema from '../../src/builders/buildBlockConfigurationSchema'; | ||
|
||
describe('builders/buildBlockConfigurationSchema', () => { | ||
it('builds a BlockConfiguration schema with an id, data, and type', () => { | ||
const blockConfigurationSchema = buildBlockConfigurationSchema({ | ||
SampleBlock: { | ||
schema: z.object({ text: z.string() }), | ||
Component: ({ text }) => <div>{text.toUpperCase()}</div>, | ||
}, | ||
}); | ||
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!' }, | ||
}, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import React from 'react'; | ||
import { z } from 'zod'; | ||
|
||
import { act, render } from '@testing-library/react'; | ||
|
||
import buildDocumentEditor from '../../src/builders/buildDocumentEditor'; | ||
|
||
describe('builders/buildDocumentEditor', () => { | ||
const { useBlockState, Block, DocumentEditorProvider } = buildDocumentEditor({ | ||
SampleBlock: { | ||
schema: z.object({ text: z.string() }), | ||
Component: ({ text }) => <div>{text.toUpperCase()}</div>, | ||
}, | ||
}); | ||
|
||
const SAMPLE_DATA = { | ||
'my id': { | ||
id: 'my id', | ||
type: 'SampleBlock' as const, | ||
data: { text: 'Test text!' }, | ||
}, | ||
}; | ||
|
||
describe('#useBlockState', () => { | ||
it('returns a getter and a setter tuple', () => { | ||
let value: any; | ||
let setValue: any; | ||
const ViewBlockConfig = ({ id }: { id: string }) => { | ||
const tuple = useBlockState(id); | ||
value = tuple[0]; | ||
setValue = tuple[1]; | ||
return ( | ||
<pre> | ||
{tuple[0].type} - {tuple[0].data.text} | ||
</pre> | ||
); | ||
}; | ||
|
||
expect( | ||
render( | ||
<DocumentEditorProvider value={SAMPLE_DATA}> | ||
<ViewBlockConfig id="my id" /> | ||
</DocumentEditorProvider> | ||
).queryAllByText('SampleBlock - Test text!') | ||
).toHaveLength(1); | ||
|
||
act(() => { | ||
setValue({ | ||
id: 'my id', | ||
type: 'SampleBlock' as const, | ||
data: { text: 'changed text?' }, | ||
}); | ||
}); | ||
|
||
expect( | ||
render( | ||
<DocumentEditorProvider value={SAMPLE_DATA}> | ||
<ViewBlockConfig id="my id" /> | ||
</DocumentEditorProvider> | ||
).queryAllByText('SampleBlock - changed text?') | ||
).toHaveLength(1); | ||
expect(value).toEqual({ | ||
id: 'my id', | ||
type: 'SampleBlock', | ||
data: { | ||
text: 'Test text!', | ||
}, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('#Block', () => { | ||
it('renders the component from the BlocksConfiguration', () => { | ||
expect( | ||
render( | ||
<DocumentEditorProvider value={SAMPLE_DATA}> | ||
<Block id="my id" /> | ||
</DocumentEditorProvider> | ||
).queryAllByText('TEST TEXT!') | ||
).toHaveLength(1); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.