If you don’t have your basic Sinatra app set up yet, see my previous post. Otherwise, let’s get ready to dig in and build this app!

Now let’s add some project folders and files to our app via the command line:

mkdir models && touch models/playlist.rb && touch models/pose.rb
mkdir views && mkdir views/playlists
mkdir public && touch public/application.css

Getting into ActiveHash

The data I am using in this application is static, so I’m not going to use a database. Instead, I’m going to store my data in hashes. I could use plain old regular hashes, but I am going to pull in a gem that is also available to Rails. I am doing this so I can get dot notation on my data-storing classes along with some other bells and whistles.

In the Gemfile, add the active_hash gem (check the gem on github for the laterst version):

gem 'active_hash', '~> 2.3.0'

I want to be able to use the Rails titleize method, so I’m pulling in another Rails gem. Add activesupport-inflector to the Gemfile:

gem 'activesupport-inflector', '~> 0.1.0'

So why am I pulling select Rails gems into this Sinatra app? Part of this Sinatra side-project journey for me is also picking apart Rails and learning how to splice in small pieces of it. However, I do feel like pulling in activesupport-inflector is overkill for just titleize. Once I get testing set up in this app, I’ll most likely write my own titleize method and pull the gem back out.

Okay while we’re here in the Gemfile, we’re also going to need a couple of Sinatra gems to help with routes. Add these to the Gemfile:

gem 'sinatra-contrib' # https://sinatrarb.com/contrib/multi_route.html
gem 'emk-sinatra-url-for' # path helpers https://github.com/emk/sinatra-url-for/

And we are going to want gem 'pry' so we can easily look at our data and do debugging, so let’s get that in there. Our Gemfile now looks like:

# Gemfile

source 'https://rubygems.org'

ruby '3.2.2'

gem 'sinatra'
gem 'puma'
gem 'rake'
gem 'rackup'
gem 'active_hash', '~> 2.3.0' # https://github.com/active-hash/active_hash
gem 'activesupport-inflector', '~> 0.1.0' # for Rails `titleize`, etc
gem 'sinatra-contrib' # https://sinatrarb.com/contrib/multi_route.html
gem 'emk-sinatra-url-for' # path helpers https://github.com/emk/sinatra-url-for/

group :development do
  gem 'pry'
end

Save and then bundle.

And then require these gems in the app by adding them to the application.rb file. Our file now looks like:

# application.rb

# Gems
require 'sinatra'
require 'sinatra/multi_route' # from sinatra-contrib gem
require 'sinatra/url_for'
if settings.environment == :development
  require 'pry'
end

# Routes
get '/' do
  "Hello World!"
end

Okey doke, now let’s build out our models. Let’s pretend that there are 5 yoga poses, each named with a number. I’m planning to use real images and real yoga pose names in this project, but for the sake of this blog post, I’m just going to use these numbers and placeholder images. So in the models/pose.rb file, add:

# models/pose.rb

# In Sinatra, we have to require any gem we're using in a file:
require 'active_hash'
require 'active_support/inflector' # for the 'titleize' behavior

class Pose < ActiveHash::Base
  fields :name, :image_file

  # This method allows me to name a pose `:standing_forward_bend` for
  # programming convenience while displaying it as "Standing Forward
  # Bend" in the views:
  def display_name
    name.to_s.titleize
  end

  self.data = [
    { id: 1, name: :one, image_file: 'https://placehold.co/100?text=One'},
    { id: 2, name: :two, image_file: 'https://placehold.co/100?text=Two'},
    { id: 3, name: :three, image_file: 'https://placehold.co/100?text=Three'},
    { id: 4, name: :four, image_file: 'https://placehold.co/100?text=Four'},
    { id: 5, name: :five, image_file: 'https://placehold.co/100?text=Five'}
  ]
end

What’s going on in that class up there? We now have pose data stored in a hash that we can access with that easy breezy dot notation just like we would a regular Ruby object. We can do:

Pose.all # => array with all the hashes
Pose.first.name # => :one
Pose.find(4).display_name # => 'Four'
Pose.find_by(name: :three) # => { id: 3, name: :three, image_file: 'https://placehold.co/100?text=Three'}
Pose.where(id: 3..) # => array with poses 3, 4, and 5

I chose to symbolize these names because I am going to be building out data in my Playlist class that references instance of this Pose class by name. I could absolutely reference them by id like a database would, but as a human, I prefer the readablity of names. In reality, yoga pose names can be long – like “standing forward bend”, so using a symbol like :standing_forward_bend feels more concrete to me and less prone to error (like rogue caps or spaces if I were to find_by(name: "standing forward bend")).

Now let’s get to that Playlist class in the models/playlist.rb file. Let’s pretend we have 2 playlists that we’ve built from those 5 poses we built above:

# models/playlist.rb

require 'active_hash'

class Playlist < ActiveHash::Base
  fields :display_name, :poses

  self.data = [
    { id: 1, display_name: 'One Two One', poses: [
      Pose.find_by(name: :one),
      Pose.find_by(name: :two),
      Pose.find_by(name: :one)
    ]},
    { id: 2, display_name: 'One to Five', poses: [
      Pose.find_by(name: :one),
      Pose.find_by(name: :two),
      Pose.find_by(name: :three),
      Pose.find_by(name: :four),
      Pose.find_by(name: :five)
    ]},
  ]
end

