How to create multilevel drag and drop menu in laravel like wordpress without packages?

Author Jiwan Thapa         Posted on 24 May, 2021         33831 Views        

In this article, I will be explaining the easiest method to create a multilevel drag and drop menu without packages in laravel just like the one you get with wordpress admin dashboard.

Of course, I found one package on Github to do the same. But, doing it on your own would give you a great sense of satisfaction and pleasure. So, lets see what do we need to create a multilevel drag and drop menu without packages in laravel.

Step 1: Get sortable js for your project

The only thing you will need would be a javascript file named sortable.js. You can download the version i used created by Jonas von Andrian. And, to support the sortable js file, you will need a jquery.js file. You can download the latest version of jQuery.js fle from here. Since, I'm using bootstrap collapse function here, I'll be adding bootstrap.min.js too over here. Sortable.js will allow you to drag and drop menu items so that you could arrange the order of the menu items and define the parent and child in menu section. It will also produce a json data of the menu content that you would save in your database and display in the view blades.

Step 2: Create Database Tables

Now, you will need two database tables to store the menu items you would like to store in a menu and preserve the order of menuitems for each of your menus.

Here's the screenshot of my menuitems table.

menuitems table screenshot

In the menuitems table, we will store the menu title along with its optional name, slug, item type either that is a category, page, post or custom link. We'll also store the target to define either the link should be opened on the same window or on a new tab in browser which will be highly useful in case you want to add an external link in the menu section. We'll also store the menu id for each menu item, so that, we can store same item in multiple menus. And editing or deleting one item from one menu won't affect the same item in another menu.

And, here's the screenshot of my menus table.

menus table screenshot

In the menus table, we'll store the menu title along with the location where we would like to display the menu. The content section would be the one where we'll store the json data created by Sortable.js that preserves the ordering of the menu items.

Although, you can do it in a single database table, I am using two tables to keep things much simpler. One would be the menuitems table and the other would be the menus table. You can skip the menuitems table part and store everything in the content column of your menus table too. But, that would require a huge amount of data to be stored on the content column as json data and editing or deleting any one of those data would be a huge pain.

So, what I do is store each data that I need from a menu item in the menuitems table where editing and deleting would be relatively convenient. And then, pass the ids of those data into the menus table. So, any changes I make in the menuitems would get reflected on the menus with greater ease.

Stetp 3: Update Models

If you are working with the latest laravel version, then, you might know that without updating your model with fillable arrays, you cannot make entry into the database tables. So, here's my models.

Model: Menuitem

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class menuitem extends Model
{
  use HasFactory;
  protected $fillable = ['title','name','slug','type','target','menu_id','created_at','updated_at'];
}	

Model: Menu

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class menu extends Model
{
  use HasFactory;
  protected $fillable = ['title','location','content','created_at','updated_at'];    
}	

Step 4: Create Controller

It's always a good idea to create separate controller for each of your models, so that, you can add individual methods for your CRUD operations. Generally, what I do is create a crudController and store all the CRUD methods inside that one, in cases, where the application is very light and go with idividual controllers for each of the models. So, I leave it up to you to decide which way you want to go by. Since, data for both tables are going to be manipulated from a single view file, I created a single controller for both models.

Here's my menuController.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\menu;
use App\Models\menuitem;

class menuController extends Controller
{	
}

If you don't like to go through all these explanations, you can scroll down to the bottom and get the whole block of code at one place. If you enjoy the explanation and want to learn the process in detail, go through the article.

Step 5: Create Routes

Now it's time to add routes for your view file which we haven't created yet. Let's say the route is manage-menus.

<?php	
use App\Http\Controllers\menuController;

Route::get('manage-menus/{id?}',[menuController::class,'index']);	

Here, I added a question mark with the id to make sure the same route works in both cases. Either, we are editing a menu or creating a new one.

Step 6: Update menuController's index method to load view file

Now, let's add the index method in our menuController to load the manage menu view file. In the view file, we can retrieve all the categories as well as pages/posts. You should add the models for your tables with the articles on top of the controller. So, my controller would look like:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\menu;
use App\Models\menuitem;
use App\Models\category;
use App\Models\post;
use Session;

class menuController extends Controller
{	
  public function index(){
	return view ('backend.menu',['categories'=>category::all(),'posts'=>post::all(),'menus'=>menu::all()]);
  }	
}

Step 7: Create View File

Now, it's time to add the view file. I've created it in bootstrap 3. Here's how it looks.

create new menu view page

And, here's the code for the view blade.

@extends('backend.master')
@section('content')
<script src="{{ url('resources/js/new-sortable.js')}}"></script>

<div class="container-fluid">
  <h2><span>Menus</span></h2>

  <div class="content info-box">		
    <a href="{{url('manage-menus?id=new')}}">Create a new menu</a>.	
  </div>

  <div class="row" id="main-row">				
	<div class="col-sm-3 cat-form @if(count($menus) == 0) disabled @endif">
      <h3><span>Add Menu Items</span></h3>			

	  <div class="panel-group" id="menu-items">
		<div class="panel panel-default">
	      <div class="panel-heading">
			<a href="#categories-list" data-toggle="collapse" data-parent="#menu-items">Categories <span class="caret pull-right"></span></a>
		  </div>
		  <div class="panel-collapse collapse in" id="categories-list">
		    <div class="panel-body">						
			  <div class="item-list-body">
				@foreach($categories as $cat)
				  <p><input type="checkbox" name="select-category[]" value="{{$cat->id}}"> {{$cat->title}}</p>
				@endforeach
			  </div>	
			  <div class="item-list-footer">
			    <label class="btn btn-sm btn-default"><input type="checkbox" id="select-all-categories"> Select All</label>
				  <button type="button" class="pull-right btn btn-default btn-sm" id="add-categories">Add to Menu</button>
			</div>
		  </div>						
		</div>
		<script>
		  $('#select-all-categories').click(function(event) {   
			if(this.checked) {
			  $('#categories-list :checkbox').each(function() {
			    this.checked = true;                        
			  });
			}else{
			  $('#categories-list :checkbox').each(function() {
			    this.checked = false;                        
			  });
			}
		  });
		</script>
	  </div>
	  <div class="panel panel-default">
	    <div class="panel-heading">
		  <a href="#posts-list" data-toggle="collapse" data-parent="#menu-items">posts <span class="caret pull-right"></span></a>
		</div>
		<div class="panel-collapse collapse" id="posts-list">
		  <div class="panel-body">						
			<div class="item-list-body">
			  @foreach($posts as $post)
				<p><input type="checkbox" name="select-post[]" value="{{$post->id}}"> {{$post->title}}</p>
			  @endforeach
			</div>	
			<div class="item-list-footer">
			  <label class="btn btn-sm btn-default"><input type="checkbox" id="select-all-posts"> Select All</label>
			  <button type="button" id="add-posts" class="pull-right btn btn-default btn-sm">Add to Menu</button>
			</div>
		  </div>						
		</div>
		<script>
		  $('#select-all-posts').click(function(event) {   
		    if(this.checked) {
			  $('#posts-list :checkbox').each(function() {
				this.checked = true;                        
			  });
			}else{
			  $('#posts-list :checkbox').each(function() {
				this.checked = false;                        
			  });
			}
		  });
		</script>
	  </div>
	  <div class="panel panel-default">
		<div class="panel-heading">
	      <a href="#custom-links" data-toggle="collapse" data-parent="#menu-items">Custom Links <span class="caret pull-right"></span></a>
		</div>
		<div class="panel-collapse collapse" id="custom-links">
		  <div class="panel-body">						
			<div class="item-list-body">
			  <div class="form-group">
				<label>URL</label>
				<input type="url" id="url" class="form-control" placeholder="https://">
			  </div>
			  <div class="form-group">
				<label>Link Text</label>
			      <input type="text" id="linktext" class="form-control" placeholder="">
				</div>
			  </div>	
			  <div class="item-list-footer">
				<button type="button" class="pull-right btn btn-default btn-sm" id="add-custom-link">Add to Menu</button>
			  </div>
			</div>
		  </div>
		</div>
	  </div>		
	</div>		

	<div class="col-sm-9 cat-view">
	  <h3><span>Menu Structure</span></h3>
	  @if(count($menus)== 0)
	    <h4>Create New Menu</h4>
	    <form method="post" action="{{url('create-menu')}}">
		  {{csrf_field()}}
		  <div class="row">
		    <div class="col-sm-12">
		      <label>Name</label>
			</div>
			<div class="col-sm-6">
			  <div class="form-group">							
			    <input type="text" name="title" class="form-control">
			  </div>
			</div>
			<div class="col-sm-6 text-right">
			  <button class="btn btn-sm btn-primary">Create Menu</button>
			</div>
		  </div>
		</form>			
	  @endif			 					
	</div>
  </div>
