Coder Social home page Coder Social logo

refactoring-views-with-helpers-reading's Introduction

Refactoring Views with Helpers

Objectives

  1. Understand how and when to use helper methods
  2. Create helper methods for specific controllers
  3. Create application-wide helpers

A Story As Old As strftime

Let's imagine we're working on a blog application. In fact, we don't even have to imagine it because there's a blog app included in the lesson. Use it to follow along and try out the code yourself.

We've already added the presentation logic to show a blog post:

<!-- app/views/posts/show.html.erb -->

<h1><%= @post.title %></h1>
<p><%= @post.description %></p>

However, our readers really want to know when the post was posted or edited. Fair enough.

We know our Post model has a timestamp field called updated_at, so we decide to display that.

<!-- app/views/posts/show.html.erb -->

<h1><%= @post.title %></h1>
<p>Last updated: <%= @post.updated_at %></p>
<p><%= @post.description %></p>

We use rails s to run the application, browse to a post, and see the time, but it's not very pretty. We want a specific, readable format, so we look to our old friend strftime.

Note: reading about strftime in the docs is great, but playing with it live is even more fun, so check out For a Good Strftime.

Okay, armed with strftime and having read the ancient runes that help us understand the different format directives (e.g. %A, %p, etc.), we end up with this:

<!-- app/views/posts/show.html.erb -->

<h1><%= @post.title %></h1>
<p><%= @post.updated_at.strftime("Last updated %A, %b %e, at %l:%M %p") %></p>
<p><%= @post.description %></p>

Top-tip: Notice how we included the text and punctuation in the argument to strftime. It will very nicely ignore anything that isn't one of the format directives and pass it through as part of the final string. This is helpful because it saves us from the awkward business of having to try to parse out the time string and concatenate parts of it together with other strings in order to create more readable output.

This is great! Now people know when a blog entry was last updated.

Except they only know it on the show template. What if they also need to see that date on the post list page, in line with each post title? We can add it to the index template. Simple enough, right?

<!-- app/views/posts/index.html.erb -->

<% @posts.each do |post| %>
  <div>
    <%= post.title %> - <%= post.updated_at.strftime("Last updated %A, %b %e, at %l:%M %p") %>
  </div>
<% end %>

Done and done. Now it's on the show and index views. But now we're thinking that it would be nice to see this when we are editing a blog post, just for our reference. Okay, we can do that. Let's add it to the edit template.

<!-- app/views/posts/edit.html.erb -->

<%= form_for(@post) do |f| %>
  <label><%= @post.updated_at.strftime("Last updated %A, %b %e, at %l:%M %p") %></label><br>
  <label>Post title:</label><br>
  <%= f.text_field :title %><br>

  <label>Post Description</label><br>
  <%= f.text_area :description %><br>

  <%= f.submit %>
<% end %>

At this point there may be little red flags shooting up in your head, but let's go one step further. A few weeks after making this change, we decide that the format should be in military (24-hour) time and that we don't want to display the day of the week anymore. It's our blog; we can do what we want.

But now we have a problem. We have to remember (or, worse, search for) every place we wrote that code to display the last updated date and time, and then we have to make the same change in all of those places. We have a small application with only a few views right now, but, as your app grows in size, this kind of repetition is a productivity killer. It's also a great way to introduce bugs into the system if we forget to make changes in all of the correct places.

Don't Repeat Yourself (DRY)

DRY is a software design principle that, like Separation of Concerns, helps us make decisions about how to organize our code. The formal, very dry statement of DRY is: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."

Put more simply for our blog application: "We shouldn't have to look in three places to know how to display the last updated date and time of a post."

//Flat-fact" The opposite of DRY is WET, which may or may not stand for "We Enjoy Typing" or "Write Everything Twice."

How do we DRY up our views for this last updated feature? If we have a goal of manipulating the updated_at field in only a single place, where can we do that?

What about the model? We only ever get updated_at from the model, so it's tempting to do something like:

# app/models/post.rb

def last_updated
  updated_at.strftime("Last updated %A, %b %e, at %l:%M %p")
end

and then use it like @post.last_updated on the view. That would certainly solve our DRY problem, but if we think back on MVC and Separation of Concerns, we've created a new problem. Model code isn't concerned with presentation logic. Displaying a formatted string isn't the job of the component that talks to the database and handles the business logic. It's the job of the view. So we need a way to keep that code in the view but also make it DRY.

Helpers Are Here To, Well, Help

