Just Do It: Learn Sinatra, Part Three

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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://opalab.com Oto Brglez

    Nice one! Thanks for writing it.

    • http://ididitmyway.heroku.com/ Darren Jones

      Cheers mate!

  • Jack

    Hi Daz,

    Thanks again for the tutorials they are really useful.

    I just had a couple of questions I was hoping you could help with:

    1. Why did we start the database in irb? Is this standard practice?
    2. We wiped the database when we added the new lists feature/model. On a live application i’m sure we wouldn’t want to do this so how would we handle this? As a simple example if we had a list of cities but added a new countries model and wanted to associate cities with countries?
    3. I really want to master the Ruby interaction between DataMapper/Slim/Sinatra – are there any resources you can recommend apart from their websites?

    Thanks

    • http://ididitmyway.heroku.com/ Darren Jones

      Hi Jack,

      Thanks for your comment. Here are some answers to your questions:

      1. We didn’t actually start the database in irb (sqlite is file based so doesn’t need ‘starting’ like server based databases), we only used irb for the migrations. You can put the migration code into main.rb if you want, but I just prefer to keep it separate as it shouldn’t happen all that often.

      2. You can use the auto_upgrade! method instead of auto_migrate! this will keep the current data that is already in your database intact. Be aware though that this could cause problems because the tasks that were already in your database would not have a list_id set, so you’d have to go and manually update this data retrospectively. I often use auto_upgrade! even in development to avoid having to enter data repeatedly, but auto_migrate! is also useful as a way of wiping the database in one swoop.

      3. There is a good book by Chang Sau Sheong that builds web apps with Sinatra and DataMapper, but uses Haml instead of slim (easy enough to switch between those though) – http://www.amazon.co.uk/Cloning-Internet-Applications-Chang-Sheong/dp/1849511063/ref=sr_1_2?s=books&ie=UTF8&qid=1315550188&sr=1-2. There’s also a Sinatra Up and Running book being published by O’Reily soon that – http://www.amazon.co.uk/Sinatra-Up-Running-Alan-Harris/dp/1449304230/ref=sr_1_1?s=books&ie=UTF8&qid=1315550188&sr=1-1. I’ve written more stuff on my I Did It My Way blog that might be worth checking out. I’d also recommend that you subscribe to the mailing lists and hang out on the irc channels for each of those projects because they are full of interesting information and ideas.

      Hope that helps, let me know if you need any more info.

      DAZ

      • Jack

        Great! I think I have enough to get started on this. All the best

  • baiki

    Just sweet :-)