Backend DevelopmentFrontend Development

Creating a Restaurant Menu App With Laravel Inertia and Vue Part 3

Creating a Restaurant Menu App With Laravel Inertia and Vue

In this part of building a restaurant menu app with Laravel and Inertiajs we will implement the menu sections CRUD.

 

 

< Back to part 2

The menu sections listing page will consist of two parts,  the sections list and the section form. The section form will be for adding and updating sections. So all work all we be in the same page.

Creating a Restaurant Menu App With Laravel Inertia and Vue - section crud

Sections Controllers

In the terminal let’s create a new controller for sections:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan make:controller SectionsController
php artisan make:controller SectionsController
php artisan make:controller SectionsController

Next add a resource route for this controller:

routes/web.php

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Route::resource('sections', App\Http\Controllers\SectionsController::class);
Route::resource('sections', App\Http\Controllers\SectionsController::class);
Route::resource('sections', App\Http\Controllers\SectionsController::class);

Open the SectionsController and add the below code:

app/Http/Controllers/SectionsController.php

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?php
namespace App\Http\Controllers;
use App\Models\Section;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SectionsController extends Controller
{
public function index()
{
$sections = Section::latest()->paginate(10);
return Inertia::render('Sections/Index', [
'sections' => $sections
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|max:2000',
'identifier' => 'nullable|max:100'
]);
Section::create($validated);
return redirect()
->back()
->with('success', 'Section created successfully.');
}
public function edit(Section $section)
{
return $section;
}
public function update(Request $request, Section $section)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|max:2000',
'identifier' => 'nullable|max:100'
]);
$section->update($validated);
return redirect()
->back()
->with('success', 'Section updated successfully.');
}
public function destroy(Section $section)
{
$section->delete();
return redirect()
->back()
->with('success', 'Section deleted successfully.');
}
}
<?php namespace App\Http\Controllers; use App\Models\Section; use Illuminate\Http\Request; use Inertia\Inertia; class SectionsController extends Controller { public function index() { $sections = Section::latest()->paginate(10); return Inertia::render('Sections/Index', [ 'sections' => $sections ]); } public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|max:2000', 'identifier' => 'nullable|max:100' ]); Section::create($validated); return redirect() ->back() ->with('success', 'Section created successfully.'); } public function edit(Section $section) { return $section; } public function update(Request $request, Section $section) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|max:2000', 'identifier' => 'nullable|max:100' ]); $section->update($validated); return redirect() ->back() ->with('success', 'Section updated successfully.'); } public function destroy(Section $section) { $section->delete(); return redirect() ->back() ->with('success', 'Section deleted successfully.'); } }
<?php

namespace App\Http\Controllers;

use App\Models\Section;
use Illuminate\Http\Request;
use Inertia\Inertia;

class SectionsController extends Controller
{
    public function index()
    {
        $sections = Section::latest()->paginate(10);

        return Inertia::render('Sections/Index', [
            'sections' => $sections
        ]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|max:2000',
            'identifier' => 'nullable|max:100'
        ]);

        Section::create($validated);

        return redirect()
            ->back()
            ->with('success', 'Section created successfully.');
    }

    public function edit(Section $section)
    {
        return $section;
    }

    public function update(Request $request, Section $section)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|max:2000',
            'identifier' => 'nullable|max:100'
        ]);

        $section->update($validated);

        return redirect()
            ->back()
            ->with('success', 'Section updated successfully.');
    }

    public function destroy(Section $section)
    {
        $section->delete();

        return redirect()
            ->back()
            ->with('success', 'Section deleted successfully.');
    }

}

In the above code i added the basic actions and required logic to handle sections CRUD. In the index() method we retrieve the list of sections ordered in descending order using laravel latest() method and display 10 items per page and then send the sections list as a prop to Sections/Index page we will see later.

The store() and update() methods handle adding and updating sections respectively. First the data is validated before inserted into the database like making the title required and max to be 255, we do the same of the other fields, then assigned the validation result to the $validate variable which contains the cleaned data. After adding or updating we redirect back with status message.

The destroy() is straightforward and deletes a menu section from database and also redirect back with a status message.

 

Sections Frontend

Now let’s move into most of the work of the sections which is the frontend part in Vuejs. First we need to add the link for the Sections in the Navbar.vue, so open Navbar.vue and update the href for the sections link to point to “/sections” url.

resources/js/Layout/Navbar.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.....
<!-- Dropdown menu -->
<div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton">
<li>
....
</li>
<li>
<Link href="/sections" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sections</Link>
</li>
<li>
.....
</li>
</ul>
</div>
....
..... <!-- Dropdown menu --> <div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600"> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton"> <li> .... </li> <li> <Link href="/sections" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sections</Link> </li> <li> ..... </li> </ul> </div> ....
.....  

        <!-- Dropdown menu -->
                <div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
                    <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton">
                    <li>
                        ....
                    </li>
                    <li>
                        <Link href="/sections" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sections</Link>
                    </li>
                    <li>
                        .....
                    </li>
                
                    </ul>
                
                </div>

....

Next create the Sections/ directory in the Pages/ directory to wrap the sections pages. Under the Sections/ directory create these files:

  • resources/js/Pages/Sections/Index.vue
  • resources/js/Pages/Sections/SectionsList.vue
  • resources/js/Pages/Sections/SectionsForm.vue

