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 } 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: } 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="times;" 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
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 '/' end delete '/list/:id' do List.get(params[:id]).destroy redirect '/' end
These should be fairly straightforward – one handler creates a new list based on the parameters given in a from 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="times;"
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 '/' end
This needs to be changed to the following:
post '/:id' do List.get(params[:id]).tasks.create params['task'] redirect '/' 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 }
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.