Fundamentals

Forms & Modals

Working with forms and modals in Nuxt Crouton

Opening Forms

<script setup lang="ts">
const { open } = useCrouton()

// Create new item
const handleCreate = () => {
  open('create', 'shopProducts')
}

// Edit existing item
const handleEdit = (productId: string) => {
  open('update', 'shopProducts', [productId])
}

// Delete items
const handleDelete = (productIds: string[]) => {
  open('delete', 'shopProducts', productIds)
}
</script>

<template>
  <UButton @click="handleCreate">New Product</UButton>
  <UButton @click="handleEdit('product-123')">Edit</UButton>
  <UButton @click="handleDelete(['id1', 'id2'])" color="red">Delete</UButton>
</template>

Container Types

Nuxt Crouton supports four container types for forms:

// Slideover (default)
open('create', 'shopProducts', [], 'slideover')

// Modal
open('create', 'shopProducts', [], 'modal')

// Dialog
open('delete', 'shopProducts', ['id1'], 'dialog')

// Inline
open('update', 'shopProducts', ['id1'], 'inline')

Slideover

  • Default container type
  • Slides in from the right
  • Good for forms with many fields
  • Center-screen overlay
  • Good for focused actions
  • Better for mobile

Dialog

  • Compact confirmation
  • Good for delete confirmations
  • Simple yes/no actions

Inline

  • Renders form inline within the page
  • No overlay or panel
  • Good for embedded editing experiences

Nested Forms

Nuxt Crouton supports nesting forms up to 5 levels deep:

<script setup lang="ts">
// Open product form
open('create', 'shopProducts')

// From inside product form, open category form
open('create', 'shopCategories')  // Opens on top of product form

// Supports up to 5 levels deep
</script>

This is useful when:

  • Adding related items from within a form
  • Creating lookup values on the fly
  • Quick-adding categories, tags, etc.

Programmatic Control

<script setup lang="ts">
const { open, close, closeAll, showCrouton } = useCrouton()

// Check if any form is open
if (showCrouton.value) {
  console.log('Form is open')
}

// Close current form
close()

// Close all forms
closeAll()
</script>

Form Props

Generated forms accept these props:

interface Props {
  action: 'create' | 'update' | 'delete' | 'view'
  activeItem?: any
  items?: string[]
  loading: LoadingState // 'notLoading' | 'create_send' | 'update_send' | 'delete_send' | 'view_send' | 'create_open' | 'update_open' | 'delete_open' | 'view_open'
  collection: string
}

action

The operation being performed:

  • create - Creating a new item
  • update - Editing an existing item
  • delete - Deleting one or more items
  • view - Viewing an item (read-only)

activeItem

The item being edited (for update action)

items

Array of item IDs (for delete action)

loading

Loading state identifier

collection

The collection name (e.g., 'shopProducts')

Form Customization

<!-- layers/shop/components/products/_Form.vue -->
<script setup lang="ts">
// Keep generated props
const props = defineProps<ShopProductsFormProps>()

// Add custom state
const uploadingImage = ref(false)
const imagePreview = ref<string | null>(null)

// Add custom methods
const handleImageUpload = async (file: File) => {
  uploadingImage.value = true
  const url = await uploadToCloudinary(file)
  state.value.imageUrl = url
  imagePreview.value = url
  uploadingImage.value = false
}

// Keep generated mutation logic
const { create, update } = useCollectionMutation(props.collection)
</script>

<template>
  <UForm @submit="handleSubmit">
    <!-- Generated fields -->
    <UFormField label="Name" name="name">
      <UInput v-model="state.name" />
    </UFormField>

    <!-- Your custom field -->
    <UFormField label="Product Image" name="imageUrl">
      <img v-if="imagePreview" :src="imagePreview" class="w-32 h-32 object-cover" />
      <UButton @click="triggerFileInput" :loading="uploadingImage">
        Upload Image
      </UButton>
    </UFormField>

    <!-- Keep generated button -->
    <CroutonFormActionButton :action="action" :loading="loading" />
  </UForm>
</template>

Multi-Step Forms

<script setup lang="ts">
const currentStep = ref(1)
const totalSteps = 3

const nextStep = () => {
  if (currentStep.value < totalSteps) {
    currentStep.value++
  }
}

const prevStep = () => {
  if (currentStep.value > 1) {
    currentStep.value--
  }
}

const handleSubmit = async () => {
  if (currentStep.value < totalSteps) {
    nextStep()
  } else {
    // Final submit
    await create(state.value)
    close()
  }
}
</script>

<template>
  <UForm @submit="handleSubmit">
    <!-- Step indicator -->
    <div class="flex justify-between mb-4">
      <div v-for="step in totalSteps" :key="step"
           :class="{ 'font-bold': step === currentStep }">
        Step {{ step }}
      </div>
    </div>

    <!-- Step 1: Basic info -->
    <div v-if="currentStep === 1">
      <UFormField label="Name" name="name">
        <UInput v-model="state.name" />
      </UFormField>
    </div>

    <!-- Step 2: Details -->
    <div v-if="currentStep === 2">
      <UFormField label="Description" name="description">
        <UTextarea v-model="state.description" />
      </UFormField>
    </div>

    <!-- Step 3: Pricing -->
    <div v-if="currentStep === 3">
      <UFormField label="Price" name="price">
        <UInput v-model.number="state.price" type="number" />
      </UFormField>
    </div>

    <!-- Navigation -->
    <div class="flex justify-between">
      <UButton v-if="currentStep > 1" @click="prevStep" variant="ghost">
        Back
      </UButton>
      <UButton type="submit">
        {{ currentStep < totalSteps ? 'Next' : 'Submit' }}
      </UButton>
    </div>
  </UForm>
</template>

Conditional Fields

<template>
  <UForm>
    <UFormField label="Product Type" name="type">
      <USelectMenu
        v-model="state.type"
        :options="['physical', 'digital']"
      />
    </UFormField>

    <!-- Show only for physical products -->
    <UFormField v-if="state.type === 'physical'" label="Weight" name="weight">
      <UInput v-model.number="state.weight" type="number" />
    </UFormField>

    <!-- Show only for digital products -->
    <UFormField v-if="state.type === 'digital'" label="Download URL" name="downloadUrl">
      <UInput v-model="state.downloadUrl" />
    </UFormField>
  </UForm>
</template>