Skip to content

Commit

Permalink
Added form templates
Browse files Browse the repository at this point in the history
- Use data-field-name to pass a custom element for a field
- Use data-placeholder-name to create a slot placeholder for an element in the form
  • Loading branch information
kirankunigiri committed Nov 30, 2024
1 parent 4b9af41 commit a04580a
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 23 deletions.
33 changes: 16 additions & 17 deletions example-client/src/routes/rooms/$id.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
import { Button, Textarea } from '@mantine/core';
import { Button, Divider, Textarea } from '@mantine/core';
import { createFileRoute } from '@tanstack/react-router';
import { useRef } from 'react';

import { meta } from '~client/form/form-config';
import { DetailHeader } from '~client/form/lib/detail-header';
import MantineZenstackUpdateForm from '~client/form/lib/mantine-update-form';
import type { ZenstackFormRef } from '~zenstack-ui/index';
import { type ZenstackFormRef } from '~zenstack-ui/index';

export const Route = createFileRoute('/rooms/$id')({
component: PeopleDetail,
});

// Override metadata to hide the aiSummary field
const metadataOverride = JSON.parse(JSON.stringify(meta));
metadataOverride.models.room.fields.aiSummary.hidden = true;

function PeopleDetail() {
const params = Route.useParams() as { id: string };
const id = Number(params.id);

// Example of useRef to get the form
const formRef = useRef<ZenstackFormRef>(null);
const form = formRef.current?.form;

const handleSomeAction = () => {
formRef.current?.form.reset();
};

return (
<div>
<DetailHeader model="Room" id={id} route="/rooms" />
<MantineZenstackUpdateForm formRef={formRef} model="Room" id={id} route="/rooms/$id" metadataOverride={metadataOverride}>
<Textarea
data-field-name={meta.models.room.fields.aiSummary.name}
label="AI Summary"
placeholder="AI Summary"
/>
<MantineZenstackUpdateForm formRef={formRef} model="Room" id={id} route="/rooms/$id">
<div className="mt-4 flex flex-col gap-2">
<Divider />
<div data-placeholder-name={meta.models.room.fields.description.name} />
<Divider />
</div>
<div className="">
<Textarea
data-field-name={meta.models.room.fields.aiSummary.name}
label="AI Summary"
placeholder="AI Summary"
/>
</div>
</MantineZenstackUpdateForm>
</div>
);
Expand Down
108 changes: 102 additions & 6 deletions package/src/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { useForm } from '@mantine/form';
import { getHotkeyHandler } from '@mantine/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { zodResolver } from 'mantine-form-zod-resolver';
import { useEffect, useImperativeHandle, useMemo, useState } from 'react';
import { cloneElement, isValidElement, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import React from 'react';
import { z, ZodSchema } from 'zod';

import { Field, FieldType, Metadata, UseFindUniqueHook, UseMutationHook, UseQueryHook } from '../metadata';
Expand Down Expand Up @@ -316,21 +317,102 @@ export const ZenstackCreateForm = (props: ZenstackCreateFormProps) => {
const ZenstackBaseForm = (props: ZenstackBaseFormProps) => {
const { metadata: originalMetadata, submitButtons } = useZenstackUIProvider();
const metadata = props.metadataOverride || originalMetadata;

// Extract information
const fields = getModelFields(metadata, props.model);

// Helper function to recursively process children
const processChildren = (element: React.ReactNode): React.ReactNode => {
if (!isValidElement(element)) return element;

// Handle custom elements with data-field-name
if (element.props['data-field-name']) {
const fieldName = element.props['data-field-name'];
const field = fields[fieldName];

if (!field) {
console.warn(`Field ${fieldName} not found in model ${props.model}`);
return element;
}

return (
<ZenstackFormInputInternal
key={fieldName}
field={field}
index={-1}
{...props}
customElement={element}
/>
);
}

// Handle placeholder elements (data-placeholder-name)
if (element.props['data-placeholder-name']) {
const fieldName = element.props['data-placeholder-name'];
console.log('found placeholder', fieldName);
const field = fields[fieldName];

if (!field) {
console.warn(`Field ${fieldName} not found in model ${props.model}`);
return element;
}

return (
<ZenstackFormInputInternal
key={fieldName}
field={field}
index={-1}
{...props}
/>
);
}

// If element has children, clone it and process its children
if (element.props.children) {
return cloneElement(element, {
...element.props,
children: React.Children.map(element.props.children, processChildren),
});
}

return element;
};

// Helper to check if a field has either a custom element or placeholder
const hasCustomOrPlaceholder = (fieldName: string) => {
return React.Children.toArray(props.children).some(
child => isValidElement(child) && (
child.props['data-field-name'] === fieldName
|| child.props['data-placeholder-name'] === fieldName
// Also check nested children
|| (child.props.children && React.Children.toArray(child.props.children).some(
nestedChild => isValidElement(nestedChild) && (
nestedChild.props['data-field-name'] === fieldName
|| nestedChild.props['data-placeholder-name'] === fieldName
),
))
),
);
};

return (
<>
{/* Render default form fields that don't have custom elements or placeholders */}
{Object.values(fields).map((field, index) => {
if (hasCustomOrPlaceholder(field.name)) return null;

return (
<ZenstackFormInputInternal key={field.name} field={field} index={index} {...props} metadataOverride={props.metadataOverride}></ZenstackFormInputInternal>
<ZenstackFormInputInternal
key={field.name}
field={field}
index={index}
{...props}
/>
);
})}

{props.children}
{/* Render custom elements and placeholders */}
{React.Children.map(props.children, processChildren)}

{/* Errors and Submit Buttons */}
{/* Existing error and submit button rendering */}
{Object.keys(props.form.errors).length > 0 && (
<div style={{ flexShrink: 1 }}>
<p
Expand Down Expand Up @@ -368,6 +450,7 @@ const ZenstackBaseForm = (props: ZenstackBaseFormProps) => {
interface ZenstackFormInputProps extends ZenstackBaseFormProps {
field: Field
index: number
customElement?: React.ReactElement
}
const ZenstackFormInputInternal = (props: ZenstackFormInputProps) => {
const { metadata: originalMetadata, elementMap, hooks, enumLabelTransformer } = useZenstackUIProvider();
Expand Down Expand Up @@ -503,6 +586,19 @@ const ZenstackFormInputInternal = (props: ZenstackFormInputProps) => {
});
};

// If we have a custom element, use it instead of the element mapping
if (props.customElement) {
return React.cloneElement(props.customElement, {
...props.form.getInputProps(fieldName),
onChange: handleChange,
required,
key: props.form.key(fieldName),
className: isDirty ? 'dirty' : '',
disabled: isDisabled,
placeholder: placeholder,
});
}

return (
<Element
placeholder={placeholder}
Expand Down

0 comments on commit a04580a

Please sign in to comment.