The Index.vue page will contain both the SectionsList and SectionsForm. Open the Index.vue and update it as shown:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import { ref } from 'vue';
import Main from '../../Layout/Main.vue';
import SectionsForm from './SectionsForm.vue';
import SectionsList from './SectionsList.vue';
import FlashMessage from '../../Components/FlashMessage.vue';
defineProps({
sections: Object
});
const edit_section = ref(null);
const getSection = async (id) => {
const { data } = await axios.get(`/sections/${id}/edit`);
edit_section.value = data;
}
</script>
<template>
<Main>
<div class="mb-2">
<FlashMessage />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<SectionsList
:sections="sections"
@clickEdit="getSection"
/>
</div>
<div>
<SectionsForm
:edit_section="edit_section"
@onAdd="edit_section = null"
/>
</div>
</div>
</Main>
</template>
<script setup> import { ref } from 'vue'; import Main from '../../Layout/Main.vue'; import SectionsForm from './SectionsForm.vue'; import SectionsList from './SectionsList.vue'; import FlashMessage from '../../Components/FlashMessage.vue'; defineProps({ sections: Object }); const edit_section = ref(null); const getSection = async (id) => { const { data } = await axios.get(`/sections/${id}/edit`); edit_section.value = data; } </script> <template> <Main> <div class="mb-2"> <FlashMessage /> </div> <div class="grid grid-cols-2 gap-4"> <div> <SectionsList :sections="sections" @clickEdit="getSection" /> </div> <div> <SectionsForm :edit_section="edit_section" @onAdd="edit_section = null" /> </div> </div> </Main> </template>
<script setup>
    import { ref } from 'vue';
    import Main from '../../Layout/Main.vue';
    import SectionsForm from './SectionsForm.vue';
    import SectionsList from './SectionsList.vue';
    import FlashMessage from '../../Components/FlashMessage.vue';

    defineProps({
        sections: Object
    });

    const edit_section = ref(null);

    const getSection = async (id) => {
        const { data } = await axios.get(`/sections/${id}/edit`);

        edit_section.value = data;
    }

</script>

<template>
    <Main>
        <div class="mb-2">
            <FlashMessage />
        </div>

        <div class="grid grid-cols-2 gap-4">
            <div>
                <SectionsList 
                    :sections="sections"
                    @clickEdit="getSection"
                      />
            </div>
            <div>
                <SectionsForm 
                    :edit_section="edit_section"
                    @onAdd="edit_section = null"
                     />
            </div>
        </div>
    </Main>
</template>

In the <template> we wrapped the page with The <Main> layout. The <FlashMessage /> component is used to show the flash messages, so i imported it above in the <script> tag. Next i added a div which takes a tailwindcss  class `grid` which displays a grid with two columns.

In the first column of the screen i added the <SectionsList /> component we will see below. to display the list of sections in a table and takes a prop `sections` and emit a custom event `@clickEdit`. The sectionsprop in fact comes from the SectionsController.

In the second column of the screen i added the <SectionsForm/> which responsible for printing a form to add or update a section and accepts a prop `edit_section` which is the selected section to update from the grid and emit a custom event `onAdd` to reset the edit_section back to null.

The getSection() function retrieves a single section by id using axios and assigns it’s value to the edit_section variable.

 

Displaying Sections List

Let’s work on the <SectionsList /> component to display sections listing, so open the SectionsList.vue and update like below:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import { ref } from 'vue';
import { router } from '@inertiajs/vue3';
import Pagination from '../../Components/Pagination.vue';
import ConfirmDialog from "../../Components/ConfirmDialog.vue";
const props = defineProps({
sections: Object
})
const deleteModal = ref(null);
const emits = defineEmits(['clickEdit']);
const changePage = (page) => {
router.get(`/sections?page=${page}`);
};
const destroy = () => {
router.delete(`/sections/${deleteModal.value}`);
deleteModal.value = null;
}
</script>
<template>
<div>
<h3 class="font-bold text-xl">Sections</h3>
<div class="relative overflow-x-auto shadow-md mt-5">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
#
</th>
<th scope="col" class="px-6 py-3">
Title
</th>
<th scope="col" class="px-6 py-3">
Identifier
</th>
<th scope="col" class="px-6 py-3">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="section in sections.data" :key="section.id" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="px-6 py-4">
{{ section.id }}
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', section.id)">{{ section.title }}</a>
</td>
<td class="px-6 py-4">
{{ section.identifier }}
</td>
<td class="px-6 py-4 text-right">
<a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', section.id)">Edit</a>
<a href="#" class="font-medium text-red-600 dark:text-red-500 hover:underline" @click.prevent="deleteModal = section.id">Delete</a>
</td>
</tr>
</tbody>
</table>
<div class="mt-7 mb-4 text-center">
<Pagination
:current-page="sections.current_page"
:total-pages="sections.last_page"
@page-changed="changePage"
/>
</div>
</div>
<Teleport to="body">
<ConfirmDialog
v-if="deleteModal"
confirmText="Are you sure you want to delete?"
@close="deleteModal = null"
@confirm="destroy" />
</Teleport>
</div>
</template>
<script setup> import { ref } from 'vue'; import { router } from '@inertiajs/vue3'; import Pagination from '../../Components/Pagination.vue'; import ConfirmDialog from "../../Components/ConfirmDialog.vue"; const props = defineProps({ sections: Object }) const deleteModal = ref(null); const emits = defineEmits(['clickEdit']); const changePage = (page) => { router.get(`/sections?page=${page}`); }; const destroy = () => { router.delete(`/sections/${deleteModal.value}`); deleteModal.value = null; } </script> <template> <div> <h3 class="font-bold text-xl">Sections</h3> <div class="relative overflow-x-auto shadow-md mt-5"> <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400"> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> <tr> <th scope="col" class="px-6 py-3"> # </th> <th scope="col" class="px-6 py-3"> Title </th> <th scope="col" class="px-6 py-3"> Identifier </th> <th scope="col" class="px-6 py-3"> <span class="sr-only">Actions</span> </th> </tr> </thead> <tbody> <tr v-for="section in sections.data" :key="section.id" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600"> <td class="px-6 py-4"> {{ section.id }} </td> <td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', section.id)">{{ section.title }}</a> </td> <td class="px-6 py-4"> {{ section.identifier }} </td> <td class="px-6 py-4 text-right"> <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', section.id)">Edit</a> <a href="#" class="font-medium text-red-600 dark:text-red-500 hover:underline" @click.prevent="deleteModal = section.id">Delete</a> </td> </tr> </tbody> </table> <div class="mt-7 mb-4 text-center"> <Pagination :current-page="sections.current_page" :total-pages="sections.last_page" @page-changed="changePage" /> </div> </div> <Teleport to="body"> <ConfirmDialog v-if="deleteModal" confirmText="Are you sure you want to delete?" @close="deleteModal = null" @confirm="destroy" /> </Teleport> </div> </template>
<script setup>
    import { ref } from 'vue';
    import { router } from '@inertiajs/vue3';
    import Pagination from '../../Components/Pagination.vue';
    import ConfirmDialog from "../../Components/ConfirmDialog.vue";

    const props = defineProps({
        sections: Object
    })

    const deleteModal = ref(null);

    const emits = defineEmits(['clickEdit']);

    const changePage = (page) => {
        router.get(`/sections?page=${page}`);
    };

    const destroy = () => {
        router.delete(`/sections/${deleteModal.value}`);

        deleteModal.value = null;
    }
