Date Archives

April 2020

A pattern for using modal dialogs in Laravel

First let me explain the context of the solution I am trying to deliver, and the pain point I want to solve. I was writing a dashboard, and I wanted to have Modal pop ups that allow the end user to add items to the dashboard without leaving the dashboard.

Modal dialogs from Bootstrap seem like a nice approach. However, I was unhappy with nesting the code for the modal forms in the same file as the dashboard. Besides the clutter, it increases the size of the page to be downloaded considerably. 

I decided to keep the modals in their own views, and return them from the controller to the dashboard using a client side fetch. The benefits of this are that you are effectively lazy loading the modal if and only if the modal form is requested by the user.

Let’s take a look at a simple example. Here we have a dashboard that contains a list of tasks. It is feasible that we want to add a task to the dashboard without actually leaving the dashboard.

For brevity we will not use a database, rather just return a hard coded array from the task controller’s index method to populate our dashboard.

First let’s set up our react project …

$>npx create-react-app react-bootstrap-modals

Switch down one level

$>cd bootstrap-modals/

And install laravel ui

$>composer require laravel/ui

Then install bootstrap scaffolding …

$>php artisan ui bootstrap

Then install laravel collective

$>composer require laravelcollective/html

Now run npm install

$>npm install

Now remove the welcome page

$>rm resources/views/welcome.blade.php

And create two resource controllers. One is for our dashboard, one for our modal tasks form.

$>php artisan make:controller DashboardController --resource
$>php artisan make:controller TaskController --resource

Then create the folder for the Dashboard view and create the dashboard view blade file

$>mkdir resources/views/dashboard
$>touch resources/views/dashboard/index.blade.php

Then create the folder for the Task modal view and create the blade file.

$>mkdir resources/views/task
$>touch resources/views/task/create.blade.php

And the final piece of scaffolding is to add the routes.

Change your routes/web.php file to look like this :

<?php
 
use Illuminate\Support\Facades\Route;
 
Route::resource('dashboard', 'DashboardController');
Route::resource('task', 'TaskController');

Now we can spin it all up …

$>php artisan serve

Then visit http://localhost:8000/dashboard

Of course it is currently just a blank page

Firstly let’s add some data to the view. Edit the index method in the controller in app/Http/Controllers/DashboardController.php

So our index method looks like …

public function index()
   {
       return view("dashboard.index");
   }

and our view looks like …

<h1>Dashboard</h1>

Create a sub folder under the views directory called layout. Create a file in this directory called layout.blade.php and add the following content

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
 
   <!-- CSRF Token -->
   <meta name="csrf-token" content="{{ csrf_token() }}">
 
   <title>{{ config('app.name', 'Laravel') }}</title>
 
   <!-- Scripts -->
   <script src="{{ asset('js/app.js') }}" ></script>
  
   <!-- Fonts -->
   <link rel="dns-prefetch" href="//fonts.gstatic.com">
   <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
   <link type="text/css" rel="stylesheet" href="{{ mix('css/app.css') }}">
 
   <!-- Styles -->
   <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
   @yield('content')
</body>
</html>

Now edit your index.blade.php in your dashboard directory and add the directive to include the layout in our dashboard index view by adding 

@extends('layout.layout')
@section('content')
<h1>Dashboard</h1>
@endsection

Also we want our view content to be displayed in the content section of the layout template so we add the necessary @section and @endsection tag.

So far, ok. Now let’s add a button for adding a task. We will wire up the onclick method to point to a method contained within our index view.

@extends('layout.layout')
@section('content')
<h1>Dashboard</h1>
<button type="button" class='btn btn-success' id='addTask'>+ Add Task</button>
<script>
   $(document).ready(function(){
       $("#addTask").click(function(){
           console.log("Add task");
       });
   });
</script>
@endsection

Next we need to add a placeholder for our add task modal dialog. A div tag is fine for this, and we can give it an id …

@extends('layout.layout')
@section('content')
<h1>Dashboard</h1>
<button type="button" class='btn btn-success' id='addTask'>+ Add Task</button>
<!--modal placeholders-->
<div id='modal-placeholder'></div>
<!--end of modal placeholders-->
<script>
   $(document).ready(function(){
       $("#addTask").click(function(){
           console.log("Add task");
       });
   });
