From e0f1b336bfd89e2db12748a97925814c38a873d1 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 19 Dec 2024 19:20:44 +0900 Subject: [PATCH 1/2] Revise custom component interface for forms --- packages/fabrix/src/customRenderer.tsx | 17 +++ packages/fabrix/src/renderer.tsx | 20 +-- packages/fabrix/src/renderers/custom/form.tsx | 24 +++ packages/fabrix/src/renderers/fields.tsx | 8 +- packages/fabrix/src/renderers/form.tsx | 140 ++++++++++++------ packages/fabrix/src/renderers/shared.tsx | 2 - 6 files changed, 142 insertions(+), 69 deletions(-) create mode 100644 packages/fabrix/src/renderers/custom/form.tsx diff --git a/packages/fabrix/src/customRenderer.tsx b/packages/fabrix/src/customRenderer.tsx index 68cc9b8a..d9672f79 100644 --- a/packages/fabrix/src/customRenderer.tsx +++ b/packages/fabrix/src/customRenderer.tsx @@ -6,6 +6,7 @@ import { getComponentFn, getComponentRendererFn, } from "@renderer"; +import { CustomComponentFormRenderer } from "@renderers/custom/form"; import { CustomComponentTableRenderer } from "@renderers/custom/table"; export type ComponentRendererProps< @@ -43,6 +44,22 @@ export const FabrixCustomComponent = ( /> ); } + case "form": { + ensureFieldType(fieldConfig, "form"); + return ( + + ); + } default: { throw new Error(`Unsupported component type: ${componentEntry.type}`); } diff --git a/packages/fabrix/src/renderer.tsx b/packages/fabrix/src/renderer.tsx index 03f626d2..41cbff0b 100644 --- a/packages/fabrix/src/renderer.tsx +++ b/packages/fabrix/src/renderer.tsx @@ -1,8 +1,8 @@ import { DirectiveNode, DocumentNode, OperationTypeNode, parse } from "graphql"; import { ReactNode, useCallback, useContext, useMemo } from "react"; import { findDirective, parseDirectiveArguments } from "@directive"; -import { ViewRenderer } from "@renderers/fields"; -import { FormRenderer } from "@renderers/form"; +import { DefaultViewRenderer } from "@renderers/fields"; +import { DefaultFormRenderer } from "@renderers/form"; import { FabrixContext, FabrixContextType } from "@context"; import { FabrixComponentFieldsRenderer, @@ -276,16 +276,13 @@ export const FabrixComponent = (props: FabrixComponentProps) => { ( field: FieldConfig, data: Value, - context: FabrixContextType, componentFieldsRenderer?: FabrixComponentFieldsRenderer, ) => { const commonProps = { - context, rootField: { name: field.name, fields: field.configs.fields, data, - type: resolveFieldTypesFromTypename(context, data), document: field.document, className: props.contentClassName, componentFieldsRenderer, @@ -293,9 +290,9 @@ export const FabrixComponent = (props: FabrixComponentProps) => { }; switch (field.type) { case "view": - return ; + return ; case "form": { - return ; + return ; } default: return null; @@ -359,17 +356,12 @@ export const getComponentRendererFn = ( type RendererFn = ( field: FieldConfig, data: Value, - context: FabrixContextType, componentFieldsRenderer?: FabrixComponentFieldsRenderer, ) => ReactNode; export const getComponentFn = (props: FabrixComponentProps, rendererFn: RendererFn) => - ( - fieldConfig: FieldConfigs, - data: FabrixComponentData, - context: FabrixContextType, - ) => + (fieldConfig: FieldConfigs, data: FabrixComponentData) => ( name: string, extraProps?: FabrixComponentChildrenExtraProps, @@ -385,7 +377,7 @@ export const getComponentFn = key={extraProps?.key} className={`fabrix renderer container ${props.containerClassName ?? ""} ${extraProps?.className ?? ""}`} > - {rendererFn(field, data[name], context, componentFieldsRenderer)} + {rendererFn(field, data[name], componentFieldsRenderer)} ); }; diff --git a/packages/fabrix/src/renderers/custom/form.tsx b/packages/fabrix/src/renderers/custom/form.tsx new file mode 100644 index 00000000..a9a8c4a6 --- /dev/null +++ b/packages/fabrix/src/renderers/custom/form.tsx @@ -0,0 +1,24 @@ +import { ComponentRendererProps } from "@customRenderer"; +import { Value } from "@fetcher"; +import { FormComponentEntry } from "@registry"; +import { FabrixComponentProps } from "@renderer"; +import { FieldConfigByType } from "@renderers/shared"; +import { renderForm } from "@renderers/form"; + +export const CustomComponentFormRenderer = ( + props: FabrixComponentProps & { + fieldConfig: FieldConfigByType<"form">; + component: ComponentRendererProps; + data: Value; + }, +) => + renderForm({ + component: props.component.entry.component, + customProps: props.component.customProps, + rootField: { + name: props.fieldConfig.name, + fields: props.fieldConfig.configs.fields, + data: props.data, + document: props.fieldConfig.document, + }, + }); diff --git a/packages/fabrix/src/renderers/fields.tsx b/packages/fabrix/src/renderers/fields.tsx index 6b0d0a16..24c503a7 100644 --- a/packages/fabrix/src/renderers/fields.tsx +++ b/packages/fabrix/src/renderers/fields.tsx @@ -14,15 +14,14 @@ import { getTableMode, renderTable } from "./table"; export type ViewFields = FieldConfigByType<"view">["configs"]["fields"]; type ViewField = ViewFields[number]; -export const ViewRenderer = ({ - context, +export const DefaultViewRenderer = ({ rootField, componentFieldsRenderer, className, }: CommonFabrixComponentRendererProps) => { // If the query is the one that can be rendered as a table, we will render the table component instead of the fields. const tableType = useMemo(() => getTableMode(rootField.fields), [rootField]); - + const context = useContext(FabrixContext); const renderFields = useCallback(() => { if (componentFieldsRenderer) { return componentFieldsRenderer({ @@ -152,8 +151,9 @@ const renderField = ({ return; } + const type = resolveFieldTypesFromTypename(context, rootField.data); const fieldName = field.field.getName(); - const fieldType = rootField.type?.[fieldName]; + const fieldType = type?.[fieldName]; assertObjectValue(rootField.data); diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 4cb60664..298de4e4 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -1,27 +1,58 @@ -import { createElement, useCallback } from "react"; +import { createElement, useCallback, useContext } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; -import { FabrixContextType } from "../context"; +import { FabrixContext, FabrixContextType } from "../context"; import { buildClassName, CommonFabrixComponentRendererProps, defaultFieldType, + FabrixComponentFieldsRenderer, FieldConfigByType, getFieldConfigByKey, Loader, } from "./shared"; import { buildAjvSchema } from "./form/validation"; import { ajvResolver } from "./form/ajvResolver"; +import { FormComponentEntry } from "@registry"; +import { DocumentNode } from "graphql"; +import { RootField } from "./fields"; export type FormFields = FieldConfigByType<"form">["configs"]["fields"]; export type FormField = FormFields[number]; -export const FormRenderer = ({ - context, - rootField, - componentFieldsRenderer, - className, -}: CommonFabrixComponentRendererProps) => { +export const DefaultFormRenderer = ( + props: CommonFabrixComponentRendererProps, +) => { + const context = useContext(FabrixContext); + const component = context.componentRegistry.getDefaultComponentByType("form"); + if (!component) { + return; + } + + return renderForm({ + ...props, + component, + customProps: {}, + }); +}; + +export const renderForm = (props: { + component: FormComponentEntry["component"]; + customProps: unknown; + componentFieldsRenderer?: FabrixComponentFieldsRenderer; + className?: string; + rootField: RootField & { + document: DocumentNode; + }; +}) => { + const { + component, + customProps, + componentFieldsRenderer, + className, + rootField, + } = props; + const context = useContext(FabrixContext); const formContext = useForm({ resolver: ajvResolver(buildAjvSchema(rootField.fields)), }); @@ -33,52 +64,22 @@ export const FormRenderer = ({ formContext.reset(); }); - const renderFields = useCallback(() => { - if (componentFieldsRenderer) { - return componentFieldsRenderer({ - getField: (name, extraProps) => { - const field = getFieldConfigByKey(rootField.fields, name); - if (!field) { - return null; - } - - return renderField({ - indexKey: extraProps?.key ?? `${rootField.name}-${name}`, - extraClassName: extraProps?.className, - field: { - ...field, - ...extraProps, - }, - context, - }); - }, - }); - } - - return rootField.fields - .sort((a, b) => (a.config.index ?? 0) - (b.config.index ?? 0)) - .flatMap((field, fieldIndex) => - renderField({ - indexKey: `${rootField.name}-${fieldIndex}`, - field, - context, - }), - ); - }, [context, rootField.name, componentFieldsRenderer]); - if (context.schemaLoader.status === "loading") { return ; } - const component = context.componentRegistry.getDefaultComponentByType("form"); - if (!component) { - return; - } - return createElement(component, { name: rootField.name, renderFields: () => { - return {renderFields()}; + return ( + + {renderFormFields({ + ...rootField, + componentFieldsRenderer, + className, + })} + + ); }, renderSubmit: (submitRenderer) => submitRenderer({ @@ -88,11 +89,52 @@ export const FormRenderer = ({ renderReset: (resetRenderer) => resetRenderer({ reset: () => formContext.reset() }), className: `fabrix form col-row ${className ?? ""}`, - customProps: {}, + customProps, }); }; -const renderField = (props: { +export const renderFormFields = (props: { + name: string; + fields: FormFields; + componentFieldsRenderer?: FabrixComponentFieldsRenderer; + className?: string; +}) => { + const context = useContext(FabrixContext); + const { name, fields, componentFieldsRenderer } = props; + + if (componentFieldsRenderer) { + return componentFieldsRenderer({ + getField: (fieldName, extraProps) => { + const field = getFieldConfigByKey(fields, fieldName); + if (!field) { + return null; + } + + return renderFormField({ + indexKey: extraProps?.key ?? `${name}-${fieldName}`, + extraClassName: extraProps?.className, + field: { + ...field, + ...extraProps, + }, + context, + }); + }, + }); + } + + return fields + .sort((a, b) => (a.config.index ?? 0) - (b.config.index ?? 0)) + .flatMap((field, fieldIndex) => + renderFormField({ + indexKey: `${name}-${fieldIndex}`, + field, + context, + }), + ); +}; + +const renderFormField = (props: { indexKey: string; field: FormField; context: FabrixContextType; diff --git a/packages/fabrix/src/renderers/shared.tsx b/packages/fabrix/src/renderers/shared.tsx index a0d4370d..1cf4ef53 100644 --- a/packages/fabrix/src/renderers/shared.tsx +++ b/packages/fabrix/src/renderers/shared.tsx @@ -52,12 +52,10 @@ export type RendererQuery = { documentResolver: DocumentResolver; }; export type CommonFabrixComponentRendererProps = { - context: FabrixContextType; rootField: { name: string; fields: F; data: Value; - type: Record; document: DocumentNode; }; componentFieldsRenderer?: FabrixComponentFieldsRenderer; From edb11a85f8b22b46d6713a123275427f67fc6c0b Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 19 Dec 2024 19:26:24 +0900 Subject: [PATCH 2/2] Fix lint errors --- packages/fabrix/src/renderer.tsx | 6 +----- packages/fabrix/src/renderers/form.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/fabrix/src/renderer.tsx b/packages/fabrix/src/renderer.tsx index 41cbff0b..733ac2d9 100644 --- a/packages/fabrix/src/renderer.tsx +++ b/packages/fabrix/src/renderer.tsx @@ -4,11 +4,7 @@ import { findDirective, parseDirectiveArguments } from "@directive"; import { DefaultViewRenderer } from "@renderers/fields"; import { DefaultFormRenderer } from "@renderers/form"; import { FabrixContext, FabrixContextType } from "@context"; -import { - FabrixComponentFieldsRenderer, - Loader, - resolveFieldTypesFromTypename, -} from "@renderers/shared"; +import { FabrixComponentFieldsRenderer, Loader } from "@renderers/shared"; import { directiveSchemaMap } from "@directive/schema"; import { mergeFieldConfigs } from "@readers/shared"; import { buildDefaultViewFieldConfigs, viewFieldMerger } from "@readers/field"; diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 298de4e4..600c80c7 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -1,7 +1,9 @@ -import { createElement, useCallback, useContext } from "react"; +import { createElement, useContext } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; -import { FabrixContext, FabrixContextType } from "../context"; +import { DocumentNode } from "graphql"; +import { FabrixContext, FabrixContextType } from "@context"; +import { FormComponentEntry } from "@registry"; import { buildClassName, CommonFabrixComponentRendererProps, @@ -13,8 +15,6 @@ import { } from "./shared"; import { buildAjvSchema } from "./form/validation"; import { ajvResolver } from "./form/ajvResolver"; -import { FormComponentEntry } from "@registry"; -import { DocumentNode } from "graphql"; import { RootField } from "./fields"; export type FormFields = FieldConfigByType<"form">["configs"]["fields"];