Using Bootstrap Modals with Laravel

9 May 2020

, ,


I use modal popups all over the place in my web applications. The most frequent use is to make the user confirm deletion, either by typing DELETE into a form or by giving a reason for deleting the item in question. And I want to make this work in Laravel.

Note: this is klunky.  I’ve been playing with Laravel for a few weeks and if I end up using it as an ongoing development platform, then yes, things will be rewritten.

1) Laravel basics

Set up my routes for delete and destory.

Route::get('projects/delete/{project}', 'ProjectsController@delete');
Route::delete('projects/delete/{project}', 'ProjectsController@destroy');

Create an initial methods for delete and destroy

public function delete(Project $project) {
    // display a view
    return view('admin.projects.delete', [
        'question' => Please confirm you would like to delete the project $project->title,
    ]);
}

public function destroy(Project $project) {
    // we'll do the validation later
    // Delete the thing!
    $project->delete();
    // Redirect back to list with a nice helpful message for the user
    return redirect(route('projects.index'))
        ->with('success', "Project has been deleted");
}

Make a blade template for my delete form

<form method="post" action="{{ url()->current() }}">
    @method('DELETE')
    @csrf

    <p>{{ $question }}</p>

    <p><input type="text" name="confirm" class="form-control" placeholder="Type DELETE into this box" value=""/></p>

    <button type="submit" class="btn btn-danger">Delete</button>
</form>

Add a link whereever (I’d usually put this on the view page)

<a href="/projects/delete/{$project->id}">Delete</a>

2) Time for bootstrap – lets make this all modally.

When I click that link, I want it to open the form into a floating modal. So I update the link and add data-toggle="modal" data-target="#modal"

 <a href="/projects/delete/{$project->id}" data-toggle="modal" data-target="#modal">Delete</a>

Then we create a basic modal template which is targeted by the link above. This HTML gets added to the same page as the delete link, and I’d normally put it at the bottom of the top-level page template.

Click on link, and the modal will open. Or something will open, but it’s not yet a modal as we know it.

We need to follow that link and load it into our modal. Bootstrap used to do this if the link had an attribute data-remote, but that’s gone the same of the dodo. But the DIY is a simple update – just use the show.bs.modal event.

$("#modal").on("show.bs.modal", function (e) {
    // link or button for this event
    var link = $(e.relatedTarget);
    if (!link.length) return ;
    if (link.attr("href")===undefined) return ;
    //  Load data from the server and place the returned HTML into matched elements
    $(this).find(".modal-content").load(link.attr("href"));
});

Now when we click the link, we’re getting our blade template.

It just needs fancied up a bit with some bootstrap elements.

<form method="post" action="{{ url()->current() }}">
    @method('DELETE')
    @csrf
    <div class="modal-header alert-danger">
        <h5 class="modal-title">Confirm Delete</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
    <div class="modal-body">
        <p>{{ $question }}</p>

        <p><input type="text" name="confirm" class="form-control" placeholder="Type DELETE into this box" value=""/></p>
    </div>

    <div class="modal-footer">
        <button type="submit" class="btn btn-danger">Delete</button>
        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
    </div>
</form>

3) Submit modal form

At the moment, the delete form will behave just like a normal form, even if it’s floating on top of the rest of the page content. If you press the submit button, the whole page will reload.

We need to submit the form using ajax.

$('body').on('submit', '#modal form', function (e) {
    e.preventDefault();
    var modalform = $(this);
    $.ajax({
        type: "POST",
        url: modalform.attr("action"),
        data: modalform.serialize()
    })
});

4) Managing a redirect from Ajax

This gets interesting. We are performing an Ajax request that has 2 possible responses: one (validation successful) that redirects the browser to a new page, and one (validation fails) that updates an existing element (our modal content) on the current page with new content.

I ended up by creating an extra header X-Ajax-Redirect and returning that to the browser.

public function destroy(Project $project) {
    ....
    // helpful message for user
    request()->session()->flash('success', 'Project has been deleted');

    // Extra header for calling page
    return response('Ajax redirect', 200)
        ->header('X-Ajax-Redirect', route('projects.index'));
}

And then in my javascript, I have this:

.done(function (data, textStatus, request) {
    var redirect = request.getResponseHeader("X-Ajax-Redirect");
    if (redirect) {
        window.location = redirect;
    }
    else {
        $("#modal").find(".modal-content").html(data);
    }
})

5) Validation

For validation, we need to check that the user has entered something (DELETE) into the confirm box.

$rules = [
    'confirm' => ['required', 'in:DELETE']
];
$messages = [
    'confirm.required' => 'Please type DELETE in capitals',
    'confirm.in' => 'Please type DELETE in capitals',
];
return request()->validate($rules, $messages);

Not that simple. Normally, the laravel validator will pass you back to the previous page when validation fails. In this case (because modals) the previous page is the containing page where we opened the modal. So we need to override that.

And if the validation is successful, we want to send our X-Ajax-Redirect header back to the calling javascript

$validator = Validator::make(request()->all(), $rules, $messages);
if ($validator->fails()) {
    // this will reload the modal content with the validation errors
    return redirect(route('projects.delete',['project'=>$project])
        ->withErrors($validator)
        ->withInput();
}
else {
    // helpful message for user
    request()->session()->flash('success', 'Project has been deleted');

    // Extra header for calling page
    return response('Ajax redirect', 200)
        ->header('X-Ajax-Redirect', route('projects.index'));
}

Still lots to learn….