</div>
<style>
  .item-list,.info-box{background: #fff;padding: 10px;}
  .item-list-body{max-height: 300px;overflow-y: scroll;}
  .panel-body p{margin-bottom: 5px;}
  .info-box{margin-bottom: 15px;}
  .item-list-footer{padding-top: 10px;}
  .panel-heading a{display: block;}
  .form-inline{display: inline;}
  .form-inline select{padding: 4px 10px;}
  .btn-menu-select{padding: 4px 10px}
  .disabled{pointer-events: none; opacity: 0.7;}
</style>	

Here, one thing to notice is that, we added a css class disabled in the left section that'll prevent the user from adding menu items before a menu exists. Now, the menu blade is created, we can create a menu and continue with all the processing and logical codes from here onwards.

Create Menu

As you can see, we have a menu title field in our view blade. Lets add a menu title then. And, for that, we'll need to define the route create-menu in our web.php file.

Route::post('create-menu',[menuController::class,'store']);	

And, add a basic create method in our controller.

public function store(Request $request)
{
  $data = $request->all(); 
  if(menu::create($data)){ 
    $newdata = menu::orderby('id','DESC')->first();          
    session::flash('success','Menu saved successfully !');             
      return redirect("manage-menus?id=$newdata->id");
  }else{
    return redirect()->back()->with('error','Failed to save menu !');
  }
}	

Once a menu is added in database, I passed the id of the new menu to the url, so that, everytime a new menu is added, the user will be redirected to the same menu edit section while the view blade will be the same.

Now, since the id is passed to the url, we'll have to modify the index method to get data related to the menu from the menu items table.

public function index(){
  $desiredMenu = '';
  $menuitems = '';
  if(isset($_GET['id']) && $_GET['id'] != 'new'){
    $id = $_GET['id'];
    $desiredMenu = menu::where('id',$id)->first();
	if($desiredMenu->content != ''){
      $menuitems = json_decode($desiredMenu->content);
    }else{
      $menuitems = menuitem::where('menu_id',$desiredMenu->id)->get();                    
    }
  }else{
    $desiredMenu = menu::orderby('id','DESC')->first();
    if($desiredMenu){
	  if($desiredMenu->content != ''){
	    $menuitems = json_decode($desiredMenu->content);
	  }else{
	    $menuitems = menuitem::where('menu_id',$desiredMenu->id)->get();                    
	  }
	}    
  }
  return view ('backend.menu',['categories'=>category::all(),'posts'=>post::all(),'menus'=>menu::all(),'desiredMenu'=>$desiredMenu,'menuitems'=>$menuitems]);
}	

Here, we checked if the menu id is passed through the url. If the id is new, $desiredMenu and $menuitems will be empty as the user will be trying to add a new menu. If the id is some value, then $desiredMenu will be the menu with the same id. If no id is passed through the url, we'll display data from the latest menu in the menus table. We'll convert the json data stored in the content column of the menus table into object and store the values in $menuitems. If the content column is empty, we'll store the menuitems from the menuitems in the same variable and display them in the menu structure section.

Minor Changes In View Blade

Now, we'll also make few minor changes in view blade to check if $desiredMenu is empty or not.

Changes In Create Menu Link Section

<div class="content info-box">
  @if(count($menus) > 0)		
	Select a menu to edit: 		
	<form action="{{url('manage-menus')}}" class="form-inline">
      <select name="id">
		@foreach($menus as $menu)
	        @if($desiredMenu != '')
			<option value="{{$menu->id}}" @if($menu->id == $desiredMenu->id) selected @endif>{{$menu->title}}</option>
		  @else
			<option value="{{$menu->id}}">{{$menu->title}}</option>
		  @endif
		@endforeach
	  </select>
	  <button class="btn btn-sm btn-default btn-menu-select">Select</button>
	</form> 
	or <a href="{{url('manage-menus?id=new')}}">Create a new menu</a>.	
  @endif 
</div>	

Here, we added a form to allow user to select the menu that he wants to edit. The initial route we defined to manage the menu will control this functionality too.

Next, we'll make changes on the right hand side too. The section will display create menu form in case the user tries to create a new menu or in case there is no menu data in the database. In case an id is passed through the url, the value stored in $menuitems will be displayed.

Since we have no data in the menuitems table and the content column of our menu is still empty. Lets leave it empty for now.

<div class="col-sm-9 cat-view">
  <h3><span>Menu Structure</span></h3>
  @if($desiredMenu == '')
    <h4>Create New Menu</h4>
	<form method="post" action="{{url('create-menu')}}">
	  {{csrf_field()}}
	  <div class="row">
		<div class="col-sm-12">
		  <label>Name</label>
		</div>
		<div class="col-sm-6">
		  <div class="form-group">							
			<input type="text" name="title" class="form-control">
		  </div>
		</div>
		<div class="col-sm-6 text-right">
		  <button class="btn btn-sm btn-primary">Create Menu</button>
		</div>
	  </div>
	</form>
	@else
	<div id="menu-content">
	  <div style="min-height: 240px;">
		<p>Select categories, pages or add custom links to menus.</p>
      </div>
	  @if($desiredMenu != '')
		<div class="form-group menulocation">
		  <label><b>Menu Location</b></label>
		  <label><input type="radio" name="location" value="1" @if($desiredMenu->location == 1) checked @endif> Header</label>
		  <label><input type="radio" name="location" value="2" @if($desiredMenu->location == 2) checked @endif> Main Navigation</label>
		</div>									
		<div class="text-right">
		  <button class="btn btn-sm btn-primary" id="saveMenu">Save Menu</button>
		</div>
		<p><a href="{{url('delete-menu')}}/{{$desiredMenu->id}}">Delete Menu</a></p>
	  @endif										
	</div>
  @endif	
</div>	

Add Menu Items to menuitems Table

Now, it's time to add data to the menuitems table. You might have noticed that all the data from the categories and posts table are already listed on the left hand side. Once the menu id is passed through the url, that section will be enabled and we can select the data and send it to the route.

Script to select all data from the list is already added to the view blade. Lets add some more script to send data to the database using ajax then.

@if($desiredMenu)
<script>
$('#add-categories').click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var n = $('input[name="select-category[]"]:checked').length;
  var array = $('input[name="select-category[]"]:checked');
  var ids = [];
  for(i=0;i<n;i++){
    ids[i] =  array.eq(i).val();
  }
  if(ids.length == 0){
	return false;
  }
  $.ajax({
	type:"get",
	data: {menuid:menuid,ids:ids},
	url: "{{url('add-categories-to-menu')}}",				
	success:function(res){				
      location.reload();
	}
  })
})
$('#add-posts').click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var n = $('input[name="select-post[]"]:checked').length;
  var array = $('input[name="select-post[]"]:checked');
  var ids = [];
  for(i=0;i<n;i++){
	ids[i] =  array.eq(i).val();
  }
  if(ids.length == 0){
	return false;
  }
  $.ajax({
	type:"get",
	data: {menuid:menuid,ids:ids},
	url: "{{url('add-post-to-menu')}}",				
	success:function(res){
  	  location.reload();
	}
  })
})
$("#add-custom-link").click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var url = $('#url').val();
  var link = $('#linktext').val();
  if(url.length > 0 && link.length > 0){
	$.ajax({
	  type:"get",
	  data: {menuid:menuid,url:url,link:link},
	  url: "{{url('add-custom-link')}}",				
	  success:function(res){
	    location.reload();
	  }
	})
  }
})
</script>
@endif	

All the checked data id from the categories and posts will be stored in an array and sent to the controller via ajax if the menu id is set.

Lets add the route in our web.php file and the methods in our controller file then.

Route::get('add-categories-to-menu',[menuController::class,'addCatToMenu']);
Route::get('add-post-to-menu',[menuController::class,'addPostToMenu']);
Route::get('add-custom-link',[menuController::class,'addCustomLink']);	

Here's the methods in menuController to handle the request.

