The starting point of this project is a solution to Helper Methods after completing Parts 1 & 2.
bin/setup
Let's start to make this project look a little nicer. In the application layout:
- Pull in Bootstrap CSS and Font Awesome with our quick-and-dirty CDN links..
- Add a Bootstrap navbar.
- For the
notice
andalert
, switch to using Bootstrap alerts. - Add a Bootstrap
div.container
around theyield
so that all of our templates are rendered within one.
Partial view templates (or just "partials", for short) are an extremely powerful tool to help us modularize and organize our view templates. Especially once we start adding in styling with Bootstrap, etc, our view files will grow to be hundreds or thousands of lines long, so it becomes increasingly helpful to break them up into partials.
Here is the official article in the Rails API reference describing all the ways you can use partials. There are lots of powerful options available, but for now we're going to focus on the most frequently used ones.
Create a partial view template in the same way that you create a regular view template, except that the first letter in the file name must be an underscore. This is how we (and Rails) distinguish partial view templates from full view templates.
For example, create a file called app/views/zebra/_giraffe.html.erb
. Within it, write the following:
<h1>Hello from the giraffe partial!</h1>
Then, in any of your other view templates, e.g. movies/index
, add:
<%= render template: "zebra/giraffe" %>
Notice that we don't include the underscore when referencing the partial in the render
method, even though the underscore must be present in the actual filename.
You can render the partial as many times as you want:
<%= render template: "zebra/giraffe" %>
<hr>
<%= render template: "zebra/giraffe" %>
A more realistic example of putting some static HTML into a partial is extracting a 200 line Bootstrap navbar into app/views/shared/_navbar.html.erb
and then render
ing it from within the application layout. Try doing that now.
Breaking up large templates by putting bits of static HTML into partials is nice, but even better is the ability to dynamically render partials based on varying inputs.
For example, create a file called app/views/zebra/_elephant.html.erb
. Within it, write the following:
<h1>Hello, <%= person %>!</h1>
Then, in movies/index
, try:
<%= render template: "zebra/elephant" %>
When you test it, it will break and complain about an undefined local variable person
. To fix it, try:
<%= render template: "zebra/elephant", locals: { person: "Alice" } %>
Now it becomes more clear why it can be useful to render the same partial multiple times:
<%= render template: "zebra/elephant", locals: { person: "Alice" } %>
<hr>
<%= render template: "zebra/elephant", locals: { person: "Bob" } %>
If we think of rendering partials as calling methods that return HTML, then the :locals
option is how we pass in arguments to those methods. This allows us to create powerful, reusable HTML components.
In this application, can you find any ERB that's re-used in multiple templates?
Well, since we evolved to using form_with model: @movie
, the two forms in movies/new
and movies/edit
are exactly the same!
-
Let's extract the common ERB into a template called
app/views/movies/_form.html.erb
. -
Then render it from both places with:
render template: "movies/form"
If you test it out, you'll notice that it works. However, we're kinda getting lucky here that we named our instance variable the same thing in both actions —— @movie
. Try making the following variable name changes in MoviesController
:
def new
@new_movie = Movie.new # instead of @movie
end
def edit
@the_movie = Movie.find(params.fetch(:id)) # instead of @movie
end
Now if you test it out, you'll get errors complaining about undefined methods for nil
, since the movies/_form
partial expects an instance variable called @movie
and we're no longer providing it.
So, should we always just use the same exact variable name everywhere? That's not very flexible, and sometimes it's just not possible. Instead, we should use the :locals
option:
Update the form
partial to use an arbitrary local variable name, e.g. foo
, rather than @movie
:
<%= form_with model: foo do |form| %>
If you test it out now, you'll get the expected "undefined local variable foo
" error.
But then, update movies/new
:
<%= render template: "movies/form", locals: { foo: @new_movie } %>
And movies/edit
:
<%= render template: "movies/form", locals: { foo: @the_movie } %>
If you test it out, everything should be working again. And, it's much better, because the movies/_form
partial is flexible enough to be called from any template, or multiple times within the same template (e.g. if we wanted to have multiple comment forms on a photos index page).
So, a rule of thumb: don't use instance variables within partials. Instead, prefer to use the :locals
option and pass in any data that the partial requires, even though it's more verbose to do it that way.
Rendering an HTML representation of a record from our database is the most common work we do in a CRUD web app. As you might expect, Rails provides several handy shortcuts for doing this efficiently with partials. Let's experiment!
First, let's improve movies#show
to make use of a Bootstrap card and some Font Awesome icons:
<div class="card">
<div class="card-header">
<%= link_to "Movie ##{@movie.id}", @movie %>
</div>
<div class="card-body">
<dl>
<dt>
Title
</dt>
<dd>
<%= @movie.title %>
</dd>
<dt>
Description
</dt>
<dd>
<%= @movie.description %>
</dd>
</dl>
<div class="row">
<div class="col">
<div class="d-grid">
<%= link_to edit_movie_path(@movie), class: "btn btn-outline-secondary" do %>
<i class="fa-regular fa-pen-to-square"></i>
<% end %>
</div>
</div>
<div class="col">
<div class="d-grid">
<%= link_to @movie, method: :delete, class: "btn btn-outline-secondary" do %>
<i class="fa-regular fa-trash-can"></i>
<% end %>
</div>
</div>
</div>
</div>
<div class="card-footer">
Last updated <%= time_ago_in_words(@movie.updated_at) %> ago
</div>
</div>
Let's add a Bootstrap grid row and cell to movies#show
to constrain the card a bit:
<div class="row">
<div class="col-md-6 offset-md-3">
<!-- code for movie card in here -->
</div>
</div>
Now that we've styled one movie nicely, can we use the same styling on the index page? We could copy-paste the ERB over from movies#show
, but there's a better way:
-
Make a partial called
movies/_movie_card.html.erb
. -
Copy the ERB that represents one movie from
movies#show
into this new partial. -
In the partial, wherever we were referencing the instance variable that was defined by
movies#show
(@movie
), replace with a local variable (let's call itbaz
). -
Render the partial within
movies#show
:<%= render partial: "movies/movie_card", locals: { baz: @movie } %>
-
Re-use the partial in
movies#index
:<% @movies.each do |movie| %> <%= render partial: "movies/movie_card", locals: { baz: movie } %> <% end %>
-
Constrain the size of each card with grid classes:
<div class="row"> <% @movies.each do |movie| %> <div class="col-md-3"> <%= render partial: "movies/movie_card", locals: { baz: movie } %> </div> <% end %> </div>
Neat! Now if we change the appearance of the movie card, or add an attribute, we only have to change it in one place.
In addition to all the other benefits, partials really help you get around your codebase efficiently. For example, if you need to make a change to the navbar, rather than going to the application layout file and digging around, you can use the Jump To File keyboard shortcut (Windows: Ctrl+P, Mac: Cmd+P). Start typing the name of the file — VSCode fuzzily matches what you type and usually finds the right file within a few characters. Hit return and boom you're transported to just where you want to be.
If you're still manually clicking files and folders in the sidebar, start trying to get used to navigating with Jump To File instead.
Read about controller filters. Where have we seen this technique before?
In this application, try using before_action
to DRY up the repetition we see with:
@movie = Movie.find(params.fetch(:id))
being repeated in the show
, edit
, update
, and destroy
actions. In order for this trick to work, we must use the same instance variable name in all four actions. This is a double-edged sword — relying on the same variable name isn't very flexible, but it does allow us to eliminate a lot of repeated code.
Try using the built-in scaffold
generator to spin up another resource, e.g. directors:
rails g scaffold director name dob:date bio:text
Carefully read through all of the code that was generated. Do you understand all of it now? Ask questions about anything that's fuzzy.
Read about the Devise gem. Most professional Rails apps, and from now all of ours, will use the devise
generator to build out sign-in/sign-out RCAVs. (We'll leave the beginner-oriented draft:account
behind.)
-
Add
gem "devise"
to your/Gemfile
andbundle install
. -
rails g devise:install
-
rails g devise user first_name:string last_name:string
-
rails db:migrate
-
Check out the user model. You'll see that Devise automatically adds columns for email and password, among other things.
-
Devise provides the following route helper methods and corresponding RCAVs for free:
- (GET)
new_user_registration_path
: displays a sign up form - (GET)
new_user_session_path
: displays a sign in form - (DELETE)
destroy_user_session_path
: signs the user out - (GET)
edit_user_registration_path
: displays an edit profile form
- (GET)
-
Add links to your navbar to reach the sign up and sign in pages. E.g.:
<%= link_to "Sign up", new_user_registration_path, class: "nav-link" %> <%= link_to "Log in", new_user_session_path, class: "nav-link" %>
-
Sign up for an account.
-
If someone is signed in, conditionally display sign out/edit profile links in the navbar. You can check if someone is signed in with the
user_signed_in?
method, which is defined by Devise and available in all view templates. -
For the edit profile link, make the content of the
<a>
tag the user's first name + last name. You can access the signed in user with thecurrent_user
helper method, which is defined by Devise and available in all actions and all view templates. -
Sign out. (If your sign-out link didn't work, you probably forgot to add
method: :delete
to it.) -
Force someone to be signed in by adding the
before_action :authenticate_user!
method to theApplicationController
. The:authenticate_user!
method is defined by Devise.
You can see my solutions for this project in this pull request.