0

So I have gotten a better understanding of the issue I have involving Javascript. The problem lies within the fact that after the AJAX call to refresh the contents of a div, the contents within that div are unlinked or uninitialized for the Javascript. What I am trying to do is make my list of activities sortable for the user. I use a priority field to save the order of the activities so when the user refreshes or leaves the page, then the order is maintained.

The problem I am receiving is the following:

ActiveRecord::RecordNotFound (Couldn't find Activity with 'id'=item):

and as a result, the activity order will not be saved. This happens after the AJAX call and refresh and it works normally beforehand, saving the order and whatnot. I have tried some solutions, such as one and moving the Javascript over into the partial to no success.

I believe the error above is the result of the activity not linking correctly with the Javascript file which leads it to an id of "item" so does anyone know how to fix this issue or any advice on how to fix it? Thanks!

Main View:

<!-- home.html.erb -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>Bootstrap 101 Template</title>

</head>

<!-- the jScrollPane script -->


<!--IF THE USER IS LOGGED IN, DISPLAY THE FOLLOWING -->
<% if current_user %>
    <!--
    <style>
    .navbar {
      background-color:#2F4F4F
    }
    </style>
    -->
    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href='home'>TimeTracker</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav navbar-right">
            <li><a href="#">Signed in as <%=current_user.email %></a></li>
            <li>
              <%= link_to 'Sign Out', sign_out_path, method: :delete %>
            </li>
          </ul>
        </div><!-- /.navbar-collapse -->
      </div><!-- /.container-fluid -->
    </nav>

    <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css">

    <h1 align = "center">Activities</h1>

    <div id = 'activity_container'>


      <ul id="Categories" class="connectedSortable">
        <% @categories.each do |cats| %>
          <% if cats.user_id == current_user.id%>
                  <div class="panel-group">
                    <div class="panel text-left">
                      <div class="panel-heading">
                        <h4 class="panel-title">
                          <a data-toggle="collapse" href="#collapse<%= cats.id%>">
                            <%= cats.c_name %>
                          </a>
                        </h4>
                      </div>
                      <div id="collapse<%= cats.id%>" class="panel-collapse collapse">
                        <ul id="category_activities" class=".connectedSortable">
                          <% cats.activities.each do |activity| %>
                            <li class="list-group-item" >
                              <% if activity.hidden == false || activity.hidden == nil%>

                                    <label class="my_label"><%= activity.a_name %></label>

                                      <!-- Delete Activity -->
                                      <%= link_to destroy_act_path(activity.id), method: :delete, data: { confirm: "Are you sure?" } do %>
                                          <i class="fa fa-trash-o" aria-hidden="true" title="Delete"></i>
                                      <% end %>


                                        <!-- Edit activity -->
                                  <%= link_to edit_act_path(activity.id)do %>
                                      <!--<button class="editActivity" style="border:none; padding:0; background-color: transparent">-->
                                        <i class="fa fa-pencil fa-fw" aria-hidden="true" title="Edit" id="editActivity"></i>
                                      <!--</button>-->
                                  <% end %>


                                      <!-- Hide activity -->
                                  <%= link_to hide_act_path(activity.id), method: :post do %>
                                      <i class="fa fa-eye" aria-hidden="true" title="Hide" id="item"></i>
                                  <% end %>

                              <% end %>
                          <% end %>
                        </li>
                        </ul>
                      </div>
                    </div>
                  </div>
            <% end %>
        <% end %>
        </ul>


      <ul id="Activities" class="connectedSortable" >

      <!-- List each activity in database -->

        <% @activities.each do |activity| %>
          <% if (activity.hidden == false || activity.hidden == nil) && activity.category_id == nil %>
              <li class="list-group-item" id='<%=activity.id%>' style="list-style: none;">

                <!-- Display activity name -->

                <label class="my_label"><%= activity.a_name %></label>

                <!-- Delete Activity -->
                <%= link_to destroy_act_path(activity.id), method: :delete, data: { confirm: "Are you sure?" }, remote: true do %>
                    <i class="fa fa-trash-o" aria-hidden="true" title="Delete"></i>
                <% end %>

                <!-- Edit activity -->
                <%= link_to edit_act_path(activity.id)do %>
                    <button class="editActivity" style="border:none; padding:0; background-color: transparent">
                      <i class="fa fa-pencil fa-fw" aria-hidden="true" title="Edit" id="editActivity"></i>
                    </button>
                <% end %>

                <!-- Hide activity -->
                <%= link_to hide_act_path(activity.id), method: :post do %>
                    <i class="fa fa-eye" aria-hidden="true" title="Hide" id="item"></i>
                <% end %>

              </li> <!-- End of list item -->
          <% end %> <!-- End of if statement -->
      <% end %> <!-- End of activity loop -->
      </ul>


    </div>


    <ul class="pager">

      <li class="previous"><a href="#"><span aria-hidden="true">&larr;</span>Previous </a></li>
      <li class="next"><a href="#"> Next<span aria-hidden="true">&rarr;</span></a></li>

      <!-- *****************NEW*********************** -->
      <%= form_for @activity, :url => create_act_path, remote: true, data: {type: 'script'} do |a| %>
          <%= a.text_field :a_name, id: 'a_name_field', placeholder: 'Activity Name'%>
          <%= a.select :category_id, Category.all.collect { |c| [c.c_name, c.id] }, include_blank: "--No Category--" %>
          <%= a.submit 'Create', id: 'submitButton', class: 'btn btn-primary'%>
      <% end %>

      <%= form_for @category, :url => create_cat_path, remote: true do |c| %>
          <%= c.text_field :c_name, id: 'c_name_field', placeholder: 'Category Name'%>
          <%= c.submit 'Create', id: 'submitButton', class: 'btn btn-primary'%>
      <% end %>

      <!-- Button for showing all hidden items -->
      <%= link_to unhide_act_path, method: :post do %>
          <button class="showHidden" >Show Hidden</button>
      <% end %>

      <!-- Button to sort -->
      <button class="sortActivity">Sort</button>
      <button class="doneSorting">Done Sorting</button>

      <!-- ***************************************** -->

    </ul>

<!-- IF THE USER IS NOT LOGGED IN, DISPLAY THE FOLLOWING -->
<% else %>

    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
            <span class="sr-only"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#">TimeTracker</a>
        </div>

        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav navbar-right">
            <li class="active">
            </li>
          </ul>
        </div>
      </div>
    </nav>

      <div class="loginContainer" align="center">
        <h1 align="center">
          <b>
            Please log in or sign up
          </b>
        </h1>

        <div class="container">
          <br>
          <%= button_to 'Login', sign_in_path, :method => 'get', class: 'btn' %>
          <br>
          <%= button_to 'Sign Up', sign_up_path, :method => 'get', class: 'btn' %>
        </div>

        <!--<div class="container" style="background-color:#D3D3D3">
          <input type="checkbox" checked="checked"> Remember me
          <span class="psw">Forgot <a align="center" href="#">password?</a></span>
        </div> -->
      </div>


<% end %>



<script type="text/javascript">
    function changeImg(img) {
        if (img.src == "<%=asset_path('_unfilledbubble.png')%>"){
            img.src = "<%=asset_path('_filledbubble.png')%>";
        }
        else {
            img.src = "<%=asset_path('_unfilledbubble.png')%>";
        }
    }
</script>
<script type="text/javascript">
    $(document).ready(function() {
        $(function () {
            $('.scroll-pane').jScrollPane({showArrows: true});
        });
    });
</script>

<script>


    $(document).ready(function(){
        function callAll(){
            set_positions = function(){
                // loop through and give each task a data-pos
                // attribute that holds its position in the DOM
                $('.list-group-item').each(function(i){
                    $(this).attr("data-pos",i+1);
                });
            }
            //    ready = function(){
            // call set_positions function
            set_positions();
            // ************NEW execept for #Activities
            $('#Activities').sortable({
                connectWith: ".connectedSortable"
            });
            $('#Activities').disableSelection();
            //        $('#Categories').sortable({
            //            connectWith: ".connectedSortable"
            //        });
            //        $('#Categories').disableSelection();
            $('#category_activities').sortable({
                connectWith: ".connectedSortable"
            });
            $('#category_activities').disableSelection();
            // *******end of NEW
            $('#Activities li').on('click','li',function (){
                var myid = $(this).attr('id');
                alert(myid);
            });
            // after the order changes
            $('#Activities').sortable().bind('sortupdate', function(e, ui) {
                // array to store new order
                var updated_order = []
                // set the updated positions
                set_positions();
                // populate the updated_order array with the new task positions
                $('#Activities li').each(function(i){
                    updated_order.push({ id: $(this).attr('id'), position: i });
                });
                // send the updated order via ajax
                $.ajax({
                    type: "PUT",
                    url: '/home/sort',
                    data: { order: updated_order }
                });
            });
        }
        $(document).ajaxComplete(callAll());
    })
</script>

Partial View:

 <!-- List each activity in database -->
  <% @activities.each do |activity| %>
      <% if (activity.hidden == false || activity.hidden == nil) && activity.category_id == nil %>
          <li class="list-group-item" id="item" data-id="<%activity.id%>" style="list-style: none;">


            <!-- Display activity name -->

            <label class="my_label"><%= activity.a_name %></label>


            <!-- Delete Activity -->
            <%= link_to destroy_act_path(activity.id), method: :delete, data: { confirm: "Are you sure?" }, remote: true do %>
                <i class="fa fa-trash-o" aria-hidden="true" title="Delete"></i>
            <% end %>

            <!-- Edit activity -->
            <%= link_to edit_act_path(activity.id)do %>
                <button class="editActivity" style="border:none; padding:0; background-color: transparent">
                  <i class="fa fa-pencil fa-fw" aria-hidden="true" title="Edit" id="editActivity"></i>
                </button>
            <% end %>

            <!-- Hide activity -->
            <%= link_to activity, method: :post, :controller => :activities, :action => :set_hidden_true, remote: true do %>
                <button class="hideActivity" style="border:none; padding:0; background-color: transparent">
                  <i class="fa fa-eye" aria-hidden="true" title="Hide" id="item"></i>
                </button>
            <% end %>

          </li> <!-- End of list item -->
      <% end %> <!-- End of if statement -->
  <% end %> <!-- End of activity loop -->

Create Activity JS File:

<!--create_activity.js.erb-->
$('#Activities').html("<%= j (render 'object') %>");

Home Controller:

class HomeController < ApplicationController
  respond_to :html, :js

  def new

  end

  def index

  end

  def home
    @activities = Activity.all
    @activity = Activity.new

    @categories = Category.all
    @category = Category.new
  end

  def create_activity
    @activity = Activity.create(activity_params)
    @activity.user_id = current_user.id
    @activity.priority = @activity.id

    @object = Category.all

    @activities = Activity.all
    @categories = Category.all

    if @activity.save
      flash[:success] = 'Activity created successfully'
    else
      flash[:notice] ='ERROR: Activity could not be create'
    end
  end

  def create_category
    @category = Category.new(category_params)
    @category.user_id = current_user.id

    #@category.priority = @category.id

    @object = Category.all
    @activities = Activity.all
    @categories = Category.all

    #@category.priority = @category.id
    if @category.save!
      flash[:success] = 'Category created successfully!'
    else
      flash[:error] = 'ERROR: Category was not saved!'
    end
  end

  def destroy_activity
    @activity = Activity.find(params[:id])
    @activity.destroy

    @object = Category.all

    @activities = Activity.all
    @categories = Category.all
  end

  def welcome
  end


  def hide_activity

    @object = Category.all

    @activities = Activity.all
    @categories = Category.all

    @activity = Activity.find(params[:id])
    @activity.update_attribute(:hidden, true)
    respond_to do |format|
      format.html {redirect_to activities_url}
      format.js
    end
  end

  #NEW 4/15
  def edit_activity
    @activity = Activity.find(params[:id])
  end

  def update_activity
    @activity = Activity.find(params[:id])

    if @activity.update_attributes(activity_params)
      flash[:success] = 'Activity updated successfully!'
    else
      flash[:notice] = 'Activity was not updated'
    end
  end

  def unhide_all

    @object = Category.all

    @activities = Activity.all
    @categories = Category.all

    @activities = Activity.all
    @activities.update_all(hidden: false)
    # redirect_to root_path
  end

  def sort
    params[:order].each do |key, value|
      Activity.find(value[:id]).update_attribute(:priority, value[:position])
    end
    render :nothing => true
  end

  private

  def activity_params
    params.require(:activity).permit(:a_name, :category_id)
  end

  def category_params
    params.require(:category).permit(:c_name)
  end
end

UPDATE

I have tried the following and as a test, replaced my script in the html with the following:

    $('#Activities').on('click','li',function (){
        var myid = $(this).attr('id');
        alert(myid);
    });

Before the Ajax, when I click on an activity, it correctly gives me its id. However, after doing an Ajax call, then when I try clicking on an activity, I get the same error where it claims it cannot find an activity with "id=item"

Community
  • 1
  • 1
VectorConvoy
  • 187
  • 1
  • 4
  • 14
  • My guess at first glance is that `$(document).ajaxComplete(callAll());` fixes some activity related stuff. This means `$('#Activities').html("<%= j (render 'object') %>");` should be followed by `callAll();`. If this triggers to much JavaScript you should extract the part that's needed for fixing activity JavaScript related stuff to it's own function, and call that function at the end. (after you replaced the `$('#activities').html(...)`). – 3limin4t0r Apr 20 '17 at 20:18
  • The question could be further cleared up by specifying exactly where the ajax request is triggered. So we know what trail to follow. – 3limin4t0r Apr 20 '17 at 20:25
  • @JohanWentholt apologies for the late reply. As of right now, I am trying to simply get the create function to use AJAX so the user does not require a full page refresh but after the AJAX call, that is where my problem occurred. – VectorConvoy Apr 20 '17 at 21:31

2 Answers2

0

You have a lot of options here, one option is to change the bind to the document or the body and not directly to the id of the element because this is created dinamically, for example:

$("body").on('click',"#Activities" ,function () {
   var myid = $(this).attr('id');
   alert(myid);
});

you can initialize the event on the ajax request again when the element is rendered that is not recommended

or use plugins like https://github.com/adampietrasiak/jquery.initialize that can reinit the plugins if you bind it to a class, id or another selector.

xploshioOn
  • 4,035
  • 2
  • 28
  • 37
0

I've created an example where you will be able to create a Person from the index page using an AJAX request. After the Person is created a new list of people is send to the index page. To simulate your JavaScript I set the background-color of every element to blue using jQuery.

Let's start with the index.html.erb itself.

<!-- app/views/people/index.html.erb -->
<p id="notice"><%= notice %></p>

<h1>People</h1>

<table>
  <thead>
    <tr>
      <th>First name</th>
      <th>Last name</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody id="people_tbody">
    <%= render 'people', people: @people %>
  </tbody>
</table>

<%= render('form', person: @people.new) %>

Next the two partials, _people.html.erb:

<!-- app/views/people/_people.html.erb -->
<% people.each do |person| %>
    <tr>
        <td><%= person.first_name %></td>
        <td><%= person.last_name %></td>
        <td><%= link_to 'Show', person %></td>
        <td><%= link_to 'Edit', edit_person_path(person) %></td>
        <td><%= link_to 'Destroy', person, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
<% end %>

and _form.html.erb from which the AJAX request gets fired:

<!-- app/views/people/_form.html.erb -->
<%= form_for(person, remote: true) do |f| %> <!-- remote true, makes it an AJAX call -->
  <% if person.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(person.errors.count, "error") %> prohibited this person from being saved:</h2>

      <ul>
      <% person.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :first_name %>
    <%= f.text_field :first_name %>
  </div>

  <div class="field">
    <%= f.label :last_name %>
    <%= f.text_field :last_name %>
  </div>

  <div class="actions">
    <%= f.submit %> <!-- when submit is clicked the AJAX request is fired to PeopleController#create -->
  </div>
<% end %>

To simulate your JavaScript problem I have the following file:

// app/assets/javascripts/people.js
function init_people() {
    $('#people_tbody tr').css('background-color', 'lightblue')
}

$(document).ready(init_people)

This gives all rows a light blue background.

As you can see I'm using AJAX to send the filled in form because the remote: true is set. If the form is submitted the request will arrive in PeopleController#create, which looks like this:

# app/controllers/people_controller.rb
class PeopleController < ApplicationController
  before_action :set_person, only: [:show, :edit, :update, :destroy]

  # GET /people
  # GET /people.json
  def index
    @people = Person.all
  end

  # GET /people/1
  # GET /people/1.json
  def show
  end

  # GET /people/new
  def new
    @person = Person.new
  end

  # GET /people/1/edit
  def edit
  end

  # POST /people
  # POST /people.json
  def create # AJAX request arrives here
    @person = Person.new(person_params)

    respond_to do |format|
      if @person.save
        format.html { redirect_to @person, notice: 'Person was successfully created.' }
        format.json { render :show, status: :created, location: @person }
        # I added the underlying line to handle the AJAX response.
        format.js { render :index, status: :created, location: @person }
      else
        format.html { render :new }
        format.json { render json: @person.errors, status: :unprocessable_entity }
        # I added the underlying line to handle the AJAX response.
        format.js   { render nothing: true }
      end
    end
  end

  # PATCH/PUT /people/1
  # PATCH/PUT /people/1.json
  def update
    respond_to do |format|
      if @person.update(person_params)
        format.html { redirect_to @person, notice: 'Person was successfully updated.' }
        format.json { render :show, status: :ok, location: @person }
      else
        format.html { render :edit }
        format.json { render json: @person.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /people/1
  # DELETE /people/1.json
  def destroy
    @person.destroy
    respond_to do |format|
      format.html { redirect_to people_url, notice: 'Person was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_person
      @person = Person.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def person_params
      params.require(:person).permit(:first_name, :last_name)
    end
end

As you can see I added two lines to handle the js format in PeopleController#create. If the render comes from format.js { ... } rails knows to pick the index.js.erb file, and not the index.html.erb file. If you don't want this behavior you can specify the specific file.

My index.js.erb looks like this:

// app/views/people/index.js.erb
$('#people_tbody').html("<%= j(render('people', people: Person.all)) %>")

This replaces the contents of the #people_tbody with the new set op people (similar to your example). But now I've the same problem as you. The JavaScript isn't triggered anymore, and the background of every row is just white. To trigger the JavaScript over the new loaded content I just need to call the init function. This means any logic you want to apply after the document is ready and after an AJAX request should be extracted to a separate function. I placed my logic in the function init_people().

This means the index.js.erb should look like this:

// app/views/people/index.js.erb
$('#people_tbody').html("<%= j(render('people', people: Person.all)) %>")
init_people()

Now, after the AJAX content is loaded, the init_people() function is triggered setting up the JavaScript for the new loaded content.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52