DEV Community

Budh Ram Gurung
Budh Ram Gurung

Posted on

Simple voting web app in Sinatra

In this tutorial, we are going to build a simple Voting web application using Sinatra.

NOTE: Source code included. See Resources section.

Demo

Prerequisites

  • Enthusiasm and passion to Learn
  • Basic knowledge of HTML and CSS
  • Basic knowledge of Ruby language
  • Basic knowledge of Sinatra

Inspiration for the tutorial

The idea and content of this tutorial has been inspired by the
Rails Guides - create your first voting app in Sinatra.
I am presenting it in my own way with little or updated content.

What is Sinatra

Even though this tutorial assumes that you already have some knowledge of Sinatra, we will go through few components which will be used to build this application.

In a simplest form, Sinatra is a tiny web framework written in Ruby to create Web applications quickly.
Visit sinatrarb.com.

Setup your tools

  • This tutorial assumes you have Ruby preinstalled else consider checking getting started with Ruby to install Ruby in your system.
  • First we need to install the web framework in our system. Run the following command to install Sinatra into the system:
  $ gem install sinatra --no-document
  Successfully installed sinatra-2.1.0
  1 gem installed
Enter fullscreen mode Exit fullscreen mode

NOTE: The flag --no-document will help to speed up the installation to gem by not installing the documentation. If you want documentation of the Sinatra gem too, consider removing it from the above command.

Basic app setup

Let's first create a project folder with name as voting_app.

$ mkdir voting_app
$ cd voting_app
Enter fullscreen mode Exit fullscreen mode

Then, create a file named voting.rb with following content:

require 'sinatra'

get '/' do
  'Hello, voter!'
end
Enter fullscreen mode Exit fullscreen mode

Run your app

Rechecking our location of project which is

$ pwd
/some/user/voting_app/

$ ls
voting.rb
Enter fullscreen mode Exit fullscreen mode

Now, run the app by running the command as:

$ ruby voting.rb
== Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from Thin
Thin web server (v1.7.2 codename Bachmanity)
Maximum connections set to 1024
Listening on localhost:4567, CTRL+C to stop
Enter fullscreen mode Exit fullscreen mode

Now, open the url http://localhost:4567 in your browser to see your app.

You should see a web page with Hello, voter! as it's content.

Code explanation

The way to define route in Sinatra is to write a HTTP method paired with URL-matching pattern. Each route is associated with a block:

get '/' do
  # code
end

post '/' do
  # code
end
Enter fullscreen mode Exit fullscreen mode

Check the Sinatra#routes to get more details.

In the above step, we have defined a GET route for our root URL (/) and inside it we are just return a string.

Simple enough so far.

HTML response

Let's update our implementation above to return HTML content instead of text.

Update the get '/' implementation as:

get '/' do
  '<h1>Hello, voter!</h1>'
end
Enter fullscreen mode Exit fullscreen mode

Now, if you restart the app (Ctrl+C to stop the existing server and then run ruby voting.rb), you will see Hello, voter! in heading H1 format in the browser.

Adding View index

Now, if you are seeing above, then the HTML part might be too tricky to handle if the response we want to send is complex.

One of the way to handle displaying content is through View which is a component or part of web application which handles displaying the content.

To ease of management, let's create a folder with name views and write our content for our root URL (/) into index.erb file.

The content for index.erb file is as follows:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <title>Voting App</title>
  </head>
  <body>
    <h2>What's for dinner?</h2>

    <form action='cast' method='post'>
      <ul>
        <% CHOICES.each do |id, text| %>
          <li>
            <label>
              <input type='radio' name='vote' value='<%= id %>' id='vote_<%= id %>' />
              <%= text %>
            </label>
          </li>
        <% end %>
      </ul>

      <button type='submit'>Cast this vote!</button>
    </form>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

And, add following dummy data for our list of voting options in voting.rb file as:

CHOICES = {
  'HAM' => 'Hamburger',
  'PIZ' => 'Pizza',
  'CUR' => 'Curry',
  'NOO' => 'Noodles'
}

get '/' do
 ...
end
Enter fullscreen mode Exit fullscreen mode

After restarting the server, the web page will now show the following:

