Introduction to Sinatra — Sinatra Battleships Project Part 3

In my previous post, we created a very simple Sinatra application to demonstrate how embedded Ruby works, and now we will delve much deeper into Sinatra itself. In my internet travels, I found precious few tutorials on Sinatra for beginners so I hope this guide might help some newbies out. Please refer to my first post in this series for a basic introduction to Sinatra and using Ruby to develop web apps.

You can use gem list sinatra to check if you already have Sinatra installed and the usual gem install sinatra to install it.

(Please note that this guide pulls from the examples provided in the RubyMonstas guide which you should check out as well.)

Routes

As discussed previously, Sinatra is built upon the Ruby Rack framework, which essentially handles the low-level HTTP communication details, which we do not need to worry about here.

All we need to know is that HTTP is the protocol which defines how web servers and web browsers ‘talk’ to each other. There are two primary types of HTTP requests that a browser may make to a web server that are enough to build fairly complex web applications.

The first type of request is a GET request, where the browser says to the web server: please fetch a resource (usually a webpage) for me. The second type is a POST request, where the browser submits information to the server.

Let’s create an ‘app.rb’ file with the following code:

(Note that Sinatra must be required at the top of Ruby files which utilise it as it is not part of the standard Ruby language: require 'sinatra'.)

get "/" do
"Hello, world!"
end

The get Sinatra method clearly corresponds to the GET HTTP request (and post corresponds to POST). These methods, using Rack, abstract away lower-level communication details (such as request status, headers and body) that you would otherwise have to specify in every route. It means that all you have to think is the response you wish to return within the do end block.

Starting a Local Server

In the terminal, in the directory of the app, you can simply execute the program as usual: ruby app.rb. Sinatra will tell you that it has “taken the stage on 4567 for development with backup from WEBrick”.

If you open the webpage ‘http://localhost:4567’, ‘Hello, world!’ should now be displayed.

So how does this work?

WEBrick is a library included in the Ruby standard library. It is a server program. When you launched the Ruby app file, Sinatra created a web server on your own computer based on WEBrick. By navigating to the webpage above, you sent a request to this server, which sent back a response.

The web server is running on your computer (and not out on the internet somewhere) so we must use the special hostname ‘localhost’ in the URL to access it. This tells your computer that the connection is from your computer to that same computer.

In the log messages in terminal, we are also given the information that the port is 4567. This is a numbered communication channel — it’s like tuning in to a specific radio channel.

By default, most web browsers make requests to port number 80, so this is the port that most web servers listen on — it is assumed so this is why you do not normally have to specify a port when using the internet. However, for reasons of security, usually you need special permissions to do this so Sinatra automatically sets up WEBrick to listen on port 4567 for apps that are still in development.

Normally a resource path would be specified after the hostname and port (for example, to read about ‘Coding’ on Wikipedia, after the domain name, the path is ‘/wiki/Coding’ which takes you to the page on Coding) — here, where no path is specified, you will be taken to a default or root webpage, captured by the lone "/" in our app.rb router file.

So, the log messages mean that the Sinatra app has started a local web server using WEBrick on port 4567. By navigating to URL ‘localhost:4567’, our browser has sent a GET request (as we have requested to view a webpage) to a local web server listening on port 4567, at the root resource path. WEBrick sends the request to our Sinatra app. In the app, we have specified that when a GET request is made at the root path, “Hello, world!” should be returned as the response. So this response is sent back to our browser, which is able to render it for us to view.

The get and post Sinatra methods (and there are others) set up routes. The request travels down the file, looking for the first matching route which sends back a response. If no matching route is found, this will trigger the familiar ‘404 Not Found’ message— or in Sinatra’s case, the message: ‘Sinatra doesn’t know this ditty.’

Params

Sinatra makes a hash available to routes called params. If you call inspect on params within a route, you will see that the only key specified to begin with is called “captures” whose value is an empty array — we can ignore this for now.

params is useful as it allows us to capture data in routes in various ways which we can feed into our templates.

The first way is by including a pattern in the route path. Let’s make a new route in our app.rb file as follows:

get "/new_site/:name" do
"Hello, #{params[:name]}!"
end

The colon in the path signifies to Sinatra that the route will accept any string, and that this should be named name in our case.