public function addCatToMenu(Request $request){
  $data = $request->all();
  $menuid = $request->menuid;
  $ids = $request->ids;
  $menu = menu::findOrFail($menuid);
  if($menu->content == ''){
      foreach($ids as $id){
        $data['title'] = category::where('id',$id)->value('title');
        $data['slug'] = category::where('id',$id)->value('slug');
        $data['type'] = 'category';
        $data['menu_id'] = $menuid;
        menuitem::create($data);
      }
    }else{      
      foreach($ids as $id){
        $olddata = json_decode($menu->content,true); 
        $data['title'] = category::where('id',$id)->value('title');
        $data['slug'] = category::where('id',$id)->value('slug');
        $data['type'] = 'category';
        $data['menu_id'] = $menuid;
        $lastdata = menuitem::create($data);
        $newdata = [];      
        $newdata['id'] = $lastdata->id;            
        $newdata['children'] = [[]];
        array_push($olddata[0],$newdata);     
        $olddata = json_encode($olddata);      
        $menu->update(['content'=>$olddata]);
      }
    }
}

public function addPostToMenu(Request $request){
  $data = $request->all();
  $menuid = $request->menuid;
  $ids = $request->ids;
  $menu = menu::findOrFail($menuid);
  if($menu->content == ''){
      foreach($ids as $id){
        $data['title'] = post::where('id',$id)->value('title');
        $data['slug'] = post::where('id',$id)->value('slug');
        $data['type'] = 'post';
        $data['menu_id'] = $menuid;        
        menuitem::create($data);
      }
    }else{      
      foreach($ids as $id){
        $olddata = json_decode($menu->content,true); 
        $data['title'] = post::where('id',$id)->value('title');
        $data['slug'] = post::where('id',$id)->value('slug');
        $data['type'] = 'post';
        $data['menu_id'] = $menuid;
        $lastdata = menuitem::create($data);
        $newdata = [];      
        $newdata['id'] = $lastdata->id;            
        $newdata['children'] = [[]];
        array_push($olddata[0],$newdata);     
        $olddata = json_encode($olddata);      
        $menu->update(['content'=>$olddata]);
      }
    }
}

public function addCustomLink(Request $request){
  $data = $request->all();
  $menuid = $request->menuid;
  $menu = menu::findOrFail($menuid);
  if($menu->content == ''){
      $data['title'] = $request->link;
      $data['slug'] = $request->url;
      $data['type'] = 'custom';
      $data['menu_id'] = $menuid;
      menuitem::create($data);
    }else{
      $olddata = json_decode($menu->content,true);       
      $data['title'] = $request->link;
      $data['slug'] = $request->url;
      $data['type'] = 'custom';
      $data['menu_id'] = $menuid;
      menuitem::create($data);
      $lastdata = menuitem::orderby('id','DESC')->first();
      $array = [];      
      $array['id'] = $lastdata->id;            
      $array['children'] = [[]];
      array_push($olddata[0],$array);      
      $olddata = json_encode($olddata);      
      $menu->update(['content'=>$olddata]);
    }
}

The data will then be stored in menuitems table as we planned. Now, it's time to display them in the menu structure section and allow the users to manipulate their orders and depth. So, lets add this element in the menu-content div.

<div style="min-height: 240px;">
  <p>Select categories, pages or add custom links to menus.</p>
  @if($desiredMenu != '')
	<ul class="menu ui-sortable" id="menuitems">
	  @if(!empty($menuitems))
		@foreach($menuitems as $key=>$item)
		  <li data-id="{{$item->id}}"><span class="menu-item-bar"><i class="fa fa-arrows"></i> @if(empty($item->name)) {{$item->title}} @else {{$item->name}} @endif</span>			
			<ul>
			  @if(isset($item->children))
				@foreach($item->children as $m)
				  @foreach($m as $in=>$data)
				    <li data-id="{{$data->id}}" class="menu-item"> <span class="menu-item-bar"><i class="fa fa-arrows"></i> @if(empty($data->name)) {{$data->title}} @else {{$data->name}} @endif</span>
					  <ul></ul>
				    </li>
				  @endforeach
				@endforeach	
			  @endif	
			</ul>
		  </li>
		@endforeach
	  @endif
	</ul>
  @endif	
</div>	
<style>
  .menu-item-bar{background: #eee;padding: 5px 10px;border:1px solid #d7d7d7;margin-bottom: 5px; width: 75%; cursor: move;display: block;}
  #serialize_output{display: none;}
  .menulocation label{font-weight: normal;display: block;}
  body.dragging, body.dragging * {cursor: move !important;}
  .dragged {position: absolute;z-index: 1;}
  ol.example li.placeholder {position: relative;}
  ol.example li.placeholder:before {position: absolute;}
  #menuitem{list-style: none;}
  #menuitem ul{list-style: none;}
  .input-box{width:75%;background:#fff;padding: 10px;box-sizing: border-box;margin-bottom: 5px;}
  .input-box .form-control{width: 50%}
  .menulocation label{font-weight: normal;display: block;}
</style>

Here's the screenshot of my menuitems list after I added some contents from the categories,posts and custom link.

menuitems table screenshot

You'll need to add a script to make the contents sortable with drag and drop.

<script>
var group = $("#menuitems").sortable({
  group: 'serialization',
  onDrop: function ($item, container, _super) {
    var data = group.sortable("serialize").get();	    
    var jsonString = JSON.stringify(data, null, ' ');
    $('#serialize_output').text(jsonString);
  	  _super($item, container);
  }
});
</script>	

Add a div with id serialize_output somewhere within the view blade and make it hidden, so that, the user can't see the json data. Add the content from the content column of the menu there for safety.

<div id="serialize_output">@if($desiredMenu){{$desiredMenu->content}}@endif</div>	

Now, you can drag and drop the menu items to order them or even define their depth. I'm working with two levels but you can go any depth you want.

Here's the screenshot of my menuitems list after sorting.

sorted menuitems screenshot

Now, we will be adding ajax script to send the menu data to the controller.

@if($desiredMenu)	
<script>
$('#saveMenu').click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var location = $('input[name="location"]:checked').val();
  var newText = $("#serialize_output").text();
  var data = JSON.parse($("#serialize_output").text());	
  $.ajax({
    type:"get",
	data: {menuid:menuid,data:data,location:location},
	url: "{{url('update-menu')}}",				
	success:function(res){
	  window.location.reload();
	}
  })	
})
</script>	
@endif

Add a route in your web.php file to send the data to the controller.

Route::get('update-menu',[menuController::class,'updateMenu']);	

And, here's the controller to update the menu data.

public function updateMenu(Request $request){
  $newdata = $request->all(); 
  $menu=menu::findOrFail($request->menuid);            
  $content = $request->data; 
  $newdata = [];  
  $newdata['location'] = $request->location;       
  $newdata['content'] = json_encode($content);
  $menu->update($newdata); 
}	

With this method, on click of the save button, the menu data will be updated.

Now, the page will show error as the variable $menuitems has no data like title, name, etc. What we stored is just plain ids like the one shown below.

[[{"id":1,"children":[[{"id":6,"children":[[]]},{"id":7,"children":[[]]}]]},{"id":2,"children":[[{"id":8,"children":[[]]}]]},{"id":3,"children":[[{"id":9,"children":[[]]}]]},{"id":4,"children":[[]]},{"id":5,"children":[[]]}]]	

Then, in order to rectify the error, we'll make a few changes on our index method again.

