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 { produce } from 'immer';
|
||||||
|
|
||||||
import type { FormerProps, FormerState } from './Former.types';
|
import type { FormerProps, FormerState } from './Former.types';
|
||||||
|
import { newUUID } from '@warkypublic/artemis-kit';
|
||||||
|
|
||||||
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
||||||
FormerState<any> & Partial<FormerProps<any>>,
|
FormerState<any> & Partial<FormerProps<any>>,
|
||||||
@@ -168,18 +169,20 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
},
|
},
|
||||||
values: undefined,
|
values: undefined,
|
||||||
}),
|
}),
|
||||||
({ onConfirmDelete, primeData, request, values }) => {
|
({ onConfirmDelete, primeData, request, values, id }) => {
|
||||||
let _onConfirmDelete = onConfirmDelete;
|
let _onConfirmDelete = onConfirmDelete;
|
||||||
if (!onConfirmDelete) {
|
if (!onConfirmDelete) {
|
||||||
_onConfirmDelete = async () => {
|
_onConfirmDelete = async () => {
|
||||||
return confirm('Are you sure you want to delete this item?');
|
return confirm('Are you sure you want to delete this item?');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onConfirmDelete: _onConfirmDelete,
|
onConfirmDelete: _onConfirmDelete,
|
||||||
primeData,
|
primeData,
|
||||||
request: request || 'insert',
|
request: request || 'insert',
|
||||||
values: { ...primeData, ...values },
|
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';
|
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> {
|
export interface FormerProps<T extends FieldValues = any> {
|
||||||
|
id?: string;
|
||||||
afterGet?: (data: T) => Promise<T> | void;
|
afterGet?: (data: T) => Promise<T> | void;
|
||||||
afterSave?: (data: T) => Promise<void> | void;
|
afterSave?: (data: T) => Promise<void> | void;
|
||||||
apiKeyField?: string;
|
apiKeyField?: string;
|
||||||
@@ -25,13 +39,17 @@ export interface FormerProps<T extends FieldValues = any> {
|
|||||||
request: RequestType;
|
request: RequestType;
|
||||||
useFormProps?: UseFormProps<T>;
|
useFormProps?: UseFormProps<T>;
|
||||||
values?: T;
|
values?: T;
|
||||||
wrapper?: (
|
wrapper?: FormerSectionRender<T>;
|
||||||
children: React.ReactNode,
|
|
||||||
opened: boolean | undefined,
|
layout?: {
|
||||||
onClose: ((data?: T) => void) | undefined,
|
renderTop?: FormerSectionRender<T>;
|
||||||
onOpen: ((data?: T) => void) | undefined,
|
renderBottom?: FormerSectionRender<T>;
|
||||||
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K]
|
saveButtonProps?: ButtonProps;
|
||||||
) => React.ReactNode;
|
closeButtonProps?: ButtonProps;
|
||||||
|
buttonOnTop?: boolean;
|
||||||
|
buttonAreaGroupProps?: GroupProps;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormerRef<T extends FieldValues = any> {
|
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 { type PropsWithChildren, useEffect } from 'react';
|
||||||
|
|
||||||
import { useFormerStore } from './Former.store';
|
import { useFormerStore } from './Former.store';
|
||||||
|
import { FormerLayoutBottom } from './FormerLayoutBottom';
|
||||||
|
import { FormerLayoutTop } from './FormerLayoutTop';
|
||||||
|
|
||||||
export const FormerLayout = (props: PropsWithChildren) => {
|
export const FormerLayout = (props: PropsWithChildren) => {
|
||||||
const {
|
const {
|
||||||
@@ -14,6 +16,9 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
reset,
|
reset,
|
||||||
save,
|
save,
|
||||||
scrollAreaProps,
|
scrollAreaProps,
|
||||||
|
id,
|
||||||
|
layout,
|
||||||
|
getState,
|
||||||
} = useFormerStore((state) => ({
|
} = useFormerStore((state) => ({
|
||||||
disableHTMlForm: state.disableHTMlForm,
|
disableHTMlForm: state.disableHTMlForm,
|
||||||
getFormMethods: state.getFormMethods,
|
getFormMethods: state.getFormMethods,
|
||||||
@@ -24,6 +29,9 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
reset: state.reset,
|
reset: state.reset,
|
||||||
save: state.save,
|
save: state.save,
|
||||||
scrollAreaProps: state.scrollAreaProps,
|
scrollAreaProps: state.scrollAreaProps,
|
||||||
|
id: state.id,
|
||||||
|
layout: state.layout,
|
||||||
|
getState: state.getState,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,8 +43,9 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
}
|
}
|
||||||
}, [getFormMethods, request]);
|
}, [getFormMethods, request]);
|
||||||
|
|
||||||
if (disableHTMlForm) {
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<FormerLayoutTop />
|
||||||
<ScrollAreaAutosize
|
<ScrollAreaAutosize
|
||||||
offsetScrollbars
|
offsetScrollbars
|
||||||
scrollbarSize={4}
|
scrollbarSize={4}
|
||||||
@@ -44,50 +53,37 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
{...scrollAreaProps}
|
{...scrollAreaProps}
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxHeight: '89vh',
|
|
||||||
padding: '0.25rem',
|
padding: '0.25rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
...scrollAreaProps?.style,
|
...scrollAreaProps?.style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{disableHTMlForm ? (
|
||||||
|
<div x-data-request={request} key={`former_d${id}`}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<LoadingOverlay
|
</div>
|
||||||
loaderProps={{ type: 'bars' }}
|
) : (
|
||||||
overlayProps={{
|
<form
|
||||||
backgroundOpacity: 0.5,
|
key={`former_${id}`}
|
||||||
}}
|
id={`former_f${id}`}
|
||||||
{...loadingOverlayProps}
|
onReset={(e) => reset(e)}
|
||||||
visible={loading}
|
onSubmit={(e) => save(e)}
|
||||||
/>
|
x-data-request={request}
|
||||||
</ScrollAreaAutosize>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollAreaAutosize
|
|
||||||
offsetScrollbars
|
|
||||||
scrollbarSize={4}
|
|
||||||
type="auto"
|
|
||||||
{...scrollAreaProps}
|
|
||||||
style={{
|
|
||||||
height: '100%',
|
|
||||||
maxHeight: '89vh',
|
|
||||||
padding: '0.25rem',
|
|
||||||
width: '100%',
|
|
||||||
...scrollAreaProps?.style,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<form onReset={(e) => reset(e)} onSubmit={(e) => save(e)}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
<LoadingOverlay
|
|
||||||
loaderProps={{ type: 'bars' }}
|
|
||||||
overlayProps={{
|
|
||||||
backgroundOpacity: 0.5,
|
|
||||||
}}
|
|
||||||
{...loadingOverlayProps}
|
|
||||||
visible={loading}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LoadingOverlay
|
||||||
|
loaderProps={{ type: 'bars' }}
|
||||||
|
overlayProps={{
|
||||||
|
backgroundOpacity: 0.5,
|
||||||
|
}}
|
||||||
|
{...loadingOverlayProps}
|
||||||
|
visible={loading}
|
||||||
|
/>
|
||||||
</ScrollAreaAutosize>
|
</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 { useRef, useState } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
import type { FormerRef } from '../Former.types';
|
import type { FormerProps, FormerRef } from '../Former.types';
|
||||||
|
|
||||||
import { Former } from '../Former';
|
import { Former } from '../Former';
|
||||||
|
|
||||||
export const FormTest = () => {
|
export const FormTest = () => {
|
||||||
const [request, setRequest] = useState<null | string>('insert');
|
const [request, setRequest] = useState<null | string>('insert');
|
||||||
const [wrapped, setWrapped] = useState(false);
|
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 [open, setOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState({ a: 99 });
|
const [formData, setFormData] = useState({ a: 99 });
|
||||||
console.log('formData', formData);
|
console.log('formData', formData);
|
||||||
@@ -16,6 +23,7 @@ export const FormTest = () => {
|
|||||||
const ref = useRef<FormerRef>(null);
|
const ref = useRef<FormerRef>(null);
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" mih="400px" w="90%">
|
<Stack h="100%" mih="400px" w="90%">
|
||||||
|
<Group>
|
||||||
<Select
|
<Select
|
||||||
data={['insert', 'update', 'delete', 'select', 'view']}
|
data={['insert', 'update', 'delete', 'select', 'view']}
|
||||||
onChange={setRequest}
|
onChange={setRequest}
|
||||||
@@ -26,6 +34,17 @@ export const FormTest = () => {
|
|||||||
label="Wrapped in Drawer"
|
label="Wrapped in Drawer"
|
||||||
onChange={(event) => setWrapped(event.currentTarget.checked)}
|
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>
|
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
@@ -65,6 +84,7 @@ export const FormTest = () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
disableHTMlForm={disableHTML}
|
||||||
onChange={setFormData}
|
onChange={setFormData}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
opened={open}
|
opened={open}
|
||||||
@@ -73,9 +93,10 @@ export const FormTest = () => {
|
|||||||
request={request as any}
|
request={request as any}
|
||||||
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
||||||
values={formData}
|
values={formData}
|
||||||
|
layout={layout}
|
||||||
wrapper={
|
wrapper={
|
||||||
wrapped
|
wrapped
|
||||||
? (children, opened, onClose, onOpen, getState) => {
|
? (children, opened, onClose, _onOpen, getState) => {
|
||||||
const values = getState('values');
|
const values = getState('values');
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
@@ -107,10 +128,12 @@ export const FormTest = () => {
|
|||||||
rules={{ required: 'Field is required' }}
|
rules={{ required: 'Field is required' }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{!disableHTML && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">HTML Submit</button>
|
||||||
<button type="reset">Reset</button>
|
<button type="reset">HTML Reset</button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Former>
|
</Former>
|
||||||
</Stack>
|
</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)
|
- [ ] Predefined wrappers (Model,Dialog,notification,popover)
|
||||||
- [ ] Headerspec API
|
- [ ] Headerspec API
|
||||||
- [ ] Relspec API
|
- [ ] Relspec API
|
||||||
- [ ] SocketSpec API
|
- [ ] SocketSpec API
|
||||||
- [ ] Layout Tool
|
- [x] Layout Tool
|
||||||
- [ ] Header Section
|
- [x] Header Section
|
||||||
- [ ] Button Section
|
- [x] Button Section
|
||||||
- [ ] Footer Section
|
- [x] Footer Section
|
||||||
- [ ] Different Loaded for saving vs loading
|
- [ ] Different Loaded for saving vs loading
|
||||||
- [ ] Better Confirm Dialog
|
- [ ] Better Confirm Dialog
|
||||||
- [ ] Reset Confirm Dialog
|
- [ ] Reset Confirm Dialog
|
||||||
|
|||||||
Reference in New Issue
Block a user