-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Custom UI Layout #3503
Comments
Ah @dcantu96 what you really want is something I've built into my local project, a GridFormLayout. Unfortunately, I'll need to build some underlying support for it first since my version is hard-coded to Material-UI 5 and I need to make it work for all themes. It is on the radar though, so stay tuned. |
@dcantu96 Do you by chance have an implementation for DefaultFlexObjectFieldTemplate and DefaultGridObjectFieldTemplate? I'm not quite sure how you are changing it so it still works... unless you are manually implementing ObjectFieldTemplate everytime. |
Do you have the full code of this? I'm interested too in seeing how you did this. |
@brampurnot This may be a start. It is by far not complete. And I'm using tailwind here to get results, but it may help you. I've since moved on to create my own json-schema package. Since I'm anyways using qwik. And didn't like the idea of qwik loading react components, which had me always hacking my way through stuff and in the end loading it client:only. Which made it less reactive. Here is how I started though: function CustomizedObjectFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(props: ObjectFieldTemplateProps<T, S, F> & { cols?: number }) {
const {
description,
disabled,
formData,
idSchema,
onAddClick,
properties,
readonly,
registry,
required,
schema,
title,
uiSchema,
cols,
} = props;
const options = getUiOptions<T, S, F>(uiSchema);
const TitleFieldTemplate = getTemplate<"TitleFieldTemplate", T, S, F>(
"TitleFieldTemplate",
registry,
options,
);
const DescriptionFieldTemplate = getTemplate<
"DescriptionFieldTemplate",
T,
S,
F
>("DescriptionFieldTemplate", registry, options);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
const colClassName = () => {
switch (cols) {
case 1:
return "md:grid-cols-1";
case 2:
return "md:grid-cols-2";
case 3:
return "md:grid-cols-3";
case 4:
return "md:grid-cols-4";
case 5:
return "md:grid-cols-5";
default:
return "md:grid-cols-1";
}
};
return (
<fieldset
id={idSchema.$id}
className={`md:grid ${colClassName()} gap-1 gap-x-4`}
>
{title && (
<TitleFieldTemplate
id={titleId<T>(idSchema)}
title={title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{description && (
<DescriptionFieldTemplate
id={descriptionId<T>(idSchema)}
description={description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)}
{canExpand<T, S, F>(schema, uiSchema, formData) && (
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</fieldset>
);
}
export function DefaultFlexObjectFieldTemplate(
props: ObjectFieldTemplateProps,
) {
return <CustomizedObjectFieldTemplate {...props} />;
}
export function DefaultFlexColObjectFieldTemplate(
props: ObjectFieldTemplateProps,
) {
return <CustomizedObjectFieldTemplate {...props} />;
}
export function DefaultGridObjectFieldTemplate(
props: ObjectFieldTemplateProps & { cols?: number },
) {
return <CustomizedObjectFieldTemplate {...props} />;
}
export const CustomObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
const maybeOptions = props.uiSchema?.["ui:layout"] as unknown;
if (maybeOptions === undefined || maybeOptions === null)
return <DefaultFlexObjectFieldTemplate {...props} />;
else if (typeof maybeOptions === "string") {
const options = maybeOptions.split(" ");
if (options.length > 1) {
console.warn(
"ui:layout can only have one option, defaulting to flex-row",
);
return <DefaultFlexObjectFieldTemplate {...props} />;
}
switch (maybeOptions) {
case "grid":
return <DefaultGridObjectFieldTemplate {...props} />;
case "flex-col":
return <DefaultFlexColObjectFieldTemplate {...props} />;
case "flex-row":
default:
return <DefaultFlexObjectFieldTemplate {...props} />;
}
} else if (typeof maybeOptions === "object") {
const { type, cols } = maybeOptions as { type?: unknown; cols?: unknown };
if (type === "grid") {
if (typeof cols !== "number")
return <DefaultGridObjectFieldTemplate {...props} />;
else {
if (cols > 5 || cols < 1) {
console.warn("cols must be between 1 and 5, defaulting to 5");
}
const validCols = Math.min(Math.max(cols, 1), 5);
return <DefaultGridObjectFieldTemplate {...props} cols={validCols} />;
}
}
}
return <DefaultFlexObjectFieldTemplate {...props} />;
}; |
You can use Here's how your form would, for example, look: "use client";
import Form, { Field, type JSONSchemaObject } from "rjsf-layout";
import validator from "@rjsf/validator-ajv8";
import { Theme as theme } from "@rjsf/mui";
const UserProfile = () => (
<Form {...{ schema, validator, theme }}>
<div style={{ display: "flex", gap: 9 }}>
<div style={{ flex: 1 }}>
<Field name="name" />
</div>
<Field name="birthDate" />
<div style={{ minWidth: "120px" }}>
<Field name="nationality" />
</div>
<Field name="vegetarian" />
</div>
<hr />
<Field name="personalData">
{/* Nested fields, also flexed horizontally */}
<div style={{ display: "flex", gap: 9 }}>
<Field name="age" />
<Field name="height" />
<Field name="drivingSkill" />
</div>
</Field>
<hr />
<div style={{ display: "flex", gap: 9 }}>
<div style={{ flex: 1 }}>
<Field name="occupation" />
</div>
<Field name="postalCode" />
</div>
</Form>
);
export default UserProfile;
const schema = {
type: "object",
title: "Columns",
properties: {
name: {
type: "string",
minLength: 3,
description: "Please enter your name",
},
vegetarian: {
type: "boolean",
},
birthDate: {
type: "string",
format: "date",
},
nationality: {
type: "string",
enum: ["DE", "IT", "JP", "US", "RU", "Other"],
},
personalData: {
type: "object",
properties: {
age: {
type: "integer",
description: "Please enter your age.",
},
height: {
type: "number",
},
drivingSkill: {
type: "number",
maximum: 10,
minimum: 1,
default: 7,
},
},
required: ["age", "height"],
},
occupation: {
type: "string",
},
postalCode: {
type: "string",
maxLength: 5,
},
},
required: ["occupation", "nationality"],
} as const satisfies JSONSchemaObject; |
@aularon Seems like we had the same thoughts about making this stuff. Nice repo. |
I've built my own custom layout template component: import { useMemo } from 'react'
import { canExpand, descriptionId, getTemplate, getUiOptions, titleId } from '@rjsf/utils'
/** The LayoutFieldTemplate` is the template to use to render all the inner properties of an object along with the
* title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
* the properties.
*
* @param props - The `LayoutFieldTemplateProps` for this component
*/
export default function LayoutFieldTemplate (props) {
const {
description,
disabled,
formData,
idSchema,
onAddClick,
properties,
readonly,
registry,
required,
schema,
title,
uiSchema
} = props
const options = getUiOptions(uiSchema)
const TitleFieldTemplate = getTemplate('TitleFieldTemplate', registry, options)
const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, options)
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton }
} = registry.templates
const layout = uiSchema['ui:layout']
const map = useMemo(() => properties.reduce((o, x) => ({ ...o, [x.name]: x }), {}), [properties])
return (
<fieldset id={idSchema.$id}>
{title && (
<TitleFieldTemplate
id={titleId(idSchema)}
title={title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{description && (
<DescriptionFieldTemplate
id={descriptionId(idSchema)}
description={description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{layout.map((row, i) => (
<div key={`L${i}j`} className='row'>
{Object.keys(row).map((name, j) => (
<div key={`L${i}${j}`} className={row[name].classNames}>
{map[name].content}
</div>
))}
</div>
))}
{canExpand(schema, uiSchema, formData) && (
<AddButton
className='object-property-expand'
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</fieldset>
)
} Because it's a template, I made it using bootstrap 3 semantics (to be used with core). Feel free to use it as: uiSchema = {
'ui:ObjectFieldTemplate': LayoutFieldTemplate
} It's built to be compatible with the rest of library. I've also made a PR #3881 to make it possible to call whitelisted custom templates using pure JSON. Working demo: https://codesandbox.io/s/nagaozen-react-jsonschema-form-playground-forked-29mz4d?file=/src/App.js |
Prerequisites
What theme are you using?
other
Is your feature request related to a problem? Please describe.
Hello everyone, I'm opening this discussion because I had trouble figuring out a way to customize my form layouts. I wanted to demonstrate how I achieved this in code.
Describe the solution you'd like
this is my ui schema
this is my json schema
notice the
"ui:layout"
property on the uiSchema object. I used that property to map myObjectFieldTemplate
. So in the example, the first object will recieve the grid + 2 cols. and the personalData will recieve theflex-col
. This way I conditionally render inside my customObjectFieldTemplate
:)kinda like this
Describe alternatives you've considered
I had to do this because I did not find a way to achieve this natively. I know the
ui:className
property is also there but I wanted to render my own custom components so that wasnt enough.The text was updated successfully, but these errors were encountered: