Removing conditional logic from a shared partial view or alternative solution
For the current project, I have duplicate code between views and I'm not sure about the best route to refactor it.
I seem to be in a position to have duplicate code in different files .html.erb
, or I could put identical code in partial and conditional expressions. I've always heard that logic should stay out of sight. None of the options seem to be perfect and I currently don't know of any alternatives.
To illustrate my question, I created a simple rails application called animals
. I painted for two models, one for cat
and one for dog
. Images display the corresponding attributes:
Display @cats
and are @dogs
almost the same. Cats
just has a column for meows
, a Dogs
is a column for barks
, and dog
has an additional attribute column plays_catch
.
Let's say we decided to reduce the duplicate code for displaying cats and dogs by making the overall view partial:
#views/shared/_animal.html.erb
<tr>
<td><%= animal.name %></td>
<td><%= animal.age %> </td>
<% if animal.class == Cat %>
<td><%= animal.meows %> </td>
<% end %>
<% if animal.class == Dog %>
<td><%= animal.barks %> </td>
<td><%= animal.plays_catch %> </td>
<% end %>
</tr>
Then for rendering @cats = Cat.all
:
<%= render partial: "shared/animal", collection: @cats %>
Then for rendering @dogs = Dog.all
:
<%= render partial: "shared/animal", collection: @dogs %>
Obviously, for this particular example, it would be overkill to do something like this, but the real world project I applied it to would not be overkill.
General question: how do you remove nearly identical code that iterates over collections, where the only difference is adding / removing a column of information? He simply does not consider it necessary to put this logic in the view itself and leave the duplication wrong.
source to share
Below is my answer after looking at the posted answers. Basically:
- I left the differences in each forest model index page.
- I've made common partials for common table headers and table data.
code below:
#app/views/cats/index.html.erb
<h1>Listing Cats</h1>
<table>
<thead>
<tr>
<%= render partial: "shared/cat_dog_table_headers" %>
<th>Meows</th>
</tr>
</thead>
<tbody>
<% @cats.each do |cat| %>
<tr>
<%= render partial: "shared/cat_dog_table_data", locals: {animal: cat} %>
<td><%= cat.meows %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Cat', new_cat_path %>
And for dogs:
#app/views/dogs/index.html.erb
<h1>Listing Dogs</h1>
<table>
<thead>
<tr>
<%= render partial: "shared/cat_dog_table_headers" %>
<th>Barks</th>
<th>Plays catch</th>
</tr>
</thead>
<tbody>
<% @dogs.each do |dog| %>
<tr>
<%= render partial: "shared/cat_dog_table_data", locals: {animal: dog} %>
<td><%= dog.barks %></td>
<td><%= dog.plays_catch %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Dog', new_dog_path %>
Common table headers for cats and dogs:
#app/views/shared/_cat_dog_table_headers
<td><%= Name %></td>
<td><%= Age %></td>
General table data for cats and dogs:
#app/views/shared/_cat_dog_table_data_headers
<td><%= animal.name %></td>
<td><%= animal.age %></td>
source to share
You can use decorators and add methods that return additional columns:
class DogDecorator < Draper::Decorator
def extra_columns
[:barks, plays_catch]
end
end
class CatDecorator < Draper::Decorator
def extra_columns
[:meows]
end
end
...
<% animal.extra_columns.each do |column| %>
<td><%= animal.attributes[column.to_s] %>
<% end %>
...
<% @cats = CatDecorator.decorate_collection(Cat.all)
<%= render partial: "shared/animal", collection: @cats %>
source to share
I don't know of any canonical way to accomplish this, but I would use one partial
to do it like this:
<tr>
<% animal.attributes.each do |_, value| %>
<td><%= value %></td>
<% end %>
</tr>
You can get rid of duplicate calls attributes
by providing the model's pre-derived attributes in a partial local variable.
EDIT : if you only want to display some attributes.
# Declare whitelist of attributes
# (you can also declare a blacklist and just calculate the difference between two array: all_attributes - blacklist_attributes):
<% whitelist = [:name, :age, :barks] %>
<%= render partial: 'shared/animal',
collection: @dogs,
locals: {attrs: (@dogs.first.attributes.keys.map(&:to_sym) & whitelist)} %>
view / general / _animal.html.erb:
<tr>
<% attrs.each do |attr| %>
<td><%= animal[attr] %></td>
<% end %>
</tr>
source to share
You can add a method with the same name to the Cat and Dog classes, which return the names and values of the attributes of specific instances. I would recommend returning two arrays (one with field names, others with field values, or vice versa) since the hashes are not exactly ordered. This way you can control the order in which they appear in the view. For example:
#models/cat.rb
def fields_and_attributes
fields = ["Name","Age","Meows"]
attributes = [self.name, self.age]
if self.meows
attributes.push("Yes")
else
attributes.push("No")
end
[fields,attributes] # make sure each attribute is positioned in the same index of its corresponding field
end
#models/dog.rb
def fields_and_attributes
fields = ["Name","Age","Plays catch"]
attributes = [self.name, self.age]
if self.plays_catch
attributes.push("Yes")
else
attributes.push("No")
end
[fields,attributes] # make sure each attribute is positioned in the same index of its corresponding field
end
#controllers/animals_controller.rb
def display_animals
@animals = Cat.all + Dog.all # an array containing the different animals
end
#views/display_animals.html.erb
for i in (0...@animals.size)
fields_and_attributes = @animals[i].fields_and_attributes
for f in (0...fields_and_attributes[0].size)
<p><%= fields_and_attributes[0][f] %> : <%= fields_and_attributes[1][f] %></p>
end
end
Here we first iterate over all the animals and call the method of .fields_and_attributes
that particular record; we then iterate over the results of calling this method, displaying the fields and attributes in the same order as the one defined in the method, and also ensure that the code displays each field and each attribute regardless of the difference in total fields for each other animal.
source to share