# 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 (
); }; ``` ## 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; export type SubmitFunc = ( values: FormValues, handlers: UseFormReturn, ) => 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 ``` 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 onSubmit(values, form))} > ```