Rails provides us with a great way to extract common presentation logic from multiple views: helpers! Helpers are methods that are available to your views and encapsulate a common bit of code. If you've used a link_to or a text_field then you've already come across helpers. Rails has a ton of them built in to help keep you from having to repeat all the code necessary to build a link to a given route, display an image, or create a form.

We can also create our own helpers to solve our own problems.

For our post's last updated label, we want to create a method that executes the strftime bit so that we can use it in our three views. But where do we put it?

Helpers are generally organized by controller. If you use rails g scaffold or rails g controller, Rails will create a helper for your controller and put it in the app/helpers directory. If we look in our helpers directory, we'll see application_helper.rb, authors_helper.rb, and posts_helper.rb. This matches what we have in our controllers directory โ€” application_controller.rb, authors_controller.rb, and posts_controller.rb.

Let's use Separation of Concerns to decide where to make our helper. Displaying the date of the last time the post was updated isn't an application-wide concern; it's only a post concern. So let's put it in the PostsHelper.

# app/helpers/posts_helper.rb

def last_updated(post)
  post.updated_at.strftime("Last updated %A, %b %e, at %l:%M %p")
end

Why are we passing post as an argument to the helper? If you look back at our three views, you'll see that sometimes we're dealing with an instance variable (@post), and sometimes we're dealing with a local variable (post). Our helper doesn't know anything that we don't directly tell it, so it can't assume a specific variable name. We will need to tell it which post we want it to act on.

Note: Helpers can get a lot more advanced, encapsulating more logic and even HTML. In this case, we just want to get the value and not output any markup โ€” we'll let the view template handle that part. It's not this helper's concern.

Now that we have our helper, let's DRY up those views.

<!-- app/views/posts/show.html.erb -->

<h1><%= @post.title %></h1>
<p><%= last_updated @post %></p>
<p><%= @post.description %></p>
<!-- app/views/posts/index.html.erb -->

<% @posts.each do |post| %>
  <div>
    <%= post.title %> - <%= last_updated post %>
  </div>
<% end %>
<!-- app/views/posts/edit.html.erb -->

<%= form_for(@post) do |f| %>
  <label><%= last_updated @post %></label><br>
  <label>Post title:</label><br>
  <%= f.text_field :title %><br>

  <label>Post Description</label><br>
  <%= f.text_area :description %><br>

  <%= f.submit %>
<% end %>

Not only have we encapsulated the formatting logic into a single method, but notice how much more readable our views are now.

Using Another Controller's Helpers

Our blog also has an author page that lists the posts by that author. What do we do if we want to also show the last updated time on that page? The answer is: the same thing we just did!

<!-- app/views/authors/show.html.erb -->

<h1><%= @author.name %></h1>
<p>Posts:</p>
<% @author.posts.each do |post| %>
  <div><%= post.title %> - <%= last_updated post %></div>
<% end %>

Helpers are organized by controller, but they aren't restricted to a single controller. Just like you can use a Rails link_to helper in any view, you can also use any of your own helpers in any view. Our last_updated helper is still a Post concern (so we know to look for it in the PostsHelper), but if an author page or any other page needs to use it they can. Just like our models, views, and controllers, helpers are separated by concern to help us keep our codebase organized.

Application Helpers

What about things that aren't the concern of a single controller and are applicable to the entire application? For these cases, we have the application_helper. This helper is created automatically with your Rails project and is where you keep helpers that are related to the application itself rather than any given model or controller.

In applications where users can log in, the application helper will often have a method to expose a current_user, something which gets used on almost every view in the system.

For our blog application, we decide we want to dynamically change the <title> of each page based on what the user is looking at. On an author's show page, it might be the author name. On a blog post page, it might be the post's title. Since we already know we're going to be repeating the same basic snippet of code across many views, we're going to jump right to creating a helper.

The page title isn't strictly the concern of a Post or an Author, even though it may use data from either of those things. Since it's a broader concern than just one controller/model, it's a prime candidate for the ApplicationHelper.

# app/helpers/application_helper.rb

def title(text)
  content_for :title, text
end

Note: We use the Rails content_for helper within our custom helper here. Helpers on helpers on helpers! What this will do is send our text to the place in our application layout that is expecting some content for the :title.

<!-- app/views/layouts/application.html.erb -->

<head>
  <title><%= yield :title %></title>
</head>

You can read more about content_for in the Layouts and Rendering RailsGuide.

Now that we have our title helper, we can use it everywhere to change the page title based on the content.

<!-- app/views/authors/show.html.erb -->

