DEV Community

Isa Levine
Isa Levine

Posted on • Updated on

Let's Use Rails Partials To Render Art from Magic: the Gathering!

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:

  1. Rails naming conventions for views and partials
  2. Using session to store data from external APIs (not specific to partials, but part of the use case)
  3. Using render partial: within views and other partials
  4. Passing variables to partials with locals:
  5. 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:

screenshot of localhost:3000 showing rails app loading index view

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:

screenshot of vscode window with new partials in the cards directory highlighted

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:

screenshot of vscode window with new partials in the forms directory highlighted

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).

screenshot of rendered card art labeled "Tuktuk the Explorer" by artist Volkan Baǵa

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

screenshot showing rendered card highlighted and developer tools with divs nested in DOM tree

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:

screenshot showing three identical cards being rendered, with bottom one highlighted to show it as a separate sibling div

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:

screenshot showing three different cards being rendered, including names and artists

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:

screenshot showing four different cards being rendered, with the browser cut off but indicating that more are rendered underneath

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:

screenshot showing bottom of the rendered cards, with refresh button and counter highlighted

And hitting refresh will update our cards (and increment the counter by one)!

screenshot showing bottom of the rendered cards with different cards than before, with highlighted refresh counter showing number 1

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

Got any tips or tricks for using Rails partial views? Please feel free to comment and share below! :)

Top comments (6)

Collapse
 
andrewbrown profile image
Andrew Brown 🇨🇦 • Edited

There is a short form for writing partials. So the following:

  <%= render partial: '/pages/forms/refresh_counter', locals: {counter: @refresh_counter} %>
Enter fullscreen mode Exit fullscreen mode

Can be written:

  <%= render  '/pages/forms/refresh_counter', counter: @refresh_counter %>
Enter fullscreen mode Exit fullscreen mode

You can omit the trailing slash

  <%= render  'pages/forms/refresh_counter', counter: @refresh_counter %>
Enter fullscreen mode Exit fullscreen mode

Since the controller is pages rails can infer this but it can only do this for top-level partials.

So this wouldn't work

  <%= render  'forms/refresh_counter', counter: @refresh_counter %>
Enter fullscreen mode Exit fullscreen mode

But this would.

  <%= render 'forms_refresh_counter', counter: @refresh_counter %>
Enter fullscreen mode Exit fullscreen mode

You would think you could use a symbol because you can in your controller for when you can call render:

PagesController < ApplicationController
def show
  render :show
end
Enter fullscreen mode Exit fullscreen mode

But it cannot be done with partials. I thought you could before but I think I am mistaken.

  <%= render :forms_refresh_counter, counter: @refresh_counter %>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
andrewbrown profile image
Andrew Brown 🇨🇦

Also another shortcut instead of this:

session[:img_array] = session[:img_array] || []
Enter fullscreen mode Exit fullscreen mode

You can do this:

session[:img_array] ||= []
Enter fullscreen mode Exit fullscreen mode
Collapse
 
isalevine profile image
Isa Levine • Edited

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. :)

Collapse
 
andrewbrown profile image
Andrew Brown 🇨🇦

I like to be as overly-verbose and non-shortcut-y as possible at the start

👍👍👍

Thread Thread
 
isalevine profile image
Isa Levine • Edited

Also, this is interesting--I was refactoring to shorten-ify-icate the partials syntax, but apparently removing the partial: bit screws up how the collection: foo , as: :bar syntax.

So, this:

<%= render '/pages/cards/card', collection: session[:img_array], as: :img_hash %>

led to this:
screenshot of Rails error, saying :img_hash is undefined

Any idea why collection: isn't iterating through session[:img_array] and repeatedly passing each img_hash without the partial: explicitly there?

Thread Thread
 
andrewbrown profile image
Andrew Brown 🇨🇦

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