</script>

<template>
    <div>
        <h3 class="font-bold text-xl">Sections</h3>

        <div class="relative overflow-x-auto shadow-md mt-5">
            <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
                <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                    <tr>
                        <th scope="col" class="px-6 py-3">
                            #
                        </th>
                        <th scope="col" class="px-6 py-3">
                            Title
                        </th>
                        <th scope="col" class="px-6 py-3">
                            Identifier
                        </th>
                        <th scope="col" class="px-6 py-3">
                            <span class="sr-only">Actions</span>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="section in sections.data" :key="section.id" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
                        <td class="px-6 py-4">
                            {{ section.id }}
                        </td>
                        <td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
                            <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', section.id)">{{ section.title }}</a>
                        </td>
                        <td class="px-6 py-4">
                            {{ section.identifier }}
                        </td>
                        <td class="px-6 py-4 text-right">
                            <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', section.id)">Edit</a>
                            <a href="#" class="font-medium text-red-600 dark:text-red-500 hover:underline" @click.prevent="deleteModal = section.id">Delete</a>
                        </td>
                    </tr>
                
                </tbody>
            </table>

            <div class="mt-7 mb-4 text-center">
                <Pagination
                :current-page="sections.current_page"
                :total-pages="sections.last_page"
                @page-changed="changePage"
                
            />
            </div>
        </div>

        <Teleport to="body">
            <ConfirmDialog 
                v-if="deleteModal"
                confirmText="Are you sure you want to delete?"
                @close="deleteModal = null"  
                @confirm="destroy" />
        </Teleport>
    </div>
</template>

As we do in the previous part for displaying menu items, the same is done here. At the top we imported the needed dependencies ref and router. And also imported the helper components <Pagination /> for pagination links and <ConfirmDialog />.

To display the sections, we must receive them first with the defineProps() macro. In the template we displayed a table and iterated over sections.data collection to display each section in a row with the section title and the action buttons.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<tr v-for="section in sections.data" :key="section.id">
</tr>
<tr v-for="section in sections.data" :key="section.id"> </tr>
<tr v-for="section in sections.data" :key="section.id">
  
</tr>

The <Pagination /> component is displayed at the bottom of the table and emits the page-changed custom event and calls changePage() function to navigate to a new page.

When clicking the edit section link we emit the custom event `clickEdit` and passing the section id:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<a href="#" @click.prevent="$emit('clickEdit', section.id)">Edit</a>
<a href="#" @click.prevent="$emit('clickEdit', section.id)">Edit</a>
<a href="#" @click.prevent="$emit('clickEdit', section.id)">Edit</a>

The clickEdit event is triggered in the above component to display the section info as shown in the Index.vue page. 

When clicking the delete button it sets the deleteModal=section.id which fires the <ConfirmDialog /> component to delete the section with the destroy() function.

 

Sections Form

The other component we will work with is the <SectionsForm > component which display the form to create and update sections.

