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.
<div id="modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="globalModal" aria-hidden="true"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> Content goes here! </div> </div> </div>
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">×</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….