Preview 1

Code explanation

You might be wondering how about code is working. Right?

It is quite easy to understand. Just think the whole files including voting.rb or any erb files as part of one system.

Now, we have defined a constant CHOICES which is a global constant and available to all the Ruby files including erb files.

Hence, in the index.erb file we are able to access it.

Considering the following code snippet:

<% CHOICES.each do |id, text| %>
  ...
  <input type='radio' name='vote' value='<%= id %>' id='vote_<%= id %>' />
  <%= text %>
  ...
<% end %>
Enter fullscreen mode Exit fullscreen mode

The ERB tag <% ... %> is used to perform execution of Ruby code. Here it is performing each iteration.
And, tag <%= ... %> is used to execute the Ruby code and replace the tag with evaluated results.

Hence, the above code will generate following HTML content:

<li>
  <label>
    <input type="radio" name="vote" value="HAM" id="vote_HAM">
    Hamburger
  </label>
</li>
<li>
  <label>
    <input type="radio" name="vote" value="PIZ" id="vote_PIZ">
    Pizza
  </label>
</li>
...
Enter fullscreen mode Exit fullscreen mode

See ERB Templates for more.

Adding the CSS

Currently, our page looks ugly. We need to add CSS to make it enough beautiful which makes our eyes little happy.

Sinatra assumes that you should store all your static files like CSS, JS or images under public folder.
See static files for more details.

While including the static file in the view files, you don't add 'public' in the path. Hence, if you store your CSS inside 'public/css/style.css' then while linking the CSS inside view file, you just write '/css/style.css' as path.

Create a folder with name public and then create another folder with name css to store CSS specific files. Then, finally create a CSS file with name style.css. Add following content inside the style.css file to have minimum styling to our existing app.

.container {
  margin: auto;
  max-width: 1100px;
  padding: 0 20px;
}

form {
  background: #eee;
  width: 450px;
  padding: 1rem;
}
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
button {
  margin-top: 10px;
}
Enter fullscreen mode Exit fullscreen mode

Preview 2

Passing data to View

Let's add the main title for the page. Update the index.erb file with following content:

<body class='container'>
  <h1><%= @title %></h1>
  <p>What's for dinner?</p>
Enter fullscreen mode Exit fullscreen mode

And, change the get action as:

get '/' do
  @title = 'Welcome to the Foo Restaurant!'
  erb :index
end
Enter fullscreen mode Exit fullscreen mode

We are able to do this as templates in Sinatra are evaluated within the same context as route handlers like get.
Instance variables set in route handlers are directly accessible by templates.

Preview 3

Ability to cast Vote (POST results)

Let's add an ability to cast vote.

Add following action into voting.rb file:

post '/cast' do
  @title = 'Thanks for casting your vote!'
  @vote  = params['vote']
  erb :cast
end
Enter fullscreen mode Exit fullscreen mode

Also, create a new file cast.erb in the views directory and put following code:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <title>Voting App</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <h1><%= @title %></h1>
    <p>You cast: <%= CHOICES[@vote] %></p>
    <p><a href='/results'>See the results!</a></p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now, restart the server and open https://loclhost:4567 and choose the item for the dinner and click on Cast this vote! button.

Preview 4

Code explanation

In the above we defined a new route post '/cast' to handle any post request who target url is /cast. Notice our index.erb for the same:

<form action='cast' method='post'>
...
</form>
Enter fullscreen mode Exit fullscreen mode

Now, when we submit the form by clicking on the Cast the vote! button, then the form request is process by this post route handler at the server end.

Now, we have set the instance variables @title and @vote whose information can be displayed in the /cast url through cast.erb view template.

Note that @vote is set through params which is of a request object which contains following when cast vote.

{ "vote" => "NOO" }
Enter fullscreen mode Exit fullscreen mode

See Rack::Request for in-depth information on params object.

Notice that this is actually the value of the radio button which we selected while voting.
And, the value NOO is coming as we had set the dummy values as:

CHOICES = {
  'HAM' => 'Hamburger',
  'PIZ' => 'Pizza',
  'CUR' => 'Curry',
  'NOO' => 'Noodles'
}
Enter fullscreen mode Exit fullscreen mode

Common layout

If you notice both index.erb and cast.erb files, you will see some shared code. We can extract the shared code in Sinatra through Layout.

Create a layout file layout.erb under views directory and add the common HTML content from both which
looks as follows:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <title>Voting App</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <h1><%= @title %></h1>
    <%= yield %>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

In general Layout is used to define the HTML structure of the web page.

In Sinatra, we deal with forms which is HTML fragments, not fully structured. Yield means that the layout should wait to the templates to render and then proceed. What happens is that the page is rendering the layout file then it stops when it hits the 'yield' statement and renders the associated template file. When the template is done, it keeps going to render everything else after the 'yield' statement in the layout. Therefore, it is very important to be cautious about choosing the right spot for yielding statement in your layout.

Update the views files

Update the views files index.erb and cast.erb as follows:

  • index.erb file:
  <!-- Remove html, title and body tags -->
  <p>What's for dinner?</p>

  <form action='cast' method='post'>
    <ul>
      <% CHOICES.each do |id, text| %>
        <li>
          <label>
            <input type='radio' name='vote' value='<%= id %>' id='vote_<%= id %>' />
            <%= text %>
          </label>
        </li>
      <% end %>
    </ul>

    <button type='submit'>Cast this vote!</button>
  </form>
Enter fullscreen mode Exit fullscreen mode
  • cast.erb file:
  <!-- Remove html, title and body tags -->
  <p>You cast: <%= CHOICES[@vote] %></p>
  <p><a href='/results'>See the results!</a></p>
Enter fullscreen mode Exit fullscreen mode

Now, if you reload the web app (https://localhost:4567) and cast the vote, you will get the same experience as before.

Add the results route and its view

The final route is the result page (/results) which you can see as a link in /cast page.

Add the following code into voting.rb file:

get '/results' do
  @votes = { 'HAM' => 7, 'PIZ' => 5, 'CUR' => 3 }
  erb :results
end
Enter fullscreen mode Exit fullscreen mode

Here, we are creating dummy data @votes which simulates the number of votes for a particular dish.

Now, create its view file as results.erb under views directory.

<h1>Voting Results</h1>
<table>
  <% CHOICES.each do |id, text| %>
    <tr>
      <th><%= text %></th>
      <td><%= @votes[id] || 0 %>
      <td><%= '#' * (@votes[id] || 0) %></td>
    </tr>
  <% end %>
</table>

<p><a href='/'>Cast more votes!</a></p>
Enter fullscreen mode Exit fullscreen mode

Now, check the results page after restarting the server (Ctrl+c and run the app again). You will see the following similar page:

Preview 5

Code explanation

The @votes is an instance variable which we had used to hold some dummy results at present.

In the results.erb page, @votes[id] has been used to get the count for the particular dish. The code snippet '#' * (@votes[id]) basically print the symbol # as many as vote count.

Persist the results using YAML::Store

So far we have played with static dummy date.

Let's store the voting done and update the count whenever we vote the specific dish.

Add following code at the top of voting.rb file:

require 'yaml/store'
Enter fullscreen mode Exit fullscreen mode

Now, update the post '/cast' and get /results handlers:

post '/cast' do
  @title = 'Thanks for casting your vote!'
  @vote  = params['vote']

  # create a votes.yml file and update the particular votes
  @store = YAML::Store.new 'votes.yml'
  @store.transaction do
    @store['votes'] ||= {}
    @store['votes'][@vote] ||= 0
    @store['votes'][@vote] += 1
  end
  erb :cast
end

get '/results' do
  @title = 'Results so far:'
  @store = YAML::Store.new 'votes.yml'
  @votes = @store.transaction { @store['votes'] }
  erb :results
end
Enter fullscreen mode Exit fullscreen mode

We are using YAML here to easily manage our voting data.

YAML stands for YAML Ain't Markup Language. It is a data serialization language that matches user’s expectations about data. It designed to be human friendly and works perfectly with other programming languages. It is highly useful in manage data.

Finally, the web app will look like as:

Preview 6

There are many areas of improvement here which I leave up to you. This is quite basic and serve as the base for further development.

Resources

Top comments (0)