InertiaJS - submitting form arrays

Constantin Druc • January 20, 2021

In case you didn't know, there's an InertiaJS discord server you can join where people discuss issues, techniques, features, and so on. Recently, someone asked how to put together a form for a model with a hasMany relationship where they can update or create multiple records at once.

Question asking how to deal with form arrays in InertiaJS

Sooo, this is an article on how to build such form using the new InertiaJS form helper.

Bellow you have our start point, which is a regular page component with some basic markup in it like: labels, inputs, buttons, and so on:

<template>
    <div class="bg-gray-50 h-screen py-10">
        <div class="mx-auto max-w-2xl bg-white rounded-lg shadow-md p-6">
            <h1 class="text-lg font-semibold mb-2">Create recipe</h1>

            <form autocomplete="off">
                <div class="mb-4">
                    <label class="block text-sm font-medium text-gray-700" for="name">Name</label>
                    <input
                        type="text"
                        id="name"
                        class="mt-1 focus:ring-yellow-500 focus:border-yellow-500 block w-full sm:text-sm border-gray-300"
                        placeholder="A great recipe">
                    <p class="text-xs mt-1 text-red-600">This is an error message</p>
                </div>

                <div class="mb-4">
                    <label class="block text-sm font-medium text-gray-700" for="description">Description</label>
                    <textarea
                        id="description"
                        class="mt-1 focus:ring-yellow-500 focus:border-yellow-500 block w-full sm:text-sm border-gray-300"
                        placeholder="Here's how you can make this recipe..."></textarea>
                    <p class="text-xs mt-1 text-red-600">This is an error message</p>
                </div>

                <fieldset class="mb-4">
                    <legend class="flex items-center space-x-2">
                        <span class="text-sm font-medium text-gray-700">Ingredients</span>
                        <button
                            type="button"
                            class="bg-white py-1 px-2 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
                            Add
                        </button>
                    </legend>
                    <div class="mt-1">
                        <div class="mb-2">
                            <div class="flex space-x-2 items-center">
                                <button type="button" class="bg-white py-1 px-1 flex-shrink-0 text-sm leading-none font-medium text-red-600 rounded-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500">
                                    <icon name="trash" class="h-4 w-4 text-red-600"></icon>
                                </button>
                                <div class="flex-1">
                                    <input
                                        class="w-full focus:ring-yellow-500 focus:border-yellow-500 block w-full sm:text-sm border-gray-300"
                                        aria-label="Ingredient 1 name"
                                        type="text"
                                        placeholder="Ingredient">
                                </div>
                                <div class="w-24 flex-shrink-0">
                                    <input
                                        class="w-full focus:ring-yellow-500 focus:border-yellow-500 sm:text-sm border-gray-300"
                                        aria-label="Ingredient 1 quantity"
                                        type="number"
                                        placeholder="Qty">
                                </div>
                            </div>
                            <p class="text-xs mt-1 text-red-600">This is an error message</p>
                        </div>
                    </div>
                </fieldset>

                <div class="flex justify-end">
                    <button type="submit" class="h-10 inline-flex items-center justify-center px-4 border border-transparent shadow-sm text-sm font-semibold rounded-md text-white bg-yellow-400 hover:bg-yellow-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-400">
                        Create recipe
                    </button>
                </div>
            </form>
        </div>
    </div>
</template>

<script>
import AppLayout from '@/Layouts/AppLayout';

export default {
    components: {
        AppLayout,
    },
}
</script>

The first thing we need to do is to initialize our form using the inertia form helper. We'll have a name string, description, and an array to hold the ingredients.

<script>
import AppLayout from '@/Layouts/AppLayout';

export default {
    components: {
        AppLayout,
    },
+   data() {
+       return {
+           name: '',
+           description: '',
+           ingredients: []
+       }
+   }
}
</script>

Now that we know our fields, let's setup our v-model directives. We'll start with the easy ones: name and description.

<template>
    <div class="bg-gray-50 h-screen py-10">
        <div class="mx-auto max-w-2xl bg-white rounded-lg shadow-md p-6">
            <h1 class="text-lg font-semibold mb-2">Create recipe</h1>

            <form autocomplete="off">
                <div class="mb-4">
                    <label class="block text-sm font-medium text-gray-700" for="name">Name</label>
                    <input
+                       v-model="form.name"
                        type="text"
                        id="name"
                        class="mt-1 focus:ring-yellow-500 focus:border-yellow-500 block w-full sm:text-sm border-gray-300"
                        placeholder="A great recipe">
                    <p class="text-xs mt-1 text-red-600">This is an error message</p>
                </div>

                <div class="mb-4">
                    <label class="block text-sm font-medium text-gray-700" for="description">Description</label>
                    <textarea
+                       v-model="form.description"
                        id="description"
                        class="mt-1 focus:ring-yellow-500 focus:border-yellow-500 block w-full sm:text-sm border-gray-300"
                        placeholder="Here's how you can make this recipe..."></textarea>
                    <p class="text-xs mt-1 text-red-600">This is an error message</p>
                </div>
    <!--more code-->
</template>    

For the ingredients we'll have to get a bit more inventive. Because we can have multiple ingredients, we set our form.ingredients default value to an empty array where we can push new items. When a new ingredient will be added to the array, we'll have to render one row displaying the delete button, name, and quantity for that ingredient. To make this dynamic, we need to loop through the existing ingredients.

<template>
    <fieldset class="mb-4">
        <legend class="flex items-center space-x-2">
            <span class="text-sm font-medium text-gray-700">Ingredients</span>
            <button
                type="button"
                class="bg-white py-1 px-2 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
                Add
            </button>
        </legend>
        <div class="mt-1">
-           <div class="mb-2">
+           <div class="mb-2" v-for="(ingredient, index) in form.ingredients" :key="index">
                <div class="flex space-x-2 items-center">
                    <button type="button" class="bg-white py-1 px-1 flex-shrink-0 text-sm leading-none font-medium text-red-600 rounded-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500">
                        <icon name="trash" class="h-4 w-4 text-red-600"></icon>
                    </button>
                    <div class="flex-1">
                        <input
+                           v-model="ingredient.name"                        
                            class="w-full focus:ring-yellow-500 focus:border-yellow-500 block w-full sm:text-sm border-gray-300"
                            aria-label="Ingredient 1 name"
                            type="text"
                            placeholder="Ingredient">
                    </div>
                    <div class="w-24 flex-shrink-0">
                        <input
+                           v-model="ingredient.qty"
                            class="w-full focus:ring-yellow-500 focus:border-yellow-500 sm:text-sm border-gray-300"
                            aria-label="Ingredient 1 quantity"
                            type="number"
                            placeholder="Qty">
                    </div>
                </div>
                <p class="text-xs mt-1 text-red-600">This is an error message</p>
            </div>
        </div>
    </fieldset>
</template> 

What's your e-mail?

Get a monthly digest of what I've been up to: screencasts, blogposts, and other goodies.