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: enter image description hereDogs

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.

+3


source to share


5 answers


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>

      

0


source


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 %>

      

+2


source


You can use respond_to?

for a more complete solution to the problem. Presentation logic doesn't seem so wrong when it's more general.

<% [:meows, :barks, :plays_catch].each do |method| %>
  <% if animal.respond_to?(method) %>
    <td><%= animal.send(method) %> </td>
  <% end %>
<% end %>

      

+1


source


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>

      

+1


source


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.

+1


source







All Articles