Pages/Sections/SectionsForm.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import { useForm } from '@inertiajs/vue3';
import { watch } from 'vue';
import InputError from '../../Components/InputError.vue';
const props = defineProps({
edit_section: Object
})
const emits = defineEmits('onAdd');
const form = useForm({
title: '',
description: '',
identifier: ''
})
const submit = () => {
if(!props.edit_section) {
form.post('/sections', {
onSuccess: () => {
form.reset();
emits('onAdd', {form});
}
});
return;
}
form.put(`/sections/${props.edit_section.id}`, {
onSuccess: () => {
form.reset();
emits('onAdd', {form, id: props.edit_section.id});
}
});
}
watch(() => props.edit_section, (newVal, oldVal) => {
if(newVal != oldVal && newVal) {
form.title = newVal.title;
form.description = newVal.description;
form.identifier = newVal.identifier;
}
})
</script>
<template>
<div class="px-2">
<h3 class="font-bold text-xl">{{ !edit_section ? 'Add' : 'Edit' }} Section</h3>
<form class="p-4 md:p-5" @submit.prevent="submit">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
<input type="text" name="title" id="title" v-model="form.title" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<InputError :errors="form.errors" field="title" />
</div>
<div class="col-span-2">
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea id="description" rows="4" v-model="form.description" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Write your description"></textarea>
<InputError :errors="form.errors" field="description" />
</div>
<div class="col-span-2">
<label for="identifier" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Identifier</label>
<input type="text" name="identifier" id="identifier" v-model="form.identifier" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<InputError :errors="form.errors" field="identifier" />
<span class="block text-gray-500 text-sm pt-1">The identifier used internally to distinguish sections when assigned in multiple menus</span>
</div>
</div>
<button type="submit" :disabled="form.processing" class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mt-3">
{{ edit_section ? 'Update Section' : 'Add Section' }}
</button>
</form>
</div>
</template>
<script setup> import { useForm } from '@inertiajs/vue3'; import { watch } from 'vue'; import InputError from '../../Components/InputError.vue'; const props = defineProps({ edit_section: Object }) const emits = defineEmits('onAdd'); const form = useForm({ title: '', description: '', identifier: '' }) const submit = () => { if(!props.edit_section) { form.post('/sections', { onSuccess: () => { form.reset(); emits('onAdd', {form}); } }); return; } form.put(`/sections/${props.edit_section.id}`, { onSuccess: () => { form.reset(); emits('onAdd', {form, id: props.edit_section.id}); } }); } watch(() => props.edit_section, (newVal, oldVal) => { if(newVal != oldVal && newVal) { form.title = newVal.title; form.description = newVal.description; form.identifier = newVal.identifier; } }) </script> <template> <div class="px-2"> <h3 class="font-bold text-xl">{{ !edit_section ? 'Add' : 'Edit' }} Section</h3> <form class="p-4 md:p-5" @submit.prevent="submit"> <div class="grid gap-4 mb-4 grid-cols-2"> <div class="col-span-2"> <label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label> <input type="text" name="title" id="title" v-model="form.title" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"> <InputError :errors="form.errors" field="title" /> </div> <div class="col-span-2"> <label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label> <textarea id="description" rows="4" v-model="form.description" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Write your description"></textarea> <InputError :errors="form.errors" field="description" /> </div> <div class="col-span-2"> <label for="identifier" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Identifier</label> <input type="text" name="identifier" id="identifier" v-model="form.identifier" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"> <InputError :errors="form.errors" field="identifier" /> <span class="block text-gray-500 text-sm pt-1">The identifier used internally to distinguish sections when assigned in multiple menus</span> </div> </div> <button type="submit" :disabled="form.processing" class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mt-3"> {{ edit_section ? 'Update Section' : 'Add Section' }} </button> </form> </div> </template>
<script setup>
    import { useForm } from '@inertiajs/vue3';
    import { watch } from 'vue';
    import InputError from '../../Components/InputError.vue';

    const props = defineProps({
        edit_section: Object
    })

    const emits = defineEmits('onAdd');

    const form = useForm({
        title: '',
        description: '',
        identifier: ''
    })

    const submit = () => {
        if(!props.edit_section) {
            form.post('/sections', {
                onSuccess: () => {
                    form.reset();

                    emits('onAdd', {form});
                }
            });
            return;
        }

        form.put(`/sections/${props.edit_section.id}`, {
            onSuccess: () => {
                form.reset();

                emits('onAdd', {form, id: props.edit_section.id});
            }
        });
    }

    watch(() => props.edit_section, (newVal, oldVal) => {
        if(newVal != oldVal && newVal) {
            form.title = newVal.title;
            form.description = newVal.description;
            form.identifier = newVal.identifier;
        }
      })
</script>

<template>
    <div class="px-2">
        <h3 class="font-bold text-xl">{{ !edit_section ? 'Add' : 'Edit' }} Section</h3>

        <form class="p-4 md:p-5" @submit.prevent="submit">
            <div class="grid gap-4 mb-4 grid-cols-2">
                <div class="col-span-2">
                    <label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
                    <input type="text" name="title" id="title" v-model="form.title" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
                    <InputError :errors="form.errors" field="title" />
                </div>

                <div class="col-span-2">
                    <label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
                    <textarea id="description" rows="4" v-model="form.description" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Write your description"></textarea>                    
                    <InputError :errors="form.errors" field="description" />
                </div>

                <div class="col-span-2">
                    <label for="identifier" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Identifier</label>
                    <input type="text" name="identifier" id="identifier" v-model="form.identifier" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
                    <InputError :errors="form.errors" field="identifier" />
                    <span class="block text-gray-500 text-sm pt-1">The identifier used internally to distinguish sections when assigned in multiple menus</span>
                </div>
                
            </div>

            <button type="submit" :disabled="form.processing" class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mt-3">
                {{ edit_section ? 'Update Section' : 'Add Section' }}
            </button>
        </form>
    </div>
</template>

In this component the useForm() helper and vue watch() function is imported. Also we import <InputError /> helper component to display input validation errors. Next we declared the defineProps() to receive prop `edit_section` which contains the section to be updated in case of edit mode.

The component emits a custom event `onAdd` after a successful add or update operation. Using the useForm() helper we declared the form state as an object with title, description, and identifier And bind the form variable in each input using v-model directive like in title field v-model="form.title".

In the submit() function we check for props.edit_section which refers to if this is update or add operation, then invoke form.post() for add or form.update() for edit. On the success callback for form.post and form.update we reset the form and emit the event:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
onSuccess: () => {
form.reset();
emits('onAdd', {form});
}
onSuccess: () => { form.reset(); emits('onAdd', {form}); }
onSuccess: () => {
                    form.reset();

                    emits('onAdd', {form});
                }

