Model Structure
Reference implementation: front/src/model/article
Design Philosophy
- •OOUI-based: One directory per domain model under
model/ - •Feature-split: Separate
list/anddetail/for different use cases - •Slice pattern: Each input field is an independent Zustand slice
- •Phase-based validation: Validate progressively (onChange → onDraftSubmit → onConfirmedSubmit)
- •Preview separation: Pure View components (props) vs store-connected wrappers
Key Patterns
Input Slice
Each form field has its own slice with value, setter, and validation getters.
code
inputs/[field]/ ├── slice.ts # Generate slice with createInputSlice factory ├── validation.ts # ArkType schema + InputValidation (when validation needed) ├── hook.ts # useXxxInput() - select from store, compute errors └── index.tsx # 'use client' UI component
Slice Factory (recommended):
Use createInputSlice factory to generate slices. Reduces boilerplate.
ts
import { createInputSlice, type InputSliceShape } from '@/model/common/lib/slice-factory'
import { titleValidation } from './validation'
// With validation
export type TitleSlice = InputSliceShape<'title', string>
export const createTitleSlice = createInputSlice('title', titleValidation)
// Without validation (enums, etc.)
export type CategorySlice = InputSliceShape<'category', ArticleCategory>
export const createCategorySlice = createInputSlice<'category', ArticleCategory>('category')
Generated slice shape: { [key], set[Key], get[Key]ErrorMessages, get[Key]IsValid }
Irregular patterns (cross-field deps, custom logic):
- •Write slice manually, or
- •Create base with factory, then spread-merge additional methods
Store Composition
Combine all slices into one store with meta functions.
- •Spread each slice:
...createXxxSlice(initialValue)(...args) - •Add
getFormIsValid()andgetFormValues()to MetaSlice - •Provide via Context +
useRefpattern
Validation
- •Use constraint values from
@ckda-cms/core/model/article(articleConstraints) - •Use
validateWithStandardSchema()frommodel/common/lib/validation.ts - •Return
InputValidationwith three phases:onChange,onDraftSubmit,onConfirmedSubmit - •Returns
ValidationError[](noisOk- errors only generated when invalid)
Pattern:
ts
import { articleConstraints, titleSchema } from '@ckda-cms/core/model/article'
import {
validateWithStandardSchema,
withValidationErrorMessages,
} from '@/model/common/lib/validation'
const c = articleConstraints.title
// withValidationErrorMessagesでメッセージを追加(otherwiseはデフォルト「不正な値です」)
const titleOnSubmitSchema = withValidationErrorMessages(titleSchema, {
minLength: 'タイトルは必須です',
maxLength: `${c.maxLength}文字以内で入力してください`,
})
export const titleValidation = (value: string): InputValidation => ({
onChange: validateWithStandardSchema(onChangeSchema, value),
onConfirmedSubmit: validateWithStandardSchema(titleOnSubmitSchema, value),
onDraftSubmit: [],
})
Query
- •Use
use cachedirective withcacheTag()for data fetching - •Located in
query.tsunder each feature
Actions
- •Server Actions in
actions.ts - •Call API via
apiClientfrommodel/common/rpc-client.ts - •Use
revalidateTag()to invalidate cache after mutations
Preview
- •
model/[entity]/preview/[field]/index.tsx: Pure View (receives value via props) - •
model/[entity]/detail/preview/[field]/index.tsx: Connects to store and renders View
Directory Structure
code
model/[entity]/ ├── actions.ts ├── list/ │ ├── query.ts │ ├── search/store/ + inputs/ │ └── table/ └── detail/ ├── query.ts ├── store/ ├── form/inputs/ └── preview/
Read front/src/model/xxx for concrete examples.