Building Single Page Web Apps with Sinatra: Part 1

Have you ever wanted to learn how to build a single page app with Sinatra and Knockout.js? Well, today is the day you learn! In this first section of a two-part series, we'll review the process fo building a single page to-do application where users can view their tasks, sort them, mark them as complete, delete them, search through them, and add new tasks.


What is Sinatra?

According to their website:

Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort.

Sinatra allows you to do things, like:

This is a route that handles GET requests for "/task/new" and renders an erb form named form.erb. We won't be using Sinatra to render Ruby templates; instead, we'll use it only to send JSON responses to our Knockout.js managed front end (and some utility functions from jQuery like $.ajax). We will be using erb only to render the main HTML file.


What is Knockout?

Knockout is a Model-View-ViewModel (MVVM) JavaScript framework that allows you to keep your models in special "observable" objects. It also keeps your UI up to date, based on those observed objects.

Here is what you'll be building:

We'll get started by defining our model and then our CRUD actions in Sinatra. We'll rely on DataMapper and SQLite for persistent storage, but you can use any ORM that you prefer.

Let's add a task model to the models.rb file:

This task model essentially consists of a few different properties that we want to manipulate in our to-do application.

Next, let's write our Sinatra JSON server. In the app.rb file, we'll start by requiring a few different modules:

The next step is to define some global defaults; in particular, we need a MIME type sent with each of our response headers to specify that every response is JSON.

The before helper function runs before every route match. You can also specify matching routes after before; if you, for instance, wanted to only run JSON responses if the URL ended in ".json", you would use this:

Next, we define our CRUD routes, as well as one route to serve our index.erb file:

So the app.rb file now looks like this:

Each of these routes maps to an action. There is only one view (the "all tasks" view) that houses every action. Remember: in Ruby, the final value returns implicitly. You can explicitly return early, but whatever content these routes return will be the response sent from the server.


Knockout: Models

Next, we start by defining our models in Knockout. In app.js, place the following code:

As you can see, these properties are directly mapped to our model in models.rb. A ko.observable keeps the value updated across the UI when it changes without having to rely on the server or on the DOM to keep track of its state.

Next, we will add a TaskViewModel.

This is the start of what will be the meat of our application. We begin by creating a TaskViewModel constructor function; a new instance of this function is passed to the Knockout applyBindings() function at the end of our file.

Inside our TaskViewModel is an initial call to retrieve tasks from the database, via the "/tasks" url. These are then mapped into the ko.observableArray, which is set to t.tasks. This array is the heart of our application's functionality.

So, now, we have a retrieval function that shows tasks. Let's make a creation function, and then create our actual template view. Add the following code to the TaskViewModel:

Knockout provides a convenient iteration ability...

First, we set newTaskDesc as an observable. This allows us to use an input field easily to type a task description. Next, we define our addTask() function, which adds a task to the observableArray; it calls the saveTask() function, passing in the new task object.

The saveTask() function is agnostic of what kind of save it performs. (Later, we use the saveTask() function to delete tasks or mark them as complete.) An important note here: we rely on a convenience function to grab the current timestamp. This will not be the exact timestamp saved into the database, but it provides some data to drop into the view.

The route is very simple:

It should also be noted that the task's id is not set until the Ajax request completes, as we need to assign it based on the server's response.

Let's create the HTML that our newly created JavaScript controls. A large portion of this file comes from the HTML5 boilerplate index file. This goes into the index.erb file:

Let's take this template and fill in the bindings that Knockout uses to keep the UI in sync. For this part, we cover the creation of To-Do items. In the part two, we will cover more advanced functionality (including searching, sorting, deleting, and marking as complete).

Before we move on, let's give our page a little bit of style. Since this tutorial isn't about CSS, we'll just drop this in and move right along. The following code is inside the HTML5 Boilerplate CSS file, which includes a reset and a few other things.

Add this code to your styles.css file.

Now, let's cover the "new task" form. We will add data-bind attributes to the form to make the Knockout bindings work. The data-bind attribute is how Knockout keeps the UI in sync, and allows for event binding and other important functionality. Replace the "new task" form with the following code.

We'll step through these one by one. First, the form element has a binding for the submit event. When the form is submitted, the addTask() function defined on the TaskViewModel executes. The first input element (which is implicitly of type="text") contains the value of the ko.observable newTaskDesc that we defined earlier. Whatever is in this field when submitting the form becomes the Task's description property.

So we have a way to add tasks, but we need to display those tasks. We also need to add each of the task's properties. Let's iterate over the tasks and add them into the table. Knockout provides a convenient iteration ability to facilitate this; define a comment block with the following syntax:

In Ruby, the final value is returns implicitly.

This uses Knockout's iteration capability. Each task is specifically defined on the TaskViewModel (t.tasks), and it stays in sync across the UI. Each task's ID is added only after we've finished the DB call (as there is no way to ensure that we have the correct ID from the database until it is written), but the interface does not need to reflect inconsistencies like these.

You should now be able to use shotgun app.rb (gem install shotgun) from your working directory and test your app in the browser at http://localhost:9393. (Note: make sure you have gem install'd all of your dependencies/required libraries before you try to run your application.) You should be able to add tasks and see them immediately appear.


Until Part Two

In this tutorial, you learned how to create a JSON interface with Sinatra, and subsequently how to mirror those models in Knockout.js. You also learned how to create bindings to keep our UI in sync with our data. In the next part of this tutorial, we will talk solely about Knockout, and explain how to create sorting, searching, and updating functionality.

Tags:

Comments

Related Articles