Software developer blog

Web app crash course (Part 3)

In the first two parts we have examined how to create web pages using Sinatra and Mustache templates. In both cases we fed a small amount of data through the url, and then we returned something that was based on that data. However having to type input data into a url is not necessarily a good user experience, especially if we wish to send a larger chunk of data. In this part we will learn how to send user inputs properly.

Another problem we haven't addressed yet, is storing that data, so that we can show it later, and show it to other users, but that will be the topic of the 4th and final part of this series.

If you haven't read the previous parts, click here.

Sending data through inputs

Let's first create a form that will let us input some data on our web page. This is going to be standard html, but since I'd like to later add results here as well, we will create a Mustache file with no placeholders for now. Here is my views/chat_view.mustache:

views/chat_view.mustache
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <form method="post">
            <input name="name" type="text" placeholder="Your name"/><input type="submit"/><br/>
            <textarea name="message" placeholder="Your message"></textarea>
        </form>
    </body>
</html>

The view class is really simple this time, since all we need to do is tell Mustache where to look for the template file. This is our views/chat_view.rb:

views/chat_view.rb
require 'mustache'
 
class ChatView < Mustache
  self.template_path = File.dirname(__FILE__)
end

And finally we need to add a path to app.rb, and require the new view:

app.rb
require 'sinatra'
 
require_relative 'views/chat_view'
 
# << I have omitted previously existing code, for clarty >> #
 
get '/chat' do
  ChatView.new.render
end

Now if we start Sinatra with bundle exec ruby app.rb and visit http://localhost:4567/chat we will see a form. If you fill it with data, and submit it, you will receive a message saying that: Sinatra doesn’t know this ditty.

It's time that we learn about the difference between get and post.

Receiving the data

The http protocol has several methods for sending and receiving data. The two most frequently used are get and post. The get method is designed to get a document from the server and display it, while the post method is for sending data. We did send small amount of data to the server in get methods in the url, but in that case we were only specifying which document are we looking for, and the document was generated for us on the fly. Now we would like to send a new document - in this case a message by the user - to the server, so we need to use the post method.

In a post we can also send variables. The form we set up earlier sends a post message with two extra variables name and message. To get hold of them, we need to add a post path to our app.rb:

app.rb
post '/chat' do
  "#{params[:name]} - #{params[:message]}"
end

Now if you restart Sinatra, and submit the form again, the name and the message will be displayed. Once you tried it, let's do another commit, before we improve:

git add views/chat_view.*
git commit -a -m 'Posting chat messages'
git push

Download here.

Rerun

Before we go on, and store the data we have received, let's take a break, and learn about a very useful gem. You might have realized by now, that web development involves a lot of starting and stopping Sinatra. Doing that over and over again, can be annoying. Luckily though there is a gem called rerun that can take that load off our shoulder. To install it run sudo gem install rerun.

Now you can just open up a terminal, go to your project directory and run rerun --pattern='**/*.{rb,css,mustache}' bundle exec ruby app.rb. Rerun will look for the files that match the pattern, and if they change, it will restart sinatra for you.

Storing the result

For simplicity we will just store the messages in memory. This has the disadvantage, that as soon as Sinatra is restarted all messages will get lost. In the next part we will look at some ways to persist the messages on disk.

First we will implement a very simple class that will store the messages. This is the content of my lib/chat_message_store.rb:

lib/chat_message_store.rb
class ChatMessageStore
  attr_reader :messages
 
  def initialize
    @messages = []
  end
 
  def add_message(name, message)
    @messages << { name: name, message: message }
  end
 
  def self.instance
    @instance ||= ChatMessageStore.new
  end
end

Basically the class has a messages array, and we push hashes containing the name and the message onto this array.

Notice the self.instance. This is the first time we are using a structure like that. It defines a singleton method, a method that can be called directly on the class, instead of having to call it on it's instances. (If you are coming from other programming languages: this is essentially the way we define static methods in Ruby.)

We use this to make sure that we have a single copy of ChatMessageStore. Instead of creating a new instance of it every time, we will call the ChatMessageStore.instance method. The implementation of that function takes the @instance member, and if it is empty, it assigns a new instance of ChatMessageStore and returns it. Note that since we are in a singleton methods definition, @instance will be a singleton member. If the @instance member is not empty - i.e. an instance of ChatMessageStore has already been assigned - that existing instance will be returned. This trick is called the singleton pattern.

Now let's change app.rb so that the name and message would be added to the ChatMessageStore, and the ChatView is rendered afterwards:

app.rb
# << Beginning of the file omitted >> #
 
get '/chat' do
  ChatView.new.render
end
 
post '/chat' do
  ChatMessageStore.instance.add_message params[:name], params[:message]
  ChatView.new.render
end

Now we already store the messages, but they still need to be displayed.

Displaying the messages

Let's update our mustache template:

views/chat_view.mustache
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        {{#messages}}
            <p><b>{{{name}}}</b>: {{{message}}}</p>
        {{/messages}}
        <form method="post">
            <input name="name" type="text" placeholder="Your name"/><input type="submit"/><br/>
            <textarea name="message" placeholder="Your message"></textarea>
        </form>
    </body>
</html>

Notice the {{#messages}} and {{/messages}} in the template. This is the only flow control in mustache. If the function messages returns an array, than the code between will be repeated for each element of the array. If messages would return a boolean, than the same structure would act as an if statement, only showing the inner part if the returned value is true.

Finally we also need to update the view, so that we have a messages function that returns the list of messages:

views/chat_view.rb
require 'mustache'
 
require_relative '../lib/chat_message_store'
 
class ChatView < Mustache
  self.template_path = File.dirname(__FILE__)
 
  def messages
    ChatMessageStore.instance.messages
  end
end

Now if you start Sinatra (or if it is still running with rerun) and you visit http://localhost:4567/chat in your browser, you can chat with yourself. After that you can open up the page on another browser tab, and the messages you sent will still be visible, until Sinatra is restarted.

Let's commit what we have done:

git add lib/chat_message_store.rb
git commit -a -m 'Store chat messages in-memory' 
git push

You can download the result here.

Summary

We have come a long way in this 3 parts. If it wasn't for power outages, and for having to restart Sinatra from time to time, so that we can update our application, we would be able to write relatively useful apps by now. Next time we will look at a database technology, that makes it really simple to store and retrieve documents.