When I was first learning Rails in bootcamp, I spent most of my time learning routing with regular views--and they were, uh, simple. At that point, I wasn't familiar with making reusable frontend components, rendering them in a dynamic and nested way.
But all that was before I learned React! Now that I'm returning to frontend Rails work, I've been spending much more time with partial views, or simply partials.
Rails Guides describes partials this way:
Partial templates - usually just called "partials" - are another device for breaking the rendering process into more manageable chunks. With a partial, you can move the code for rendering a particular piece of a response to its own file.
Overview
In this article, we'll create some partials to render art from Magic: the Gathering , queried from the Scryfall API. We'll cover these topics along the way:
- Rails naming conventions for views and partials
- Using
session
to store data from external APIs (not specific to partials, but part of the use case) - Using
render partial:
within views and other partials - Passing variables to partials with
locals:
- Rendering partials repeatedly by iterating through a collection with
collection:
Views and Partials
Rails' convention-over-configuration gives us a lazy option for rendering views: if a controller's method has a view with a matching name, and there's no other render
or redirect_to
invoked, Rails will automatically render the view when that method is called. Or, more eloquently put from Rails Guides with the following code example:
class BooksController < ApplicationController
def index
@books = Book.all
end
end
Note that we don't have explicit render at the end of the index action in accordance with "convention over configuration" principle. The rule is that if you do not explicitly render something at the end of a controller action, Rails will automatically look for the action_name.html.erb template in the controller's view path and render it. So in this case, Rails will render the app/views/books/index.html.erb file.
Partials Love Underscores
Partials, however, are named with underscores at the beginning. This convention gives two advantages: we can differentiate partials from regular views, and it also allows us to drop the underscore when invoking render partial:
in ERB.
We'll explore this more once we have some examples in front of us. :)
Use Case: Querying the Scryfall API for Magic: the Gathering Card Art
I'm a sucker for art from Magic: the Gathering. So, our Rails app will query the Scryfall API for card art (along with identifying names and artists), and render a sampling of 9 images--with a button to refresh and re-query the API.
Rails App Setup
Let's get started by using rails new
to create our app:
$ rails new partial-view-demo
After that, let's add a Pages controller, along with an empty index
method:
$ rails g controller pages index
By default, this will populate our routes.rb
file with a get 'pages/index
and get page/index
route. For our use case, we'll want both Get and Post requests to /
to go to the index
method on our Pages controller:
# /config/routes.rb
Rails.application.routes.draw do
get '/', to: 'pages#index'
post '/', to: 'pages#index'
end
And, because we'll be using RestClient
in our API query, go ahead and add gem 'rest-client
to the Gemfile, and update with Bundler:
$ bundle install
Now, let's test our /pages/index.html.erb
view with our routes to make sure everything's working:
<%# /app/views/pages/index.html.erb %>
<h1>Pages#index</h1>
<p>Find me in app/views/pages/index.html.erb</p>
<h2>Let's add some Magic: the Gathering card art!</h2>
Run rails s
and open up localhost:3000
in your browser:
Perfect! Now let's create some partials as components.
Card Components: _card, _card_image, _card_name, _card_artist
First, we'll add components to render each piece of art on its own card component. These cards will simply have an image, a name, and the artist.
To keep our components organized, create a new /views/pages/cards
directory.
We name partials with underscores at the beginning, so we'll create 4 new files in our new directory: _card.html.erb
, _card_image.html.erb
, _card_name.html.erb
, and _card_artist.html.erb
.
Heres how our views are looking:
We'll fill out these partials with HTML and ERB like any other view once we have some API data to render.
Form Components: _refresh_button, _refresh_counter
We'll also add a /views/pages/forms
directory, where we'll stash a button to refresh our 9 pieces of art, as well as a counter to keep track of how many times we've hit the button. (This will help illustrate how we can pass variables through render partial:
.)
Add two files to our new directory: _refresh_button.html.erb
, and _refresh_counter.html.erb
:
We'll fill these out with our card components shortly.
Controller Methods to Query the API
Before we dive into rendering, here's a quick snapshot of the code used to query the Scryfall API and return 9 random pieces of art:
# /app/controller/pages_controller.rb
class PagesController < ApplicationController
def index
session[:img_array] = session[:img_array] || []
if session[:img_array].empty? || params["button_action"] == "refresh"
session[:img_array] = get_scryfall_images
end
end
private
def get_json(url)
response = RestClient.get(url)
json = JSON.parse(response)
end
def parse_cards(json, img_array)
data_array = json["data"]
data_array.each do |card_hash|
if card_hash["image_uris"]
img_hash = {
"image" => card_hash["image_uris"]["art_crop"],
"name" => card_hash["name"],
"artist" => card_hash["artist"]
}
img_array << img_hash
end
end
if json["next_page"]
json = get_json(json["next_page"])
parse_cards(json, img_array)
end
end
def get_scryfall_images
api_url = "https://api.scryfall.com/cards/search?q="
img_array = []
creature_search_array = ["merfolk", "goblin", "angel", "sliver"]
creature_search_array.each do |creature_str|
search_url = api_url + "t%3Alegend+t%3A" + creature_str
json = get_json(search_url)
parse_cards(json, img_array)
sleep(0.1) # per the API documentation: https://scryfall.com/docs/api
end
img_array.sample(9)
end
end
Here, we're using the session
variable to store an array of img_hash
objects containing the "image" URL, the card's "name", and the "artist" (all as strings).
The API query is set up to look for "legend" card that are also creatures of the type "merfolk", "goblin", "angel", or "sliver"--my favorite creature types! (Sorry, my old beloved elf deck...) Also note that, per the API documentation, a 0.1 second delay is built in-between any searches, for good citizenship.
If we print the contents of session[:img_array]
at the end of the index
method, here's what we have when we re-navigate to localhost:3000
:
# session[:img_array]
[
{"image"=>"https://img.scryfall.com/cards/art_crop/front/b/c/bc4c0d5b-6424-44bd-8445-833e01bb6af4.jpg?1562275603", "name"=>"Tuktuk the Explorer", "artist"=>"Volkan Baǵa"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/9/a/9a8aea2f-1e1d-4e0d-8370-207b6cae76e3.jpg?1562740084", "name"=>"Tiana, Ship's Caretaker", "artist"=>"Eric Deschamps"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/3/7/37ed04d3-cfa1-4778-aea6-b4c2c29e6e0a.jpg?1559959382", "name"=>"Krenko, Tin Street Kingpin", "artist"=>"Mark Behm"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/4/2/4256dcc1-0eee-4385-9a5c-70abb212bf49.jpg?1562397424", "name"=>"Slobad, Goblin Tinkerer", "artist"=>"Kev Walker"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/2/7/27907985-b5f6-4098-ab43-15a0c2bf94d5.jpg?1562728142", "name"=>"Bruna, the Fading Light", "artist"=>"Clint Cearley"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/7/2/722b1e02-2268-4e02-8d09-9b337da2a844.jpg?1562405249", "name"=>"Vial Smasher the Fierce", "artist"=>"Deruchenko Alexander"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/8/b/8bd37a04-87b1-42ad-b3e2-f17cd8998f9d.jpg?1562923246", "name"=>"Sliver Legion", "artist"=>"Ron Spears"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/d/d/dd199a48-5ac8-4ab9-a33c-bbce6f7c9d1b.jpg?1559959197", "name"=>"Zegana, Utopian Speaker", "artist"=>"Slawomir Maniak"},
{"image"=>"https://img.scryfall.com/cards/art_crop/front/d/d/ddb92ef6-0ef8-4b1d-8a45-3064fea23926.jpg?1562854687", "name"=>"Avacyn, Angel of Hope", "artist"=>"Jason Chan"}
]
Cool! We have our array of 9 img_hash
objects, each with a URL, name, and artist. Now let's render them!
Rendering Partials Inside Views and Other Partials
Render a partial inside a view
Back in our /views/pages/index.html.erb
view, we can now use render partial:
to access the partials we created.
Let's start by simply rendering a _card_image
partial with session[:img_array][0]["image"]
supplying the URL:
_card_image.html.erb
<%# /app/views/pages/cards/_card_image.html.erb %>
<%= image_tag(session[:img_array][0]["image"]) %>
index.html.erb
<%# /app/views/pages/index.html.erb %>
<h1>Pages#index</h1>
<p>Find me in app/views/pages/index.html.erb</p>
<h2>Let's add some Magic: the Gathering card art!</h2>
<%= render partial: '/pages/cards/card_image' %>
Note that in render partial:
, we drop the underscore from the beginning of _card_image.html.erb
and simply call it as card_image
(plus its path relative to the views
directory).
Cool! Our partial is rendering with the URL from our img_hash
.
Render a partial inside another partial
Let's make use of our _card.html.erb
partial, and render the _card_image.html.erb
partial inside it. We'll also wrap each partial's contents in a <div>
so we can see the DOM tree more clearly in our inspector:
index.html.erb
<%# /app/views/pages/index.html.erb %>
<div class="card_container">
<%= render partial: '/pages/cards/card' %>
</div>
_card.html.erb
<%# /app/views/pages/cards/_card.html.erb %>
<div class="card">
<h3>This is the _card partial</h3>
<%= render partial: '/pages/cards/card_image' %>
</div>
_card_image.html.erb
<%# /app/views/pages/cards/_card_image.html.erb %>
<div class="card_image">
<h4>This is the _card_image partial</h4>
<%= image_tag(session[:img_array][0]["image"]) %>
</div>
In our browser, with inspector open, we can see that the _card_image
partial is its own <div>
within the _card
partial's <div>
:
This is exactly the nested behavior we expected!
Rendering partials multiple times
We can also use render partial:
to render the same partial multiple times:
index.html.erb
<%# /app/views/pages/index.html.erb %>
<div class="card_container">
<%= render partial: '/pages/cards/card' %>
<%= render partial: '/pages/cards/card' %>
<%= render partial: '/pages/cards/card' %>
</div>
This results in three cards being created as sibling DOM elements:
Passing Variables to Partials with locals:
Instead of accessing our long-winded session[:img_array]
variable repeatedly, we can pass variables directly to partials with the locals:
option inside render partial:
.
In our index.html.erb
, let's change our three render partial:
lines to include a different img_hash
from sessions[:img_array]
in each card:
index.html.erb
<%# /app/views/pages/index.html.erb %>
<div class="card_container">
<%= render partial: '/pages/cards/card', locals: {img_hash: session[:img_array][0]} %>
<%= render partial: '/pages/cards/card', locals: {img_hash: session[:img_array][1]} %>
<%= render partial: '/pages/cards/card', locals: {img_hash: session[:img_array][2]} %>
</div>
Now, each _card
partial now has a different piece of art to render!
Let's go back and build out our _card
partial to render the _card_image
, _card_name
, and _card_artist
partials. Each _card
will also use locals:
to pass the contents of its img_hash
to those partials. (Note that, for ease of reading, we are nesting all these partials inside one <div class="card>
tag.)
_card.html.erb
<%# /app/views/pages/cards/_card.html.erb %>
<div class="card">
<h3>This is the _card partial</h3>
<%= render partial: '/pages/cards/card_image', locals: {image: img_hash["image"]} %>
<%= render partial: '/pages/cards/card_name', locals: {name: img_hash["name"]} %>
<%= render partial: '/pages/cards/card_artist', locals: {artist: img_hash["artist"]} %>
</div>
_card_image.html.erb
<%# /app/views/pages/cards/_card_image.html.erb %>
<%= image_tag(image) %>
_card_name.html.erb
<%# /app/views/pages/cards/_card_name.html.erb %>
<p> Name: <%= name %> </p>
_card_artist.html.erb
<%# /app/views/pages/cards/_card_artist.html.erb %>
<p> Artist: <%= artist %> </p>
Let's check back on localhost:3000
to see if we've got some different cards now:
Perfect! Now, let's try iterating through our whole sessions[:img_array]
to display all 9 cards!
Rendering Partials Repeatedly by Iterating with collection:
We can refactor our card-rendering code in index.html.erb
by adding collection: session[:img_array], as: :img_hash
. This will tell Rails to use the array stored in session[:img_array]
and pass each object aliased as img_hash
to its partial:
index.html.erb
<%# /app/views/pages/index.html.erb %>
<div class="card_container">
<%= render partial: '/pages/cards/card', collection: session[:img_array], as: :img_hash %>
</div>
Now, we should expect 9 different cards to be rendered at localhost:3000
. Since we previously used locals:
to pass our hashes with the alias img_hash
, our other partials should need no changes:
Success!!
Finishing Up: Adding a Form with a Refresh Button and Counter
Okay, you're probably bored of looking at Tuktuk by now--I know I am! So, let's go ahead and add our _refresh_button
and _refresh_counter
partials to our index.html.erb
:
index.html.erb
<%# /app/views/pages/index.html.erb %>
<div class="refresh_form">
<%= render partial: '/pages/forms/refresh_button' %>
<%= render partial: '/pages/forms/refresh_counter', locals: {counter: @refresh_counter} %>
</div>
Since we are passing a @refresh_counter
variable through locals:
, lets go ahead and define that in our Pages controller:
pages_controller.rb
# /app/controllers/pages/page_controller.rb
def index
...
session[:refresh_counter] = session[:refresh_counter] || 0
if params["button_action"] == "refresh"
session[:refresh_counter] += 1
end
@refresh_counter = session[:refresh_counter]
end
Great! Now our session
will keep track of the number of times we've hit the refresh button.
And in our form partials:
_refresh_button.html.erb
<%# /app/views/pages/forms/_refresh_button.html.erb %>
<%= form_for :form_data do |f| %>
<%= f.button "Refresh", name: "button_action", value: "refresh" %>
<% end %>
_refresh_counter.html.erb
<%# /app/views/pages/forms/_refresh_counter.html.erb %>
<p>Refresh counter: <%= counter %> </p>
Now, our page on displays a Refresh button along with the counter's value:
And hitting refresh will update our cards (and increment the counter by one)!
Conclusion
We've covered how to:
- create partial views in Rails
- render them with
render partial:
- pass variables to them with
locals:
- iterate through collections to repeatedly render a partial with
collection:
.
(Hopefully, you've seen some good Magic: the Gathering art along the way too!)
Here's the GitHub repo if you're interested in seeing the code or playing around with it yourself: https://github.com/isalevine/devto-rails-partial-view-demo
Top comments (6)
There is a short form for writing partials. So the following:
Can be written:
You can omit the trailing slash
Since the controller is
pages
rails can infer this but it can only do this for top-level partials.So this wouldn't work
But this would.
You would think you could use a symbol because you can in your controller for when you can call render:
But it cannot be done with partials. I thought you could before but I think I am mistaken.
Also another shortcut instead of this:
You can do this:
These are both excellent pieces of advice, thank you Andrew! I did incorporate the
||=
bit (is there a name for that??) into the most recent refactor, and am on the lookout for other places to use it in my code. I gave you a shoutout at the bottom of my followup article where I used it, let me know if you want me to change it/remove it/link to something else of yours: dev.to/isalevine/using-rails-servi...I'll definitely be referring back to your point about the simpler syntax for partials the next time I'm working with them too! I definitely like to be as overly-verbose and non-shortcut-y as possible at the start, but no question the shortened syntax looks better and more readable. :)
👍👍👍
Also, this is interesting--I was refactoring to shorten-ify-icate the partials syntax, but apparently removing the
partial:
bit screws up how thecollection: foo , as: :bar
syntax.So, this:
led to this:
Any idea why
collection:
isn't iterating throughsession[:img_array]
and repeatedly passing eachimg_hash
without thepartial:
explicitly there?With
collection
, I supposed short form doesn't work since the logic is too difficult.So I guess short form only works for non-collection partials