To populate the form fields in case of update we use Vue watch api to check for changes in props.edit_section and bind each field respectively:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
watch(() => props.edit_section, (newVal, oldVal) => {
if(newVal != oldVal && newVal) {
form.title = newVal.title;
..
..
watch(() => props.edit_section, (newVal, oldVal) => { if(newVal != oldVal && newVal) { form.title = newVal.title; .. ..
watch(() => props.edit_section, (newVal, oldVal) => {
        if(newVal != oldVal && newVal) {
            form.title = newVal.title;
            ..
            ..
          

 

Menus CRUD

The last thing we will implement in this article is the menus CRUD. The menus CRUD will be the same as the sections CRUD in the previous section where we will display both the menu listing and menu form in the same page.

Create a new controller for menus manipulation:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan make:controller MenusController
php artisan make:controller MenusController
php artisan make:controller MenusController

Add a route for this controller in routes/web.php

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Route::resource('menus', App\Http\Controllers\MenusController::class);
Route::resource('menus', App\Http\Controllers\MenusController::class);
Route::resource('menus', App\Http\Controllers\MenusController::class);

Now all the url’s for menus will prefixed with `menus`.

Open this new controller MenusController and update as below:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?php
namespace App\Http\Controllers;
use App\Models\Menu;
use App\Models\Section;
use Illuminate\Http\Request;
use Inertia\Inertia;
class MenusController extends Controller
{
public function index()
{
$menus = Menu::latest()->paginate(10);
$sections = Section::all();
return Inertia::render('Menus/Index', [
'menus' => $menus,
'sections' => $sections
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|max:2000'
]);
$menu = Menu::create($validated);
$menu->sections()->sync($request->sections);
return redirect()
->back()
->with('success', 'Menu created successfully.');
}
public function edit($id)
{
$menu = Menu::with('sections')->findOrFail($id);
return $menu;
}
public function update(Request $request, Menu $menu)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|max:2000'
]);
$menu->update($validated);
$menu->sections()->sync($request->sections);
return redirect()
->back()
->with('success', 'Menu updated successfully.');
}
public function destroy(Menu $menu)
{
$menu->delete();
return redirect()
->back()
->with('success', 'Menu deleted successfully.');
}
}
<?php namespace App\Http\Controllers; use App\Models\Menu; use App\Models\Section; use Illuminate\Http\Request; use Inertia\Inertia; class MenusController extends Controller { public function index() { $menus = Menu::latest()->paginate(10); $sections = Section::all(); return Inertia::render('Menus/Index', [ 'menus' => $menus, 'sections' => $sections ]); } public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|max:2000' ]); $menu = Menu::create($validated); $menu->sections()->sync($request->sections); return redirect() ->back() ->with('success', 'Menu created successfully.'); } public function edit($id) { $menu = Menu::with('sections')->findOrFail($id); return $menu; } public function update(Request $request, Menu $menu) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|max:2000' ]); $menu->update($validated); $menu->sections()->sync($request->sections); return redirect() ->back() ->with('success', 'Menu updated successfully.'); } public function destroy(Menu $menu) { $menu->delete(); return redirect() ->back() ->with('success', 'Menu deleted successfully.'); } }
<?php

namespace App\Http\Controllers;

use App\Models\Menu;
use App\Models\Section;
use Illuminate\Http\Request;
use Inertia\Inertia;

class MenusController extends Controller
{
    public function index()
    {
        $menus = Menu::latest()->paginate(10);

        $sections = Section::all();

        return Inertia::render('Menus/Index', [
            'menus' => $menus,
            'sections' => $sections
        ]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|max:2000'
        ]);

        $menu = Menu::create($validated);

        $menu->sections()->sync($request->sections);

        return redirect()
            ->back()
            ->with('success', 'Menu created successfully.');
    }

    public function edit($id)
    {
        $menu = Menu::with('sections')->findOrFail($id);

        return $menu;
    }

    public function update(Request $request, Menu $menu)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|max:2000'
        ]);

        $menu->update($validated);

        $menu->sections()->sync($request->sections);

        return redirect()
            ->back()
            ->with('success', 'Menu updated successfully.');
    }

    public function destroy(Menu $menu)
    {
        $menu->delete();

        return redirect()
            ->back()
            ->with('success', 'Menu deleted successfully.');
    }

}

As you see in this code the same is done like in the SectionsController. In the index() method beside sending the $menus collection, we also send all the available $sections as we need this when adding new menu. 

In the store() and update() methods after creating or updating menu we do sync the sections to the menu using eloquent sync() method. The sync() method is dedicated for many to many relationships to attach items to the relation:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$menu->sections()->sync($request->sections);
$menu->sections()->sync($request->sections);
$menu->sections()->sync($request->sections);

 

Menus Frontend Pages

Let’s add the link for the menus page. So open resources/js/Layout/Navbar.vue and update the menus link in the dropdown:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!-- Dropdown menu -->
<div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton">
<li>
<Link href="/menus" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Menus</Link>
</li>
<li>
....
</li>
<li>
....
</li>
</ul>
</div>
<!-- Dropdown menu --> <div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600"> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton"> <li> <Link href="/menus" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Menus</Link> </li> <li> .... </li> <li> .... </li> </ul> </div>
<!-- Dropdown menu -->
<div id="dropdownNavbar" class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
    <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownLargeButton">

                    <li>
                        <Link href="/menus" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Menus</Link>
                    </li>
                    <li>
                        ....
                    </li>
                    <li>
                        ....
                    </li>
                
   </ul>
</div>

Next create a new directory in Pages/ folder named `Menus`. Inside of it create these vue files:

  • Index.vue
  • MenusList.vue
  • MenusForm.vue

 

