Just Do It: Learn Sinatra, Part Three

Share this article

In Part 2 of this tutorial, we used DataMapper to save tasks to a database back end and created a web front end that used Sinatra to show, add, delete and complete tasks. In this tutorial we will make it look a bit better and add some extra functionality by letting you create multiple lists of tasks.

Adding Some Style

At the moment our app is functioning fine, but looks a bit clunky. Let’s sort that out by creating a stylesheet. Check that you have the following line of code in the ‘layout.slim’ file:

link rel="stylesheet" media="screen, projection" href="/styles.css"

Now create a file called ‘styles.css’ and save it in the public folder, then add the following lines of CSS:

.completed{
  text-decoration: line-through;
  }

.tasks{
  padding:0;
  list-style:none;
  width:400px;
  }

.task{
  position:relative;
  padding:2px 0 2px 28px;
  border-bottom: dotted 1px #ccc;
}

form.update{
  position:absolute;
  bottom:2px;
  left:0;
  }
form.update input{
  background:white;
  color:white;
  padding:0 2px;
  border:solid 1px gray;
  cursor:pointer;
}

.tasks li.completed form.update input{
  color:#47FD6B;
  }

form.delete{
  display:inline;
  }
  
form.delete input{
  background:none;
  cursor:pointer;
  border:none;
  }

Reload the page and you’ll see that it looks much nicer and much more like an actual list of tasks. You might have also noticed that the styles makes reference to a class of ‘completed’. At the moment we have been showing a task is completed by adding the date it was completed on, but this doesn’t look great, so let’s change the task view so that it adds a class of completed instead. We can then use our stylesheet to make completed tasks look different. Open up ‘task.slim’ and change it to the following:

li.task id=task.id class=(task.completed_at.nil? ? "" : "completed")
  = task.name
  form.update action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="PUT"
    -if task.completed_at.nil?
      input type="submit" value="  " title="Complete Task"
    -else
      input type="submit" value="✓" title="Uncomplete Task"
  form.delete action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="DELETE"
    input type="submit" value="×" title="Delete Task"

The key line here is at the top, which uses the ternary operator to check if the task is completed and add class of completed if it has been. The CSS makes sure that the tasks are crossed out as they are completed. If you try the app out now, it is starting to feel much better and natural – click on the box to mark a task as done and get some visual feedback.

Lists of Tasks

We’re now going to add the ability to add multiple lists of tasks. To do that we will need to create a List class. Lists will contain many tasks. DataMapper deals with this by using associations to show the relationship between lists and tasks. This is done by adding a line at the bottom of each class. The task model uses the belongs_to declaration and the List model uses the has n declaration. In the background, this will add a list_id property to the Task class which is used to keep track of which list a task belongs to, although we shouldn’t have any need to access this property directly. Each class also gains some extra methods, so you can access a lists tasks using List.tasks and access a task’s list using Task.list. Open up main.rb and add the List class as well as modifying the Task class so it looks like the following:

class Task
  include DataMapper::Resource
  property :id,           Serial
  property :name,         String, :required => true
  property :completed_at, DateTime
  belongs_to :list
end


class List
  include DataMapper::Resource
  property :id,           Serial
  property :name,         String, :required => true
  has n, :tasks, :constraint => :destroy  
end

DataMapper.finalize

Since we have created some new models, we need to update the underlying database. This can be done using DataMapper’s auto_migrate! method. Go into a console and open up irb:

$> irb
require './main'
DataMapper.auto_migrate!

Note that this will delete all of the tasks that were already in your database from before.

Adding and Deleting Lists

We now need to create some handlers to deal with the lists. They are very similar to the task handlers from part 2 and are listed here in full, place them at the bottom of main.rb:

post '/new/list' do
  List.create params['list']
  redirect to('/')
end

delete '/list/:id' do
  List.get(params[:id]).destroy
  redirect to('/')
end

These should be fairly straightforward – one handler creates a new list based on the parameters given in a form and the other deletes a list based on the id given in the url. We now need to make a slight change to the form on the index page so it is used for adding lists instead of tasks:

form.new action="/new/list" method="POST"
  input type="text" name="list[name]"
  input type="submit" value="+ List"
ul.lists
  - @lists.each do |list|
    == slim :list, locals: { list: list }

This view contains the instance variable @lists, which represents all of the lists in the database. This doesn’t exist yet, so we need to update the relevant handler, in main.rb find the following handler for the root url:

get '/' do
  @tasks = Task.all
  slim :index
end

And change it to the following:

get '/' do
  @lists = List.all(:order => [:name])
  slim :index
end

This searches for all the lists rather than all the tasks. We will get the tasks from the database on a list by list basis.

The last part of the index view makes reference to a view called ‘list’, so we also need to create this. This will be the view that is used for each list and will display the tasks. Create a new text file and save the following in the views folder as ‘list.slim’:

li.list
  h2= list.name
  form.new action="/#{list.id}" method="POST"
    input type="text" name="task[name]"
  ul.tasks
    - list.tasks.each do |task|
      == slim :task, locals: { task: task }
  form.destroy action="/list/#{list.id}" method="POST"
    input type="hidden" name="_method" value="DELETE"
    input type="submit" value="×"

This is actually very similar to the code that we originally had in the index view. It starts by giving the name of the list and then there is a form that is used to add a task. This is followed by a list of the tasks that uses the same task view from part 2. The last bit of code is a form that is used to access the delete handler so the list can be deleted.

Adding Tasks to Lists

We now just have one small change that we need to make to ensure that tasks are added correctly. Because they belong to a list, we need to make sure that we specify which list the task is added to. This is done by adding the id of the list to the url that creates the task. Notice this line in list.slim:

form.new action="/#{list.id}" method="POST"

This specifies that the action that the form posts to should contain the id of the list that the form is in. Currently, we have a the following handler that deals with adding tasks:

post '/' do
  Task.create params['task']
  redirect to('/')
end

This needs to be changed to the following:

post '/:id' do
  List.get(params[:id]).tasks.create params['task']
  redirect to('/')
end

This handler uses the id of the list that is specified in the url and then finds the list in the database and creates a new task that belongs to that list using the parameters specified in the form.

Some More Style

We’re almost there now, all that is left to do is a bit of styling for the lists. Open up styles.css and add the following lines:

.lists{
  padding:0;
  list-style:none;
  overflow:hidden;
  }
  
.list{
  float: left;
  width:23%;
  margin:0 1%;
  border-top:solid 5px #ccc;
  }

You also need to make sure that you remove the following line from this file (it should be around line 8):

width:400px;

And that’s it! You should now have a fully functioning To Do list app that also looks the part. And to top it all off, main.rb is still weighing in at under 60 lines of code! In the final part of this series, we’re going to look at adding a bit more style using Sass and deploying it to the web using Heroku.

Darren JonesDarren Jones
View Author

Darren loves building web apps and coding in JavaScript, Haskell and Ruby. He is the author of Learn to Code using JavaScript, JavaScript: Novice to Ninja and Jump Start Sinatra.He is also the creator of Nanny State, a tiny alternative to React. He can be found on Twitter @daz4126.

justdoitrubysinatratutorial
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week