Skip to content

Commit

Permalink
Adding document builders
Browse files Browse the repository at this point in the history
  • Loading branch information
cohitre committed Feb 14, 2024
1 parent d41cab7 commit 85ad0cd
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 94 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@usewaypoint/document",
"version": "0.0.1",
"version": "0.0.4",
"description": "Tools to render waypoint-style documents.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
15 changes: 15 additions & 0 deletions src/builders/buildBlockComponent.ts
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);
};
}
12 changes: 12 additions & 0 deletions src/builders/buildBlockConfigurationByIdSchema.ts
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);
}
24 changes: 24 additions & 0 deletions src/builders/buildBlockConfigurationSchema.ts
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>);
}
53 changes: 53 additions & 0 deletions src/builders/buildDocumentEditor.ts
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 });
},
};
}
41 changes: 41 additions & 0 deletions src/builders/buildDocumentReader.ts
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 });
},
};
}
41 changes: 0 additions & 41 deletions src/builders/index.tsx

This file was deleted.

6 changes: 5 additions & 1 deletion src/index.tsx
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/buildDocumentReader';
export { DocumentBlocksDictionary } from './utils';
18 changes: 18 additions & 0 deletions src/utils.ts
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;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`builders buildBlockComponent renders the specified component 1`] = `
exports[`builders/buildBlockComponent renders the specified component 1`] = `
<DocumentFragment>
<div>
TEST TEXT!
Expand Down
18 changes: 18 additions & 0 deletions tests/builder/buildBlockComponent.spec.tsx
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();
});
});
32 changes: 32 additions & 0 deletions tests/builder/buildBlockConfigurationByIdSchema.spec.tsx
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!' },
},
},
});
});
});
28 changes: 28 additions & 0 deletions tests/builder/buildBlockConfigurationSchema.spec.tsx
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!' },
},
});
});
});
Loading

0 comments on commit 85ad0cd

Please sign in to comment.