</script>
@endsection

Now we add another view for the task creation by adding a folder called “tasks” under the views folder and adding an create.blade.php in this folder.

Then we can add the following to the create method of our tasks controller.

public function create()
   {
       return view('task.create');
   }

And add the following content to the create.blade.php file

<div class="modal" tabindex="-1" role="dialog">
   <div class="modal-dialog" role="document">
       <div class="modal-content">
       <div class="modal-header">
           <h5 class="modal-title">Add Task</h5>
           <button type="button" class="close" data-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">×</span>
           </button>
       </div>
       <div class="modal-body">
           {!! Form::open(['method' => 'post','enctype' => 'multipart/form-data', 'id' => 'add-task-form']) !!}
               @csrf 
               <div class="form-group">
                   {{Form::label('task_label', 'Task')}}
                   {{Form::text('task', '', ['class' => 'form-control', 'placeholder' => 'E.g. wash car', 'required'])}}
               </div>
           {!! Form::close() !!}
       </div>
       <div class="modal-footer">
           <button type="button" class="btn btn-primary" id='add_task_submit'>Save changes</button>
           <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
       </div>
       </div>
   </div>
</div>

Note that we don’t add the layout template, as this is just the content of our modal form which will be injected into our div placeholder in the dashboard. Also note the @csrf declaration just inside the Form::open statement. This will include the cross site request antiforgery token.

And we can test the view by visiting the URL we will be calling in the button click event in our dashboard.

http://localhost:8000/task/create

It hasn’t got its Sunday best on as it has not got any of the bootstrap styles loaded, but it will have when we inject it into our div in our dashboard view, which does. At this stage all we are checking is that the route, controller and view are working correctly.

So we know our modal works as a page in itself, it should work fine as a modal.

We need to make the following changes to our index view :

@extends('layout.layout')
@section('content')
<h1>Dashboard</h1>
<button type="button" class='btn btn-success' id='addTask' value = {{ route('task.create') }}>+ Add Task</button>
<!--modal placeholders-->
<div id='modal-placeholder'></div>
<!--end of modal placeholders-->
<script>
   $(document).ready(function(){
       $("#addTask").click(function(){
           var url = $(this).val();
           $('#modal-placeholder').load(url, function (){
               $('.modal').modal('show');
           });
       });
   });
</script>
@endsection

As you can see we have added a value to our button that is the url of our modal, by using the laravel route method from the laravel router. This is then retrieved in our click function and supplied to the load method. We also supply a callback to the load method and in there we can call the modal to show it.

Now when we click on the + Add Task button the dashboard should dim with an overlay, and the modal form should appear

The final thing to do is to add the code that will receive the form post data when the user clicks on the “Save changes” button. As we’re not using a database, we will just echo the request contents back in the response object.

First we need to add the following to the store method on our task controller :

public function store(Request $request)
   {
       return $request;
   }

And then we can add the following to our dashboard view …

$(document).on('click', "#add_task_submit", function(e) {
     e.preventDefault();
     e.stopPropagation();
     fetch('task', {
       method: 'POST',
       headers: {
         'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
         'Content-Type': 'application/x-www-form-urlencoded'
       },
       body : $('#add-task-form').serialize()
       })
     .then((response) => {
       return response.text();
     })
     .then((data) => {
       console.log(data);
     });
     e.preventDefault();
     e.stopPropagation();
     $('.modal').modal('hide');
     return false;
   });

A few points to note. 

We are suppressing the default events as we don’t actually want the form to be submitted.

The document click event is listened to. This is because the modal form was loaded *after* the page loaded, so any attempt to listen to any of the components on the modal dialog would not be wired up correctly.

We are setting the cross site request antiforgery token on the HTTP headers.

We are serialising the contents of the form in the body of the request.

The response is sent to the console so you will need to nudge F12 to see the response working effectively.

Future thoughts :

We could change the add button on the dashboard to check if the modal form dialog has already been loaded, and not re-load every time the button is pushed.

You can find the code for this post at https://github.com/suityou01/blog/tree/master/laravel/bootstrap4/modals