Call a controller method automatically when rendering a partial - ruby-on-rails

I have a partial that needs to have some controller logic run before it can render without issue. Is there some way to associate the partial with some controller logic that is run whenever it is rendered?
For example, this is what my current code looks like:
MyDataController:
class MyDataController < ApplicationController
def view
#obj = MyData.find(params[:id])
run_logic_for_partial
end
def some_method_i_dont_know_about
#obj = MyData.find(params[:id])
# Doesn't call run_logic_for_partial
end
def run_logic_for_partial
#important_hash = {}
for item in #obj.internal_array
#important_hash[item] = "Important value"
end
end
end
view.html.erb:
Name: <%= #obj.name %>
Date: <%= #obj.date %>
<%= render :partial => "my_partial" %>
some_method_i_dont_know_about.html.erb:
Name: <%= #obj.name %>
User: <%= #obj.user %>
<%# This will fail because #important_hash isn't initialized %>
<%= render :partial => "my_partial" %>
_my_partial.html.erb:
<% for item in #obj.internal_array %>
<%= item.to_s %>: <%= #important_hash[item] %>
<% end %>
How can I make sure that run_logic_for_partial is called whenever _my_partial.html.erb is rendered, even if the method isn't explicitly called from the controller? If I can't, are there any common patterns used in Rails to deal with these kinds of situations?

