346 lines
9.2 KiB
Markdown
346 lines
9.2 KiB
Markdown
# Views Layer
|
|
|
|
## Table of Contents
|
|
|
|
- [Views Layer](#views-layer)
|
|
- [Components](#components)
|
|
- [Components Purpose](#components-purpose)
|
|
- [Components Responsibilities](#components-responsibilities)
|
|
- [What Components should NOT do](#what-components-should-not-do)
|
|
- [Widgets](#widgets)
|
|
- [Widget Purpose](#widget-purpose)
|
|
- [Widget Characteristics](#widget-characteristics)
|
|
- [Widget Responsibilities](#widget-responsibilities)
|
|
- [Forms](#forms)
|
|
- [Form Architecture](#form-architecture)
|
|
- [Form Logic](#form-logic)
|
|
- [Form Validation](#form-validation)
|
|
- [Form Values](#form-values)
|
|
- [Form Types](#form-types)
|
|
- [Submit Functions](#submit-functions)
|
|
- [Purpose](#submit-functions-purpose)
|
|
- [API Example](#api-example)
|
|
- [Responsibilities](#responsibilities)
|
|
- [Composed Form Elements](#composed-form-elements)
|
|
- [Example: InputSwitch](#example-inputswitch)
|
|
- [Usage in a Form](#usage-in-a-form)
|
|
|
|
## Components
|
|
|
|
### Components Purpose
|
|
|
|
Components are reusable UI blocks used to build user interfaces.
|
|
|
|
They encapsulate presentation and simple UI behavior, but do not contain business logic or data fetching. `UserAvatar`, `ProductBadge`
|
|
|
|
Components are designed to be small, composable, and reusable across multiple parts of the application.
|
|
|
|
### Components Responsibilities:
|
|
|
|
- render UI
|
|
- accept props
|
|
- handle simple UI state (hover, toggle, open/close)
|
|
- compose other components
|
|
|
|
### What **Components** should NOT do
|
|
|
|
Components should not contain logic such as:
|
|
|
|
- API calls
|
|
- mutations
|
|
- complex state management
|
|
- business workflows
|
|
|
|
## Widgets
|
|
|
|
### Widget Purpose
|
|
|
|
Widgets are **self-contained UI blocks that implement a specific piece of functionality**.
|
|
|
|
Unlike simple components, widgets combine **UI, logic, and interactions** to deliver a complete user-facing feature.
|
|
|
|
Widgets are typically used to **compose pages** and represent meaningful parts of the interface such as panels, sections, or complex interactive blocks. Widgets should remain modular building blocks.
|
|
|
|
### Widget Characteristics
|
|
|
|
A widget usually:
|
|
|
|
- encapsulates **UI and feature logic**
|
|
- composes **multiple components**
|
|
- may include **forms, dialogs, or actions**
|
|
- can interact with **data**
|
|
- exposes a **simple API for pages**
|
|
|
|
Widgets should hide their internal implementation and provide a **clean interface** to the outside.
|
|
|
|
### Widget Responsibilities
|
|
|
|
Widgets may:
|
|
|
|
- orchestrate UI interactions
|
|
- manage local UI state
|
|
- connect to features or data hooks
|
|
- combine multiple components/forms into a functional block
|
|
- may fetch data or rely on data passed in props
|
|
- A **widget** can use another **widget** if:
|
|
- the nested widget represents a clear functional sub-block
|
|
- the parent widget is grouping or orchestrating UI
|
|
- the dependency does not create tight coupling
|
|
|
|
```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
|
|
| --- Component.tsx
|
|
| --- submit.ts
|
|
| --- constants.ts # Optional
|
|
| --- schema.ts # Optional validation schema. Good for large forms or multistep forms
|
|
| --- index.ts
|
|
```
|
|
|
|
| File | Responsibility |
|
|
| --------------- | ---------------------------------- |
|
|
| `form.ts` | validation schema, types, defaults |
|
|
| `Component.tsx` | UI + react-hook-form integration |
|
|
| `submit.ts` | Submission logic |
|
|
| `constants.ts` | form IDs, UI constants |
|
|
| `index.ts` | public exports |
|
|
|
|
### Form Logic
|
|
|
|
`form.ts` contains pure form logic.
|
|
|
|
It should include:
|
|
|
|
- validation schema
|
|
- form types
|
|
- default values
|
|
- helper templates
|
|
|
|
#### Form Validation
|
|
|
|
```ts
|
|
export const validationSchema = z.object({
|
|
first_name: z.string().min(3),
|
|
last_name: z.string().min(3),
|
|
});
|
|
```
|
|
|
|
If you use `i18n` you can create function that returns validation schema.
|
|
|
|
```ts
|
|
function createValidationSchema(
|
|
translationFn: TFunction<"translation", undefined>, // Depend on libraty 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
|
|
|
|
#### Form Values
|
|
|
|
Default values should be defined using factory functions.
|
|
Avoid inline default objects.
|
|
|
|
```ts
|
|
export function createFormValues(): FormValues {
|
|
return {
|
|
first_name: "",
|
|
last_name: "",
|
|
};
|
|
}
|
|
```
|
|
|
|
```ts
|
|
export function toFormValues(data: SomeData): FormValues {
|
|
return {
|
|
first_name: data?.first_name ?? "",
|
|
last_name: data?.last_name ?? "",
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Form Types
|
|
|
|
Define types for form values and submit functiuons
|
|
|
|
```ts
|
|
export type FormValues = z.infer<typeof validationSchema>;
|
|
export type SubmitFunc = (
|
|
values: FormValues,
|
|
handlers: UseFormReturn<FormValues>,
|
|
) => void;
|
|
```
|
|
|
|
#### Submit Functions
|
|
|
|
##### Purpose {#submit-functions-purpose}
|
|
|
|
The `useSubmit` hook standardizes form submission logic:
|
|
|
|
- Handles create and update operations
|
|
- Integrates with `data/` layer hooks (`useCreate`, `useUpdate`)
|
|
- Automatically handles validation errors from the API
|
|
- Invalidates lists or detailed queries to refresh UI
|
|
- Exposes a unified API for forms
|
|
|
|
This pattern allows keep submission logic centralized.
|
|
|
|
##### API Example
|
|
|
|
```ts
|
|
interface Params {
|
|
onSuccess?: (id?: number) => void;
|
|
}
|
|
export const useSubmit = (params?: Params) => {
|
|
const onSuccess = params?.onSuccess;
|
|
|
|
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
|
|
const { mutate: updateMutation, isPending: isUpdateending } = useUpdate();
|
|
|
|
const handleSubmit: SubmitFunc = (values, handlers) => {
|
|
if (values.id) {
|
|
handleUpdate(values, handlers);
|
|
} else {
|
|
handleCreate(values, handlers);
|
|
}
|
|
};
|
|
|
|
const handleCreate: SubmitFunc = (values, handlers) => {
|
|
createMutation(values, {
|
|
onSuccess() {
|
|
invalidateList();
|
|
if (typeof onSuccess === "function") onSuccess();
|
|
},
|
|
onError(error) {
|
|
extractValidationErrors(error).forEach((e) => {
|
|
handlers.setError(e.field as keyof typeof values, {
|
|
type: "apiError",
|
|
message: e.message,
|
|
});
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleUpdate: SubmitFunc = (values, handlers) => {
|
|
if (!values.id) return;
|
|
|
|
updateMutation(
|
|
{ ...values, id: values.id },
|
|
{
|
|
onSuccess() {
|
|
invalidateList();
|
|
if (values.id) invalidateDetailed(values.id);
|
|
if (typeof onSuccess === "function") {
|
|
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 || isUpdateending,
|
|
};
|
|
};
|
|
```
|
|
|
|
##### Responsibilities
|
|
|
|
1. Determine operation type
|
|
- If values.id exists -> update
|
|
- Otherwise -> create
|
|
2. Call the appropriate mutation
|
|
- `createMutation` or `updateMutation` from `data` layer
|
|
- Hooks handle API calls internally
|
|
3. Handle success
|
|
- Invalidate queries (list/detail)
|
|
- Trigger optional `onSuccess` callback
|
|
4. Handle errors
|
|
- Extract validation errors from API
|
|
- Set errors on form fields using `handlers.setError`
|
|
5. Expose state
|
|
- `submit` -> function to call from form
|
|
- `isSubmitting` -> combines pending state from both mutations
|
|
|
|
#### Composed Form Elements
|
|
|
|
To avoid repetitive boilerplate code when building forms, we encourages creating composed form elements that integrate React Hook Form with shared UI components.
|
|
|
|
- Combines label, input, validation message, and layout in one reusable component.
|
|
- Can encapsulate any input type: text, switch, select, date, textarea, etc.
|
|
- Works seamlessly with FormProvider for context propagation.
|
|
|
|
##### Example: InputSwitch
|
|
|
|
InputSwitch wraps a Switch component into a reusable form field:
|
|
|
|
```tsx
|
|
<InputSwitch
|
|
name="notificationsEnabled"
|
|
label="Enable notifications"
|
|
description="Toggle notifications on/off"
|
|
required
|
|
/>
|
|
```
|
|
|
|
Key points:
|
|
|
|
- Uses `useFormContext` to access form control.
|
|
- Wraps `FormField`, `FormItem`, `FormLabel`, `FormMessage`, and `FormControl`.
|
|
- Handles required and disabled states.
|
|
- This allows you to reuse the same input type across forms without writing repetitive boilerplate.
|
|
|
|
##### Usage in a Form
|
|
|
|
```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>
|
|
```
|