diff --git a/packages/core/package.json b/packages/core/package.json index e95c504..c05dd2d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,7 @@ "name": "@react-formgen/core", "description": "A headless, type-safe, customizable, and super simple React form generator.", "private": false, - "version": "0.0.0-alpha.23", + "version": "0.0.0-alpha.24", "license": "MIT", "author": "m6io", "repository": { diff --git a/packages/json-schema/package.json b/packages/json-schema/package.json index 9595999..4c1fd55 100644 --- a/packages/json-schema/package.json +++ b/packages/json-schema/package.json @@ -2,7 +2,7 @@ "name": "@react-formgen/json-schema", "description": "A headless, type-safe, customizable, and super simple React form generator.", "private": false, - "version": "0.0.0-alpha.23", + "version": "0.0.0-alpha.24", "license": "MIT", "author": "m6io", "repository": { @@ -35,7 +35,7 @@ "analyze": "vite build --mode analyze" }, "dependencies": { - "@react-formgen/core": "0.0.0-alpha.23" + "@react-formgen/core": "0.0.0-alpha.24" }, "peerDependencies": { "ajv": "^8.16.0", diff --git a/packages/json-schema/src/components/Templates.tsx b/packages/json-schema/src/components/Templates.tsx deleted file mode 100644 index 6f6220a..0000000 --- a/packages/json-schema/src/components/Templates.tsx +++ /dev/null @@ -1,1299 +0,0 @@ -import React from "react"; -import Ajv, { ErrorObject } from "ajv"; -import addFormats from "ajv-formats"; -import { - StringSchema, - ArraySchema, - NumberSchema, - BooleanSchema, - ObjectSchema, - Templates, - FormRootProps, - FormgenJSONSchema7, -} from "./types"; -import { - useFormDataAtPath, - useErrorsAtPath, - useFormContext, - useArrayTemplate, - FormState, - useRenderTemplate, -} from ".."; -import { generateInitialData, resolveSchema } from "../utils"; -import { useIsRequired } from "../hooks/useIsRequired"; - -export function useWindowSize(): { - width: number | null; - height: number | null; -} { - const [size, setSize] = React.useState<{ - width: number | null; - height: number | null; - }>({ - width: null, - height: null, - }); - - React.useLayoutEffect(() => { - const handleResize = () => { - setSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - }; - - handleResize(); - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - return size; -} - -export const ReadonlyPrimitiveTemplate: React.FC<{ - title?: string; - value?: string | number | boolean; - description?: string; -}> = ({ title, value, description }) => { - return ( -
- {title && } -
{value ?? "N/A"}
- {description && {description}} -
- ); -}; - -/** - * ReadonlyComplexTemplate - * Renders a readonly view of complex data (objects or arrays) with a title and description. - * @param {Object} props - The props for the component. - * @param {string} [props.title] - The title of the field. - * @param {string} [props.description] - The description of the field. - * @param {React.ReactNode} props.children - The child components to render. - * @returns {JSX.Element} - The readonly complex component. - */ -export const ReadonlyComplexTemplate: React.FC<{ - title?: string; - description?: string; - children: React.ReactNode; -}> = ({ title, description, children }) => { - return ( -
- {title && {title}} - {description && ( -

{description}

- )} -
{children}
-
- ); -}; - -/** - * ErrorsList - * Displays a list of validation errors for a given path. - * @param {Object} props - The props for the component. - * @param {ErrorObject[]} props.errorsAtPath - The list of error objects at the path. - * @returns {JSX.Element} - The errors list component. - */ -export const ErrorsList: React.FC<{ errorsAtPath: ErrorObject[] }> = ({ - errorsAtPath, -}) => { - return errorsAtPath.map((error, index) => ( -
- {error.message} -
- )); -}; - -const WrapperStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", -}; - -const ComplexWrapperStyle: React.CSSProperties = { - display: "flex", - flexWrap: "wrap", - gap: "1rem", -}; - -/** - * StringTemplate - * Handles switching between input, select, and date fields based on schema metadata. - * @param {Object} props - The props for the component. - * @param {StringSchema} props.schema - The schema for the string property. - * @param {string[]} props.path - The path to the string property in the form data. - * @returns {JSX.Element} - The string template component. - */ -export const StringTemplate: React.FC<{ - schema: StringSchema; - path: string[]; -}> = ({ schema, path }) => { - const stringMatchers = [ - { - matcher: (schema: StringSchema) => schema.enum || schema.oneOf, - render: () => , - // Alternative render method - // render: () => ( - // - // ), - }, - { - matcher: (schema: StringSchema) => - schema.format && ["date", "date-time"].includes(schema.format), - render: () => ( - - ), - }, - { - matcher: (schema: StringSchema) => schema.format === "email", - render: () => ( - - ), - }, - { - matcher: (schema: StringSchema) => schema.format === "uri", - render: () => ( - - ), - }, - { - matcher: () => true, - render: () => , - }, - ]; - - for (const { matcher, render } of stringMatchers) { - if (matcher(schema)) { - return render(); - } - } - return ; -}; - -/** - * InputTemplate - * Renders a text input field for string and number schemas. - * @param {Object} props - The props for the component. - * @param {StringSchema | NumberSchema} props.schema - The schema for the input field. - * @param {string[]} props.path - The path to the input field in the form data. - * @param {string} [props.htmlType] - The HTML input type (default: "text"). - * @returns {JSX.Element} - The input field component. - */ -export const InputTemplate: React.FC<{ - schema: StringSchema | NumberSchema; - path: string[]; - htmlType?: string; -}> = ({ schema, path, htmlType = "text" }) => { - const [valueAtPath, setValueAtPath] = useFormDataAtPath(path); - const errorsAtPath = useErrorsAtPath(path); - const readonly = useFormContext((state: FormState) => state.readonly); - const size = useWindowSize(); - const required = useIsRequired(path); - - if (readonly) { - return ( - - ); - } - - return ( -
640 ? "min-content" : "100%", - ...WrapperStyle, - }} - > - {schema.title && ( - - )} - setValueAtPath(e.target.value ? e.target.value : null)} - placeholder={schema.title || ""} - style={{ - width: size.width && size.width > 640 ? "12rem" : "100%", - padding: "8px", - border: "1px solid #d1d5db", - borderRadius: "0.375rem", - boxSizing: "border-box", - }} - /> - {schema.description && {schema.description}} - {errorsAtPath && } -
- ); -}; - -// Memoized MultipleChoiceOptionTemplate to prevent re-renders unless its props change -const MultipleChoiceOptionTemplate: React.FC<{ - value: string; - title: string; - checked: boolean; - onChange: (event: React.ChangeEvent) => void; - disabled?: boolean; -}> = React.memo(({ value, title, checked, onChange, disabled }) => { - return ( - - ); -}); - -/** - * MultipleChoiceTemplate - * Renders a multiple-choice field as radio buttons for string schemas with enum or oneOf options. - * @param {Object} props - The props for the component. - * @param {StringSchema} props.schema - The schema for the multiple-choice field. - * @param {string[]} props.path - The path to the field in the form data. - * @returns {JSX.Element} - The multiple-choice field component. - */ -export const MultipleChoiceTemplate: React.FC<{ - schema: StringSchema; - path: string[]; -}> = ({ schema, path }) => { - const [valueAtPath, setValueAtPath] = useFormDataAtPath(path); - const errorsAtPath = useErrorsAtPath(path); - const readonly = useFormContext((state: FormState) => state.readonly); - const required = useIsRequired(path); - - const handleChange = React.useCallback( - (event: React.ChangeEvent) => { - setValueAtPath(event.target.value); - }, - [setValueAtPath] - ); - - // Memoize the options to prevent unnecessary recalculations - const opts: { value: string; title: string }[] = React.useMemo(() => { - const options: { value: string; title: string }[] = []; - if (schema.enum) { - schema.enum.forEach((opt) => options.push({ value: opt, title: opt })); - } else if (schema.oneOf) { - schema.oneOf.forEach((opt) => - options.push({ value: opt.const, title: opt.title ?? opt.const }) - ); - } - return options; - }, [schema.enum, schema.oneOf]); - - // Memoize the option rendering to avoid recreating the function on every render - const renderOption = React.useCallback( - (option: { value: string; title: string }, index: number) => { - return ( - - ); - }, - [valueAtPath, handleChange, schema.readOnly] - ); - - if (readonly) { - const selectedOption = - schema.enum?.find((opt) => opt === valueAtPath) || - schema.oneOf?.find((opt) => opt.const === valueAtPath)?.title || - valueAtPath; - return ( - - ); - } - - return ( -
- {schema.title && ( - - )} -
{opts.map((option, index) => renderOption(option, index))}
- {schema.description && {schema.description}} - {errorsAtPath && } -
- ); -}; - -/** - * TextareaTemplate - * Renders a textarea input for multi-line string input. - * @param {Object} props - The props for the component. - * @param {StringSchema} props.schema - The schema for the textarea field. - * @param {string[]} props.path - The path to the textarea field in the form data. - * @returns {JSX.Element} - The textarea field component. - */ -export const TextareaTemplate: React.FC<{ - schema: StringSchema; - path: string[]; -}> = ({ schema, path }) => { - const [valueAtPath, setValueAtPath] = useFormDataAtPath(path); - const errorsAtPath = useErrorsAtPath(path); - const readonly = useFormContext((state: FormState) => state.readonly); - const required = useIsRequired(path); - - if (readonly) { - return ( - - ); - } - - const handleChange = (event: React.ChangeEvent) => { - setValueAtPath(event.target.value); - }; - - return ( -
- {schema.title && ( - - )} -