AdminGuides

SmartForm Guide

TL;DR (Minimal Usable Form)

import { SmartModalForm } from '@/components/admin/smart-form'
const fieldsConfig = [{ key: 'name', title: 'Name', type: 'text', form: { required: true } }]
<SmartModalForm title="Add" open={open} onOpenChange={setOpen} fieldsConfig={fieldsConfig} onFinish={async(v)=>{ await create(v); return true }} />

Detailed Guide to Universal Form Components

Component Types · Configuration Details · Advanced Usage


🎯 Overview

SmartForm series components are NextJS Base's form solution, automatically generating forms based on fieldsConfig configuration.

Component Types

ComponentDescriptionUse Case
SmartFormBase FormInline Form in Page
SmartModalFormModal FormAdd/Edit Modal
SmartDrawerFormDrawer FormSidebar Form

Core Features

  • ✅ Configuration-driven, auto-generate form fields
  • ✅ Automatic form validation handling
  • ✅ Support multiple field types
  • ✅ Support field linkage
  • ✅ Support async data loading

📦 Component Types

SmartModalForm

Most commonly used form component, displayed as modal:

import { SmartModalForm } from '@/components/admin/smart-form'

<SmartModalForm
  title="Add User"
  open={modalOpen}
  onOpenChange={setModalOpen}
  fieldsConfig={fieldsConfig}
  onFinish={async (values) => {
    await createUser(values)
    return true  // Return true to close modal
  }}
/>

SmartDrawerForm

Form displayed as drawer:

import { SmartDrawerForm } from '@/components/admin/smart-form'

<SmartDrawerForm
  title="Edit User"
  open={drawerOpen}
  onOpenChange={setDrawerOpen}
  fieldsConfig={fieldsConfig}
  initialValues={currentRecord}
  onFinish={async (values) => {
    await updateUser(values)
    return true
  }}
  width={600}
/>

SmartForm

Base form, embedded in page:

import { SmartForm } from '@/components/admin/smart-form'

<SmartForm
  fieldsConfig={fieldsConfig}
  initialValues={initialData}
  onFinish={async (values) => {
    await saveData(values)
  }}
  submitter={{
    searchConfig: {
      submitText: 'Save',
      resetText: 'Reset',
    },
  }}
/>

📋 Configuration Details

Basic Configuration

const fieldsConfig = [
  {
    key: 'name',
    title: 'Username',
    type: 'text',
    form: {
      required: true,
      placeholder: 'Enter username',
      rules: [
        { min: 2, message: 'At least 2 characters' },
        { max: 20, message: 'At most 20 characters' },
      ],
    },
  },
]

Field Types and Form Components

text - Text Input

{
  key: 'name',
  title: 'Name',
  type: 'text',
  form: {
    required: true,
    placeholder: 'Enter name',
    maxLength: 100,
    showCount: true,  // Show character count
  },
}

textarea - Multi-line Text

{
  key: 'description',
  title: 'Description',
  type: 'textarea',
  form: {
    rows: 4,
    maxLength: 500,
    showCount: true,
    placeholder: 'Enter description',
  },
}

number - Number Input

{
  key: 'price',
  title: 'Price',
  type: 'number',
  form: {
    min: 0,
    max: 99999,
    precision: 2,     // Decimal places
    step: 0.01,       // Step
    prefix: '$',      // Prefix
    suffix: '',       // Suffix
  },
}

select - Dropdown

{
  key: 'status',
  title: 'Status',
  type: 'select',
  options: [
    { label: 'Enabled', value: 'active' },
    { label: 'Disabled', value: 'inactive' },
  ],
  form: {
    required: true,
    defaultValue: 'active',
    allowClear: true,
    showSearch: true,  // Searchable
  },
}

// Dynamic Options
{
  key: 'categoryId',
  title: 'Category',
  type: 'select',
  options: async () => {
    const res = await getCategoryList()
    return res.data.map(item => ({
      label: item.name,
      value: item.id,
    }))
  },
}

switch - Switch

{
  key: 'enable',
  title: 'Enabled',
  type: 'switch',
  form: {
    defaultValue: true,
    checkedChildren: 'On',
    unCheckedChildren: 'Off',
  },
}

date / datetime - Date Picker

// Date
{
  key: 'birthday',
  title: 'Birthday',
  type: 'date',
  form: {
    format: 'YYYY-MM-DD',
  },
}

// DateTime
{
  key: 'publishTime',
  title: 'Publish Time',
  type: 'datetime',
  form: {
    format: 'YYYY-MM-DD HH:mm:ss',
    showTime: true,
  },
}

tree-select - Tree Select