public function index(){
  $menuitems = '';
  $desiredMenu = '';  
  if(isset($_GET['id']) && $_GET['id'] != 'new'){
    $id = $_GET['id'];
    $desiredMenu = menu::where('id',$id)->first();
    if($desiredMenu->content != ''){
      $menuitems = json_decode($desiredMenu->content);
      $menuitems = $menuitems[0]; 
      foreach($menuitems as $menu){
        $menu->title = menuitem::where('id',$menu->id)->value('title');
        $menu->name = menuitem::where('id',$menu->id)->value('name');
        $menu->slug = menuitem::where('id',$menu->id)->value('slug');
        $menu->target = menuitem::where('id',$menu->id)->value('target');
        $menu->type = menuitem::where('id',$menu->id)->value('type');
        if(!empty($menu->children[0])){
          foreach ($menu->children[0] as $child) {
            $child->title = menuitem::where('id',$child->id)->value('title');
            $child->name = menuitem::where('id',$child->id)->value('name');
            $child->slug = menuitem::where('id',$child->id)->value('slug');
            $child->target = menuitem::where('id',$child->id)->value('target');
            $child->type = menuitem::where('id',$child->id)->value('type');
          }  
        }
      }
    }else{
      $menuitems = menuitem::where('menu_id',$desiredMenu->id)->get();                    
    }            
  }else{
    $desiredMenu = menu::orderby('id','DESC')->first();
    if($desiredMenu){
      if($desiredMenu->content != ''){
        $menuitems = json_decode($desiredMenu->content);
        $menuitems = $menuitems[0]; 
        foreach($menuitems as $menu){
          $menu->title = menuitem::where('id',$menu->id)->value('title');
          $menu->name = menuitem::where('id',$menu->id)->value('name');
          $menu->slug = menuitem::where('id',$menu->id)->value('slug');
          $menu->target = menuitem::where('id',$menu->id)->value('target');
          $menu->type = menuitem::where('id',$menu->id)->value('type');
          if(!empty($menu->children[0])){
            foreach ($menu->children[0] as $child) {
              $child->title = menuitem::where('id',$child->id)->value('title');
              $child->name = menuitem::where('id',$child->id)->value('name');
              $child->slug = menuitem::where('id',$child->id)->value('slug');
              $child->target = menuitem::where('id',$child->id)->value('target');
              $child->type = menuitem::where('id',$child->id)->value('type');
            }  
          }
        }
      }else{
        $menuitems = menuitem::where('menu_id',$desiredMenu->id)->get();
      }                                   
    }           
  }
  return view ('index',['categories'=>category::all(),'posts'=>post::all(),'menus'=>menu::all(),'desiredMenu'=>$desiredMenu,'menuitems'=>$menuitems]);
}	

Here, we are simply adding the values related to the menuitems in the content column according to their ids. You can go with the Eloquent Json relations to do the same by updating your models or just go with these five lines. The result will be the same.

Refresh the browser and you'll get your menu items listed according to the order and depth you defined earlier.

Now, to make it look more better we'll add the option to add a name to the menu item that can be displayed instead of the menu title along with the url and target option for custom links. And, for that, we'll need to make a few changes in our view file.

<ul class="menu ui-sortable" id="menuitems">
  @if(!empty($menuitems))
	@foreach($menuitems as $key=>$item)
	  <li data-id="{{$item->id}}"><span class="menu-item-bar"><i class="fa fa-arrows"></i> @if(empty($item->name)) {{$item->title}} @else {{$item->name}} @endif <a href="#collapse{{$item->id}}" class="pull-right" data-toggle="collapse"><i class="caret"></i></a></span>
	    <div class="collapse" id="collapse{{$item->id}}">
	      <div class="input-box">
	        <form method="post" action="{{url('update-menuitem')}}/{{$item->id}}">
			  {{csrf_field()}}
			  <div class="form-group">
				<label>Link Name</label>
				<input type="text" name="name" value="@if(empty($item->name)) {{$item->title}} @else {{$item->name}} @endif" class="form-control">
			  </div>
			  @if($item->type == 'custom')
				<div class="form-group">
				  <label>URL</label>
				  <input type="text" name="slug" value="{{$item->slug}}" class="form-control">
				</div>					
				<div class="form-group">
				  <input type="checkbox" name="target" value="_blank" @if($item->target == '_blank') checked @endif> Open in a new tab
				</div>
			  @endif
			  <div class="form-group">
				<button class="btn btn-sm btn-primary">Save</button>
				<a href="{{url('delete-menuitem')}}/{{$item->id}}/{{$key}}" class="btn btn-sm btn-danger">Delete</a>
			  </div>
			</form>
		  </div>
		</div>
	    <ul>
		  @if(isset($item->children))
		    @foreach($item->children as $m)
		  	  @foreach($m as $in=>$data)
			    <li data-id="{{$data->id}}" class="menu-item"> <span class="menu-item-bar"><i class="fa fa-arrows"></i> @if(empty($data->name)) {{$data->title}} @else {{$data->name}} @endif <a href="#collapse{{$data->id}}" class="pull-right" data-toggle="collapse"><i class="caret"></i></a></span>
			      <div class="collapse" id="collapse{{$data->id}}">
			        <div class="input-box">
			          <form method="post" action="{{url('update-menuitem')}}/{{$data->id}}">
					    {{csrf_field()}}
					    <div class="form-group">
						  <label>Link Name</label>
						  <input type="text" name="name" value="@if(empty($data->name)) {{$data->title}} @else {{$data->name}} @endif" class="form-control">
					    </div>
					    @if($data->type == 'custom')
						  <div class="form-group">
						    <label>URL</label>
						    <input type="text" name="slug" value="{{$data->slug}}" class="form-control">
						  </div>					
						  <div class="form-group">
						    <input type="checkbox" name="target" value="_blank" @if($data->target == '_blank') checked @endif> Open in a new tab
						  </div>
					    @endif
					    <div class="form-group">
						  <button class="btn btn-sm btn-primary">Save</button>
						  <a href="{{url('delete-menuitem')}}/{{$data->id}}/{{$key}}/{{$in}}" class="btn btn-sm btn-danger">Delete</a>
					    </div>
					  </form>
				    </div>
				  </div>
			      <ul></ul>
		        </li>
		      @endforeach
	        @endforeach	
	      @endif	
        </ul>
      </li>
    @endforeach
  @endif
</ul>	

Now, the interface to edit/delete menu items is added to the view file. Here, I've added the array key for the menu item as well as the children items too. That is needed to delete the key data from the json string with relative ease. Lets add the routes then.

Route::post('update-menuitem/{id}',[menuController::class,'updateMenuItem']);
Route::get('delete-menuitem/{id}/{key}/{in?}',[menuController::class,'deleteMenuItem']);	

And the methods in the controller as well.

public function updateMenuItem(Request $request){
  $data = $request->all();        
  $item = menuitem::findOrFail($request->id);
  $item->update($data);
  return redirect()->back();
}

public function deleteMenuItem($id,$key,$in=''){        
  $menuitem = menuitem::findOrFail($id);
  $menu = menu::where('id',$menuitem->menu_id)->first();
  if($menu->content != ''){
    $data = json_decode($menu->content,true);            
    $maindata = $data[0];            
    if($in == ''){
      unset($data[0][$key]);
      $newdata = json_encode($data); 
      $menu->update(['content'=>$newdata]);                         
    }else{
    	unset($data[0][$key]['children'][0][$in]);
	    $newdata = json_encode($data);
      $menu->update(['content'=>$newdata]); 
    }
  }
  $menuitem->delete();
  return redirect()->back();
}	

Now, the last thing remaining is to delete the menu. That can be done easily. Here's the route.

Route::get('delete-menu/{id}',[menuController::class,'destroy']);	

And, here's the controller to delete all data related to the menu from the menuitems table as well delete the menu from the menus table.

public function destroy(Request $request)
{
  menuitem::where('menu_id',$request->id)->delete();	
  menu::findOrFail($request->id)->delete();
  return redirect('manage-menus')->with('success','Menu deleted successfully');
}	

