jjudge's avatar

Handling AJAX DELETE

Not sure if this is the best channel, but I'll ask here. I'm use resource controllers, and they include a destroy route using a DELETE method. I would like to support the DELETE function using AJAX. The main requirements are:

  • I would it to be unobtrusive - the delete links should just be anchor elements, not forms.
  • I would like to be able to handle errors on the server side, with the ability to pass a message back to the user if the model fails to delete.
  • I would like the user to confirm they do want to delete before the DELETE is sent.
  • Redirect on success should - I guess - happen on the client, with the appropriate success data sent back.

On that last point, the success return should be a data structure that includes a success message, the ID of the item deleted, and the URL for the index page to go to. The page on which the delete button is used can decide whether it needs to redirect the user to the index page, or simply remove a row (by ID) from a table of models on the page (assuming the user is deleted from the index page).

I have not been able to find a standard way to do this in Laravel. It makes sense for it to use jQuery and vue.js, I expect. All examples I have seen lack one of the above requirements - they force a redirect, or don't return error messages, or don't do a confirm, or build a form in the background so a browser POST is issued. So, is there an off-the-shelf way to handle all this, perhaps using a jQuery package, or just a simple block of code, because I feel like I'm going to have to reinvent something here that must have been implemented a thousand times before. Thanks!

Edit: I'm interested in something that is both simple to implement and maintain (unobtrusive is key ) and uses best-practice or commonly-used data structures, just so I'm not going off on a limb here.

Edit2: another requirement - the CSRF tag is in the header in a standard meta field. I would like the solution to use that, and not have to mess around with CSRF values in the delete buttons.

0 likes
8 replies
martinbean's avatar

@consil Why do you want the delete links to be anchors and not forms? You really want to use forms for an action that modifies a resource, which DELETE does.

Asking a user to confirm a deletion is easy with a bit of JavaScript. You could add a simple data attribute to your form and then listen for clicks on that:

<form action="/your/item/url" method="POST" data-confirm="Are you sure you wish to delete this resource?">
  <input type="hidden" name="_method" value="DELETE" />
  <button type="submit">Delete</button>
</form>
$('[data-confirm]').on('click', function (e) {
  if (! confirm($(this).data('confirm')) {
    e.preventDefault();
    e.stopImmediatePropagation();
  }
});

If you want to use JavaScript, you can then intercept the form submission and do an AJAX request instead, and act based on the response status.

$('.some-selector').filter('form').on('submit', function (e) {
  e.preventDefault();

  var $form = $(this);

  $.ajax({
    beforeSend: function () {
      // Do something before the AJAX request is sent
      // Maybe show a loading indicator, disable the submit button, etc.
    },
    method: 'DELETE',
    url: $form.attr('action')
  })
    .fail(function (jqXHR) {
      // Deletion failed
      // If you returned validation messages as a JSON response,
      // you’ll find them in jqXHR.responseJSON
    })
    .done(function (response) {
      // Do something with response
      // i.e. remove row from table, display success message, etc
    })
    .always(function () {
      // If you did something in beforeSend, un-do it here
      // i.e. re-enable any buttons that were disabled
    });
});

For CSRF, the Laravel docs has a section on how to handle this: https://laravel.com/docs/master/routing#csrf-x-csrf-token

So in some common bit of JavaScript that’s included before your form handlers, attach the CSRF token to AJAX requests:

$.ajaxSetup({
  headers: {
    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
  }
});
1 like
jjudge's avatar

@martinbean I understand what you are saying about the links, but I'm not talking about using GET to modify anything. The request would still be a DELETE, which no anchor can deliver directly. The route for the resource delete is identical to the resource show, but with a different verb, so even accidentally going to that route (e.g. if the JS isn't working) would not do any harm.

From my POV, I just want to keep the markup simple, as I have a lot of it to maintain.

I've got the CSRF token in the header already, so your sample bit of JS should pick that up for the AJAX calls:

<meta name="csrf-token" content="{{ csrf_token() }}">

One downside with using the form, is that if the JS does not pick it up to catch it, then a single click will delete the resource without ANY confirmation message. I think this is why I have seen some libraries that will turn an anchor into a form dynamically on the page, so there is no DELETE form at all if JS has not recognised the delete link. A link could by AJAX and converted into a DELETE request may also have some issues that I've miss too...?

Anyway - your code looks like it handles the main functionality nicely :-) I'll try it out wrapping it around and anchor and see how that goes.

jjudge's avatar

@tomschlick Thanks. I've seen that one around, which dynamically turns the anchor into a DELETE form. That keeps the source markup much neater, which is certainly one of the things I was looking for. However, it does then involve a full POST to the server, rather than using AJAX. That is okay for resource show pages where the user will always (assuming there was no problem deleting) be redirected after deleting, but if the user is deleting from inside the resource index page, then a redirect gets messy - you need to get the user back to the page they were on, rather than just removing the table row that contained the resource.

tomschlick's avatar

Yeah the script I posted should give you a good base to work off of for the AJAX side of things instead of a form submit. If you do come up with something be sure to post it up as I'm sure others would find it useful.

jjudge's avatar

This is what I have tried first:

Include this JS in the page (after jQuery, with no need to initialise it): https://github.com/rails/jquery-ujs/blob/master/src/rails.js

In the page head add these two meta elements:

<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="csrf-param" content="_token">

The first element is standard Laravel stuff. The second element tells rails.js what field the CSRF token needs to go into when sending the DELETE message.

The delete link then looks like this:

<a href="{{ route('my_resource.destroy', [$my_model_instance]) }}" data-method="delete" data-confirm="Confirm delete?" class="btn btn-danger">Delete</a>

That gives me a big red delete button, that asks if I'm sure, then posts a DELETE to the resource. It does a full POST rather than AJAX, but I suspect I can fix that with some settings. It's 500 lines of JS, which is certainly overkill, but it's just for admins, and I guess once it's loaded, it's loaded. I'll explore this a little more after tea.

jjudge's avatar

Got it working now in a very generic way, and will document it later (hopefully today). Some of the features of the solution are:

  • Each model looks after its own pre-delete checks, and raises an exception if any fail (e.g. trying to delete a product that has already been ordered). The exception message text ends up on the front end.
  • The backend destroy() does not care if it has been called by AJAX or a form. It returns the appropriate result in both cases.
  • There is minimum to add and do in both the controllers and the front end.
  • It uses rails.js for convenience. With a bit more custom code this could be avoided and some lines of JS avoided.

Please or to participate in this conversation.