{
  key: 'parentId',
  title: 'Parent Category',
  type: 'tree-select',
  options: async () => {
    const res = await getCategoryTree()
    return res.data
  },
  form: {
    allowClear: true,
    showSearch: true,
    treeDefaultExpandAll: true,
    placeholder: 'Select parent category',
  },
}

image - Image Upload

{
  key: 'avatar',
  title: 'Avatar',
  type: 'image',
  form: {
    maxCount: 1,
    accept: 'image/*',
    maxSize: 2,  // MB
    tip: 'Supports jpg, png format, max 2MB',
  },
}

images - Multiple Image Upload

{
  key: 'gallery',
  title: 'Gallery',
  type: 'images',
  form: {
    maxCount: 9,
    accept: 'image/*',
    maxSize: 5,
  },
}

markdown - Markdown Editor

{
  key: 'content',
  title: 'Content',
  type: 'markdown',
  form: {
    height: 400,
    placeholder: 'Enter article content...',
  },
}

icon - Icon Picker

{
  key: 'icon',
  title: 'Icon',
  type: 'icon',
  form: {
    placeholder: 'Select icon',
  },
}

json - JSON Editor

{
  key: 'config',
  title: 'Configuration',
  type: 'json',
  form: {
    height: 200,
    defaultValue: {},
  },
}

🎨 Advanced Usage

Field Linkage

const fieldsConfig = [
  {
    key: 'discountType',
    title: 'Discount Type',
    type: 'select',
    options: [
      { label: 'Fixed Amount', value: 'fixed' },
      { label: 'Percentage', value: 'percent' },
    ],
  },
  {
    key: 'discountValue',
    title: 'Discount Value',
    type: 'number',
    form: {
      // Dependent Field
      dependencies: ['discountType'],
      // Dynamic Configuration
      fieldProps: (form) => {
        const type = form.getFieldValue('discountType')
        return {
          suffix: type === 'percent' ? '%' : '',
          max: type === 'percent' ? 100 : 99999,
        }
      },
    },
  },
]

Conditional Display

{
  key: 'reason',
  title: 'Rejection Reason',
  type: 'textarea',
  form: {
    // Only show when status is rejected
    dependencies: ['status'],
    visible: (form) => form.getFieldValue('status') === 'rejected',
    required: (form) => form.getFieldValue('status') === 'rejected',
  },
}

Custom Validation

{
  key: 'password',
  title: 'Password',
  type: 'text',
  form: {
    required: true,
    rules: [
      { min: 8, message: 'Password at least 8 characters' },
      {
        pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
        message: 'Password must contain uppercase, lowercase letters and numbers',
      },
      {
        validator: async (_, value) => {
          if (value && value.includes(' ')) {
            throw new Error('Password cannot contain spaces')
          }
        },
      },
    ],
  },
}

Form Layout

<SmartModalForm
  fieldsConfig={fieldsConfig}
  // Form Layout
  layout="horizontal"
  labelCol={{ span: 6 }}
  wrapperCol={{ span: 18 }}
  // Or use grid layout
  grid={true}
  colProps={{ span: 12 }}  // Each field takes half width
/>

// Control individual field width
{
  key: 'description',
  title: 'Description',
  type: 'textarea',
  form: {
    colSpan: 24,  // Full row width
  },
}

Form Grouping

const fieldsConfig = [
  // Basic Info Group
  {
    key: 'basicInfo',
    title: 'Basic Information',
    type: 'group',
    children: [
      { key: 'name', title: 'Name', type: 'text' },
      { key: 'code', title: 'Code', type: 'text' },
    ],
  },
  // Advanced Config Group
  {
    key: 'advancedConfig',
    title: 'Advanced Configuration',
    type: 'group',
    children: [
      { key: 'enable', title: 'Enabled', type: 'switch' },
      { key: 'remark', title: 'Remarks', type: 'textarea' },
    ],
  },
]

📊 Props Reference

Common Props

PropertyTypeRequiredDescription
fieldsConfigFieldConfig[]Field configuration array
initialValuesobjectInitial values
onFinish(values) => Promise<boolean>Submit callback
onValuesChange(changedValues, allValues) => voidValue change callback
layout'horizontal' | 'vertical' | 'inline'Layout mode
disabledbooleanDisable entire form
readonlybooleanRead-only mode

SmartModalForm Props

PropertyTypeRequiredDescription
titlestringModal title
openbooleanWhether to show
onOpenChange(open: boolean) => voidShow state change
widthnumberModal width, default 600
destroyOnClosebooleanDestroy on close, default true

SmartDrawerForm Props

PropertyTypeRequiredDescription
titlestringDrawer title
openbooleanWhether to show
onOpenChange(open: boolean) => voidShow state change
widthnumberDrawer width, default 400
placement'left' | 'right'Position, default right