One more thing to worry about, what if a user tries to add menu items to the existing menu. If you remember, we only added the code to update menu content only if the menu content column is empty right? Lets modify those code blocks a bit.

  public function addCatToMenu(Request $request){
    $data = $request->all();
    $menuid = $request->menuid;
    $ids = $request->ids;
    $menu = menu::findOrFail($menuid);
    if($menu->content == ''){
      foreach($ids as $id){
        $data['title'] = category::where('id',$id)->value('title');
        $data['slug'] = category::where('id',$id)->value('slug');
        $data['type'] = 'category';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
    }else{
      $olddata = json_decode($menu->content,true); 
      foreach($ids as $id){
        $data['title'] = category::where('id',$id)->value('title');
        $data['slug'] = category::where('id',$id)->value('slug');
        $data['type'] = 'category';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
      foreach($ids as $id){
        $array['title'] = category::where('id',$id)->value('title');
        $array['slug'] = category::where('id',$id)->value('slug');
        $array['name'] = NULL;
        $array['type'] = 'category';
        $array['target'] = NULL;
        $array['id'] = menuitem::where('slug',$array['slug'])->where('name',$array['name'])->where('type',$array['type'])->value('id');
        $array['children'] = [[]];
        array_push($olddata[0],$array);
        $oldata = json_encode($olddata);
        $menu->update(['content'=>$olddata]);
      }
    }
  }

  public function addPostToMenu(Request $request){
    $data = $request->all();
    $menuid = $request->menuid;
    $ids = $request->ids;
    $menu = menu::findOrFail($menuid);
    if($menu->content == ''){
      foreach($ids as $id){
        $data['title'] = post::where('id',$id)->value('title');
        $data['slug'] = post::where('id',$id)->value('slug');
        $data['type'] = 'post';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
    }else{
      $olddata = json_decode($menu->content,true); 
      foreach($ids as $id){
        $data['title'] = post::where('id',$id)->value('title');
        $data['slug'] = post::where('id',$id)->value('slug');
        $data['type'] = 'post';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
      foreach($ids as $id){
        $array['title'] = post::where('id',$id)->value('title');
        $array['slug'] = post::where('id',$id)->value('slug');
        $array['name'] = NULL;
        $array['type'] = 'post';
        $array['target'] = NULL;
        $array['id'] = menuitem::where('slug',$array['slug'])->where('name',$array['name'])->where('type',$array['type'])->orderby('id','DESC')->value('id');                
        $array['children'] = [[]];
        array_push($olddata[0],$array);
        $oldata = json_encode($olddata);
        $menu->update(['content'=>$olddata]);
      }
    }
  }

  public function addCustomLink(Request $request){
    $data = $request->all();
    $menuid = $request->menuid;
    $menu = menu::findOrFail($menuid);
    if($menu->content == ''){
      $data['title'] = $request->link;
      $data['slug'] = $request->url;
      $data['type'] = 'custom';
      $data['menu_id'] = $menuid;
      $data['updated_at'] = NULL;
      menuitem::create($data);
    }else{
      $olddata = json_decode($menu->content,true); 
      $data['title'] = $request->link;
      $data['slug'] = $request->url;
      $data['type'] = 'custom';
      $data['menu_id'] = $menuid;
      $data['updated_at'] = NULL;
      menuitem::create($data);
      $array = [];
      $array['title'] = $request->link;
      $array['slug'] = $request->url;
      $array['name'] = NULL;
      $array['type'] = 'custom';
      $array['target'] = NULL;
      $array['id'] = menuitem::where('slug',$array['slug'])->where('name',$array['name'])->where('type',$array['type'])->orderby('id','DESC')->value('id');                
      $array['children'] = [[]];
      array_push($olddata[0],$array);
      $oldata = json_encode($olddata);
      $menu->update(['content'=>$olddata]);
    }
  }	

Now, if the menu content column is not empty, the content will be converted into an array and the new data will be added to that array. Then, the data will be reverted back to json format and that will replace the existing content on the content column of the menus table.

To display the menu items according to the menu structure defined in menus table with the menu item details from the menuitems table, you can use the same technique we used to display the menu title and all in the backend view blade.

Here's my frontController.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\menu;
use App\Models\menuitem;

class frontController extends Controller
{
  public function __construct(){
    $topNav = menu::where('location',1)->first();
	$topNavItems = json_decode($topNav->content);
    $topNavItems = $topNavItems[0]; 
    foreach($topNavItems as $menu){
      $menu->title = menuitem::where('id',$menu->id)->value('title');
      $menu->name = menuitem::where('id',$menu->id)->value('name');
      $menu->slug = menuitem::where('id',$menu->id)->value('slug');
      $menu->target = menuitem::where('id',$menu->id)->value('target');
      $menu->type = menuitem::where('id',$menu->id)->value('type');
      if(!empty($menu->children[0])){
        foreach ($menu->children[0] as $child) {
          $child->title = menuitem::where('id',$child->id)->value('title');
          $child->name = menuitem::where('id',$child->id)->value('name');
          $child->slug = menuitem::where('id',$child->id)->value('slug');
          $child->target = menuitem::where('id',$child->id)->value('target');
          $child->type = menuitem::where('id',$child->id)->value('type');
        }  
      }
    }
    view()->share([
      'topNavItems' => $topNavItems,
	]);
  }
  
  public function index(){
    return view ('frontend.index');
  }
}	

And here's my fronend view blade menu section.

<ul class="nav navbar-nav navbar-right">
  <li><a href="{{url('/')}}">home</a></li>
  @if(!empty($topNavItems))
	@foreach($topNavItems as $nav)
	  @if(!empty($nav->children[0]))
		<li><a href="#" class="dropdown" data-toggle="dropdown">@if($nav->name == NULL) {{$nav->title}} @else {{$nav->name}} @endif <i class="caret"></i>
		  <ul class="dropdown-menu">
			@foreach($nav->children[0] as $childNav)
			  @if($childNav->type == 'custom')
				<li><a href="{{$childNav->slug}}" target="_blank">@if($childNav->name == NULL) {{$childNav->title}} @else {{$childNav->name}} @endif</a></li>
			  @elseif($childNav->type == 'category')
				<li><a href="{{url('category')}}/{{$childNav->slug}}">@if($childNav->name == NULL) {{$childNav->title}} @else {{$childNav->name}} @endif</a></li>
			  @else
				<li><a href="{{url('pages')}}/{{$childNav->slug}}">@if($childNav->name == NULL) {{$childNav->title}} @else {{$childNav->name}} @endif</a></li>	
			  @endif
			@endforeach
		  </ul>
		</a></li>
      @else
		@if($nav->type == 'custom')
		  <li><a href="{{$nav->slug}}" target="_blank">@if($nav->name == NULL) {{$nav->title}} @else {{$nav->name}} @endif</a></li>
		@elseif($nav->type == 'category')
		  <li><a href="{{url('category')}}/{{$nav->slug}}">@if($nav->name == NULL) {{$nav->title}} @else {{$nav->name}} @endif</a></li>
		@else
		  <li><a href="{{url('pages')}}/{{$nav->slug}}">@if($nav->name == NULL) {{$nav->title}} @else {{$nav->name}} @endif</a></li>	
		@endif
	  @endif	
	@endforeach
  @endif					
  <li><a href="{{url('contact-us')}}">contact us</a></li>
  <li><a href="{{url('donate-us')}}" class="btn btn-warning btn-rsn">donate us</a></li>
</ul>	

Here, you can find all block of codes in one single place.

Database Tables

Menus Table

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";

CREATE TABLE `menus` (
  `id` bigint(20) UNSIGNED NOT NULL,
  `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `content` longtext COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE `menus`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `menus`
  MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
COMMIT;

Menuitems Table

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";

CREATE TABLE `menuitems` (
  `id` bigint(20) UNSIGNED NOT NULL,
  `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `target` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `menu_id` int(11) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE `menuitems`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `menuitems`
  MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
COMMIT;	

Categories Table (With Dummy Data)

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";

CREATE TABLE `categories` (
  `id` bigint(20) UNSIGNED NOT NULL,
  `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `categories` (`id`, `title`, `slug`, `status`, `created_at`, `updated_at`) VALUES
(1, 'about us', 'about-us', 'show', '2021-05-21 07:11:51', NULL),
(2, 'projects', 'projects', 'show', '2021-05-21 07:16:38', NULL),
(3, 'get involved', 'get-involved', 'show', '2021-05-21 07:19:49', NULL),
(4, 'news & events', 'news-events', 'show', '2021-05-21 07:20:17', NULL);

ALTER TABLE `categories`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `categories`
  MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;
COMMIT;	

Posts Table (With Dummy Data)

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";

CREATE TABLE `posts` (
  `id` bigint(20) UNSIGNED NOT NULL,
  `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` longtext COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `category` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `posts` (`id`, `title`, `slug`, `description`, `image`, `category`, `status`, `created_at`, `updated_at`) VALUES
(2, 'Illo culpa dolore eo', 'Nulla dolore sapient', '<p>Nisi perspiciatis, e.</p>', '1621651621slider2.jpg', '1', 'show', '2021-05-21 21:02:01', NULL),
(3, 'Distinctio Et itaqu', 'Labore neque facere', '<p>Autem reiciendis off.</p>', '1621651638slider3.jpg', '3', 'show', '2021-05-21 21:02:18', '2021-05-21 21:05:39'),
(4, 'Illum dolorum accus', 'Nulla reiciendis con', '<p>Quis laboris ut est.</p>', '1621651650slider4.jpg', '2', 'show', '2021-05-21 21:02:30', '2021-05-21 21:05:42'),
(5, 'Et nulla mollit culp', 'et-nulla-mollit-culp', '<p>Delectus, aut aut au.</p>', '1621652968logo.png', '1', 'show', '2021-05-22 02:57:03', '2021-05-22 03:09:28');

ALTER TABLE `posts`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `posts`
  MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
COMMIT;	

Menu Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class menu extends Model
{
  use HasFactory;
  protected $fillable = ['title','location','content','created_at','updated_at'];      
}	

Menuitems Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class menuitem extends Model
{
  use HasFactory;
  protected $fillable = ['title','name','slug','type','target','menu_id','created_at','updated_at'];    
}	

Routes

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\menuController;

Route::get('manage-menus/{id?}',[menuController::class,'index']);
Route::post('create-menu',[menuController::class,'store']);	
Route::get('add-categories-to-menu',[menuController::class,'addCatToMenu']);
Route::get('add-post-to-menu',[menuController::class,'addPostToMenu']);
Route::get('add-custom-link',[menuController::class,'addCustomLink']);	
Route::get('update-menu',[menuController::class,'updateMenu']);	
Route::post('update-menuitem/{id}',[menuController::class,'updateMenuItem']);
Route::get('delete-menuitem/{id}/{key}/{in?}',[menuController::class,'deleteMenuItem']);
Route::get('delete-menu/{id}',[menuController::class,'destroy']);	

menuController

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\menu;
use App\Models\menuitem;
use App\Models\category;
use App\Models\post;
use Session;

class menuController extends Controller
{	
  public function index(){
    $menuitems = '';
    $desiredMenu = '';  
    if(isset($_GET['id']) && $_GET['id'] != 'new'){
      $id = $_GET['id'];
      $desiredMenu = menu::where('id',$id)->first();
      if($desiredMenu->content != ''){
        $menuitems = json_decode($desiredMenu->content);
        $menuitems = $menuitems[0]; 
        foreach($menuitems as $menu){
          $menu->title = menuitem::where('id',$menu->id)->value('title');
          $menu->name = menuitem::where('id',$menu->id)->value('name');
          $menu->slug = menuitem::where('id',$menu->id)->value('slug');
          $menu->target = menuitem::where('id',$menu->id)->value('target');
          $menu->type = menuitem::where('id',$menu->id)->value('type');
          if(!empty($menu->children[0])){
            foreach ($menu->children[0] as $child) {
              $child->title = menuitem::where('id',$child->id)->value('title');
              $child->name = menuitem::where('id',$child->id)->value('name');
              $child->slug = menuitem::where('id',$child->id)->value('slug');
              $child->target = menuitem::where('id',$child->id)->value('target');
              $child->type = menuitem::where('id',$child->id)->value('type');
            }  
          }
        }
      }else{
        $menuitems = menuitem::where('menu_id',$desiredMenu->id)->get();                    
      }             
    }else{
      $desiredMenu = menu::orderby('id','DESC')->first();
      if($desiredMenu){
        if($desiredMenu->content != ''){
          $menuitems = json_decode($desiredMenu->content);
          $menuitems = $menuitems[0]; 
          foreach($menuitems as $menu){
            $menu->title = menuitem::where('id',$menu->id)->value('title');
            $menu->name = menuitem::where('id',$menu->id)->value('name');
            $menu->slug = menuitem::where('id',$menu->id)->value('slug');
            $menu->target = menuitem::where('id',$menu->id)->value('target');
            $menu->type = menuitem::where('id',$menu->id)->value('type');
            if(!empty($menu->children[0])){
              foreach ($menu->children[0] as $child) {
                $child->title = menuitem::where('id',$child->id)->value('title');
                $child->name = menuitem::where('id',$child->id)->value('name');
                $child->slug = menuitem::where('id',$child->id)->value('slug');
                $child->target = menuitem::where('id',$child->id)->value('target');
                $child->type = menuitem::where('id',$child->id)->value('type');
              }  
            }
          }
        }else{
          $menuitems = menuitem::where('menu_id',$desiredMenu->id)->get();
        }                                   
      }           
    }
    return view ('index',['categories'=>category::all(),'posts'=>post::all(),'menus'=>menu::all(),'desiredMenu'=>$desiredMenu,'menuitems'=>$menuitems]);
  }	

  public function store(Request $request){
	$data = $request->all(); 
	if(menu::create($data)){ 
	  $newdata = menu::orderby('id','DESC')->first();          
	  session::flash('success','Menu saved successfully !');             
	  return redirect("manage-menus?id=$newdata->id");
	}else{
	  return redirect()->back()->with('error','Failed to save menu !');
	}
  }	

  public function addCatToMenu(Request $request){
    $data = $request->all();
    $menuid = $request->menuid;
    $ids = $request->ids;
    $menu = menu::findOrFail($menuid);
    if($menu->content == ''){
      foreach($ids as $id){
        $data['title'] = category::where('id',$id)->value('title');
        $data['slug'] = category::where('id',$id)->value('slug');
        $data['type'] = 'category';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
    }else{
      $olddata = json_decode($menu->content,true); 
      foreach($ids as $id){
        $data['title'] = category::where('id',$id)->value('title');
        $data['slug'] = category::where('id',$id)->value('slug');
        $data['type'] = 'category';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
      foreach($ids as $id){
        $array['title'] = category::where('id',$id)->value('title');
        $array['slug'] = category::where('id',$id)->value('slug');
        $array['name'] = NULL;
        $array['type'] = 'category';
        $array['target'] = NULL;
        $array['id'] = menuitem::where('slug',$array['slug'])->where('name',$array['name'])->where('type',$array['type'])->value('id');
        $array['children'] = [[]];
        array_push($olddata[0],$array);
        $oldata = json_encode($olddata);
        $menu->update(['content'=>$olddata]);
      }
    }
  }

  public function addPostToMenu(Request $request){
    $data = $request->all();
    $menuid = $request->menuid;
    $ids = $request->ids;
    $menu = menu::findOrFail($menuid);
    if($menu->content == ''){
      foreach($ids as $id){
        $data['title'] = post::where('id',$id)->value('title');
        $data['slug'] = post::where('id',$id)->value('slug');
        $data['type'] = 'post';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
    }else{
      $olddata = json_decode($menu->content,true); 
      foreach($ids as $id){
        $data['title'] = post::where('id',$id)->value('title');
        $data['slug'] = post::where('id',$id)->value('slug');
        $data['type'] = 'post';
        $data['menu_id'] = $menuid;
        $data['updated_at'] = NULL;
        menuitem::create($data);
      }
      foreach($ids as $id){
        $array['title'] = post::where('id',$id)->value('title');
        $array['slug'] = post::where('id',$id)->value('slug');
        $array['name'] = NULL;
        $array['type'] = 'post';
        $array['target'] = NULL;
        $array['id'] = menuitem::where('slug',$array['slug'])->where('name',$array['name'])->where('type',$array['type'])->orderby('id','DESC')->value('id');                
        $array['children'] = [[]];
        array_push($olddata[0],$array);
        $oldata = json_encode($olddata);
        $menu->update(['content'=>$olddata]);
      }
    }
  }

  public function addCustomLink(Request $request){
    $data = $request->all();
    $menuid = $request->menuid;
    $menu = menu::findOrFail($menuid);
    if($menu->content == ''){
      $data['title'] = $request->link;
      $data['slug'] = $request->url;
      $data['type'] = 'custom';
      $data['menu_id'] = $menuid;
      $data['updated_at'] = NULL;
      menuitem::create($data);
    }else{
      $olddata = json_decode($menu->content,true); 
      $data['title'] = $request->link;
      $data['slug'] = $request->url;
      $data['type'] = 'custom';
      $data['menu_id'] = $menuid;
      $data['updated_at'] = NULL;
      menuitem::create($data);
      $array = [];
      $array['title'] = $request->link;
      $array['slug'] = $request->url;
      $array['name'] = NULL;
      $array['type'] = 'custom';
      $array['target'] = NULL;
      $array['id'] = menuitem::where('slug',$array['slug'])->where('name',$array['name'])->where('type',$array['type'])->orderby('id','DESC')->value('id');                
      $array['children'] = [[]];
      array_push($olddata[0],$array);
      $oldata = json_encode($olddata);
      $menu->update(['content'=>$olddata]);
    }
  }

  public function updateMenu(Request $request){
    $newdata = $request->all(); 
    $menu=menu::findOrFail($request->menuid);            
    $content = $request->data; 
    $newdata = [];  
    $newdata['location'] = $request->location;       
    $newdata['content'] = json_encode($content);
    $menu->update($newdata); 
  }

  public function updateMenuItem(Request $request){
    $data = $request->all();        
    $item = menuitem::findOrFail($request->id);
    $item->update($data);
    return redirect()->back();
  }

  public function deleteMenuItem($id,$key,$in=''){        
    $menuitem = menuitem::findOrFail($id);
    $menu = menu::where('id',$menuitem->menu_id)->first();
    if($menu->content != ''){
      $data = json_decode($menu->content,true);            
      $maindata = $data[0];            
      if($in == ''){
        unset($data[0][$key]);
        $newdata = json_encode($data); 
        $menu->update(['content'=>$newdata]);                         
      }else{
        unset($data[0][$key]['children'][0][$in]);
	    $newdata = json_encode($data);
        $menu->update(['content'=>$newdata]); 
      }
    }
    $menuitem->delete();
    return redirect()->back();
  }	

  public function destroy(Request $request){
    menuitem::where('menu_id',$request->id)->delete();  
    menu::findOrFail($request->id)->delete();
    return redirect('manage-menus')->with('success','Menu deleted successfully');
  }		
}	

And, here's the backend view blade.

@extends('layout')
@section('content')

<div class="container-fluid">
  <h2><span>Menus</span></h2>
  
  <div class="content info-box">
  	@if(count($menus) > 0)		
	Select a menu to edit: 		
	<form action="{{url('manage-menus')}}" class="form-inline">
      <select name="id">
		@foreach($menus as $menu)
	        @if($desiredMenu != '')
			<option value="{{$menu->id}}" @if($menu->id == $desiredMenu->id) selected @endif>{{$menu->title}}</option>
		  @else
			<option value="{{$menu->id}}">{{$menu->title}}</option>
		  @endif
		@endforeach
	  </select>
	  <button class="btn btn-sm btn-default btn-menu-select">Select</button>
	</form> 
	or
	@endif 
	<a href="{{url('manage-menus?id=new')}}">Create a new menu</a>.	
  </div>


  <div class="row" id="main-row">				
	<div class="col-sm-3 cat-form @if(count($menus) == 0) disabled @endif">
      <h3><span>Add Menu Items</span></h3>			

	  <div class="panel-group" id="menu-items">
		<div class="panel panel-default">
	      <div class="panel-heading">
			<a href="#categories-list" data-toggle="collapse" data-parent="#menu-items">Categories <span class="caret pull-right"></span></a>
		  </div>
		  <div class="panel-collapse collapse in" id="categories-list">
		    <div class="panel-body">						
			  <div class="item-list-body">
				@foreach($categories as $cat)
				  <p><input type="checkbox" name="select-category[]" value="{{$cat->id}}"> {{$cat->title}}</p>
				@endforeach
			  </div>	
			  <div class="item-list-footer">
			    <label class="btn btn-sm btn-default"><input type="checkbox" id="select-all-categories"> Select All</label>
				  <button type="button" class="pull-right btn btn-default btn-sm" id="add-categories">Add to Menu</button>
			</div>
		  </div>						
		</div>
		<script>
		  $('#select-all-categories').click(function(event) {   
			if(this.checked) {
			  $('#categories-list :checkbox').each(function() {
			    this.checked = true;                        
			  });
			}else{
			  $('#categories-list :checkbox').each(function() {
			    this.checked = false;                        
			  });
			}
		  });
		</script>
	  </div>
	  <div class="panel panel-default">
	    <div class="panel-heading">
		  <a href="#posts-list" data-toggle="collapse" data-parent="#menu-items">posts <span class="caret pull-right"></span></a>
		</div>
		<div class="panel-collapse collapse" id="posts-list">
		  <div class="panel-body">						
			<div class="item-list-body">
			  @foreach($posts as $post)
				<p><input type="checkbox" name="select-post[]" value="{{$post->id}}"> {{$post->title}}</p>
			  @endforeach
			</div>	
			<div class="item-list-footer">
			  <label class="btn btn-sm btn-default"><input type="checkbox" id="select-all-posts"> Select All</label>
			  <button type="button" id="add-posts" class="pull-right btn btn-default btn-sm">Add to Menu</button>
			</div>
		  </div>						
		</div>
		<script>
		  $('#select-all-posts').click(function(event) {   
		    if(this.checked) {
			  $('#posts-list :checkbox').each(function() {
				this.checked = true;                        
			  });
			}else{
			  $('#posts-list :checkbox').each(function() {
				this.checked = false;                        
			  });
			}
		  });
		</script>
	  </div>
	  <div class="panel panel-default">
		<div class="panel-heading">
	      <a href="#custom-links" data-toggle="collapse" data-parent="#menu-items">Custom Links <span class="caret pull-right"></span></a>
		</div>
		<div class="panel-collapse collapse" id="custom-links">
		  <div class="panel-body">						
			<div class="item-list-body">
			  <div class="form-group">
				<label>URL</label>
				<input type="url" id="url" class="form-control" placeholder="https://">
			  </div>
			  <div class="form-group">
				<label>Link Text</label>
			      <input type="text" id="linktext" class="form-control" placeholder="">
				</div>
			  </div>	
			  <div class="item-list-footer">
				<button type="button" class="pull-right btn btn-default btn-sm" id="add-custom-link">Add to Menu</button>
			  </div>
			</div>
		  </div>
		</div>
	  </div>		
	</div>		

	<div class="col-sm-9 cat-view">
  <h3><span>Menu Structure</span></h3>
  @if($desiredMenu == '')
    <h4>Create New Menu</h4>
	<form method="post" action="{{url('create-menu')}}">
	  {{csrf_field()}}
	  <div class="row">
		<div class="col-sm-12">
		  <label>Name</label>
		</div>
		<div class="col-sm-6">
		  <div class="form-group">							
			<input type="text" name="title" class="form-control">
		  </div>
		</div>
		<div class="col-sm-6 text-right">
		  <button class="btn btn-sm btn-primary">Create Menu</button>
		</div>
	  </div>
	</form>
	@else
	<div id="menu-content">
		<div id="result"></div>
	  <div style="min-height: 240px;">
		  <p>Select categories, pages or add custom links to menus.</p>
		  @if($desiredMenu != '')
			<ul class="menu ui-sortable" id="menuitems">
  @if(!empty($menuitems))
	@foreach($menuitems as $key=>$item)
	  <li data-id="{{$item->id}}"><span class="menu-item-bar"><i class="fa fa-arrows"></i> @if(empty($item->name)) {{$item->title}} @else {{$item->name}} @endif <a href="#collapse{{$item->id}}" class="pull-right" data-toggle="collapse"><i class="caret"></i></a></span>
	    <div class="collapse" id="collapse{{$item->id}}">
	      <div class="input-box">
	        <form method="post" action="{{url('update-menuitem')}}/{{$item->id}}">
			  {{csrf_field()}}
			  <div class="form-group">
				<label>Link Name</label>
				<input type="text" name="name" value="@if(empty($item->name)) {{$item->title}} @else {{$item->name}} @endif" class="form-control">
			  </div>
			  @if($item->type == 'custom')
				<div class="form-group">
				  <label>URL</label>
				  <input type="text" name="slug" value="{{$item->slug}}" class="form-control">
				</div>					
				<div class="form-group">
				  <input type="checkbox" name="target" value="_blank" @if($item->target == '_blank') checked @endif> Open in a new tab
				</div>
			  @endif
			  <div class="form-group">
				<button class="btn btn-sm btn-primary">Save</button>
				<a href="{{url('delete-menuitem')}}/{{$item->id}}/{{$key}}" class="btn btn-sm btn-danger">Delete</a>
			  </div>
			</form>
		  </div>
		</div>
	    <ul>
		  @if(isset($item->children))
		    @foreach($item->children as $m)
		  	  @foreach($m as $in=>$data)
			    <li data-id="{{$data->id}}" class="menu-item"> <span class="menu-item-bar"><i class="fa fa-arrows"></i> @if(empty($data->name)) {{$data->title}} @else {{$data->name}} @endif <a href="#collapse{{$data->id}}" class="pull-right" data-toggle="collapse"><i class="caret"></i></a></span>
			      <div class="collapse" id="collapse{{$data->id}}">
			        <div class="input-box">
			          <form method="post" action="{{url('update-menuitem')}}/{{$data->id}}">
					    {{csrf_field()}}
					    <div class="form-group">
						  <label>Link Name</label>
						  <input type="text" name="name" value="@if(empty($data->name)) {{$data->title}} @else {{$data->name}} @endif" class="form-control">
					    </div>
					    @if($data->type == 'custom')
						  <div class="form-group">
						    <label>URL</label>
						    <input type="text" name="slug" value="{{$data->slug}}" class="form-control">
						  </div>					
						  <div class="form-group">
						    <input type="checkbox" name="target" value="_blank" @if($data->target == '_blank') checked @endif> Open in a new tab
						  </div>
					    @endif
					    <div class="form-group">
						  <button class="btn btn-sm btn-primary">Save</button>
						  <a href="{{url('delete-menuitem')}}/{{$data->id}}/{{$key}}/{{$in}}" class="btn btn-sm btn-danger">Delete</a>
					    </div>
					  </form>
				    </div>
				  </div>
			      <ul></ul>
		        </li>
		      @endforeach
	        @endforeach	
	      @endif	
        </ul>
      </li>
    @endforeach
  @endif
</ul>	
		  @endif	
		</div>	
	  @if($desiredMenu != '')
		<div class="form-group menulocation">
		  <label><b>Menu Location</b></label>
		  <p><label><input type="radio" name="location" value="1" @if($desiredMenu->location == 1) checked @endif> Header</label></p>
		  <p><label><input type="radio" name="location" value="2" @if($desiredMenu->location == 2) checked @endif> Main Navigation</label></p>
		</div>									
		<div class="text-right">
		  <button class="btn btn-sm btn-primary" id="saveMenu">Save Menu</button>
		</div>
		<p><a href="{{url('delete-menu')}}/{{$desiredMenu->id}}">Delete Menu</a></p>
	  @endif										
	</div>
  @endif	
</div>	
  </div>
</div>
<div id="serialize_output">@if($desiredMenu){{$desiredMenu->content}}@endif</div>	
<script src="{{ url('js/sortable.js')}}"></script>
@if($desiredMenu)
<script>
$('#add-categories').click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var n = $('input[name="select-category[]"]:checked').length;
  var array = $('input[name="select-category[]"]:checked');
  var ids = [];
  for(i=0;i<n;i++){
    ids[i] =  array.eq(i).val();
  }
  if(ids.length == 0){
	return false;
  }
  $.ajax({
	type:"get",
	data: {menuid:menuid,ids:ids},
	url: "{{url('add-categories-to-menu')}}",				
	success:function(res){				
      location.reload();
	}
  })
})
$('#add-posts').click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var n = $('input[name="select-post[]"]:checked').length;
  var array = $('input[name="select-post[]"]:checked');
  var ids = [];
  for(i=0;i<n;i++){
	ids[i] =  array.eq(i).val();
  }
  if(ids.length == 0){
	return false;
  }
  $.ajax({
	type:"get",
	data: {menuid:menuid,ids:ids},
	url: "{{url('add-post-to-menu')}}",				
	success:function(res){
  	  location.reload();
	}
  })
})
$("#add-custom-link").click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var url = $('#url').val();
  var link = $('#linktext').val();
  if(url.length > 0 && link.length > 0){
	$.ajax({
	  type:"get",
	  data: {menuid:menuid,url:url,link:link},
	  url: "{{url('add-custom-link')}}",				
	  success:function(res){
	    location.reload();
	  }
	})
  }
})
</script>
<script>
var group = $("#menuitems").sortable({
  group: 'serialization',
  onDrop: function ($item, container, _super) {
    var data = group.sortable("serialize").get();	    
    var jsonString = JSON.stringify(data, null, ' ');
    $('#serialize_output').text(jsonString);
  	  _super($item, container);
  }
});
</script>	
<script>
$('#saveMenu').click(function(){
  var menuid = <?=$desiredMenu->id?>;
  var location = $('input[name="location"]:checked').val();
  var newText = $("#serialize_output").text();
  var data = JSON.parse($("#serialize_output").text());	
  $.ajax({
    type:"get",
	data: {menuid:menuid,data:data,location:location},
	url: "{{url('update-menu')}}",				
	success:function(res){
	  window.location.reload();
	}
  })	
})
</script>
@endif		
<style>
  .item-list,.info-box{background: #fff;padding: 10px;}
  .item-list-body{max-height: 300px;overflow-y: scroll;}
  .panel-body p{margin-bottom: 5px;}
  .info-box{margin-bottom: 15px;}
  .item-list-footer{padding-top: 10px;}
  .panel-heading a{display: block;}
  .form-inline{display: inline;}
  .form-inline select{padding: 4px 10px;}
  .btn-menu-select{padding: 4px 10px}
  .disabled{pointer-events: none; opacity: 0.7;}
  .menu-item-bar{background: #eee;padding: 5px 10px;border:1px solid #d7d7d7;margin-bottom: 5px; width: 75%; cursor: move;display: block;}
  #serialize_output{display: block;}
  .menulocation label{font-weight: normal;display: block;}
  body.dragging, body.dragging * {cursor: move !important;}
  .dragged {position: absolute;z-index: 1;}
  ol.example li.placeholder {position: relative;}
  ol.example li.placeholder:before {position: absolute;}
  #menuitem{list-style: none;}
  #menuitem ul{list-style: none;}
  .input-box{width:75%;background:#fff;padding: 10px;box-sizing: border-box;margin-bottom: 5px;}
  .input-box .form-control{width: 50%}
</style>	
@stop	

Hope this post helps you achieve a milestone in web application developement in laravel. I'll be looking forward to make these code blocks further more simpler in coming days. Please do let me know how do you feel about this post through the comments. I'm sure, some of you might have done better than this but never shared the code with anybody else. Let me know through the comments if you can make this code further better. Any constructive comments to make the post better is always welcome.


3 Likes 3 Dislikes 12 Comments Share


Mr Ben

20 Aug, 2021

Hello, I have read this article but can't find file js and css, can you send me the download link of full css and js by email or update this post? Thanks very much !

Jiwan Thapa

20 Aug, 2021

Hi Ben, additional CSS i used in this page is given above on the post itself. Apart from that, I've used css and js from bootstrap 3 along with sortable js. You can type these keywords on your browser's search box and you will easily find the links to download them. Thank you.

Reply


Tuan

30 Oct, 2021

Post is very helpful. Can i have this source. I try to do hard but i can't. Can you help me, pls.

Jiwan Thapa

08 Nov, 2021

Copy everything from the line "Here's my frontController." in the file names as said. And, you'll get the exact code on each pages as I've used.

Reply


Amin Arjmand

01 Jan, 2022

Hello thanks for this code , i've used your cods but i can't have nestable menu. how can i have nestable menu? please share your code on github

Jiwan Thapa

07 Apr, 2022

In that case, you better watch this video and follow along https://www.youtube.com/watch?v=q5--awapsts

Reply


Khan Sunny

07 Feb, 2022

That is awesome ❤️

Reply


Wahyu

02 Apr, 2022

I have problem here 1. I didnt know where i must put my jquery file 2. you import Category and Post model, but i didnt see u make migration or database Category and Post

Jiwan Thapa

07 Apr, 2022

You can put your jquery file in your assets and connect it to your view file. To make the article shorter, I skipped the database make and migrate step. Either, you make and migrate the tables through your terminal or make it directly on your db.

Reply


Esraa

08 Jun, 2022

It's a credible tutorial.I followed it and made a drag and drop menu. I have a question if you can help me. How can I display the menu on the front end? Thanks and have a good day.

Jiwan Thapa

20 Aug, 2022

Thanks Esraa for your feedback. You can display the menu items just like you display it in backend view. The only difference would be the css for the menuitems.

Reply


Nguyễn An

11 Aug, 2022

you can share source to github?

Jiwan Thapa

20 Aug, 2022

Acutally, you can find a couple of packages in github for drag and drop menu. Not sure, if any of them support nested items. I created this article to make the intermediate developers understand how things are done. That's all.

Reply


abbas turi

04 Sep, 2022

Hi, dear can you send me your source code means the Laravel application that you worked on it.

Jiwan Thapa

06 Sep, 2022

Hi Abbas, the entire code is given above. Please put some effort from your side and copy them.

Reply


Nitin

13 Sep, 2022

Is there any download option for this complete code

Jiwan Thapa

05 Apr, 2023

Hi Nitin, the entire code is given above. Please put some effort from your side and copy them. The primary motive of this site is to make you understand the concept so that you can dig deeper and be able to grow yourself from here onwards. If you want to download the code, you can go to github and find packages there.

Reply


Licha

09 Mar, 2023

How to make it n multi-level parent-child?

Jiwan Thapa

05 Apr, 2023

The process will be the same. You'll ony need to make the loop run further checking the children item in each category.

Reply


Pijus

06 Feb, 2024

"Just finished reading your blog – it's excellent! I also have an article on this topic with a great infographic. Feel free to check it out. Thanks!"

Reply


Palak Garg

13 Feb, 2024

Hi, I couldn't locate the JS file mentioned in the article. Could you please provide the download link for the full JS file

Webtrickshome

18 Feb, 2024

You can find the js file here https://johnny.github.io/jquery-sortable/

Reply


Leave a comment