Adding an action to resources

rails

#1

Hi! I have a quite newbie question.

There is a Rails app. There is an entity called “cars”. So for them, I have all of CRUD methods routes.rbresources :cars. There is Index page where I have a list of all cars http://mysite.com/cars. You can choose cars from the list.

Now I want to add a button that sends ajax request and clears selected cars on Index page. I’ve added post 'cars/clear' => 'cars#clear' to routes.rb and I’ve made a button in the view. Everything works perfectly however I’m thinking about:

  1. Was I right that I used a button and not a link?
  2. Isn’t it better in this case to send GET request?
  3. Can I improve somehow my routes.rb?

#2

When you say “clears”, does it actually remove the cars from the database? If that’s the case, it definitely should not be GET.


#3

Selected cars are stored in the flash. So #clears just flash[:cars] = []


#4

Interesting. Flash is volatile, it clears after one request, so I’m curious why you chose to store the data in flash.

We may have wondered into the XY Problem a bit here. Maybe describe a little bit what the application it supposed to do, instead of the solution you are attempting, and we might be able to help you more.


#5

There are entities called “results” (there are no cars). The application shows you the list of these results, you can choose some of them and press Compare button to compare them. There are no registred users, the application works in a private network. There is a paginator and a filter. The paginator and filter asynchronously update the list. So the problem was to keep the selected results. I’ve decided to use flash with #keep.

routes.rb:

resources :results do
  put 'toggle'
end

results.coffee:

...
$.ajax({
  url: "/results/#{resultId}/toggle"
  type: 'PUT'
  dataType: 'script'
  complete: ->
    $(".result-checkbox").removeAttr("disabled")
  })

results_controller.rb:

  def toggle
    flash[:result_ids] ||= []
    if flash[:result_ids].include? params[:result_id]
      flash[:result_ids].delete params[:result_id]
    else
      flash[:result_ids].push params[:result_id]
    end
    flash.keep(:result_ids)
    @checked_results = flash[:result_ids]

    respond_to do |format|
      format.js
    end
  end

toggle.js.erb:

...
$("#result-to-compare-list").html("<%= escape_javascript(render partial: 'result_to_compare_list', locals: { items: @checked_results} ) %>");

Recently I’ve added “clear” button but I’m not sure that I’m doing it right (however everything works).


#6

I’m glad to see you’re using UJS. I wouldn’t recommend using flash as a data store hash.

With the amount of work you’re doing it might just be easier to write it in jQuery. Have an action on the checkbox trigger a jQuery function which will search for all selected checkboxes and retrieve the “value” of their first sibling td (table data) and display the list at the top with the same target data you’re using in your UJS.


#7

But how can I solve the problem “The paginator and filter asynchronously update the list”?

And I chose results
And I apply a filter
And I see result list is updated
Then I want to see previously selected results in the table

#8

Have the updated jQuery result submit a UJS request which updates the content. Instead of the server side holding the data in flash you’ll have the client side jQuery handle what’s selected.


#9

Can you provide an example or give a link to an example?


#10

Here’s some of my own checkbox implementation.

// Generate Form (used for submitting check-boxes)
jQuery(function($) { $.extend({
  form: function(url, data, method) {
    if (method == null) method = 'POST';
    if (data == null) data = {};

    var form = $('<form>').attr({
      method: method,
      action: url
    }).css({
      display: 'none'
    });

    var addData = function(name, data) {
      if ($.isArray(data)) {
        for (var i = 0; i < data.length; i++) {
          var value = data[i];
          addData(name + '[]', value);
        }
      } else if (typeof data === 'object') {
        for (var key in data) {
          if (data.hasOwnProperty(key)) {
            addData(name + '[' + key + ']', data[key]);
          }
        }
      } else if (data != null) {
        form.append($('<input>').attr({
          type: 'hidden',
          name: String(name),
          value: String(data)
        }));
      }
    };

    for (var key in data) {
      if (data.hasOwnProperty(key)) {
        addData(key, data[key]);
      }
    }

    return form.appendTo('body');
  }
}); });

// Filter Submissions
function filterCheckboxes(route, thing, cmd) {
  var formSend = new Object;
  formSend["utf8"] = "✓";
  formSend["_method"] = "patch";
  var csrfName = $("meta[name='csrf-param']").attr('content');
  formSend[csrfName] = $("meta[name='csrf-token']").attr('content');
  formSend[thing] = new Object;
  formSend[thing]["command"] = cmd;
  formSend[thing][thing + "_ids"] = $("." + thing + "-check:checked").map(
    function () {
      return this.value;
    }
  ).get();
  $.form(route, formSend, 'POST').submit();
}

// Will check all items that have the same class name as the link/button
function checkByClass(source) {
  var aInputs = document.getElementsByTagName('input');
  for (var i=0;i<aInputs.length;i++) {
    if (aInputs[i] != source && aInputs[i].className == source.className) {
      aInputs[i].checked = source.checked;
    }
  }
}

And in the view header

<div class="row filters">
  <div class="form-inline">
    <input type="checkbox" class='profile-check' onchange='checkByClass(this)' />
    <%= content_tag 'span', 'delete', class: 'filter-delete', onclick: "filterCheckboxes('#{profile_ids_profiles_path}', 'profile','delete')", style: 'cursor:pointer;' %>
  </div>
</div>

Above the first input is a checkbox at the top of the page that will select or unselect every checkbox that has the profile-check class on it via the checkByClass Javascript method I wrote. The line below that submits a list of all checkboxes selected to the profile route I have for handling multiple ids and depending on what the command is executes it for the current user.

And the individual checkbox item looks like

<%= hidden_field_tag "profile[profile_ids][]", '' %>
<%= check_box_tag "profile[profile_ids][]", profile.id, nil, class: "profile-check" %>

At the moment that controller only has delete as an option

def profile_ids
  current_user.profiles.where(id: params["profile"]["profile_ids"]).destroy_all if params["profile"]["command"]["delete"]
  redirect_to(profiles_path, notice: "#{params['profile']['command'].capitalize} completed.") and return unless params['profile']["command"].empty?
  redirect_to profiels_path
end

The route is just

resources :profiles do
  collection do
    patch 'profile_ids'
  end
end

You can use this example and just change both the Javascript .submit() method to the rails UJS hook for submitting in rails and the controller to return a UJS response. The UJS hook for that can be done like: $('form#myForm').trigger('submit.rails'); - (source origin). Just use the CSS selector and jQuery trigger() in the filterCheckboxes Javascript. And you can add an onclick action to each checkbox to run the UJS through filterCheckboxes.


#11

It doesn’t look simple.

Why is this solution better? The logic is moved to JS. Isn’t it better to control everything from the controller? In the controller, you can extend everything, validate, etc. It will be easier to change something.

Actually, I’m going to remove the checkboxes and make just custom elements.


#12

The logic is not moved to Javascript. All Javascript does here is collect which checkboxes are selected. The controller and UJS will still have control of everything as you would like.

The only reason the form builder is there in Javascript is because you can’t have forms within forms in a web page. With this Javascript usage for checkboxes you can now put editable content within each row on your page without any problems (like trying forms within forms). Other than that it’s not complicated. The filterCheckboxes method simply creates all of the same params fields that Rails does.

Just know that the decisions made in the design of this wasn’t just pulled out of my arse. A lot of work and hard lessons were learned in the process. You’re more than welcome to try your custom elements, it will likely be a great learning experience for you.


#13

I think I got it. I’ve rewritten the UI logic in CoffeeScript + JQuery + js-cookie (without any ajax). Works really faster and more flexible in my case. Thanks!