In that class, I’ve built 2 playlists that are consuming the Pose objects. As I said, I’m referencing them by symbolized name. Now in our app we can do things like…

@playlists = Playlist.all

@playlists.each do |playlist|
  playlist.display_name
end

…which looks eerily like what we’d do on a playlists index page, so let’s render this content on an index page!

Now, if we want to have access to this model on our index page (and we do), we’re going to need to add the models to our application.rb file. I add them after the gems and before the routes:

# application.rb

# Gems
require 'sinatra'
require 'sinatra/multi_route' # from sinatra-contrib gem
require 'sinatra/url_for'
if settings.environment == :development
  require 'pry'
end

# Models
require_relative 'models/pose'
require_relative 'models/playlist'

# Routes
get '/' do
  "Hello World!"
end

Routes that render a view file

In addition to adding the models, we’re going to need to add a new route to the application.rb:

get '/playlists' do
  @playlists = Playlist.all
  # yep, you need this weird symbolized string syntax:
  erb :'playlists/index'
end

Let’s take a look at that data before we render it in the view so we know what we have to work with. Put a binding.pry in there like this:

get '/playlists' do
  @playlists = Playlist.all
  binding.pry  # <-- stick it right here
  erb :'playlists/index'
end

Now save the file, start (or restart) your server, and go to http://localhost:4567/playlists.

In the server output, you can see the binding.pry breakpoint. Let’s play around with that @playlists object to see what ActiveHash is giving us:

@playlists.all
@playlists.first
@playlists.first.id
@playlists.first.display_name
@playlists.first.poses
@playlists.first.poses.first
@playlists.first.poses.first.id
@playlists.first.poses.first.display_name

Our hashes are acting just like Ruby objects! Pretty neat.

Okay, get out of pry with exit, then stop the server (cmd + c). Remove the binding.pry from your route. We won’t need this breakpoint anymore.

We do need a view file though, so make an index.erb (not index.html.erb) for your playlists:

<!-- views/playlists/index.erb -->

<h1>Playlists Index</h1>

<ul>
  <% @playlists.each do |playlist| %>
    <li><%= playlist.display_name %></li>
  <% end %>
</ul>

Restart your server and go back to http://localhost:4567/playlists. You should see a bulleted list of playlists. Hooray! (Take note that /playlists is not the same as /playlists/. So if you’re having troble rendering this in the browser, this maybe why. 🤦‍♀️ Check out the Sinatra docs for routes to see how to handle that.)

Well it’s nice that we can see the list of playlists, but it’s kind of boring if we can’t click on each name. So how do we get to a show page for a playlist?

Routing to a show page with an id

As you may guess, back in the application.rb, we need a new route:

# this route takes an :id param, like /playlists/42
get '/playlists/:id' do
  # and we use that param to find our specific playlist
  @playlist = Playlist.find(params[:id])
  erb :'playlists/show'
end

And a new view for the show page:

<!-- views/playlists/show.erb -->

<h1><%= @playlist.display_name %></h1>

<ul>
  <% @playlist.poses.each do |pose| %>
    <li><%= pose.display_name %></li>
  <% end %>
</ul>

Restart your server and go to http://localhost:4567/playlists/2 and you’ll see our glorious show page for playlist “One to Five”. And since none of us cares to memorize id numbers for random internet pages, next, we’ll link to this show page from the playlists index page.

Linking from the index to the show page

In the views/playlists/index.erb file, replace this:

<li><%= playlist.display_name %></li>

with this:

<li><a href='<%= url_for "/playlists/#{playlist.id}" %>'><%= playlist.display_name %></a></li>

Is that link syntax ugly? Ohhhhh yeah it is. I haven’t looked into how to make links much prettier in Sinatra, so for now, this gets the job done.

Add a layout for consistent page design

And lastly, now that we have a few pages in our application, we’re probably going to want a nav bar and we’re probably going to want that on more than one page. We can accomplish this with an HTML layout.

Create a file called layout.erb and save it in your views folder. Fill it with basic HTML boilerplate and place a <%= yield %> where you want your page content to go.

<!-- views/layout.erb -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="robots" content="noindex">
    <link href="<%= url('/application.css') %>" rel="stylesheet" type="text/css" />
    <title>Yoga Pose Playlist</title>
  </head>
  <body>
    <nav>
      <a href="<%= url_for '/' %>">Yoga Pose Playlist</a>
    </nav>

    <!-- Your page content will render here: -->
    <%= yield %>

  </body>
</html>

When you refresh your page, you’ll see your new content.

Now there is no styling on that nav bar. You have a file called application.css where you can put all of the styling you’d like. I’ll leave that up to you.

Our file tree looks like:

yogaposeplaylist/
- models/
  - playlist.rb
  - pose.rb
- public/
  - application.css
- views/
  - playlists/
    - index.erb
    - show.erb
  - layout.erb
- .ruby_version
- application.rb
- config.ru
- Gemfile
- Gemfile.lock
- Procfile
- README.md

But that’s it! You have all of the building blocks of basic app functionality. It’s been interesting for me to see how light and simple Sinatra feels compared to Rails – even though I miss having link helpers and am bothered by the clutter of the application.rb file. But these are preferences built from habit and habit is always worth challenging.

I hope you’ve enjoyed this foray into Sinatra and that whatever project you’re working on that lead you here inspires and challenges you in all of the good ways!