resources/js/Pages/Index.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import { ref } from 'vue';
import Main from '../../Layout/Main.vue';
import MenusForm from './MenusForm.vue';
import MenusList from './MenusList.vue';
import FlashMessage from '../../Components/FlashMessage.vue';
defineProps({
menus: Object,
sections: Array
});
const edit_menu = ref(null);
const getMenu = async (id) => {
const { data } = await axios.get(`/menus/${id}/edit`);
edit_menu.value = data;
}
</script>
<template>
<Main>
<div class="mb-2">
<FlashMessage />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<MenusList
:menus="menus"
@clickEdit="getMenu"
/>
</div>
<div>
<MenusForm
:edit_menu="edit_menu"
:sections="sections"
@onAdd="edit_menu = null"
/>
</div>
</div>
</Main>
</template>
<script setup> import { ref } from 'vue'; import Main from '../../Layout/Main.vue'; import MenusForm from './MenusForm.vue'; import MenusList from './MenusList.vue'; import FlashMessage from '../../Components/FlashMessage.vue'; defineProps({ menus: Object, sections: Array }); const edit_menu = ref(null); const getMenu = async (id) => { const { data } = await axios.get(`/menus/${id}/edit`); edit_menu.value = data; } </script> <template> <Main> <div class="mb-2"> <FlashMessage /> </div> <div class="grid grid-cols-2 gap-4"> <div> <MenusList :menus="menus" @clickEdit="getMenu" /> </div> <div> <MenusForm :edit_menu="edit_menu" :sections="sections" @onAdd="edit_menu = null" /> </div> </div> </Main> </template>
<script setup>
    import { ref } from 'vue';
    import Main from '../../Layout/Main.vue';
    import MenusForm from './MenusForm.vue';
    import MenusList from './MenusList.vue';
    import FlashMessage from '../../Components/FlashMessage.vue';

    defineProps({
        menus: Object,
        sections: Array
    });

    const edit_menu = ref(null);

    const getMenu = async (id) => {
        const { data } = await axios.get(`/menus/${id}/edit`);

        edit_menu.value = data;
    }

</script>

<template>
    <Main>
        <div class="mb-2">
            <FlashMessage />
        </div>

        <div class="grid grid-cols-2 gap-4">
            <div>
                <MenusList 
                    :menus="menus"
                    @clickEdit="getMenu"
                      />
            </div>
            <div>
                <MenusForm 
                    :edit_menu="edit_menu"
                    :sections="sections"
                    @onAdd="edit_menu = null"
                     />
            </div>
        </div>
    </Main>
</template>

This page similar to the same page for Sections so i am not going to explain each part of this code. It contains the <MenusList /> and <MenusForm /> components for menus listing and menu form respectively. 

 

Displaying Menus List

Open MenusList.vue file and add this code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import { ref } from 'vue';
import { router } from '@inertiajs/vue3';
import Pagination from '../../Components/Pagination.vue';
import ConfirmDialog from "../../Components/ConfirmDialog.vue";
const props = defineProps({
menus: Object
})
const deleteModal = ref(null);
const emits = defineEmits(['clickEdit']);
const changePage = (page) => {
router.get(`/menus?page=${page}`);
};
const destroy = () => {
router.delete(`/menus/${deleteModal.value}`);
deleteModal.value = null;
}
</script>
<template>
<div>
<h3 class="font-bold text-xl">Menus</h3>
<div class="relative overflow-x-auto shadow-md mt-5">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
#
</th>
<th scope="col" class="px-6 py-3">
Title
</th>
<th scope="col" class="px-6 py-3">
Description
</th>
<th scope="col" class="px-6 py-3">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="menu in menus.data" :key="menu.id" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="px-6 py-4">
{{ menu.id }}
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', menu.id)">{{ menu.title }}</a>
</td>
<td class="px-6 py-4">
{{ menu.identifier }}
</td>
<td class="px-6 py-4 text-right">
<a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', menu.id)">Edit</a>
<a href="#" class="font-medium text-red-600 dark:text-red-500 hover:underline" @click.prevent="deleteModal = menu.id">Delete</a>
</td>
</tr>
</tbody>
</table>
<div class="mt-7 mb-4 text-center">
<Pagination
:current-page="menus.current_page"
:total-pages="menus.last_page"
@page-changed="changePage"
/>
</div>
</div>
<Teleport to="body">
<ConfirmDialog
v-if="deleteModal"
confirmText="Are you sure you want to delete?"
@close="deleteModal = null"
@confirm="destroy" />
</Teleport>
</div>
</template>
<script setup> import { ref } from 'vue'; import { router } from '@inertiajs/vue3'; import Pagination from '../../Components/Pagination.vue'; import ConfirmDialog from "../../Components/ConfirmDialog.vue"; const props = defineProps({ menus: Object }) const deleteModal = ref(null); const emits = defineEmits(['clickEdit']); const changePage = (page) => { router.get(`/menus?page=${page}`); }; const destroy = () => { router.delete(`/menus/${deleteModal.value}`); deleteModal.value = null; } </script> <template> <div> <h3 class="font-bold text-xl">Menus</h3> <div class="relative overflow-x-auto shadow-md mt-5"> <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400"> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> <tr> <th scope="col" class="px-6 py-3"> # </th> <th scope="col" class="px-6 py-3"> Title </th> <th scope="col" class="px-6 py-3"> Description </th> <th scope="col" class="px-6 py-3"> <span class="sr-only">Actions</span> </th> </tr> </thead> <tbody> <tr v-for="menu in menus.data" :key="menu.id" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600"> <td class="px-6 py-4"> {{ menu.id }} </td> <td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', menu.id)">{{ menu.title }}</a> </td> <td class="px-6 py-4"> {{ menu.identifier }} </td> <td class="px-6 py-4 text-right"> <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', menu.id)">Edit</a> <a href="#" class="font-medium text-red-600 dark:text-red-500 hover:underline" @click.prevent="deleteModal = menu.id">Delete</a> </td> </tr> </tbody> </table> <div class="mt-7 mb-4 text-center"> <Pagination :current-page="menus.current_page" :total-pages="menus.last_page" @page-changed="changePage" /> </div> </div> <Teleport to="body"> <ConfirmDialog v-if="deleteModal" confirmText="Are you sure you want to delete?" @close="deleteModal = null" @confirm="destroy" /> </Teleport> </div> </template>
<script setup>
    import { ref } from 'vue';
    import { router } from '@inertiajs/vue3';
    import Pagination from '../../Components/Pagination.vue';
    import ConfirmDialog from "../../Components/ConfirmDialog.vue";

    const props = defineProps({
        menus: Object
    })

    const deleteModal = ref(null);

    const emits = defineEmits(['clickEdit']);

    const changePage = (page) => {
        router.get(`/menus?page=${page}`);
    };

    const destroy = () => {
        router.delete(`/menus/${deleteModal.value}`);

        deleteModal.value = null;
    }
