Multiple Todo Lists

The next evolution in our Todo List.

In this next example, we go a bit more complex with our application. Instead of letting users manage a single todo list, we've set it up so that users can have a list of multiple todo lists each with a title, description, and multiple todos. The user is able to create new todo lists at will, edit that todo list, and remove the todo list. Different views will be presented to the user (create todo list form, view todo list, etc) within a general layout that will contain a list of all of the user's current todo lists.

The point of this example is to show how Falcon allows for multiple views to be created and injected inside one-another (for example, like having a global layout with many different 'pages'). Also we show how Falcon implements an event messaging architecture to help pass messages and listen to events between objects.

Some key things to look out for in this example are the on() method, the trigger() method, and the view binding (in the html tab of the jsFiddle).

Show Comments Hide Comments
  1 // The Todo model hasn't changed
  2 var Todo = Falcon.Model.extend({
  3 	url: 'todo',
  4 	
  5 	observables: {
  6 		'text': '',
  7 		'is_complete': ''
  8 	}
  9 });
 10 
 11 // The Todos collection hasn't changed
 12 var Todos = Falcon.Collection.extend({
 13 	model: Todo
 14 });
 15 
 16 // Here we're adding a TodoList model to represent a specific list 
 17 // of todos that also has a title and description.
 18 var TodoList = Falcon.Model.extend({
 19 	url: 'todo_list',
 20 	
 21 	defaults: {
 22 		// Here we're saying that the 'todos' attribute of a TodoList will be a
 23 		// Todos collection who's parent is this TodoList (hence the 'this' 
 24 		// that's being passed into the constrcutor). A model or collection's
 25 		// parent object is used for generating RESTful urls in the makeUrl 
 26 		// function that will be used in any sync() related method.
 27 		// This concept will be discussed more in the following tutorial
 28 		'todos': function() { return new Todos( this ); }
 29 	},
 30 	
 31 	observables: {
 32 		'title': '',
 33 		'description': ''
 34 	}
 35 });
 36 
 37 var TodoLists = Falcon.Collection.extend({
 38 	model: TodoList 
 39 });
 40 
 41 // Here we're definine a View to display a single todo list, it's title
 42 // and description, and any of it's todos
 43 var TodoListView = Falcon.View.extend({
 44 	url: '#todo_list_tmpl',
 45 	
 46 	// If a function is defined for a specific default, that function is 
 47 	// called with the same arguments passed into the constructor of this 
 48 	// class. Hence, in the example of todo_list, a TodoList is passed into 
 49 	// the TodoListView constrcutor, which then initializes all of the 
 50 	// defaults including the todo_list. Since the todo_list default simply 
 51 	// returns the todo_list that was passed into it, that value is assigned 
 52 	// to the todo_list attribute.  Note: This is the same as writting
 53 	// "this.todo_list = todo_list;" in the 'initialize' method below.
 54 	defaults: {
 55 		'todo_list': function( todo_list ){ return todo_list; }
 56 	},
 57 	
 58 	observables: {
 59 		'new_todo_text': ''
 60 	},
 61 	
 62 	// Just a stub method to show how this class should be constrcuted
 63 	initialize: function(todo_list){},
 64 	
 65 	// Adds a todo to this view's todo list
 66 	addTodo: function()
 67 	{
 68 		var todo = new Todo({ text: this.new_todo_text() });
 69 		this.todo_list.todos.append( todo );
 70 		this.new_todo_text('');
 71 	},
 72 	
 73 	// Removes a todo from this view's todo list
 74 	removeTodo: function(todo)
 75 	{
 76 		this.todo_list.todos.remove( todo );
 77 	},
 78 	
 79 	// Method used to mark a todo as complete
 80 	completeTodo: function(todo)
 81 	{
 82 		todo.set('is_complete', true);
 83 	},
 84 	
 85 	// Method used to trigger an internal 'edit' event.
 86 	editTodoList: function()
 87 	{
 88 		// Trigger an 'edit' event on this view and pass this view's todo list 
 89 		// to anything that's listening for the 'edit' event. This is 100%
 90 		// custom and has no particular special meaning to Falcon. Instead,
 91 		// this can be used for other classes to listen in on activity 
 92 		// happening within this view. This will be demonstrated later on in
 93 		// more depth, but essentially this allows us to define a parent view
 94 		// that can react to an 'edit' event. Here we see the view triggering
 95 		// a custom 'edit' event that will pass this view's todo_list to any
 96 		// other class that's listening for the 'edit' event.
 97 		this.trigger("edit", this.todo_list);
 98 	},
 99 	
100 	// Method used to trigger an internal 'delete' event. Same concept as 
101 	// the edit event.
102 	deleteTodoList: function()
103 	{
104 		this.trigger("delete", this.todo_list);
105 	}
106 });
107 
108 // Here we're defining a NewTodoListView which will be used to show a blank
109 // form that can be used to create a new todo list.
110 var NewTodoListView = Falcon.View.extend({
111 	
112 	// A generic form template for editting and creating todo lists
113 	url: '#todo_list_form_tmpl',
114 	
115 	observables: {
116 		'title': 'Untitled Todo List',
117 		'description': ''
118 	},
119 	
120 	// Method used to trigger a cancel event
121 	cancelSave: function()
122 	{
123 		this.trigger("cancel");
124 	},
125 	
126 	// Method used to save the todo list
127 	saveTodoList: function()
128 	{
129 		//Create the todo list
130 		todo_list = new TodoList({
131 			'title': this.title(),
132 			'description': this.description()
133 		});
134 		
135 		// Trigger a custom save event and pass the new todo list to anything
136 		// that may be listening for events on this view
137 		this.trigger("save", todo_list);
138 	}
139 });
140 
141 // Here we're creating a view that will be used to let us edit a todo list
142 var EditTodoListView = Falcon.View.extend({
143 	//We'll use the same template as the new todo list view
144 	url: '#todo_list_form_tmpl',
145 	
146 	defaults: {
147 		// Again, the todo_list here is the same as what's sent to the
148 		// initialize method
149 		'todo_list': function(todo_list) { return todo_list; }
150 	},
151 	
152 	observables: {
153 		'title': '',
154 		'description': ''
155 	},
156 	
157 	// This time we'll be using the initialize method to set up the intial
158 	// values for a few of our observables defined above (title and description)
159 	initialize: function(todo_list) {
160 		this.title( todo_list.get('title') );
161 		this.description( todo_list.get('description') );
162 	},
163 	
164 	// Method used to trigger a custom 'cancel' event
165 	cancelSave: function()
166 	{
167 		this.trigger("cancel", this.todo_list);
168 	},
169 	
170 	// Method used to save the form data to this view's todo_list
171 	saveTodoList: function()
172 	{
173 		this.todo_list.set({
174 			'title': this.title(),
175 			'description': this.description()
176 		});
177 		
178 		// Trigger a custom 'save' event with the updated todo list as 
179 		// an argument to pass to any listeners
180 		this.trigger("save", this.todo_list );
181 	}
182 });
183 
184 // Here will wrap everything togethor by creating a view for the entire 
185 // layout that can be used to switch between child views (todo list edit 
186 // form, todo list create form, todo list view)
187 // Imagine this as the view that would show your footer and header elements 
188 // but has dynamic content that changes depending what state your application
189 // is in.
190 var LayoutView = Falcon.View.extend({
191 	url: '#layout_tmpl',
192 	
193 	defaults: {
194 		'todo_lists': function() { return new TodoLists }
195 	},
196 	
197 	observables: {
198 		// This observable will hold the currently visible child view.
199 		// Specifically, in this example, this will be either a TodoListView,
200 		// EditTodoListView, or NewTodoListView that were defined above
201 		'current_view': null,
202 		
203 		// This observable will store the currently selected todo list and 
204 		// will only be used to determine the todo list from the 'todo_lists'
205 		// attribute that is currently being viewed. In the template this is
206 		// used to highlight a menu item on the left to show users which todo
207 		// list is being focussed on.
208 		'selected_todo_list': null
209 	},
210 	
211 	// This method is used to select a specific todo list and display 
212 	// a TodoListView
213 	selectTodoList: function(todo_list) {
214 		//Create the view
215 		var view = new TodoListView( todo_list );
216 		
217 		// Attach event listeners for specific events. Remember the
218 		// this.trigger() methods from above? The view.on() method is how 
219 		// Falcon allows for assignment of event listeners to those trigger
220 		// methods. Notice that the callback method has a first 'todo_list'
221 		// argument defined. This is how a listener receives data sent by a
222 		// trigger() event. (so when the TodoListView calls this.trigger("edit",
223 		// todo_list), this view.on listener is called and the todo_list is
224 		// passed into its callback as the first argument). 
225 		//
226 		// Note: the third argument to the view.on() method below is the 
227 		// context in which the callbacks should be called with. This is 
228 		// how we can keep scope into this view's methods. Therefore when 
229 		// we call this.editTodoList(todo_list), we're actually calling
230 		// editTodoList on this LayoutView
231 		view.on("edit", function(todo_list) {
232 			// LayoutView method for showing the EditTodoListView
233 			this.editTodoList(todo_list);
234 		}, this);
235 		
236 		// Listen to delete events
237 		view.on("delete", function(todo_list) {
238 			// Remove the todo_list from the layout's collection of todo_lists
239 			this.todo_lists.remove( todo_list )
240 			
241 			// Deselect the selected todo list
242 			this.selected_todo_list( null );
243 			
244 			// Remove the current view (prompting the user to pick a different
245 			// todo list or make a new one)
246 			this.current_view( null );
247 		}, this);
248 		
249 		// Set up the current view to be displayed and state which todo list
250 		// we're currently viewing
251 		this.current_view( view );
252 		this.selected_todo_list( todo_list );
253 	},
254 	
255 	// Method used to generate a NewTodoListView, attach all necessary events,
256 	// and display the view in the content area
257 	newTodoList: function() {
258 		view = new NewTodoListView();
259 		
260 		// When the save event is triggered from the NewTodoListView we'll
261 		// append the newly created todo list to our todo list collection 
262 		// and we'll show the TodoListView associated with that todo list
263 		view.on("save", function(todo_list) {
264 			
265 			// Add the generated todo_list to the layout's collection of 
266 			// todo lists
267 			this.todo_lists.append( todo_list );
268 			
269 			// Show the TodoListView associated with this todo list
270 			this.selectTodoList( todo_list );
271 		}, this);
272 		
273 		// On a cancel event, we'll just deselect the current view
274 		view.on("cancel", function() {
275 			this.current_view( null );
276 		}, this);
277 		
278 		this.current_view( view );
279 		this.selected_todo_list( null );
280 	},
281 	
282 	// Method used to show an EditTodoListView for a specific todo list
283 	// This will attach all necessary events and set up all observables
284 	// to their correct values.
285 	editTodoList: function(todo_list) {
286 		view = new EditTodoListView( todo_list );
287 		
288 		// On save we'll just show the specific todo list's TodoListView
289 		view.on("save", function() {
290 			this.selectTodoList( todo_list );
291 		}, this);
292 		
293 		// On cancel we'll just show the specific todo list's TodoListView
294 		view.on("cancel", function() {
295 			this.selectTodoList( todo_list );
296 		}, this);
297 		
298 		this.current_view( view );
299 		this.selected_todo_list( todo_list );
300 	},
301 	
302 	// Helper mehtod to determine if a specific todo list is currently selected
303 	// This is used in the template to determine if it should highlight which
304 	// todo list we're currently viewing.
305 	isSelectedList: function(todo_list) {
306 		return ( todo_list.equals(this.selected_todo_list) );
307 	}
308 });
309 
310 //Initialize our app and the initial view
311 view = new LayoutView
312 Falcon.apply(view, "#application");
Application Javascript
 1 <!DOCTYPE html>
 2 <html>
 3 	<head>
 4 		<script src="knockout-3.1.0.js"></script>
 5 		<script src="falcon.min.js"></script>
 6 		<script src="application.js"></script>
 7 	</head>
 8 	<body>
 9 		<div id="application"></div>
