feat(form): ✨ enhance form layout and functionality
* Add FormerButtonArea component for action buttons * Introduce FormerLayoutTop and FormerLayoutBottom for structured layout * Update Former types to include new properties * Implement dynamic ID generation for forms * Refactor example to demonstrate new layout features * Mark tasks as completed in todo.md
This commit is contained in:
@@ -2,6 +2,7 @@ import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import type { FormerProps, FormerState } from './Former.types';
|
||||
import { newUUID } from '@warkypublic/artemis-kit';
|
||||
|
||||
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
||||
FormerState<any> & Partial<FormerProps<any>>,
|
||||
@@ -168,18 +169,20 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
||||
},
|
||||
values: undefined,
|
||||
}),
|
||||
({ onConfirmDelete, primeData, request, values }) => {
|
||||
({ onConfirmDelete, primeData, request, values, id }) => {
|
||||
let _onConfirmDelete = onConfirmDelete;
|
||||
if (!onConfirmDelete) {
|
||||
_onConfirmDelete = async () => {
|
||||
return confirm('Are you sure you want to delete this item?');
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
onConfirmDelete: _onConfirmDelete,
|
||||
primeData,
|
||||
request: request || 'insert',
|
||||
values: { ...primeData, ...values },
|
||||
id: !id ? newUUID() : id,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import type { LoadingOverlayProps, ScrollAreaAutosizeProps } from '@mantine/core';
|
||||
import type {
|
||||
ButtonProps,
|
||||
GroupProps,
|
||||
LoadingOverlayProps,
|
||||
ScrollAreaAutosizeProps,
|
||||
} from '@mantine/core';
|
||||
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
||||
|
||||
export type FormerSectionRender<T extends FieldValues = any> = (
|
||||
children: React.ReactNode,
|
||||
opened: boolean | undefined,
|
||||
onClose: ((data?: T) => void) | undefined,
|
||||
onOpen: ((data?: T) => void) | undefined,
|
||||
getState: FormerState<T>['getState']
|
||||
) => React.ReactNode;
|
||||
|
||||
export interface FormerProps<T extends FieldValues = any> {
|
||||
id?: string;
|
||||
afterGet?: (data: T) => Promise<T> | void;
|
||||
afterSave?: (data: T) => Promise<void> | void;
|
||||
apiKeyField?: string;
|
||||
@@ -25,13 +39,17 @@ export interface FormerProps<T extends FieldValues = any> {
|
||||
request: RequestType;
|
||||
useFormProps?: UseFormProps<T>;
|
||||
values?: T;
|
||||
wrapper?: (
|
||||
children: React.ReactNode,
|
||||
opened: boolean | undefined,
|
||||
onClose: ((data?: T) => void) | undefined,
|
||||
onOpen: ((data?: T) => void) | undefined,
|
||||
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K]
|
||||
) => React.ReactNode;
|
||||
wrapper?: FormerSectionRender<T>;
|
||||
|
||||
layout?: {
|
||||
renderTop?: FormerSectionRender<T>;
|
||||
renderBottom?: FormerSectionRender<T>;
|
||||
saveButtonProps?: ButtonProps;
|
||||
closeButtonProps?: ButtonProps;
|
||||
buttonOnTop?: boolean;
|
||||
buttonAreaGroupProps?: GroupProps;
|
||||
title?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormerRef<T extends FieldValues = any> {
|
||||
|
||||
46
src/Former/FormerButtonArea.tsx
Normal file
46
src/Former/FormerButtonArea.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Group, Button } from '@mantine/core';
|
||||
import { IconX, IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useFormerStore } from './Former.store';
|
||||
|
||||
export const FormerButtonArea = () => {
|
||||
const { save, onClose, buttonAreaGroupProps } = useFormerStore((state) => ({
|
||||
save: state.save,
|
||||
onClose: state.onClose,
|
||||
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify="center"
|
||||
w="100%"
|
||||
p="xs"
|
||||
style={{ boxShadow: '2px 2px 5px rgba(47, 47, 47, 0.1)' }}
|
||||
{...buttonAreaGroupProps}
|
||||
>
|
||||
<Group justify="space-evenly" grow>
|
||||
{typeof onClose === 'function' && (
|
||||
<Button
|
||||
color="orange"
|
||||
leftSection={<IconX />}
|
||||
size="sm"
|
||||
px="md"
|
||||
onClick={() => onClose()}
|
||||
miw={'8rem'}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="green"
|
||||
leftSection={<IconDeviceFloppy />}
|
||||
size="sm"
|
||||
px="md"
|
||||
onClick={() => save()}
|
||||
miw={'8rem'}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,8 @@ import { LoadingOverlay, ScrollAreaAutosize } from '@mantine/core';
|
||||
import { type PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { useFormerStore } from './Former.store';
|
||||
import { FormerLayoutBottom } from './FormerLayoutBottom';
|
||||
import { FormerLayoutTop } from './FormerLayoutTop';
|
||||
|
||||
export const FormerLayout = (props: PropsWithChildren) => {
|
||||
const {
|
||||
@@ -14,6 +16,9 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
||||
reset,
|
||||
save,
|
||||
scrollAreaProps,
|
||||
id,
|
||||
layout,
|
||||
getState,
|
||||
} = useFormerStore((state) => ({
|
||||
disableHTMlForm: state.disableHTMlForm,
|
||||
getFormMethods: state.getFormMethods,
|
||||
@@ -24,6 +29,9 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
||||
reset: state.reset,
|
||||
save: state.save,
|
||||
scrollAreaProps: state.scrollAreaProps,
|
||||
id: state.id,
|
||||
layout: state.layout,
|
||||
getState: state.getState,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,8 +43,9 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
||||
}
|
||||
}, [getFormMethods, request]);
|
||||
|
||||
if (disableHTMlForm) {
|
||||
return (
|
||||
<>
|
||||
<FormerLayoutTop />
|
||||
<ScrollAreaAutosize
|
||||
offsetScrollbars
|
||||
scrollbarSize={4}
|
||||
@@ -44,50 +53,37 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
||||
{...scrollAreaProps}
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: '89vh',
|
||||
padding: '0.25rem',
|
||||
width: '100%',
|
||||
...scrollAreaProps?.style,
|
||||
}}
|
||||
>
|
||||
{disableHTMlForm ? (
|
||||
<div x-data-request={request} key={`former_d${id}`}>
|
||||
{props.children}
|
||||
<LoadingOverlay
|
||||
loaderProps={{ type: 'bars' }}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
}}
|
||||
{...loadingOverlayProps}
|
||||
visible={loading}
|
||||
/>
|
||||
</ScrollAreaAutosize>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize
|
||||
offsetScrollbars
|
||||
scrollbarSize={4}
|
||||
type="auto"
|
||||
{...scrollAreaProps}
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: '89vh',
|
||||
padding: '0.25rem',
|
||||
width: '100%',
|
||||
...scrollAreaProps?.style,
|
||||
}}
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
key={`former_${id}`}
|
||||
id={`former_f${id}`}
|
||||
onReset={(e) => reset(e)}
|
||||
onSubmit={(e) => save(e)}
|
||||
x-data-request={request}
|
||||
>
|
||||
<form onReset={(e) => reset(e)} onSubmit={(e) => save(e)}>
|
||||
{props.children}
|
||||
<LoadingOverlay
|
||||
loaderProps={{ type: 'bars' }}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
}}
|
||||
{...loadingOverlayProps}
|
||||
visible={loading}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<LoadingOverlay
|
||||
loaderProps={{ type: 'bars' }}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
}}
|
||||
{...loadingOverlayProps}
|
||||
visible={loading}
|
||||
/>
|
||||
</ScrollAreaAutosize>
|
||||
<FormerLayoutBottom />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
23
src/Former/FormerLayoutBottom.tsx
Normal file
23
src/Former/FormerLayoutBottom.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useFormerStore } from './Former.store';
|
||||
import { FormerButtonArea } from './FormerButtonArea';
|
||||
|
||||
export const FormerLayoutBottom = () => {
|
||||
const { renderBottom, getState, opened, buttonOnTop } = useFormerStore((state) => ({
|
||||
renderBottom: state.layout?.renderBottom,
|
||||
buttonOnTop: state.layout?.buttonOnTop,
|
||||
getState: state.getState,
|
||||
opened: state.opened,
|
||||
}));
|
||||
|
||||
if (renderBottom) {
|
||||
return renderBottom(
|
||||
<FormerButtonArea />,
|
||||
opened,
|
||||
getState('onClose'),
|
||||
getState('onOpen'),
|
||||
getState
|
||||
);
|
||||
}
|
||||
|
||||
return buttonOnTop ? <></> : <FormerButtonArea />;
|
||||
};
|
||||
22
src/Former/FormerLayoutTop.tsx
Normal file
22
src/Former/FormerLayoutTop.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useFormerStore } from './Former.store';
|
||||
import { FormerButtonArea } from './FormerButtonArea';
|
||||
|
||||
export const FormerLayoutTop = () => {
|
||||
const { renderTop, getState, opened, buttonOnTop } = useFormerStore((state) => ({
|
||||
renderTop: state.layout?.renderTop,
|
||||
buttonOnTop: state.layout?.buttonOnTop,
|
||||
getState: state.getState,
|
||||
opened: state.opened,
|
||||
}));
|
||||
|
||||
if (renderTop) {
|
||||
return renderTop(
|
||||
<FormerButtonArea />,
|
||||
opened,
|
||||
getState('onClose'),
|
||||
getState('onOpen'),
|
||||
getState
|
||||
);
|
||||
}
|
||||
return buttonOnTop ? <FormerButtonArea /> : <></>;
|
||||
};
|
||||
@@ -2,13 +2,20 @@ import { Button, Drawer, Group, Paper, Select, Stack, Switch } from '@mantine/co
|
||||
import { useRef, useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
import type { FormerRef } from '../Former.types';
|
||||
import type { FormerProps, FormerRef } from '../Former.types';
|
||||
|
||||
import { Former } from '../Former';
|
||||
|
||||
export const FormTest = () => {
|
||||
const [request, setRequest] = useState<null | string>('insert');
|
||||
const [wrapped, setWrapped] = useState(false);
|
||||
const [disableHTML, setDisableHTML] = useState(false);
|
||||
const [layout, setLayout] = useState({
|
||||
buttonOnTop: false,
|
||||
title: 'Custom Former Title',
|
||||
buttonAreaGroupProps: { justify: 'center' },
|
||||
} as FormerProps['layout']);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({ a: 99 });
|
||||
console.log('formData', formData);
|
||||
@@ -16,6 +23,7 @@ export const FormTest = () => {
|
||||
const ref = useRef<FormerRef>(null);
|
||||
return (
|
||||
<Stack h="100%" mih="400px" w="90%">
|
||||
<Group>
|
||||
<Select
|
||||
data={['insert', 'update', 'delete', 'select', 'view']}
|
||||
onChange={setRequest}
|
||||
@@ -26,6 +34,17 @@ export const FormTest = () => {
|
||||
label="Wrapped in Drawer"
|
||||
onChange={(event) => setWrapped(event.currentTarget.checked)}
|
||||
/>
|
||||
<Switch
|
||||
checked={disableHTML}
|
||||
label="Disable HTML Form"
|
||||
onChange={(event) => setDisableHTML(event.currentTarget.checked)}
|
||||
/>
|
||||
<Switch
|
||||
checked={layout?.buttonOnTop ?? false}
|
||||
label="Button On Top"
|
||||
onChange={(event) => setLayout({ ...layout, buttonOnTop: event.currentTarget.checked })}
|
||||
/>
|
||||
</Group>
|
||||
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
|
||||
<Group>
|
||||
<Button
|
||||
@@ -65,6 +84,7 @@ export const FormTest = () => {
|
||||
}, 1000);
|
||||
});
|
||||
}}
|
||||
disableHTMlForm={disableHTML}
|
||||
onChange={setFormData}
|
||||
onClose={() => setOpen(false)}
|
||||
opened={open}
|
||||
@@ -73,9 +93,10 @@ export const FormTest = () => {
|
||||
request={request as any}
|
||||
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
||||
values={formData}
|
||||
layout={layout}
|
||||
wrapper={
|
||||
wrapped
|
||||
? (children, opened, onClose, onOpen, getState) => {
|
||||
? (children, opened, onClose, _onOpen, getState) => {
|
||||
const values = getState('values');
|
||||
return (
|
||||
<Drawer
|
||||
@@ -107,10 +128,12 @@ export const FormTest = () => {
|
||||
rules={{ required: 'Field is required' }}
|
||||
/>
|
||||
</Stack>
|
||||
{!disableHTML && (
|
||||
<Stack>
|
||||
<button type="submit">Submit</button>
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit">HTML Submit</button>
|
||||
<button type="reset">HTML Reset</button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Former>
|
||||
</Stack>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
- [ ] Wrapper must receive button areas etc. Better scroll areas.
|
||||
- [x] Wrapper must receive button areas etc. Better scroll areas.
|
||||
- [ ] Predefined wrappers (Model,Dialog,notification,popover)
|
||||
- [ ] Headerspec API
|
||||
- [ ] Relspec API
|
||||
- [ ] SocketSpec API
|
||||
- [ ] Layout Tool
|
||||
- [ ] Header Section
|
||||
- [ ] Button Section
|
||||
- [ ] Footer Section
|
||||
- [x] Layout Tool
|
||||
- [x] Header Section
|
||||
- [x] Button Section
|
||||
- [x] Footer Section
|
||||
- [ ] Different Loaded for saving vs loading
|
||||
- [ ] Better Confirm Dialog
|
||||
- [ ] Reset Confirm Dialog
|
||||
|
||||
Reference in New Issue
Block a user