</script>

<template>
    <div>
        <h3 class="font-bold text-xl">Menus</h3>

        <div class="relative overflow-x-auto shadow-md mt-5">
            <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
                <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                    <tr>
                        <th scope="col" class="px-6 py-3">
                            #
                        </th>
                        <th scope="col" class="px-6 py-3">
                            Title
                        </th>
                        <th scope="col" class="px-6 py-3">
                            Description
                        </th>
                        <th scope="col" class="px-6 py-3">
                            <span class="sr-only">Actions</span>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="menu in menus.data" :key="menu.id" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
                        <td class="px-6 py-4">
                            {{ menu.id }}
                        </td>
                        <td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
                            <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', menu.id)">{{ menu.title }}</a>
                        </td>
                        <td class="px-6 py-4">
                            {{ menu.identifier }}
                        </td>
                        <td class="px-6 py-4 text-right">
                            <a href="#" class="font-medium text-blue-600 dark:text-blue-500 hover:underline mx-3" @click.prevent="$emit('clickEdit', menu.id)">Edit</a>
                            <a href="#" class="font-medium text-red-600 dark:text-red-500 hover:underline" @click.prevent="deleteModal = menu.id">Delete</a>
                        </td>
                    </tr>
                
                </tbody>
            </table>

            <div class="mt-7 mb-4 text-center">
                <Pagination
                :current-page="menus.current_page"
                :total-pages="menus.last_page"
                @page-changed="changePage"
                
            />
            </div>
        </div>

        <Teleport to="body">
            <ConfirmDialog 
                v-if="deleteModal"
                confirmText="Are you sure you want to delete?"
                @close="deleteModal = null"  
                @confirm="destroy" />
        </Teleport>
    </div>
</template>

The MenusList page displays a table for menus with the menu title and action buttons to update and delete each menu. It similar to the listing page for Sections.

 

Menus Form

