I am developing a Ruby-on-Rails web app that is essentially used to track product technical specifications. This is my first go at a RoR app since following Michael Hartl's Rails Tutorial. I'm not too sharp on Ajax or RoR yet, and didn't realize I was tackling something so intense, but I'm eager to see it through.
To understand the model relationships, see my ER diagram.
I have 3 models:
@entity: { :id, :name, :label, :img }
@group: { :id, :name, :default_label }
@property: { :id, :name, :units, :units_short, :default_label, :default_value, :default_visibility }
Then each of these models has unique many-to-many associations (where I need to track specifics about the relationships) so I created models for those associations.
@group_property_relationship: { :group_id, :property_id, :order }
@entity_group_relationship: { :entity_id, :group_id, :order, :label }
@entity_property_relationship: { :entity_id, :property_id, :group_id, :label, :order, :value, :visibility }
I already have these models and their associations talking to one another and it works wonderfully from the console. But setting up the client is where it gets complicated. See my mock-up for an idea of how I want it to look, hopefully this will inspire an understanding of the functionality.
I am trying to create a one-stop view where everything can be managed on one page using ajax. The user can create an entity, property, or group, create associations between groups and properties, and create associations between entities and groups.
See a static view of my progress here:
https://trello-attachments.s3.amazonaws.com/519e948c97d3fd4579000a79/537fa898eef9844886200be0/463x710/b04e83d4e4e56253a1818048264e1fa3/spex_hub_140617.png
Apparently I don't have a high enough SO reputation for more links. The search, sorting, and pagination all work via ajax, for both models, independently of one another. I followed this tutorial:
http://asciicasts.com/episodes/240-search-sort-paginate-with-ajax
to get most of that functionality, but had to tweak it a bit to update for Rails 4 and to work for multiple models in one view.
But, where it isn't working, is when an entity is created–successfully or not–from this view using the form seen near the top of the 'progress' image, the instance is successfully created, the list is updated, sorting works, but search is broken.
So, in order to track down the error, I need to provide a ton of code...here goes:
The view: hub
app/views/hub/main.html.erb
<div id="hub-alert"></div>
<div class="row">
<aside class="col-xs-4 v-divider">
<a style="color: #494A4A" href="<%= entities_path %>">Entities</a>
<section>
<h6>
Create New Entity
</h6>
<%= render 'new_entity' %>
</section>
<section>
<h6>
Existing Entities
</h6>
<%= render partial: 'entities/search', locals: { path: hub_path } %>
<div id="entities">
<%= render 'entities/entities' %>
</div>
<div id='ajax-test'></div>
</section>
</aside>
<aside class="col-xs-4 v-divider">
<a style="color: #494A4A" href="<%= groups_path %>">Groups</a>
<section>
<h6>
Selected Entity's Groups
</h6>
<div id="entitys_groups">
</div>
</section>
<section>
<h6>
Create New Group
</h6>
<%= render 'groups/new' %>
</section>
<section>
<h6>
Existing Groups
</h6>
<%= render partial: 'groups/search', locals: { path: hub_path } %>
<div id="groups">
<%= render 'groups/groups' %>
</div>
<div id='ajax-test'></div>
</section>
</aside>
</div>
Create Entity Partial: _create_entity.html.erb
app/views/hub/_create_entity.html.erb
<h1> Create New Entity</h1>
<div class="row">
<div class="col-xs-10 col-xs-offset-1">
<%= bootstrap_form_for(@entity, url: { controller: "hub", action: "create_entity" }, inline_errors: false, method: :post, remote: true) do |f| %>
<%= render 'entities/fields', f: f %>
<%= f.submit "Create Entity", class: "btn btn-sm btn-primary btn100", id: "create-entity" %>
<% end %>
</div>
</div>
Entity Search Partial: _search.html.erb
app/views/entities/_search.html.erb
<%= form_tag path, method: 'get', remote: true do %>
<%= hidden_field_tag :entity_direction, params[:entity_direction] %>
<%= hidden_field_tag :entity_sort, params[:entity_sort] %>
<%= hidden_field_tag :entity_event, true %>
<div class="input-group" style="margin-bottom: 15px">
<%= text_field_tag :entity_search, params[:entity_search], class: "form-control" %>
<span class="input-group-btn" >
<%= submit_tag "Search", name: nil, class: "btn btn-default" %>
</span>
</div>
<% end %>
The controller: hub_controller
.
app/controllers/hub_controller.rb
class HubController < ApplicationController
helper_method :entity_sort_column, :entity_sort_direction, :group_sort_column, :group_sort_direction
before_action do
unless current_user.nil?
redirect_to root_url unless current_user.admin?
else
redirect_to root_url
end
end
def main
@entities = Entity.search(params[:entity_search]).order(entity_sort_column + ' ' + entity_sort_direction).paginate(page: params[:entities_page], per_page: 10, order: 'created_at DESC')
@groups = Group.search(params[:group_search]).order(group_sort_column + ' ' + group_sort_direction).paginate(page: params[:groups_page], per_page: 10, order: 'created_at DESC')
@entity = Entity.new
@group = Group.new
end
def create_entity
@entity = Entity.find_by(name: entity_params[:name])
@result = {msg: "", r: -1}
@entities = Entity.search(params[:entity_search]).order(entity_sort_column + ' ' + entity_sort_direction).paginate(page: params[:entities_page], per_page: 10, order: 'created_at DESC')
respond_to do |format|
if @entity.nil?
@entity = Entity.new(entity_params)
if [email protected]
@result[:r] = 0
@result[:msg] = "'#{@entity.name}' failed to save."
else
@result[:r] = 1
@result[:msg] = "'#{@entity.name}' was saved."
#entities needs to be updated to get the latest addition
@entities = Entity.search(params[:entity_search]).order(entity_sort_column + ' ' + entity_sort_direction).paginate(page: params[:entities_page], per_page: 10, order: 'created_at DESC')
end
else
@result[:r] = 0
@result[:msg] = "Name: '#{@entity.name}' is already taken."
end
format.js
format.html { redirect_to hub_path }
end
end
def create_group
#not yet implemented
end
def create_property
#not yet implemented
end
def create_entity_group_relation
#not yet implemented
end
def create_group_property_relation
#not yet implemented
end
private
def entity_params
params.require(:entity).permit(:name, :label, :img)
end
def group_params
params.require(:group).permit(:name, :default_label)
end
def entity_sort_column
Entity.column_names.include?(params[:entity_sort]) ? params[:entity_sort] : "name"
end
def entity_sort_direction
%w[asc desc].include?(params[:entity_direction]) ? params[:entity_direction] : "asc"
end
def group_sort_column
Group.column_names.include?(params[:group_sort]) ? params[:group_sort] : "name"
end
def group_sort_direction
%w[asc desc].include?(params[:group_direction]) ? params[:group_direction] : "asc"
end
end
The routes: routes.rb
Spex::Application.routes.draw do
get "hub/main"
post "hub/create_entity"
post "hub/create_group"
post "hub/create_property"
post "hub/create_entity_group_relation"
post "hub/create_group_property_relation"
get "groups/new"
get "entities/new"
get "entities/hub"
get "properties/new"
resources :users
resources :group_property_relationships
resources :entity_group_relationships
resources :entity_property_relationships
resources :properties do
member do
get :groups, :entities
post :serve
end
end
resources :groups do
member do
get :properties, :entities
post :own
end
end
resources :entities do
member do
get :groups, :properties
post :own
end
end
resources :sessions, only: [:new, :create, :destroy]
root 'static_pages#home'
match '/hub/create_entity', to: 'hub#main', via: 'get'
match '/signup', to: 'users#new', via: 'get'
match '/hub', to: 'hub#main', via: 'get'
match '/signin', to: 'sessions#new', via: 'get'
match '/signout', to: 'sessions#destroy', via: 'delete'
match '/help', to: 'static_pages#help', via: 'get'
end
The application.js
which ajaxifies the sorting and some other basic functions–Nothing here for the create call:
/app/assets/javascripts/application.js
$(function()
{
$('#entities').on('click', "th a", function () {
$.getScript(this.href);
return false;
});
$('#groups').on('click', "th a", function () {
$.getScript(this.href);
return false;
});
$("body").on("click", '.pagination a', function(e){
e.preventDefault();
$.getScript(this.href);
return false;
});
$("body").on("click", '.table tr.entity', function(e){
// e.preventDefault();
$.getScript(this.id+"/groups");
// return false;
});
$("[data-toggle='tooltip']").tooltip();
});
Here's the callback js for the create_entity
action:
app/views/hub/create_entity.js.erb
var entities_html = " <%= j render 'entities/entities' %>";
$('#entities').html(entities_html)
//clear the entity form fields
$('input[id^=entity]').val('');
var alert_html = "";
<% unless @result.nil? %>
<% if @result[:r] == 1 %>
alert_html += " <div class='alert alert-success'>";
alert_html += " <%= @result[:msg] %>";
alert_html += " </div>";
<% else %>
alert_html += " <div class='alert alert-danger'>";
alert_html += " <%= @result[:msg] %>";
alert_html += " </div>";
<% end %>
<% else %>
alert_html += " <div class='alert alert-danger'>";
alert_html += " There was an error saving #{@entity.name}";
alert_html += " </div>";
<% end %>
$('#hub-alert').html(alert_html);
So at this point, there is something funky happening with the routes or something. I can see a change in what the server prints out before a call to create_entity
and after the call. Mostly notice the change in the parameters. Why would they change?
Before:
Started GET "/hub?utf8=%E2%9C%93&entity_direction=&entity_sort=&entity_event=true&entity_search=o+v" for 127.0.0.1 at 2014-06-17 14:48:13 -0600
Processing by HubController#main as JS
Parameters: {"utf8"=>"✓", "entity_direction"=>"", "entity_sort"=>"", "entity_event"=>"true", "entity_search"=>"o v"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = 'e39061f2d24bec8416f5319586f87f27def3804c' LIMIT 1
DEPRECATION WARNING: #apply_finder_options is deprecated. (called from main at /Users/astoutj/Documents/Work/_sync/Work-sync/ror/workspace/spex/app/controllers/hub_controller.rb:13)
DEPRECATION WARNING: #apply_finder_options is deprecated. (called from main at /Users/astoutj/Documents/Work/_sync/Work-sync/ror/workspace/spex/app/controllers/hub_controller.rb:14)
Entity Load (0.5ms) SELECT "entities".* FROM "entities" WHERE ((name LIKE '%o%' OR label LIKE '%o%' OR created_at LIKE '%o%' OR updated_at LIKE '%o%') AND (name LIKE '%v%' OR label LIKE '%v%' OR created_at LIKE '%v%' OR updated_at LIKE '%v%')) ORDER BY name asc, created_at DESC LIMIT 10 OFFSET 0
Rendered entities/_entity.html.erb (1.5ms)
Rendered entities/_entities.html.erb (8.0ms)
Group Load (0.4ms) SELECT "groups".* FROM "groups" ORDER BY name asc, created_at DESC LIMIT 10 OFFSET 0
(0.1ms) SELECT COUNT(*) FROM "groups"
Rendered groups/_group.html.erb (10.4ms)
Rendered groups/_groups.html.erb (19.2ms)
Rendered entities/_entity.html.erb (1.2ms)
Rendered entities/_entities.html.erb (6.3ms)
Rendered hub/main.js.erb (45.5ms)
Completed 200 OK in 53ms (Views: 49.3ms | ActiveRecord: 1.2ms)
After:
Started GET "/hub?utf8=%E2%9C%93&entity_direction=&entity_sort=&entity_event=&entity_search=o+v" for 127.0.0.1 at 2014-06-17 14:48:33 -0600
Processing by HubController#main as JS
Parameters: {"utf8"=>"✓", "entity_direction"=>"", "entity_sort"=>"", "entity_event"=>"", "entity_search"=>"o v"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = 'e39061f2d24bec8416f5319586f87f27def3804c' LIMIT 1
DEPRECATION WARNING: #apply_finder_options is deprecated. (called from main at /Users/astoutj/Documents/Work/_sync/Work-sync/ror/workspace/spex/app/controllers/hub_controller.rb:13)
DEPRECATION WARNING: #apply_finder_options is deprecated. (called from main at /Users/astoutj/Documents/Work/_sync/Work-sync/ror/workspace/spex/app/controllers/hub_controller.rb:14)
Group Load (0.4ms) SELECT "groups".* FROM "groups" ORDER BY name asc, created_at DESC LIMIT 10 OFFSET 0
(0.1ms) SELECT COUNT(*) FROM "groups"
Rendered groups/_group.html.erb (6.0ms)
Rendered groups/_groups.html.erb (14.8ms)
Entity Load (0.7ms) SELECT "entities".* FROM "entities" WHERE ((name LIKE '%o%' OR label LIKE '%o%' OR created_at LIKE '%o%' OR updated_at LIKE '%o%') AND (name LIKE '%v%' OR label LIKE '%v%' OR created_at LIKE '%v%' OR updated_at LIKE '%v%')) ORDER BY name asc, created_at DESC LIMIT 10 OFFSET 0
Rendered entities/_entity.html.erb (2.1ms)
Rendered entities/_entities.html.erb (12.8ms)
Rendered hub/main.js.erb (35.7ms)
Completed 200 OK in 44ms (Views: 39.4ms | ActiveRecord: 1.5ms)
The question: How do I get the create action to not interfere subsequent search actions?
Let me know if anything else is needed.
Okay here's what I had to do.
I also had a main.js.erb
which would be the callback for the main
action. In there, I was unintentionally blocking changes to my partials by checking the params. The param I was checking was the entity_event
and the group_event
which would be true for any action for the respective model, (i.e. sort, search, page, new). So I added a param to the search fields params[:search]
and when the search was for an entity
, params[:search]
was set to "entity"
and when the search was for a group
, it was set to "group"
. Here are the updated files.
Search Entity
app/views/hub/_search_entity.html.erb
<%= form_tag path, method: 'get', url: { controller: "hub", action: "main" }, remote: true do %>
<%= hidden_field_tag :entity_direction, params[:entity_direction] %>
<%= hidden_field_tag :entity_sort, params[:entity_sort] %>
<%= hidden_field_tag :entity_event, true %>
<%= hidden_field_tag :search, "entity" %>
<div class="input-group" style="margin-bottom: 15px">
<%= text_field_tag :entity_search, params[:entity_search], class: "form-control", id: "entity_search_field" %>
<span class="input-group-btn" >
<%= submit_tag "Search", name: nil, class: "btn btn-default", id: "entity_search" %>
</span>
</div>
<% end %>
Search Group
app/views/hub/_search_group.html.erb
<%= form_tag path, method: 'get', url: { controller: "hub", action: "main" }, remote: true do %>
<%= hidden_field_tag :group_direction, params[:group_direction] %>
<%= hidden_field_tag :group_sort, params[:group_sort] %>
<%= hidden_field_tag :group_event, true %>
<%= hidden_field_tag :search, "group" %>
<div class="input-group" style="margin-bottom: 15px">
<%= text_field_tag :group_search, params[:group_search], class: "form-control", id: "group_search_field" %>
<span class="input-group-btn" >
<%= submit_tag "Search", name: nil, class: "btn btn-default", id: "group_search" %>
</span>
</div>
<% end %>
Main action callback
app/views/hub/main.js.erb
<% unless params[:search] == "entity" %>
$('#groups').html('<%= j render "groups/groups" %>');
console.log("entity event empty");
<% end %>
<% unless params[:search] == "group" %>
$('#entities').html('<%= j render "entities/entities" %>');
console.log("group event empty");
<% end %>
Now it is fully responsive before and after a create action.