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 three container types for forms:
// Slideover (default)
open('create', 'shopProducts', [], 'slideover')
// Modal
open('create', 'shopProducts', [], 'modal')
// Dialog
open('delete', 'shopProducts', ['id1'], 'dialog')
Slideover
- Default container type
- Slides in from the right
- Good for forms with many fields
Modal
- Center-screen overlay
- Good for focused actions
- Better for mobile
Dialog
- Compact confirmation
- Good for delete confirmations
- Simple yes/no actions
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'
activeItem?: any
items?: string[]
loading: string
collection: string
}
action
The operation being performed:
create- Creating a new itemupdate- Editing an existing itemdelete- Deleting one or more items
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 -->
<CroutonButton :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>