You should be using a views helper for this sort of logic. If you generated your resource using rails generate, a helper file for your resource should already be in your app/helpers directory. Otherwise, you can create it yourself:
# app/helpers/my_data.rb
module MyDataHelper
def run_logic_for_partial(obj)
important_hash = {}
for item in obj.internal_array
important_hash[item] = "Important value" // you'll need to modify this keying to suit your purposes
end
important_hash
end
end
Then, in your partial, pass the object you want to operate on to your helper:
# _my_partial.html.erb
<% important_hash = run_logic_for_partial(#obj) %>
<% for item in important_hash %>
<%= item.to_s %>: <%= important_hash[item] %>
<% end %>
Or:
# app/helpers/my_data.rb
module MyDataHelper
def run_logic_for_partial(item)
# Do your logic
"Important value"
end
end
# _my_partial.html.erb
<% for item in #obj.internal_array %>
<%= item.to_s %>: <%= run_logic_for_partial(item) %>
<% end %>
EDIT:
As commented Ian Kennedy points out, this logic can also reasonably be abstracted into a convenience method in your model:
# app/models/obj.rb
def important_hash
hash = {}
for item in internal_array
important_hash[item] = "Important value"
end
hash
end
Then, you'd access the important_hash attribute in the following manner in your partial:
# _my_partial.html.erb
<% for item in #obj.important_hash %>
<%= item.to_s %>: <%= item %>
<% end %>

What you're trying to do runs against the grain of how Rails controllers/views are designed to be used. It would be better to structure things a bit differently. Why not put run_logic_for_partial into a helper, and make it take an argument (rather than implicitly working on #obj)?
To see an example of a view "helper", look here: http://guides.rubyonrails.org/getting_started.html#view-helpers

Related

Is it possible write a partial in Ruby on Rails that work to different models?

Is possible to create partials to be used inside forms that are semanticaly similar but have diferent attributes?
One example to explain the idea.
Imagine that we have the follow models:
class UnitedStatesTemperature < ApplicationRecord
# higher_fahrenheit
# lower_fahrenheit
end
and
class BrazilTemperature < ApplicationRecord
# higher_celsius
# lower_celsius
end
and the partial shared/_temperatures.html.erb
<%= form.label :higher_celsius, 'Higher' %>
<%= form.text_field :higher_celsius, id:
:brazil_temperature_higher_celsius %>
<%= form.label :lower_celsius, 'Lower' %>
<%= form.text_field :lower_celsius, id:
:brazil_temperature_lower_celsius %>
If we use this partial into a brazil_temperature form, it runs fine
<%= form_with(model: brazil_temperature, local: true) do |form| %>
<%= render 'shared/temperatures', form: form %>
<%= form.submit %>
<% end %>
If we use the same partial in the united_states_temperature it does not work (because attributes are differents).
My question is:
Is possible to rewrite the partial (shared/_temperatures.html.erb) in a generic way that it can be used in both forms (brazil_temperature and united_states_temperature)?
Answer
You could make it more generic if you pass the form partial the attribute names.
<% underscored_klass = form.object.class.name.underscore %>
<% form_id = ->(attr) { "#{underscored_klass}_#{attr}".to_sym } %>
<% # use the two lines below only if you want certain default attributes %>
<% lower_temp_attr = lower_temp_attr || :lower_celsius %>
<% higher_temp_attr = higher_temp_attr || :higher_celsius %>
<%= form.label higher_temp_attr, 'Higher' %>
<%= form.text_field higher_temp_attr, id: form_id[higher_temp_attr] %>
<%= form.label lower_temp_attr, 'Lower' %>
<%= form.text_field lower_temp_attr, id: form_id[lower_temp_attr] %>
Then call the partial as follows:
<%= form_with(model: brazil_temperature, local: true) do |form| %>
<%= render 'shared/temperatures', form: form, lower_temp_attr: :lower_celsius, higher_temp_attr: :higher_celsius %>
<%= form.submit %>
<% end %>
If I'm not mistaken the id gets automatically set to model_name_attribute_name. This means you could leave out the <% form_id = ->(attr) { "#{underscored_klass}_#{attr}".to_sym } %> and id: form_id[higher_temp_attr] parts to clean up the code a bit.
Variations
The use of the variations is meant to clean up the view a bit. This way you don't have to pass lower_temp_attr: :lower_celsius, higher_temp_attr: :higher_celsius when rendering the partial for different variations of the form.
Use a Helper
You could create a helper that returns the attribute name based on the object.
app/helpers/temperature_helper.rb
module TemperatureHelper
def lower_temp_attr(temp) # variation #1
attribute_names = temp.attribute_names
return :lower_celsius if attribute_names.include?('lower_celsius')
return :lower_fahrenheit if attribute_names.include?('lower_fahrenheit')
raise ArgumentError,
'Passed object should have either "lower_celsius" or ' +
'"lower_fahrenheit" as attribute.'
end
def higher_temp_attr(temp) # variation #2
attribute_names = temp.attribute_names
options = %w[higher_celsius higher_fahrenheit]
attr_name = attribute_names.find { |attr_name| options.include?(attr_name) }
return attr_name.to_sym if attr_name
raise ArgumentError,
'Passed object should have either "lower_celsius" or ' +
'"lower_fahrenheit" as attribute.'
end
end
Then call lower_temp_attr(form.object) in your partial.
Use the Model
Last but not least you could leave out the helper and make sure every temperature model responds to a certain interface method that returns the attribute name.
app/models/generic_temperature.rb
class GenericTemperature < ApplicationRecord
self.abstract_class = true
def lower_temp_attr # variation #1
return :lower_celsius if attribute_names.include?('lower_celsius')
return :lower_fahrenheit if attribute_names.include?('lower_fahrenheit')
raise NotImplementedError
end
def higher_temp_attr # variation #2
options = %w[higher_celsius higher_fahrenheit]
attr_name = attribute_names.find { |attr_name| options.include?(attr_name) }
attr_name&.to_sym or raise NotImplementedError
end
end
Than let your Temperature models inherit from GenericTemperature.
class UnitedStatesTemperature < GenericTemperature
end
And call form.object.lower_temp_attr in your partial.
There are many more ways to solve this issue. For example you could also choose to always raise the NotImplementedError exception in GenericTemperature and implement the method for every model. In this case inheriting from GenericTemperature does nothing except making sure that an NotImplementedError exception is raised if you didn't define the method in the model that inherits e.g. UnitedStatesTemperature.
If you're unable to inherit from a parent model for some reason (maybe you use it for something different) you can always place the code in a module and include it.
First define a class method which returns scale of the temparature
class UnitedStatesTemperature < ApplicationRecord
# higher_fahrenheit
# lower_fahrenheit
def self.scale
:fahrenheit
end
end
class BrazilTemperature < ApplicationRecord
# higher_celsius
# lower_celsius
def self.scale
:celsius
end
end
Then write a partial like below shared/temperatures
<%
higher_scale = "higher_#{form.object.class.scale}".to_sym
lower_scale = "lower_#{form.object.class.scale}".to_sym
%>
<%= form.label higher_scale, 'Higher' %>
<%= form.text_field higher_scale, id:
"#{form.object.class.table_name}_#{higher_scale}" %>
<%# same for lower --%>

Helper method within each do loop not working

I have a loop that looks like this
<% #user.collections.each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= collection.stories.count %>
<% end %>
It works perfectly to show the Collections that belongs to a User, and then show how many Stories are in each Collection.
However, I want to use a helper that does this.
in the view
<% #user.collections.each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= number_of_stories_in_collection %>
<% end %>
in the helper
module CollectionsHelper
def number_of_stories_in_collection
collection.stories.count
end
def render_stories_count
if number_of_stories_in_collection.zero?
'No stories in this collection yet'
else
"#{number_of_stories_in_collection} #{'story'.pluralize(number_of_stories_in_collection)}"
end
end
end
I get an error that says
undefined method `stories' for #<Collection::ActiveRecord_Relation:0x007f510f504af8>
Any help is appreciated, thanks!
The 'collection' variable isn't an instance variable, so the helper can't see it.
Change your view to this:
<% #user.collections.each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= number_of_stories_in(collection) %>
<% end %>
And your helper method to:
def number_of_stories_in(collection)
collection.stories.count
end
This way you are passing the variable to the helper correctly.
extending #Richard's answer and little bit of optimisation to avoid n+1 queries..
<% #user.collections.includes(:stories).each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= render_stories_count(collection) %>
<% end %>
helper:
module CollectionsHelper
def number_of_stories_in(collection)
collection.stories.length
end
def render_stories_count(collection)
if (count = number_of_stories_in(collection)).zero?
'No stories in this collection yet'
else
"#{count} #{'story'.pluralize(count)}"
end
end
end

proper syntax for a form_for in ruby

I am trying to write a form for an array
<%= form_for #user, html: {multipart:true} do |f| %>
<%= render "shared/error_messages", object: f.object %>
<label for="user-amenities" class="top">Amenities</label>
<ul class="group" id="user-amenities">
<% User.amenities_list.each_with_index do |amenity, index| %>
<li class="checkbox-li">
<input type="checkbox" name="user_amenities_indicies[]" value="<%= index %>">
<%= amenity %>
</input>
</li>
</ul>
<% end %>
However I am not utilizing the |f| and it is not saving the options in the amenities_indices. Any idea on how to refactor this code to utilize the f so the user information can be saved?
Try simple_form https://github.com/plataformatec/simple_form/blob/master/README.md
What you're looking for is :collection and then :as
Code Block
The refactoring you seek is basically that you need to use the f. with all your inputs
Reason being that if you call form_for, it essentially means you're calling a huge code block, in which every attribute needs to be bound to your form object, in order to render with your params hash correctly
In short - your form_for renders an HTML form, keeping the names of the <input> elements in line with the requirement of your application to load the params. The problem you have is that omitting the f. call will keep those inputs outside the scope for your params, leading to the issue you're seeing.
--
Save
If you don't have any associative data (which I've described below), you'll want to include your inputs in the f. code block:
<%= form_for #user do |f| %>
<% User.amenities_list.each_with_index do |amenity, index| %>
<%= f.check_box :user_amenities_indicies, index %>
<% end %>
<% end %>
This will pass the checked values of the user_amenities_indicies checkboxes through to your controller. This should work, and is the correct syntax for you
--
fields_for
A further addition is that I don't know whether you're trying to populate associative data or not here - but if you were trying to create data for another model, you'll want to use fields_for:
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def new
#user = User.new
#user.user_amenities.build
end
def create
#user = User.new(user_params)
#user.save
end
private
def user_params
params.require(:user).permit(:user, :params, user_amenities_attributes: [])
end
end
This will allow you to create a form using fields_for, like this:
<%= form_for #user do |f| %>
<%= f.fields_for :user_amenities do |ua| %>
# fields for user_amenities
<% end %>
<% end %>

Creating multiple objects in a form Rails

So I have an interesting problem I'm working on. I am trying to create multiple objects of the same model in one view. I would like to display all the possible objects in my view, check boxes to select which ones to create, then submit and create all the corresponding objects.
Now the objects to select are gotten using an API request and returned in JSON format. The JSON is then displayed on the view for the user to select, then an array containing all the selected objects is sent back to the controller for creation.
Here is the relevant code that I've tried so far.
objects_controller.rb
def new
#possible_objects = <api call to get objs>
#objects = []
end
def create
params[:objects].each do |obj|
# create and save obj
end
end
objects/new.html.erb
<% form_for #objects do |f| %>
<% #possible_objects.each do |api_obj| %>
<%= check_box_tag(api_obj["name"])%>
<%= api_obj["name"] %>
<% end %>
<%= f.submit %>
<% end %>
This is definitely not the right approach, as the form will not accept an empty array as a parameter. I'm not sure where else to go with this, any pointers in the right direction would be great. Thanks.
Thanks to MrYoshiji for pointing me in the right direction, this is what ended up working
objects_controller.rb
def
#possible_objects = <api call to get objs>
end
def create
params[:objects].each do |object|
new_obj = Object_Model.new( <params> )
new_obj.save
if !new_obj.save
redirect_to <path>, alert: new_obj.errors.full_messages and return
end
end
redirect_to <path>, notice: 'Successfully created.'
end
objects/new.html.erb
<%= form_tag objects_path(method: :post) do %>
<% #possible_objects.each do |api_obj| %>
<%= check_box_tag 'objects[]', api_obj %>
<%= possible_object["name"] %>
<% end %>
<%= submit_tag 'Create'%>
<% end %>
Can you try the following?
# view
<% form_tag my_objects_path(method: :post) do |f| %>
<% #possible_objects.each do |api_obj| %>
<%= check_box_tag 'objects[names][]', api_obj["name"] %>
<%= api_obj["name"] %>
<% end %>
<%= f.submit %>
<% end %>
# controller
def create
params[:objects][:names].each do |obj_name|
YourModelForObject.create(name: obj_name)
end
end
See this comment on the documentation of check_box_tag: http://apidock.com/rails/ActionView/Helpers/FormTagHelper/check_box_tag#64-Pass-id-collections-with-check-box-tags

How can the cache for the views with translations be invalidated?

Imagine you have two views with code like the following:
controller_a/a.html.erb
<%= content_tag(:div) do %>
<%= I18n.t "some.key" %>
<% end %>
controller_b/b.html.erb
<%= content_tag(:div) do %>
<%= I18n.t "some.key" %>
<% end %>
<%= content_tag(:div) do %>
<%= I18n.t "some.other_key" %>
<% end %>
So, a.html.erb is on controller_a#a, while b.html.erb is on controller_b#b. Both actions are cached by caches_action. How can I make sure that when I change the some.key translation key, both views are invalidated? How could I build a generic mechanism?
Say, in your ApplicationController create the following class-method (or in a lib and extend by it):
def self.i18n_digest(*scopes)
Digest::MD5.hexdigest I18n.t(scopes).to_s
end
Then you can use :cache_path option in your caches_action this way:
caches_action :some_action, cache_path: { some_key: i18n_digest('some', 'foo') }
Just make sure that you set the locale in a before_filter before this statement.
Docs on cache_path.
Note: I'm using the scope of translation ('some') to get all its nested messages as a hash.

Resources