10 
11 		<template id="layout_tmpl">
12 			<div id="layout-sidebar">
13 				<button data-bind="click: $view.newTodoList">New Todo List</button>
14 				<ul id="todolist-list" data-bind="foreach: $view.todo_lists">
15 					<li data-bind="click: $view.selectTodoList, css: {'selected': $view.isSelectedList( $data )}">
16 						<strong data-bind="text: title"></strong>
17 						<p data-bind="text: description"></p>
18 					</li>
19 				</ul>
20 			</div>
21 			<div id="content-view">
22 				<!-- ko ifnot: $view.current_view -->
23 					<div class="alert alert-info">
24 						<h4>No List Selected</h4>
25 						<p>Start by selecting a todo list from the menu on the left.</p>
26 					</div>
27 				<!-- /ko -->
28 				<!-- ko if: $view.current_view -->
29 					<!-- ko view: $view.current_view --><!-- /ko -->
30 				<!-- /ko -->
31 			</div>
32 		</template>
33 
34 		<template id="todo_list_form_tmpl">
35 			<form class="form-horizontal" data-bind="submit: $view.saveTodoList">
36 				<div class="control-group">
37 					<label class="control-label">Title</label>
38 					<div class="controls">
39 						<input type="text" class="input-large" data-bind="value: $view.title" />
40 					</div>
41 				</div>
42 				<div class="control-group">
43 					<label class="control-label">Description</label>
44 					<div class="controls">
45 						<textarea data-bind="value: $view.description"></textarea>
46 					</div>
47 				</div>
48 				<div class="form-actions">
49 					<input type="button" class="btn" value="Cancel" data-bind="click: $view.cancelSave" />
50 					<input type="submit" class="btn btn-primary" value="Save" />
51 				</div>
52 			</form>
53 		</template>
54 
55 		<template id="todo_list_tmpl">
56 			<!-- ko with: todo_list -->
57 				<h3>
58 					<!-- ko text: title --><!-- /ko -->
59 					<a data-bind="click: $view.editTodoList">Edit</a>
60 					<a data-bind="click: $view.deleteTodoList">Delete</a>
61 				</h3>
62 				<p data-bind="text: description"></p>
63 				<ul class="todo-list" data-bind="foreach: todos">
64 					<li data-bind="css: {'completed': is_complete}">
65 						<!-- ko text: text --><!-- /ko -->
66 						<a data-bind="click: $view.completeTodo">[Complete]</a>
67 						<a data-bind="click: $view.removeTodo">[X]</a>
68 					</li>
69 				</ul>
70 				<input type="text" data-bind="value: $view.new_todo_text" />
71 				<button data-bind="click: $view.addTodo">Add</button>
72 			<!-- /ko -->
73 		</template>
74 	</body>
75 </html>
Application HTML