0

Rails: 5.2.0, Ruby: 2.4.1p111, Browser: Chrome 66.0.3359.181, jquery-rails gem: 4.3.3, Turbolinks disabled.

I am following this AJAX on Rails with Unobtrusive JS tutorial and have reached the end, but have some strange behavior that I can't figure out.

The app uses UJS to allow a user to add new records to the bottom of list via AJAX without having to update the entire page like Rails normally would. Adding new records works fine, but when I click on 'Delete', the JS function in application.js is not triggered so nothing seems to happen, but when I refresh the page the record is gone. If I add a new record, refresh the page and then try and delete the record JS is triggered and the record is deleted/removed from the list immediately.

The web page is very simple:

<!––  index.html.erb ––> 
<h1>Tutorials</h1>

<ul id=tutorials>
  <%= render @tutorials %>
</ul>

<%= form_with model: Tutorial.new, data: {'js-tutorial-form' => true} do |form| %>
  Title <%= form.text_field :title %><br>
  URL <%= form.text_field :url %><br>
  <%= form.submit %>
<% end %>

with a small partial:

<!––  _tutorial.html.erb ––> 
<li data-js-tutorial-id=<%= tutorial.id %>>    
  <%= tutorial.title %> <%= link_to "Delete", tutorial, remote: true, method: :delete %>
</li>

all the JS is in the application.js file

$(document).ready(function() {
  $('[data-js-tutorial-form]').on("ajax:success", function(event, data, status, xhr){
    var tutorial = $(xhr.responseText).hide();
    $('#tutorials').append(tutorial);
    tutorial.fadeIn(2000);
  });

     $('[data-js-tutorial-id]').on("ajax:success", function(event, data, status, xhr){
    var tutorial_id = xhr.responseJSON.id;
    $('[data-js-tutorial-id=' + tutorial_id + ']').hide();

  }); 

});

the only other file is the controller

class TutorialsController < ApplicationController

    def index
        @tutorials = Tutorial.all
    end

    def create
        @tutorial = Tutorial.new(params[:tutorial].permit(:title, :url))
        if @tutorial.save
                render partial: "tutorial", locals: {tutorial: @tutorial}
        end
    end

    def destroy
        @tutorial = Tutorial.find(params[:id])
        @tutorial.destroy
        render json: @tutorial
    end

end

What I don't get is that the HTML source code is identical before and after the page is refreshed, but I have confirmed using Chrome developer tools that the $('[data-js-tutorial-id]') is not being called until unless the page is refreshed. I have used byebug in the controller to confirm that is stepping through correctly. Any ideas?

1 Answers1

1

Your problem is the javascript listener is bound to the <li> tag, not to the actual link. The listener never had any chance to fire.

Personally I'd change the destroy method, why return the tutorial you just deleted?

def destroy
  @tutorial = Tutorial.find(params[:id])
  @tutorial.destroy
  render json: {success: true}
end

Secondly, your partial looks wrong, didn't test though

<!––  _tutorial.html.erb ––> 
<li>    
  <%= tutorial.title %> <%= link_to "Delete", tutorial, remote: true, method: :delete, "data-js-delete": tutorial.id %>
</li>

which of course needs a change to the javascript:

$('[data-js-delete]').on("ajax:success", function(event, data, status, xhr){
  $(event.target).closest('li').hide();
}); 
Thomas R. Koll
  • 3,131
  • 1
  • 19
  • 26
  • The tutorial which is being deleted is being returned so that I can render it and pick it up with the ajax:success method, but your solution is much tidier - just use event.target then the record being deleted doesn't need to be passed on. – desertNomady May 29 '18 at 12:16
  • The bad news is that even after I implemented the changes I had the same behavior, but you pointed me in the right direction - this is an issue with jQuery event bindings not being updated and I need to use delegated events as per this solution: https://stackoverflow.com/a/17453051/9154357 I am following up on this and will update when I figure out what exactly is going on. – desertNomady May 29 '18 at 12:24
  • OK, got it. The error was related to event delegation - this is necessary since the 'li' element that I want to hide was not present when the page loaded, so it never got the hide event bound to it. The 'ul' element in the html page should be headed `
      ` and the jQuery function should be called `$('#tutorials').on(...`. Thanks for the assistance @Thomas R. Koll, you were right that there was something fishy with the 'li' element.
    – desertNomady May 29 '18 at 20:18
  • How does `render json: {success: true}` have any chance of firing if it does not know the id of the object to hide? – Jerome May 20 '19 at 22:44
  • @Jerome I'm not 100% sure if I can answer your question: It doesn't need to know the id. `$(event.target).closest('li')` is a bit crude but will do the job. – Thomas R. Koll May 21 '19 at 08:22