<% title @author.name %>

<h1><%= @author.name %></h1>
<p>Posts:</p>
<% @author.posts.each do |post| %>
  <div><%= post.title %> - <%= last_updated post %></div>
<% end %>
<!-- app/views/posts/show.html.erb -->

<% title @post.title %>

<h1><%= @post.title %></h1>
<p><%= last_updated @post %></p>
<p><%= @post.description %></p>

And just like that, we are using an application helper to dynamically set the page title on our post and author pages!

Summary

When we start repeating the same pieces of presentation logic across more than one view, we want to use the Don't Repeat Yourself (DRY) principle as our guide and extract that code into helpers. Helpers are organized by concern in the same way that models, views, and controllers are, so always make sure you're putting your helper code in the appropriate place. If it's code specific to a given model/controller, it belongs in that controller's helper. If it's code that's broadly applicable to the entire application, then it goes in the application_helper.

refactoring-views-with-helpers-reading's People

Contributors

annjohn avatar blake41 avatar curiositypaths avatar dakotalmartinez avatar danielseehausen avatar dependabot[bot] avatar drakeltheryuujin avatar howardbdev avatar ihollander avatar jmburges avatar lizbur10 avatar maxwellbenton avatar rrcobb avatar scottcreynolds avatar veloblank avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

refactoring-views-with-helpers-reading's Issues

In the lesson hierarchy, the names of the second-to-lowest level might never be seen

Flatiron curriculum's hierarchy has 4 levels. From top to bottom, they are:

  • Track (e.g., Full Stack Web Dev. with React)
  • Module (e.g., Intro to Ruby Development)
  • Sub-module (e.g., Intro to Programming)
  • Lesson (e.g., Intro to Tic Tac Toe)

Some Sub-module names (e.g., Full Stack Web Dev. with React > Rails > Refactoring with Helpers and Model Methods) are never seen by the student unless the student looks somewhere obscure, like the browser footer. That's because the left-hand nav truncates them (as it does all levels of the hierarchy), but the lesson-view shows only the top level (Track) and bottom level (Lesson). By that logic, some Module names might also never be seen, but, in practice, that appears to be rare, since Module names are concise.

helpers are bad news

I would prefer we teach view objects instead of helpers.

Helpers are global throughout views, and thus don't really bring in benefits of separations of concerns.

Problems with tests

Hello Learn Team -
The tests for this lab are a bit frustrating.

  1. authors_controller_spec.rb - the get :show does not supply an :id with is needed by the controller.
  2. post_spec.rb - wants to find "My edit" yet during another lab we titlelized the title of posts before saving them.

It would be great if you would fix these issues.
Thanks

spec asks for title as "My edit" but uppercase function makes it "My Edit"

I think this was an incorrect test, although I may be wrong.

  1. form shows an update form that submits content and redirects and prints out params
    Failure/Error: expect(page).to have_content("My edit")
    expected to find text "My edit" in "My Edit Last updated Sunday, Oct 22, at 6:13 PM My post description"

The spec expected "My edit" but the provided program automatically uppercases titles. Probably this was not intended to fail for what I believe is a follow along lab..

Also there are 3 spec stubs that needed to be deleted before I got the green "tests passed".

Authors_helper_spec generated with rails g?

Hello! I am not sure if this is the case, but it looks like someone might have done a rails generation and left in the rails tests. The spec/helpers/authors_heper_spec file has pending tests in it, and it is not mentioned in the lab.

Walk Through Lab Has a Failing Test

After following along and posting the provided code, I was failing one test. Here is the error:

`1) form shows an update form that submits content and redirects and prints out params
Failure/Error: @post.update(params.require(:post))

 ActiveModel::ForbiddenAttributesError:
   ActiveModel::ForbiddenAttributesError
 # ./app/controllers/posts_controller.rb:22:in `update'`

To pass it, I changed the update method in posts_controller.rb to:

def update @post = Post.find(params[:id]) @post.update(params.require(:post).permit!) redirect_to post_path(@post) end

Needs strong params.

In order to get the tests passing you will need to go into post_controller#update and pass in strong params and create a post_params method.

Pending Test Appears To Prevent Student From Passing

There is one pending test in the AuthorsHelper specs that causes the test suite to not be recognized as passing by learn-co. Commenting out this pending spec fixes the issue.

Also, the color option is not set in the spec_helper.rb file, which makes the tests much harder to read.

It can be set like this:

RSpec.configure do |config|

  config.color = true

  ...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.