Now, if you navigate to ‘localhost:4567/new_site/Devlin’, ‘Hello, Devlin!’ should appear. Whatever you enter at the place of the pattern in our route path will be saved in the params hash — the pattern will be the key — in our case, :name, and the data in the place of the pattern in the URL will be the value (saved as a string).

Using binding, the new params key-value pair from the URL can be passed to any ERB object specified in the route too.

get "/new_site/:name" do
ERB.new("<html><h1>Hello, <%= params[:name] %>!</h1></html>".result(binding)
end

This will produce a string containing the above HTML code with the value of params[:name] resolved as a response for the browser, and will show the same message as above: ‘Hello, Devlin!’ (this time as an h1 HTML heading).

Templates in Sinatra

We learned about ERB, Ruby’s built-in templating system in a previous blog post.

Sinatra has built-in support for templating systems like erb to remove the need to spell out the ERB object specifics every time.

The Sinatra method erb abstracts away the creation of the ERB object and calls result(binding) on it (as well as some other things) and accepts a template. It also accepts a hash allowing us to specify various options. The first of which is the first way that we may pass local variables in, by specifying the key :locals and passing this a hash value.

get "/new_site/:name" do
erb "<html><h1>Hello, <%= name %>!</h1></html>", {:locals => { :name => params[:name} }
end

Here, we have passed the erb method a template as the first argument, and a hash as the second. In the template, we wish to use a local variable called name. In the hash second argument, we specify a :locals key which means that Sinatra will make each of the key/value pairs of the corresponding hash available as local variables (‘locals’) in our erb template. In the example above, we have stated that at key :name , the corresponding value is the value of the :name key from the params hash (i.e. ‘Devlin’, if that is what was typed in at the URL). So, the local variable name will resolve to Devlin (matching the symbol :name to the local variable name).

However, neatly, params is of course already a hash with the key name already defined. Therefore, you can just pass params in as the hash:

get "/new_site/:name" do
erb "<html><h1>Hello, <%= name %>!</h1></html>", {:locals => params}
end

Layouts

There will be certain elements that will be included in all or most of your HTML templates. Sinatra enables you to wrap all templates within a layout file by setting the layout option of the hash and using the keyword yield as follows:

get "/new_site/:name" do
erb "<html><h1>Hello, <%= name %>!</h1></html>", {:locals => params, :layout => "<html><body><%= yield %></body></html>"}
end

As you can probably work out, now all the erb templates will be wrapped with the <html> and <body> tags — in other words, the yield above in the layout will be replaced with the rest of the template. In general, yield in Ruby calls a block that was passed to a method.

Templates in Views

So far, we have passed the erb method a string as the template. If instead we pass it a symbol (such as :hello), it will assume that this is part of a filename which it will look for in a directory called ‘Views’ in the root directory of the project.

So we can create the Views directory in our root directory and add two files — ‘hello.erb’ with the code for the HTML template above and ‘layout.erb’ with the layout code. We can then change our app file to:

get "/new_site/:name" do
erb :hello, {:locals => params, :layout => :layout}
end

Even better, the :layout option will actually search for a file called ‘layout’ automatically, so you can simply set this option to true :layout => true . And on top of that, Sinatra will automatically assume the option is true so we don’t need to set it at all!

get "/new_site/:name" do
erb :hello, {:locals => params}
end

If you wish to disregard the default layout for any route or template then simply set the option to false.

Instance variables instead of locals

There is a second way of passing data around by using Ruby instance variables — it is particularly important as this is the option that Rails uses:

get "/new_site/:name" do   @name = params[:name]
erb :hello
end

Here you have created an instance variable in the app file called @name. In your hello.erb template, you will also need to replace the local variable name with the instance variable @name and this should work as before.

It is up to you which approach you take — currently I am using instance variables as this is what I am familiar with in my Battleships game in the terminal.

Sinatra Reloading

A neat additional gem which you can install using gem install sinatra-contrib allows you to see any changes you save in your files instantly on the webpage by reloading it without having to restart the entire Sinatra application. Simply place require 'sinatra/reloader' at the top of your app.rb file. Shotgun at gem install shotgun then running the app using shotgun app.rb also achieves the same thing.

--

--