151 lines
4.6 KiB
Markdown
151 lines
4.6 KiB
Markdown
# Views Layer
|
|
|
|
## Components
|
|
|
|
Reusable UI blocks: render UI, accept props, handle simple UI state (hover, toggle, open/close), compose other components. `UserAvatar`, `ProductBadge`.
|
|
|
|
No API calls, mutations, complex state management, or business workflows.
|
|
|
|
## Widgets
|
|
|
|
Self-contained UI blocks that implement a complete piece of functionality — panels, sections, complex interactive blocks used to compose pages. A widget combines UI, logic, and interactions: it composes multiple components, may include forms, dialogs, or actions, may fetch data via data hooks or take it from props, and exposes a simple API to pages while hiding its internals.
|
|
|
|
A widget can use another widget when the nested widget is a clear functional sub-block, the parent is grouping or orchestrating UI, and the dependency stays loose:
|
|
|
|
```ts
|
|
export const ProjectOverviewWidget = ({ projectId }: Props) => {
|
|
return (
|
|
<div>
|
|
<ProjectStatsWidget projectId={projectId} />
|
|
<ProjectActivityWidget projectId={projectId} />
|
|
<ProjectMembersWidget projectId={projectId} />
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
## Forms
|
|
|
|
### Form Architecture
|
|
|
|
```plaintext
|
|
view/forms/SomeForm/
|
|
| --- form.ts # validation schema, types, default values
|
|
| --- Component.tsx # UI + react-hook-form integration
|
|
| --- submit.ts # submission logic
|
|
| --- constants.ts # Optional: form IDs, UI constants
|
|
| --- schema.ts # Optional validation schema. Good for large or multistep forms
|
|
| --- index.ts # public exports
|
|
```
|
|
|
|
### Form Logic
|
|
|
|
`form.ts` contains pure form logic: validation schema, form types, default values, helper templates.
|
|
|
|
```ts
|
|
export const validationSchema = z.object({
|
|
first_name: z.string().min(3),
|
|
last_name: z.string().min(3),
|
|
});
|
|
```
|
|
|
|
With `i18n`, create a function that returns the schema:
|
|
|
|
```ts
|
|
function createValidationSchema(
|
|
translationFn: TFunction<"translation", undefined>, // Depend on library you use
|
|
) {
|
|
return z.object({
|
|
first_name: z.string({ message: translationFn("required") }),
|
|
last_name: z.string({ message: translationFn("required") }),
|
|
});
|
|
}
|
|
```
|
|
|
|
> [! WARNING]
|
|
> Dont include `i18n` singleton last_name: z.string().min(1, i18n.t('required'))
|
|
> It will not translate errors when language state change
|
|
|
|
Default values come from factory functions, not inline objects:
|
|
|
|
```ts
|
|
export function createFormValues(): FormValues {
|
|
return { first_name: "", last_name: "" };
|
|
}
|
|
|
|
export function toFormValues(data: SomeData): FormValues {
|
|
return {
|
|
first_name: data?.first_name ?? "",
|
|
last_name: data?.last_name ?? "",
|
|
};
|
|
}
|
|
```
|
|
|
|
Types for values and submit functions:
|
|
|
|
```ts
|
|
export type FormValues = z.infer<typeof validationSchema>;
|
|
export type SubmitFunc = (
|
|
values: FormValues,
|
|
handlers: UseFormReturn<FormValues>,
|
|
) => void;
|
|
```
|
|
|
|
### Submit Functions
|
|
|
|
A `useSubmit` hook centralizes submission: it picks create vs update by `values.id`, calls the `data/` layer mutations, invalidates list/detail queries on success, maps API validation errors onto form fields, and exposes `submit` + `isSubmitting`.
|
|
|
|
```ts
|
|
export const useSubmit = (params?: { onSuccess?: (id?: number) => void }) => {
|
|
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
|
|
const { mutate: updateMutation, isPending: isUpdatePending } = useUpdate();
|
|
|
|
const handleSubmit: SubmitFunc = (values, handlers) => {
|
|
const mutation = values.id ? updateMutation : createMutation;
|
|
mutation(values, {
|
|
onSuccess() {
|
|
invalidateList();
|
|
if (values.id) invalidateDetailed(values.id);
|
|
params?.onSuccess?.(values.id ?? undefined);
|
|
},
|
|
onError(error) {
|
|
extractValidationErrors(error).forEach((e) => {
|
|
handlers.setError(e.field as keyof typeof values, {
|
|
type: "apiError",
|
|
message: e.message,
|
|
});
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
return {
|
|
submit: handleSubmit,
|
|
isSubmitting: isCreatePending || isUpdatePending,
|
|
};
|
|
};
|
|
```
|
|
|
|
### Composed Form Elements
|
|
|
|
To avoid form boilerplate, wrap React Hook Form + shared UI into composed inputs. Each combines label, input, validation message, and layout, works with `FormProvider` via `useFormContext`, and can encapsulate any input type: text, switch, select, date, textarea, etc.
|
|
|
|
```tsx
|
|
<FormProvider {...form}>
|
|
<Component.FormElement
|
|
id={MAIN_FORM_ID}
|
|
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
|
|
>
|
|
<Component.FormGrid columns={2}>
|
|
<Component.GridElement colSpan={1}>
|
|
<Component.InputSwitch
|
|
name="notificationsEnabled"
|
|
required
|
|
label="Enable notifications"
|
|
/>
|
|
</Component.GridElement>
|
|
</Component.FormGrid>
|
|
</Component.FormElement>
|
|
</FormProvider>
|
|
```
|