Skip to content
This repository was archived by the owner on Dec 25, 2025. It is now read-only.
Open
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
17 changes: 17 additions & 0 deletions packages/fabrix/src/customRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getComponentFn,
getComponentRendererFn,
} from "@renderer";
import { CustomComponentFormRenderer } from "@renderers/custom/form";
import { CustomComponentTableRenderer } from "@renderers/custom/table";

export type ComponentRendererProps<
Expand Down Expand Up @@ -43,6 +44,22 @@ export const FabrixCustomComponent = (
/>
);
}
case "form": {
ensureFieldType(fieldConfig, "form");
return (
<CustomComponentFormRenderer
{...props}
key={`table-${fieldConfig.name}`}
fieldConfig={fieldConfig}
data={data}
component={{
name: props.component.name,
entry: componentEntry,
customProps: props.component.customProps,
}}
/>
);
}
default: {
throw new Error(`Unsupported component type: ${componentEntry.type}`);
}
Expand Down
26 changes: 7 additions & 19 deletions packages/fabrix/src/renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
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,
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";
Expand Down Expand Up @@ -276,26 +272,23 @@ 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,
},
};
switch (field.type) {
case "view":
return <ViewRenderer {...commonProps} />;
return <DefaultViewRenderer {...commonProps} />;
case "form": {
return <FormRenderer {...commonProps} />;
return <DefaultFormRenderer {...commonProps} />;
}
default:
return null;
Expand Down Expand Up @@ -359,17 +352,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,
Expand All @@ -385,7 +373,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)}
</div>
);
};
Expand Down
24 changes: 24 additions & 0 deletions packages/fabrix/src/renderers/custom/form.tsx
Original file line number Diff line number Diff line change
@@ -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<FormComponentEntry>;
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,
},
});
8 changes: 4 additions & 4 deletions packages/fabrix/src/renderers/fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ViewFields>) => {
// 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({
Expand Down Expand Up @@ -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);

Expand Down
140 changes: 91 additions & 49 deletions packages/fabrix/src/renderers/form.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
import { createElement, useCallback } from "react";
import { createElement, useContext } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useMutation } from "urql";
import { FabrixContextType } from "../context";
import { DocumentNode } from "graphql";
import { FabrixContext, FabrixContextType } from "@context";
import { FormComponentEntry } from "@registry";
import {
buildClassName,
CommonFabrixComponentRendererProps,
defaultFieldType,
FabrixComponentFieldsRenderer,
FieldConfigByType,
getFieldConfigByKey,
Loader,
} from "./shared";
import { buildAjvSchema } from "./form/validation";
import { ajvResolver } from "./form/ajvResolver";
import { RootField } from "./fields";

export type FormFields = FieldConfigByType<"form">["configs"]["fields"];
export type FormField = FormFields[number];

export const FormRenderer = ({
context,
rootField,
componentFieldsRenderer,
className,
}: CommonFabrixComponentRendererProps<FormFields>) => {
export const DefaultFormRenderer = (
props: CommonFabrixComponentRendererProps<FormFields>,
) => {
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)),
});
Expand All @@ -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 <Loader />;
}

const component = context.componentRegistry.getDefaultComponentByType("form");
if (!component) {
return;
}

return createElement(component, {
name: rootField.name,
renderFields: () => {
return <FormProvider {...formContext}>{renderFields()}</FormProvider>;
return (
<FormProvider {...formContext}>
{renderFormFields({
...rootField,
componentFieldsRenderer,
className,
})}
</FormProvider>
);
},
renderSubmit: (submitRenderer) =>
submitRenderer({
Expand All @@ -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;
Expand Down
2 changes: 0 additions & 2 deletions packages/fabrix/src/renderers/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,10 @@ export type RendererQuery = {
documentResolver: DocumentResolver;
};
export type CommonFabrixComponentRendererProps<F> = {
context: FabrixContextType;
rootField: {
name: string;
fields: F;
data: Value;
type: Record<string, FieldType>;
document: DocumentNode;
};
componentFieldsRenderer?: FabrixComponentFieldsRenderer;
Expand Down
Loading