The next thing is the menus form which in MenusForm.vue. Open this file and update with this code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
import { useForm } from '@inertiajs/vue3';
import { watch } from 'vue';
import InputError from '../../Components/InputError.vue';
const props = defineProps({
edit_menu: Object,
sections: Array
})
const emits = defineEmits('onAdd');
const form = useForm({
title: '',
description: '',
sections: []
})
const submit = () => {
if(!props.edit_menu) {
form.post('/menus', {
onSuccess: () => {
form.reset();
emits('onAdd', {form});
}
});
return;
}
form.put(`/menus/${props.edit_menu.id}`, {
onSuccess: () => {
form.reset();
emits('onAdd', {form, id: props.edit_menu.id});
}
});
}
watch(() => props.edit_menu, (newVal, oldVal) => {
if(newVal != oldVal && newVal) {
form.title = newVal.title;
form.description = newVal.description;
form.sections = newVal.sections.map(sect => sect.id);
}
})
</script>
<template>
<div class="px-2">
<h3 class="font-bold text-xl">{{ !edit_menu ? 'Add' : 'Edit' }} Menu</h3>
<form class="p-4 md:p-5" @submit.prevent="submit">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
<input type="text" name="title" id="title" v-model="form.title" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<InputError :errors="form.errors" field="title" />
</div>
<div class="col-span-2">
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea id="description" rows="4" v-model="form.description" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Write your description"></textarea>
<InputError :errors="form.errors" field="description" />
</div>
<div class="col-span-2">
<label class="block mb-2 text-lg font-medium text-gray-900 dark:text-white">Menu Sections</label>
<div class="flex mb-2" v-for="(section, index) in sections" :key="index">
<div class="flex items-center h-5">
<input :id="'checkbox-'+index" type="checkbox" :value="section.id" v-model="form.sections" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" >
</div>
<div class="ms-2 text-sm">
<label :for="'checkbox-'+index" class="ms-2 text-sm text-gray-900 dark:text-gray-300">{{ section.title }} </label>
<p class="text-xs font-normal text-gray-500 dark:text-gray-400">{{ section.identifier }}</p>
</div>
</div>
</div>
</div>
<button type="submit" :disabled="form.processing" class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mt-3">
{{ edit_menu ? 'Update Menu' : 'Add Menu' }}
</button>
</form>
</div>
</template>
<script setup> import { useForm } from '@inertiajs/vue3'; import { watch } from 'vue'; import InputError from '../../Components/InputError.vue'; const props = defineProps({ edit_menu: Object, sections: Array }) const emits = defineEmits('onAdd'); const form = useForm({ title: '', description: '', sections: [] }) const submit = () => { if(!props.edit_menu) { form.post('/menus', { onSuccess: () => { form.reset(); emits('onAdd', {form}); } }); return; } form.put(`/menus/${props.edit_menu.id}`, { onSuccess: () => { form.reset(); emits('onAdd', {form, id: props.edit_menu.id}); } }); } watch(() => props.edit_menu, (newVal, oldVal) => { if(newVal != oldVal && newVal) { form.title = newVal.title; form.description = newVal.description; form.sections = newVal.sections.map(sect => sect.id); } }) </script> <template> <div class="px-2"> <h3 class="font-bold text-xl">{{ !edit_menu ? 'Add' : 'Edit' }} Menu</h3> <form class="p-4 md:p-5" @submit.prevent="submit"> <div class="grid gap-4 mb-4 grid-cols-2"> <div class="col-span-2"> <label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label> <input type="text" name="title" id="title" v-model="form.title" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"> <InputError :errors="form.errors" field="title" /> </div> <div class="col-span-2"> <label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label> <textarea id="description" rows="4" v-model="form.description" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Write your description"></textarea> <InputError :errors="form.errors" field="description" /> </div> <div class="col-span-2"> <label class="block mb-2 text-lg font-medium text-gray-900 dark:text-white">Menu Sections</label> <div class="flex mb-2" v-for="(section, index) in sections" :key="index"> <div class="flex items-center h-5"> <input :id="'checkbox-'+index" type="checkbox" :value="section.id" v-model="form.sections" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" > </div> <div class="ms-2 text-sm"> <label :for="'checkbox-'+index" class="ms-2 text-sm text-gray-900 dark:text-gray-300">{{ section.title }} </label> <p class="text-xs font-normal text-gray-500 dark:text-gray-400">{{ section.identifier }}</p> </div> </div> </div> </div> <button type="submit" :disabled="form.processing" class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mt-3"> {{ edit_menu ? 'Update Menu' : 'Add Menu' }} </button> </form> </div> </template>
<script setup>
    import { useForm } from '@inertiajs/vue3';
    import { watch } from 'vue';
    import InputError from '../../Components/InputError.vue';

    const props = defineProps({
        edit_menu: Object,
        sections: Array
    })

    const emits = defineEmits('onAdd');

    const form = useForm({
        title: '',
        description: '',
        sections: []
    })

    const submit = () => {
        if(!props.edit_menu) {
            form.post('/menus', {
                onSuccess: () => {
                    form.reset();

                    emits('onAdd', {form});
                }
            });
            return;
        }

        form.put(`/menus/${props.edit_menu.id}`, {
            onSuccess: () => {
                form.reset();

                emits('onAdd', {form, id: props.edit_menu.id});
            }
        });
    }

    watch(() => props.edit_menu, (newVal, oldVal) => {
        if(newVal != oldVal && newVal) {
            form.title = newVal.title;
            form.description = newVal.description;
            
            form.sections = newVal.sections.map(sect => sect.id);
        }
      })
</script>

<template>
    <div class="px-2">
        <h3 class="font-bold text-xl">{{ !edit_menu ? 'Add' : 'Edit' }} Menu</h3>

        <form class="p-4 md:p-5" @submit.prevent="submit">
            <div class="grid gap-4 mb-4 grid-cols-2">
                <div class="col-span-2">
                    <label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
                    <input type="text" name="title" id="title" v-model="form.title" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
                    <InputError :errors="form.errors" field="title" />
                </div>

                <div class="col-span-2">
                    <label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
                    <textarea id="description" rows="4" v-model="form.description" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Write your description"></textarea>                    
                    <InputError :errors="form.errors" field="description" />
                </div>

                <div class="col-span-2">
                    <label class="block mb-2 text-lg font-medium text-gray-900 dark:text-white">Menu Sections</label>

                    <div class="flex mb-2" v-for="(section, index) in sections" :key="index">
                        <div class="flex items-center h-5">
                            <input :id="'checkbox-'+index" type="checkbox" :value="section.id" v-model="form.sections" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" >
                        </div>
                        <div class="ms-2 text-sm">
                            <label :for="'checkbox-'+index" class="ms-2 text-sm text-gray-900 dark:text-gray-300">{{ section.title }} </label>
                            <p class="text-xs font-normal text-gray-500 dark:text-gray-400">{{ section.identifier }}</p>
                        </div>
                     </div>
                </div>
                
            </div>

            <button type="submit" :disabled="form.processing" class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 mt-3">
                {{ edit_menu ? 'Update Menu' : 'Add Menu' }}
            </button>
        </form>
    </div>
</template>

This component expects the sections prop which is sections to assign this menu to and edit_menu prop which is menu to update in case of edit mode.

In the <template> we displayed the title and description inputs along with the <InputError /> to display validation errors. In addition to that we displayed the list of sections as checkboxes which allow us to assign this menu to multiple sections as we learned the relation between menus and sections is many to many.

The vue watch function is where we populate inputs in edit mode. As shown how we set the form.sections by using javascript map function on the sections array:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
watch(() => props.edit_menu, (newVal, oldVal) => {
if(newVal != oldVal && newVal) {
..
..
form.sections = newVal.sections.map(sect => sect.id);
}
})
watch(() => props.edit_menu, (newVal, oldVal) => { if(newVal != oldVal && newVal) { .. .. form.sections = newVal.sections.map(sect => sect.id); } })
watch(() => props.edit_menu, (newVal, oldVal) => {
        if(newVal != oldVal && newVal) {
            ..
            ..
            
            form.sections = newVal.sections.map(sect => sect.id);
        }
      })

 

At this stage we have finished the CRUD for the menus, sections and menu items. You can test and experiment adding data. Just run `npm run dev` and another terminal launch the project with `php artisan serve`

sections

menu items

menus

 

Continue to part 4 – Menus Preview >

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest


0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments