How to create nested menu in laravel

How to create nested menu in laravel

3,023

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; 
} 
PHP

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
PHP

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
PHP

With this artisan command we tell laravel to make controller named MenuController in App\Http\Controllers folder.

Step 3

  1. 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');
});
PHP

remember columns id , parent_id & order are important to us rest of them are optional.

Now save the file and close it.

  1. 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);
}
PHP

 Now save add close it.

  1. Migrate your schema with following command
php artisan migrate
PHP

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();
}
PHP

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
PHP

 

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)
Markup

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">&times;</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">&times;</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
Markup

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
Markup

 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 4 years ago

Mahammadtaufiq Kharadi
Mahammadtaufiq Kharadi May 03, 2023

dropCallback: function(details) This Function not called when we Drag and Drop Please Check

You must login to leave a comment