##Goals
- refactor views to remove database queries
- use partials to tidy up repeated code
- use built-in Rails Helper methods
- use custom helper methods
- utilizing presenters to dry up code and push logic further down the stack
-
I remember growing up and my room was always messy and gross. There where so many moments when my mother would scold me and tell me to clean up my room. Unfortunately because I was lazy and disobedient I would tend to sweep my mess underneath my bed. My mom being the smart women she was would catch my laziness and I'd typically be in trouble.
-
The views, and our models can feel like this sometimes. Things can become super repetitive, or because we are crunched for time our views and models get neglected. We are better than that and we should strive to apply foundational programming principles into the way we format our views
- Logic in our views should make us feel uncomfortable! Obviously our application can function with logic in the views but we should strive to push some of that logic down the stack or look to ways in which we can optimize our code.
- We will be using this repo shouts to my dude YUNG-Mike for coming up with this beauty.
- each branch has a problem and we will walk through that together.
The first thing we are going to talk about is making sure that database queries are never, ever in our views. Seeing queries in the view should make you feel uncomfortable! Lets checkout the idea_one
branch in this repo and look at ways we can fix our views. If we look at the Albums index.html.erb
we should see something along these lines.
Albums Index
<% Album.all.each do |album| %>
<%= album.year %> - <%= album.name %>
<% end %>
Now what is wrong with this code? If you said that we are calling Album.all in our view you've guessed correctly. With that being said what can I do to fix this problem?
if we went into the albums_controller.rb
and added this bit of code into it we should be able to call this database query in the view.
class AlbumsController < ApplicationController
def index
@albums = Album.all
end
def show
end
end
Now that we've pushed that query to an instance variable coming from our controller we can call @albums into our index. Now that we've done that go ahead and fix the albums show.html.erb
view.
Typically in our application we'll see a lot of repetitive code. Now as developers our job is to combat bugs. Bug are no good. So one of the only real ways to make sure we have less bugs is to write less code. The less code we have typically the less opportunity we are going to have for bugs to arise.
With that being said we can use partial to reuse code in our views! Utilizing this code allows us to keep things dry, and write less code.
Rails gives you a lot of tools to hopefully set you up for success. With that being said we can pull our rails new form into a rails edit form and have them share that information.
so if we run touch app/views/albums/_form.html.erb
we will effectively create a partial file. With that being said lets go ahead and reuse our rails form! It should look a little like this
<%= form_for(@album) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :year %>
<%= f.text_field :year %>
<%= f.submit %>
<% end %>
Now we have this really cool partial but haven't really done much with it. Now if we want to render this file all we have to do is call <%= render 'form' %>
in our form view and rails will take care of the rest!
You can also put your partial in a folder call shared and render it from there. for example <%= render 'shared/form'%>
Also you can can also pass in local variables through the partial for example I can say
<%= render 'form', locals:{ album: @album} %>
and then I'll have access to the local variable album
in my form which will allow me to change my form to look like this
<%= form_for(album) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :year %>
<%= f.text_field :year %>
<%= f.submit %>
<% end %>
It's your turn! Implement the edit and update actions in your controller and add in the partials into the edit view. ####if you're up for the challenge.. Look for other code you can break into partials and do that!
Rails helpers are really, really cool. They help streamline your views, and hopefully move some complexity out of the view. So the cool part about rails helpers is that they are essentially Modules. When you create a rails project they come bulit into the project. You can create your own custom module but for this implementation we're basically going to put sparkles around some text. Now we could go into ever single album and add those sparkles into view or we could create a function that will do it for us. Whats great with his option is that our code is essentially agnostic to whatever is being passed in and will format whatever we want!
So if we go into our helper file we can add this code inside of it to create our method.
module AlbumsHelper
def sparkle(text)
"*~* " + text + " *~*"
end
end
Because this is a floating module and rails allows it so that you can call this function inside of the corresponding view I can just call this function!
<% @albums.each do |album| %>
<li> <%= album.year %> - <%= sparkle(album.name) %> <%= link_to "Edit", edit_album_path(album) %>
<% end %>
You've done some great work lets go ahead and take a pom! Before you take your pom make sure to clone down this repo as well!
###Presenters
When we are building programs some issues we tend to run into is the idea of pushing logic down the stack. Many times in our efforts to do so things can have the tendency become a storage dump of information. Our model and our helpers become bloated and hard to manage. One way to combat that issue is to add another layer to our Model.
###What is a presenter?
A presenter is essentially a PORO that knows about is aware of the current view and the current object. With that knowledge we can extract the logic inside these views and essentially call on them. When the logic is extracted we into this class we can also test these methods.
You are essentially adding another layer of complexity to your application. Which at times can feel like a lot of work to get something to format our information correctly.
- The presenter when implemented correctly can help manage behavior in your views (formatting in views)
- We can pull out our logic from the view and put into methods that testable.
So looking at our views we can see that there is a lot of logic. Since we are programmers that are uncomfortable with this we are going to extract that logic out of the view and place it into a presenter class.
If we look at our show page for the trainer there is a lot of logic in our view it is accommodating for so much. We need to move that logic out of the view so that our application is testable and the responsibility of formatting this view is passed off to another object.
so first thing we are going to do is create a presenter class for our trainer view.
If we were to break this down a presenter is just a PORO that knows about the specific view we're in and the object it's talking to.
touch app/models/trainer_presenter.rb
we'll add this code into that class
class TrainerPresenter
def initialize(trainer, template)
@trainer = trainer
@template = template
end
end
What we're doing is here is saying that we want our class to be aware of the object we're talking to and the view that correlates to it. In order for this to work correctly our presenter needs to be aware of these two objects
but jhun how does the presenter become aware of this presenter class? We'll it would be nice if we created a custom helper method to pass that information to our presenter class! Sounds great to me!
first things first lets go into our into our view and lets dream up how we're going to do this.
I'm going to add this block of code to the view so that we can pass the presenter class to the whole view. This may not make sense but just trust for a little bit here.
<% present @trainer do |trainer_presenter|
#view logic goes here
<% end %>
currently this code will do nothing because we have not defined this method inside our application helper so lets do that.
module ApplicationHelper
def present(object, klass = nil)
klass ||= "{object.class}Presenter".constantize
presenter = klass.new(object,self)
yield presenter if block_given?
presenter
end
So what is this code actually doing? so we've defined a method called present and we are passing it two things one which is defaulted and the actual object we are calling the view on which is in our case the trainer.
Like we've defined previously our presenter needs to know about two things. It needs to know about the class and our object. So in line 4 we're making our code agnostic and allowing us to call upon any presenter we choose by grabbing the objects class, and interpolating it into this string. We then call constantize which will create for us our TrainerPresenter class. Now that we have that we can then say our presenter is defined as TrainerPresenter.new(the object(which is our trainer), self(which is our view)).
We then will say that we if a block is provided which is what we added in our view inject the presenter into that block and of course after that we will return it.
What this will allow us to do now is clean up some of the logic in our views.
we can now define a method inside our TrainerPresenter class called avatar and which will render that logic in the view for us.
implement the rest of the logic in this view into our presenter class.
to make our code agnostic to whatever is being passed through we can take the initialize method inside our TrainerPresenter class and move it to a BasePresenter class that it can inherit from.
touch app/model/base_presenter.rb
class BasePresenter
attr_reader :template
def initialize(object, template)
@object = object
@template = template
end
def self.presents(name)
define_method(name) do
@object
end
end
end
while adding
class TrainerPresenter < BasePresenter
presents :trainer
So what is going on. We've made this base controller and what it's doing is moving the logic from the previous class into a super class. That super class is still aware of the object that we're passing through and the template. Now the self.presents method is doing a little bit of meta programming. In a nut shell this method define_method allows us to create almost a partial function. In our case when we pass in the attribute :trainer this self.presents method creates a method called trainer which it calls on. All that is doing for us is returning the object that is being passed through.
Because the object is being passed through we can now get rid of the instance variable in our TrainerPresenter class!
Our class should look a little like this.
class TrainerPresenter < BasePresenter
presents :trainer
def avatar
if trainer.avatar.present?
template.image_tag(trainer.avatar, class: 'square', id: 'avatar')
else
template. image_tag('blank_photo.jpg', class: 'square', id: 'avatar')
end
end
def bio
if trainer.bio.present?
trainer.bio
else
"None Given"
end
end
def twitter_info
if trainer.twitter_username.present?
template.link_to(trainer.twitter_username, "http://twitter.com/#{trainer.twitter_username}")
else
"None Given"
end
end
def trainer_info
"#{trainer.name} has been a member since #{trainer.created_at.strftime("%b. %Y")}"
end
end
touch app/tests/model/trainer_presenter_test.rb
Whats great about testing is that we can now talk to the presenter to get our expect outcomes and render that information to the views. This extra level of test will insure that we get exactly what were expecting from the views
Our test should look a little like this.
require 'test_helper'
class TrainerPresenterTest < ActionView::TestCase
test 'returns none given when attributes are not defined' do
presenter = TrainerPresenter.new(Trainer.new, view)
assert_equal 'None Given',presenter.twitter_info
end
test 'returns correct template link when given a twitter username' do
trainer = Trainer.create(name:'yung-jhun', twitter_username: "joshuajhun",avatar:'some-picture')
presenter = TrainerPresenter.new(trainer, view)
assert_equal "<a href=\"http://twitter.com/joshuajhun\">joshuajhun</a>", presenter.twitter_info
end
end
We inherit form ActionView which will give us the access to our view.
Finish implementing tests for the rest of our methods!