Initial commit
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# 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>
|
||||
```
|
||||
Reference in New Issue
Block a user