
Updated (August 2019): Thanks for helping us to fix typo issues of this tutorial.
Overview
Today I will cover one of the most wanted & useful topics "Nested Menu in Laravel"
In this article we will demonstrates usage of Nestable plugin jQuery plugin to provide the user with nice menu ordering experience without a page refresh.
This article is unique in web, at least I couldn’t find any similar article for nestable menus in laravel or even close to this.
What we need
Creating the menu controller in app/controllers/MenuController.php and the menu model is in app/models/Menu.php
A note on the data structure for the menu
The important columns of the "menus" table are:
id
parent_id
order
With these 3 fields we can build nested menus as many levels deep as you want. The Nestable plugin helps modify the values of these fields for the appropriate rows of data.
Use of recursion
The hard part that took me a long time to build is a very small function inside of app/models/Menu.php:
public function buildMenu($menu, $parentid = 0)
{
$result = null;
foreach ($menu as $item)
if ($item->parent_id == $parentid) {
$result .= "<li class='dd-item nested-list-item' data-order='{$item->order}' data-id='{$item->id}'>
<div class='dd-handle nested-list-handle'>
<span class='glyphicon glyphicon-move'></span>
</div>
<div class='nested-list-content'>{$item->label}
<div class='pull-right'>
<a href='".url("admin/menu/edit/{$item->id}")."'>Edit</a> |
<a href='#' class='delete_toggle' rel='{$item->id}'>Delete</a>
</div>
</div>".$this->buildMenu($menu, $item->id) . "</li>";
}
return $result ? "\n<ol class=\"dd-list\">\n$result</ol>\n" : null;
}
This function uses recursion to display the menu to the user even if the menu is many many levels deep. This function alone can save you a bunch of time.
Let’s code
Step 1
Make Menu model and migration for it. Open your terminal and type command below
Php artisan make:model Menu -m
With this artisan command we tell laravel to make model named Menu in App folder and migration file for our schema in database/migrations folder.
Step 2
Make Menu controller. Open your terminal and type command below
Php artisan make:controller MenuController
With this artisan command we tell laravel to make controller named MenuController in App\Http\Controllers folder.
Step 3
- Make schema
Open your migration file that you created in step 1 and add following codes,
Schema::create('menus', function (Blueprint $table) {
$table->increments('id');
$table->string('title')->nullable();
$table->string('slug')->nullable();
$table->integer('parent_id')->unsigned()->nullable();
$table->integer('order')->unsigned()->default(0);
$table->timestamps();
});
Schema::table('menus', function (Blueprint $table) {
$table->foreign('parent_id')->references('id')->on('menus')->onUpdate('cascade')->onDelete('cascade');
});
remember columns id , parent_id & order are important to us rest of them are optional.
Now save the file and close it.
- Prepare model
- Prepare model
Open your Menu model and add following codes,
protected $fillable = [
'title', 'slug', 'order', 'parent_id'
];
public function buildMenu($menu, $parentid = 0)
{
$result = null;
foreach($menu as $item)
if ($item->parent_id == $parentid) {
$result .= "<li class='dd-item nested-list-item' data-order='{$item->order}' data-id='{$item->id}'>
<div class='dd-handle nested-list-handle'>
<i class='fas fa-arrows-alt'></i>
</div>
<div class='nested-list-content'>{$item->title}
<div class='float-right'>
<a href='/admin/menustop/{$item->id}'>Edit</a> |
<a href='#' class='delete_toggle text-danger' rel='{$item->id}'>Delete</a>
</div>
</div>".$this->buildMenu($menu, $item->id) . "</li>";
}
return $result ? "\n<ol class=\"dd-list\">\n$result</ol>\n" : null;
}
// Getter for the HTML menu builder
public function getHTML($items)
{
return $this->buildMenu($items);
}
Now save add close it.
- Migrate your schema with following command
php artisan migrate
Step 4
Now we have everything ready let’s take care of our MenuController and then make our views.
Open your MenuController which you create in step 2 and add following code.
use Illuminate\Support\Facades\Input;
use App\Menu;
//index page and return menu data by code we defined in our model (getHTML)
public function index()
{
//menu
$menus = Menu::orderby('order', 'asc')->get();
$menu = new Menu;
$menu = $menu->getHTML($menus);
return view('admin.menus.index', compact('menus', 'menu'));
}
//get edit page
public function getEdit($id)
{
$item = Menu::findOrFail($id);
return view('admin.menus.edit', compact('item'));
}
// same as update function when you make resource controller
public function postEdit(MenuRequest $request, $id) //done
{
$item = Menu::find($id);
$item = Menu::where('id',$id)->first();
$item->title = $request->input('title');
$item->slug = $request->input('slug');
$item->parent_id = $request->input('parent_id');
$item->save();
return redirect()->route('menus', $item->id)->with('success', 'Item, '. $item->title.' updated');
}
// AJAX Reordering function (update menu item orders by ajax)
public function postIndex(MenuRequest $request)
{
$source = $request->input('source');
$destination = $request->input('destination');
$item = Menu::find($source);
$item->parent_id = $destination;
$item->save();
$ordering = json_decode(Input::get('order'));
$rootOrdering = json_decode(Input::get('rootOrder'));
if($ordering){
foreach($ordering as $order => $item_id){
if($itemToOrder = Menu::find($item_id)){
$itemToOrder->order = $order;
$itemToOrder->save();
}
}
} else {
foreach($rootOrdering as $order=>$item_id){
if($itemToOrder = Menu::find($item_id)){
$itemToOrder->order = $order;
$itemToOrder->save();
}
}
}
return 'ok ';
}
//store function (create new item)
public function postNew(MenuRequest $request)
{
$item = new Menu;
$item->title = $request->input('title');
$item->slug = $request->input('slug');
$item->order = Menu::max('order')+1;
$item->save();
return redirect()->back();
}
//destroy function
public function postDelete(Request $request)
{
$id = $request->input('delete_id');
// Find all items with the parent_id of this one and reset the parent_id to null
$items = Menu::where('parent_id', $id)->get()->each(function($item)
{
$item->parent_id = '';
$item->save();
});
// Find and delete the item that the user requested to be deleted
$item = Menu::findOrFail($id);
$item->delete();
Session::flash('danger', 'Menu Item successfully deleted.');
return redirect()->back();
}
Step 5
Adding routes
add this routes to your web.php file
//menu
Route::get('menus','MenuController@index')->name('menus'); //index
Route::post('menustop/reorder','MenuController@postIndex'); //re-order
Route::post('menustop/new','MenuController@postNew')->name('topnew'); //create
Route::get('menustop/{id}','MenuController@getEdit'); //edit page
Route::put('menustop/{id}','MenuController@postEdit')->name('topeditupdate'); //update data (edit)
Route::delete('topmenudelete','MenuController@postDelete'); //delete item
Route::get('getCategoryDetails/{id}','MenuController@getCategoryDetails'); //get category title and slug based on selected option
Step 6
Creating views
In views folder create new folder name it admin and inside that create new folder name it menus, we will make all this views in menus folder.
views->admin->menus-> (our blades here)
Create index.blade.php add this codes: (remember change the design as your admin panel style goes)
@extends('layouts.app')
@section('title', 'Menus')
@section('styles')
<link rel="stylesheet" href="{{asset('css/nestable.css')}}">
@endsection
@section('content')
<div class="container">
{{-- menu --}}
<div class="row justify-content-center">
<div class="col-md-12 mt-5">
<div class="card">
<div class="card-body">
<div class="header-title">
Menu
<span class="float-right">
<a href="#newModal" class="btn btn-default pull-right" data-toggle="modal">
<i class="fas fa-plus"></i> Create menu item
</a>
</span>
</div>
{{-- new --}}
<div class="row mt-4 mb-4">
<div class="col-md-8">
<div class="dd" id="nestable">
{!! $menu !!}
</div>
<p id="success-indicator" style="display:none; margin-right: 10px;">
<i class="fas fa-check-circle"></i> Menu order has been saved
</p>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<p>Drag items to move them in a different order <br> <span class="text-info">Supports (2) level deep</span></p>
</div>
</div>
</div>
</div>
<!-- Create new item Modal -->
<div class="modal fade" id="newModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Provide details of new menu item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{{ Form::open(array('route'=>'topnew','class'=>'form-horizontal'))}}
<div class="modal-body">
<div class="form-group row">
<label for="title" class="col-md-3 control-label">Title</label>
<div class="col-md-9">
{{ Form::text('title',null,array('class'=>'form-control'))}}
</div>
</div>
<div class="form-group row">
<label for="slug" class="col-md-3 control-label">Slug</label>
<div class="col-md-9">
{{ Form::text('slug',null,array('class'=>'form-control'))}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
{{ Form::close()}}
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Delete item Modal -->
<div class="modal border-danger fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">Delete Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
</div>
{{ Form::open(array('url'=>'/admin/topmenudelete', 'method' => 'DELETE')) }}
<div class="modal-body">
<p>Are you sure you want to delete this menu item?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<input type="hidden" name="delete_id" id="postvalue" value="" />
<input type="submit" class="btn btn-danger" value="Delete Item" />
</div>
{{ Form::close() }}
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
{{-- new --}}
</div>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script src="{{asset('js/jquery.nestable.js')}}"></script>
{{-- topmenu --}}
<script type="text/javascript">
$(document).ready(function() {
$(function() {
$('.dd').nestable({
dropCallback: function(details) {
var order = new Array();
$("li[data-id='"+details.destId +"']").find('ol:first').children().each(function(index,elem) {
order[index] = $(elem).attr('data-id');
});
if (order.length === 0){
var rootOrder = new Array();
$("#nestable > ol > li").each(function(index,elem) {
rootOrder[index] = $(elem).attr('data-id');
});
}
var token = $('form').find( 'input[name=_token]' ).val();
$.post('{{url("admin/menustop/reorder/")}}',
{
source : details.sourceId,
destination: details.destId,
order:JSON.stringify(order),
rootOrder:JSON.stringify(rootOrder),
_token: token
},
function(data) {
// console.log('data '+data);
})
.done(function() {
$( "#success-indicator" ).fadeIn(100).delay(1000).fadeOut();
})
.fail(function() { })
.always(function() { });
}
});
//delete item
$('.delete_toggle').each(function(index,elem) {
$(elem).click(function(e){
e.preventDefault();
$('#postvalue').attr('value',$(elem).attr('rel'));
$('#deleteModal').modal('toggle');
});
});
});
});
</script>
@endsection
Create edit.blade.php and add following code.
@extends('layouts.app')
@section('title', 'Edit Menu Item')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12 mt-5">
<div class="card">
<div class="card-header">
<h2>
Edit Menu Item
<span class="float-right">
<a class="btn btn-outline-danger" href="{{route('menus')}}">Back</a>
</span>
</h2>
</div>
<div class="card-body">
{{ Form::model($item, array('route' => array('footeditupdate', $item->id), 'method' => 'PUT')) }}
<div class="row">
<div class="col-md-12 mt-3">
{{ Form::label('title', 'Title') }}
{{ Form::text('title', null, array('class' => 'form-control')) }}
</div>
<div class="col-md-12 mt-3">
{{ Form::label('slug', 'Slug') }}
{{ Form::text('slug', null, array('class' => 'form-control')) }}
</div>
<div class="col-md-12 mt-3 text-center">
{{ Form::submit('Update', array('class' => 'btn btn-primary')) }}
</div>
</div>
{{Form::close()}}
</div>
</div>
</div>
</div>
</div>
@endsection
That's all you need to make nestable menu in your laravel application, hope you find this article useful to you. If so, don't forget to hit love emoji below this post.
- Last updated 5 years ago
Be the first to leave a comment.
You must login to leave a comment