From e24fb6df217a10f2c791589082ffaed2d8002faf Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 25 Nov 2024 23:47:21 +0800 Subject: [PATCH] fix: all rendering-related issues on FormComponentSlots, SimpleFormRenderer, and FormRenderer --- lib/components/FormComponentSlots.tsx | 35 +++---- lib/components/FormRenderer.tsx | 134 ++++++++++++------------ lib/render_context.tsx | 7 +- lib/simple_form/context_component.tsx | 145 +++++++++++++------------- lib/simple_form/renderer.tsx | 12 +-- src/App.tsx | 56 ++++++++-- 6 files changed, 210 insertions(+), 179 deletions(-) diff --git a/lib/components/FormComponentSlots.tsx b/lib/components/FormComponentSlots.tsx index 2476f11..9b5b58f 100644 --- a/lib/components/FormComponentSlots.tsx +++ b/lib/components/FormComponentSlots.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from "react"; +import { memo } from "react"; import { FormFieldRenderer, FormFieldRendererProps, @@ -31,12 +31,12 @@ export interface FormComponentSlotProps { const FormComponentSlots = memo( ({ - byType: _byType = {}, - byFormType: _byFormType = {}, - byProperty: _byProperty = {}, - byCustomType: _byCustomType = {}, - byCustomControlType: _byCustomControlType = {}, - byLayoutName: _byLayoutName = {}, + byType = {}, + byFormType = {}, + byProperty = {}, + byCustomType = {}, + byCustomControlType = {}, + byLayoutName = {}, ...props }: FormFieldRendererProps & FormComponentSlotProps) => { const { @@ -45,17 +45,6 @@ const FormComponentSlots = memo( preferSchemaTypeComponent, } = props.preference; - // Memoize all components - const byType = useMemo(() => _byType, [_byType]); - const byFormType = useMemo(() => _byFormType, [_byFormType]); - const byProperty = useMemo(() => _byProperty, [_byProperty]); - const byCustomType = useMemo(() => _byCustomType, [_byCustomType]); - const byCustomControlType = useMemo( - () => _byCustomControlType, - [_byCustomControlType], - ); - const byLayoutName = useMemo(() => _byLayoutName, [_byLayoutName]); - // Hierarchical order of precedence for component lookup: // 1. formComponentsByProperty // 2. formComponentsByFormType @@ -82,7 +71,7 @@ const FormComponentSlots = memo( const customType = props.formProperties.customContentType; if (byCustomType[customType]) { const FormComponent = byCustomType[customType] as FormFieldRenderer; - return ; + return FormComponent(props); } } else if (formType === "custom-control") { const controlType = props.formProperties.controlType; @@ -90,7 +79,7 @@ const FormComponentSlots = memo( const FormComponent = byCustomControlType[ controlType ] as FormFieldRenderer; - return ; + return FormComponent(props); } } else if ( formType === "layout" && @@ -100,10 +89,10 @@ const FormComponentSlots = memo( const FormComponent = byLayoutName[ props.formProperties.name ] as FormFieldRenderer; - return ; + return FormComponent(props); } else if (byFormType[formType]) { const FormComponent = byFormType[formType] as FormFieldRenderer; - return ; + return FormComponent(props); } } else if (preferSchemaTypeComponent) { const typesList = Array.isArray(props.schema.type) @@ -113,7 +102,7 @@ const FormComponentSlots = memo( for (const type of typesList) { if (byType[type]) { const FormComponent = byType[type] as FormFieldRenderer; - return ; + return FormComponent(props); } } } diff --git a/lib/components/FormRenderer.tsx b/lib/components/FormRenderer.tsx index 9e2fa02..d72c711 100644 --- a/lib/components/FormRenderer.tsx +++ b/lib/components/FormRenderer.tsx @@ -1,5 +1,5 @@ import { getProperty } from "dot-prop"; -import { FC, useCallback, useMemo } from "react"; +import { FC, Fragment, memo, useCallback, useMemo } from "react"; import { JSONSchemaForm } from "../jsf"; import { expandSectionSelector } from "../json_schema"; import { FormRenderContext, useFormRenderContext } from "../render_context"; @@ -9,7 +9,7 @@ import { OutletRendererProps, } from "../form_types"; -function FormRendererChild< +const FormRendererChild = < RootSchemaType extends JSONSchemaForm, SchemaType extends JSONSchemaForm, >({ @@ -19,12 +19,9 @@ function FormRendererChild< preferFormTypeComponent = true, preferPropertyComponent = true, preferSchemaTypeComponent = true, -}: FormRendererProps) { - const { rootSchema, render: RenderComponent } = useFormRenderContext< - RootSchemaType, - SchemaType - >(); - +}: FormRendererProps) => { + const { rootSchema, render: RenderComponent } = + useFormRenderContext(); const _FormRendererChild = useCallback>( ({ parentProperty: _parentProperty, @@ -33,19 +30,17 @@ function FormRendererChild< preferFormTypeComponent, preferPropertyComponent, preferSchemaTypeComponent, - }) => { - return ( - - ); - }, + }) => ( + + ), [schema, parentProperty, property], ); @@ -74,57 +69,62 @@ function FormRendererChild< } return ( - + + {RenderComponent({ + rootSchema: rootSchema, + schema: schema, + Outlet: _FormRendererChild, + fullProperty: fullProperty, + property: property, + formProperties: schema.formProperties, + preference: preference, + })} + ); -} - -export default function FormRenderer({ - className, - render, - section, - schema, -}: Omit, "property" | "parentProperty"> & { - section?: string; - render: FormFieldRenderer; - className?: string; -}) { - const _render = useCallback(render, []); +}; +FormRendererChild.displayName = "FormRendererChild"; - const expandedSectionSelector = useMemo( - () => (section ? expandSectionSelector(schema, section) : undefined), - [schema, section], - ); +const FormRenderer = memo( + ({ + className, + render, + section, + schema, + }: Omit, "property" | "parentProperty"> & { + section?: string; + render: FormFieldRenderer; + className?: string; + }) => { + const expandedSectionSelector = useMemo( + () => (section ? expandSectionSelector(schema, section) : undefined), + [schema, section], + ); - const selectedSchema = useMemo( - () => - expandedSectionSelector - ? getProperty(schema, expandedSectionSelector)! - : schema, - [schema, expandedSectionSelector], - ); + const selectedSchema = useMemo( + () => + expandedSectionSelector + ? getProperty(schema, expandedSectionSelector)! + : schema, + [schema, expandedSectionSelector], + ); - return ( - + return (
- + + +
-
- ); -} + ); + }, +); FormRenderer.displayName = "FormRenderer"; + +export default FormRenderer; diff --git a/lib/render_context.tsx b/lib/render_context.tsx index ad454bb..ae382ad 100644 --- a/lib/render_context.tsx +++ b/lib/render_context.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from "react"; +import { createContext, useContext, useMemo } from "react"; import { JSONSchemaForm } from "./jsf"; import { FormFieldRenderer } from "./form_types"; @@ -29,16 +29,19 @@ export const useFormRenderContext = < export function FormRenderContext({ children, - rootSchema, + rootSchema: _rootSchema, render, }: { children: React.ReactNode; rootSchema: RS; render: FormFieldRenderer; }) { + const rootSchema = useMemo(() => _rootSchema, [_rootSchema]); return ( <_FormRenderContext.Provider value={{ rootSchema, render }}> {children} ); } + +FormRenderContext.displayName = "FormRenderContext"; diff --git a/lib/simple_form/context_component.tsx b/lib/simple_form/context_component.tsx index 20570c4..4a44ec3 100644 --- a/lib/simple_form/context_component.tsx +++ b/lib/simple_form/context_component.tsx @@ -1,90 +1,93 @@ -import { ReactNode, useCallback } from "react"; +import { memo, ReactNode, useCallback, useMemo } from "react"; import { SimpleFormContext, SimpleFormContextValue } from "./context"; import { getProperty, setProperty } from "dot-prop"; import { produce } from "immer"; -export function SimpleForm({ - children, - onChange, - onEvent, - value, -}: Omit, "getValue" | "setValue" | "dispatch"> & { - onEvent?: (name: string, payload: unknown) => void; - children: ReactNode; -}) { - const getValue = useCallback( - (key: string) => { - if (typeof value !== "object") { - if (key === "*") { - return value; - } else { - return undefined; +export const SimpleForm = memo( + ({ + children, + onChange, + onEvent, + value, + }: Omit, "getValue" | "setValue" | "dispatch"> & { + onEvent?: (name: string, payload: unknown) => void; + children: ReactNode; + }) => { + const getValue = useCallback( + (key: string) => { + if (typeof value !== "object") { + if (key === "*") { + return value; + } else { + return undefined; + } } - } - return getProperty(value, key); - }, - [value], - ); + return getProperty(value, key); + }, + [value], + ); - const setValue = useCallback( - (key: string, newValue: unknown) => { - if (typeof value === "undefined" || typeof value === "undefined") { - console.error("Value is undefined"); - return; - } else if (!value) { - console.error("Value is falsy", { value: value }); - return; - } - - const prevValue = key === "*" ? value : getProperty(value, key); - if (typeof prevValue !== typeof newValue) { - console.error("New value has different type than the old value", { - value, - newValue, - }); - return; - } else if (typeof newValue === "undefined") { - console.error("Value is undefined"); - return; - } else if (prevValue === value && typeof prevValue !== "object") { - if (key !== "*") { - console.error("Invalid key", key); + const setValue = useCallback( + (key: string, newValue: unknown) => { + if (typeof value === "undefined" || typeof value === "undefined") { + console.error("Value is undefined"); + return; + } else if (!value) { + console.error("Value is falsy", { value: value }); return; } - onChange?.(newValue as T); - } else if (key === "*") { - console.error("Rewriting the whole value is not allowed"); - return; - } + const prevValue = key === "*" ? value : getProperty(value, key); + if (typeof prevValue !== typeof newValue) { + console.error("New value has different type than the old value", { + value, + newValue, + }); + return; + } else if (typeof newValue === "undefined") { + console.error("Value is undefined"); + return; + } else if (prevValue === value && typeof prevValue !== "object") { + if (key !== "*") { + console.error("Invalid key", key); + return; + } - onChange(produce(value, (draft) => setProperty(draft, key, newValue))); - }, - [value], - ); + onChange?.(newValue as T); + } else if (key === "*") { + console.error("Rewriting the whole value is not allowed"); + return; + } - /* eslint-disable @typescript-eslint/no-unused-vars */ - const dispatch = useCallback( - onEvent ?? ((_n: string, _p: unknown) => {}), - [], - ); - /* eslint-disable @typescript-eslint/no-unused-vars */ + onChange(produce(value, (draft) => setProperty(draft, key, newValue))); + }, + [value], + ); - return ( - {}), + [], + ); + + const _contextValue = useMemo( + () => + ({ onChange, value, getValue, setValue, dispatch, - } as SimpleFormContextValue - } - > - {children} - - ); -} + }) as SimpleFormContextValue, + [value], + ); + + return ( + + {children} + + ); + }, +); SimpleForm.displayName = "SimpleForm"; diff --git a/lib/simple_form/renderer.tsx b/lib/simple_form/renderer.tsx index 2698b87..2ec4bcc 100644 --- a/lib/simple_form/renderer.tsx +++ b/lib/simple_form/renderer.tsx @@ -41,7 +41,7 @@ export const SimpleFormRenderer = memo( }: FormFieldRendererProps & SimpleFormRendererProps) => { const formComponentsByType = useMemo( () => mergeWithObject(defaultFormComponentsByType, _formComponentsByType), - [], + [_formComponentsByType], ); const formComponentsByFormType = useMemo( @@ -50,27 +50,27 @@ export const SimpleFormRenderer = memo( defaultFormComponentsByFormType, _formComponentsByFormType, ), - [], + [_formComponentsByFormType], ); const formComponentsByProperty = useMemo( () => mergeWithObject({}, _formComponentsByProperty), - [], + [_formComponentsByProperty], ); const formComponentsByCustomType = useMemo( () => mergeWithObject({}, _formComponentsByCustomType), - [], + [_formComponentsByCustomType], ); const formComponentsByCustomControlType = useMemo( () => mergeWithObject({}, _formComponentsByCustomControlType), - [], + [_formComponentsByCustomControlType], ); const formComponentsByLayoutName = useMemo( () => mergeWithObject({}, _formComponentsByLayoutName), - [], + [_formComponentsByLayoutName], ); return ( diff --git a/src/App.tsx b/src/App.tsx index 11e47a9..5d51a0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,11 +3,44 @@ import { JSONSchemaForm, FormRenderer, createDataFromSchema, + FormTypeFieldRenderer, + FormFieldRenderer, } from "../dist/formuler"; -import { Input } from "../dist/simple_form"; -import { SimpleForm, SimpleFormRenderer } from "../dist/simple_form"; +import { + Input, + useSimpleFormController, + SimpleForm, + SimpleFormRenderer, +} from "../dist/simple_form"; import "./App.css"; +const Specific: FormFieldRenderer = (props) => { + const { value: nameValue } = useSimpleFormController( + "personal_information.name", + ); + + if ( + props.fullProperty !== "personal_information.name" && + nameValue !== "123" + ) { + return null; + } + + return

Matched!

; +}; + +const SampleInput: FormTypeFieldRenderer<"input"> = (props) => { + const { value } = useSimpleFormController(props.fullProperty); + + return ( +
+

{props.formProperties.label}

+ +

{value}

+
+ ); +}; + const schema: JSONSchemaForm = { type: "object", properties: { @@ -125,6 +158,11 @@ function App() { render={(props) => ( { + return

{JSON.stringify(data)}

; + }, + }} formComponentsByType={{ string: () =>

test

, object: ({ schema, Outlet, fullProperty }) => { @@ -153,19 +191,17 @@ function App() { }, }} formComponentsByFormType={{ - input: (props) => ( -
-

- {props.formProperties.label} -

- -
- ), + input: SampleInput, }} /> )} /> + +

{JSON.stringify(data)}

} + />