Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding document builders #2

Merged
merged 2 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 });
},
};
}
42 changes: 42 additions & 0 deletions src/builders/buildDocumentReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 {
useDocument,
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
Loading