Todo List with a Backend

Finally, adding some memory to our todo list.

In this final example, we take the previous multiple todo list example, and add a remote server to the mix. Here we'll be loading the view templates and model/collection data from a remote server. For this example, we're actually using a mock javascript server with local storage. If you're curious to see how that works, the javascript can be found here.

In this example we'll show how Falcon loads and caches view templates. We'll also demonstrate how Falcon's makeUrl method on views, models, and collections generate URLs and what it means for a model or a collection have a 'parent' model.

Show Comments Hide Comments
  1 // First we'll begin by configuring the global settings 'baseTemplateUrl' and
  2 // 'baseApiUrl'.
  3 //
  4 // The 'baseTemplateUrl' defines what url should be prepended to all remote view
  5 // template ajax requests. This is useful if you have a specific directory or
  6 // content server that you would like to serve individual templates from.  This
  7 // variable is utilized by a View's 'makeUrl' method.
  8 //
  9 // The 'baseApiUrl' defines what url should be prepended to all remote model and
 10 // collection ajax requests. Like the template url, this is useful for defining a
 11 // base endpoint for an api that Falcon should use to interact with RESTful Server
 12 // APIs
 13 Falcon.baseTemplateUrl = "/template";
 14 Falcon.baseApiUrl = "/api";
 15 
 16 // Overall, the todo model is the same except for the url attribute which now
 17 // points to a remote server resource.  How the url attribute is actually used 
 18 // by Falcon is discussed later in the tutorial
 19 var Todo = Falcon.Model.extend({
 20 	// I didn't have to add the .json to the end of the url attribute but 
 21 	// for the sake of this tutorial, I decided to to show how urls are
 22 	// generated, even with those that have extensions on them.  So just 
 23 	// note that this tutorial would work just the same without the .json
 24 	// extension.  I'll be describing exactly how url's are generated 
 25 	// throughout the tutorial by the 'sync' related methods located within 
 26 	// the tutorial's views (TodoListView, NewTodoListView, EditTodoListView,
 27 	// LayoutView) for models and collections via their 'sync' methods (fetch,
 28 	// create, save, destroy)
 29 	url: 'todo.json',
 30 	
 31 	observables: {
 32 		'text': '',
 33 		'is_complete': ''
 34 	}
 35 });
 36 
 37 // Todos is the same
 38 var Todos = Falcon.Collection.extend({
 39 	model: Todo
 40 });
 41 
 42 // TodoList is the same except for the url with it's new extension
 43 var TodoList = Falcon.Model.extend({
 44 	url: 'todo_list.json',
 45 	
 46 	defaults: {
 47 		'todos': function() { return new Todos( this ); }
 48 	},
 49 	
 50 	observables: {
 51 		'title': '',
 52 		'description': ''
 53 	}
 54 });
 55 
 56 // TodoLists is the same
 57 var TodoLists = Falcon.Collection.extend({
 58 	model: TodoList 
 59 });
 60 
 61 
 62 
 63 // The TodoListView has the same functions as before except now it's url is set
 64 // up to load a template from a remote server rather than a local <template>
 65 // element.  Also, the models and collections in this method will be using
 66 // sync() methods to retrieve, save, and delete data from a remote server.
 67 // The TodoListView is used to display information.
 68 var TodoListView = Falcon.View.extend({
 69 	// Here we changed our url to use a remote resource by getting rid of 
 70 	// the "#" symbol that identifies a local <template> element. Instead,
 71 	// Falcon will make an ajax request to the server to find this template. 
 72 	//
 73 	// In this scenario, the view's 'makeUrl' method will combine the 
 74 	// 'url' attribute with the configurable Falcon.baseTemplateUrl variable.
 75 	// The Falcon.baseTemplateUrl variable is set to '/template' so the url 
 76 	// that will be generated here will be:
 77 	// 
 78 	//   '/template/todo_list.tmpl'.
 79 	//
 80 	// This url will be utilized in an ajax request to load up and cache the
 81 	// view's html template.
 82 	url: 'todo_list.tmpl',
 83 
 84 	defaults: {
 85 		'todo_list': function(todo_list) { return todo_list; }
 86 	},
 87 	
 88 	observables: {
 89 		'new_todo_text': ''
 90 	},
 91 	
 92 	//Stubbed initialize method just to show the input parameters
 93 	initialize: function(todo_list){},
 94 	
 95 	// Adds a new todo to the todo list and saves it to the server
 96 	addTodo: function()
 97 	{
 98 		// Initialize the todo with the todo_list as its 'parent'
 99 		var todo = new Todo({ text: this.new_todo_text() }, this.todo_list);
100 		
101 		// Next we'll create the todo on the server and append it into the
102 		// todo_list's collection of todos (todo_list.todos).
103 		// 
104 		// The create method will send a POST ajax request to the server.
105 		// Because the Todo was created with this view's TodoList as its
106 		// parent, the url that will be generated is:
107 		//
108 		// /api/todo_list/{todo_list_id}/todo.json
109 		//
110 		// Let me disect this URL.
111 		// 
112 		// Firstly, the '/api' part is the value of Falcon.baseApiUrl. This
113 		// variable is similar to Falcon.baseTemplateUrl except that it
114 		// applies to urls generated for Models and Collections.
115 		// 
116 		// Next, the '/todo_list' portion of the url is taken from the TodoList
117 		// model which is the 'parent' of this todo.  TodoList is defined with a
118 		// 'url' of 'todo_list.json'. Falcon's Model 'makeUrl' method takes
119 		// everything before the extension (.json) and considers that to be the
120 		// appropriate piece for the url
121 		//
122 		// Next, the '{todo_list_id}' is set to the id of the current todo list.
123 		// Hence, if the id of the todo list is 1, then {todo_list_id} will 
124 		// take the value of '1'.  The 'id' attribute of a model is generated
125 		// by the server and stored upon a successful response. Here the
126 		// response from the server for a todo model will be a json object 
127 		// that will represent this todo, including it's unique id
128 		//
129 		// Lastly, the '/todo.json' piece is taken directly from the Todo
130 		// Model's url attribute.
131 		this.todo_list.todos.create(todo);
132 		
133 		// Reset the todo text
134 		this.new_todo_text('');
135 	},
136 	
137 	// Method used to remove a todo from a todo list and from a remote server
138 	removeTodo: function(todo)
139 	{
140 		// Here we'll call the todos collection's destroy method to remove the
141 		// specified todo.  The destroy function on a collection  will both
142 		// remove all instances of a model (based on equal ids) after it makes
143 		// a successful DELETE ajax request to the server.  Here the url that
144 		// will be generated during the DELETE ajax request is:
145 		// 
146 		// /api/todo_list/{todo_list_id}/todo/{todo_id}.json
147 		//
148 		// I'll disect this url as well.
149 		//
150 		// The '/api/todo_list/{todo_list_id}' portion is all the same as the
151 		// todos.create() example above.
152 		//
153 		// The '/todo' part is extracted from the Todo model's url attribute
154 		// ('todo.json') and is stripped of the extension. Since we're
155 		// attempting to delete a specific Todo, the Todo's id is appended
156 		// to the url which makes up the '{todo_id}' portion.
157 		//
158 		// Lastly, the .json extension is appended from the second half of the
159 		// Todo model's url attribute.
160 		this.todo_list.todos.destroy( todo );
161 	},
162 	
163 	// Method used to mark a todo as complete
164 	completeTodo: function(todo)
165 	{
166 		todo.set('is_complete', true);
167 		
168 		// Here we're going to save the specific Todo to the server. The save
169 		// method generates a PUT request to the server. Here the url that will
170 		// be generated for the todo is the same as the 'destroy' method above
171 		// because we're affecting a specific todo who's parent is a todo list:
172 		//
173 		//   /api/todo_list/{todo_list_id}/todo/{todo_id}.json
174 		//
175 		todo.save();
176 	},
177 	
178 	// Trigger to signal the start of an edit to a specific todo list
179 	editTodoList: function()
180 	{
181 		this.trigger("edit", this.todo_list);
182 	},
183 	
184 	// Method used to destroy the internal todo list from the server
185 	deleteTodoList: function()
186 	{
187 		// Here we're going to be destroying a specific todo list from the
188 		// server. Once the ajax call is complete, a custom 'delete' event is
189 		// triggered.
190 		//
191 		// In this 'sync' related example the url that will be generated is:
192 		//
193 		//   /api/todo_list/{todo_list_id}.json
194 		//
195 		// Since we're only affecting a specific todo list with no parent model
196 		// or collection the url is generated using the model's 'url' attribute
197 		// (todo_list.json) while injecting the model's specific id into the url
198 		//
199 		// In this method, we pass in a callback method and a context argument
200 		// to the destroy method. Underneath the hood, Falcon realizes that the
201 		// first argument is a function and assigns that function to be called
202 		// when the ajax call is completed (either successful or errornous). The
203 		// resultant model (todo_list) is always passed in as the first argument
204 		// to any of the 'sync' related callbacks (complete, success, error).
205 		// The second argument given to the destroy method is the context in
206 		// which to call any of the sync related methods on.  In this scenario
207 		// it's this view.
208 		this.todo_list.destroy(function(todo_list) {
209 			this.trigger("delete", todo_list);
210 		}, this);
211 	}
212 });
213 
214 
215 
216 // Same New Todo List View as before except with a remote template and actual
217 // model and collection server interactions
218 var NewTodoListView = Falcon.View.extend({
219 	// Updated template url, will generate to: '/template/edit_todo_list.tmpl'
220 	url: 'todo_list_form.tmpl',
221 	
222 	observables: {
223 		'title': 'Untitled Todo List',
224 		'description': ''
225 	},
226 	
227 	// Method to trigger a custom canel event
228 	cancelSave: function()
229 	{
230 		this.trigger("cancel");
231 	},
232 	
233 	// Method used to create a new todo list on the server and trigger a custom
234 	// 'save' event on completion.
235 	saveTodoList: function()
236 	{
237 		// Initialize the todo list
238 		todo_list = new TodoList({
239 			'title': this.title(),
240 			'description': this.description()
241 		});
242 		
243 		// Here we're going to create the TodoList on the server and then, on
244 		// complete, we'll trigger a custom 'save' event.  On this line, the
245 		// create() method creates a POST request to the server and will generate
246 		// a url:
247 		//
248 		//   /api/todo_list.json
249 		//
250 		// Here the url is simply generated from the parentless TodoList's url
251 		// attribute and the Falcon.baseApiUrl variable.
252 		//
253 		// Lastly, this method is set up to call it's callbacks on complete with
254 		// the context of this view (hence the second argument of the create
255 		// method).
256 		todo_list.create(function(todo_list) {        
257 			this.trigger("save", todo_list);
258 		}, this);
259 	}
260 });
261 
262 
263 
264 // Again, the EditTodoListView is the same as previous tutorials except that it
265 // now refers to a remote resource to locate its template and utilizes the sync
266 // methods for collections and models.
267 var EditTodoListView = Falcon.View.extend({
268 	// Remote url for template, generated url: '/template/edit_todo_list.tmpl'
269 	url: 'todo_list_form.tmpl',
270 	
271 	defaults: {
272 		'todo_list': function(todo_list){ return todo_list; }
273 	},
274 	
275 	observables: {
276 		'title': '',
277 		'description': ''
278 	},
279 	
280 	initialize: function(todo_list) {
281 		this.title( todo_list.get('title') );
282 		this.description( todo_list.get('description') );
283 	},
284 	
285 	// Method used to trigger a custom 'cancel' event on the form
286 	cancelSave: function()
287 	{
288 		this.trigger("cancel", this.todo_list);
289 	},
290 	
291 	// Method used to save the todo list to the server and then trigger a custom
292 	// 'save' event.
293 	saveTodoList: function()
294 	{
295 		// Update the todo list's data
296 		this.todo_list.set({
297 			'title': this.title(),
298 			'description': this.description()
299 		});
300 		   
301 		// Save the TodoList to the server and then on complete trigger a custom
302 		// 'save' event.
303 		//
304 		// In this example the save method will send an ajax PUT request to:
305 		//
306 		//   /api/todo_list/{todo_list_id}.json
307 		//
308 		// This url is generated because we're modifying a specific todo list 
309 		// that doesn't have a parent Model or Collection
310 		this.todo_list.save(function(todo_list) {
311 			this.trigger("save", todo_list);
312 		}, this);
313 	}
314 });
315 
316 
317 // Lastly, tie everything together with the layout.  This is the same as the
318 // the LayoutView in previous examples except for the use of remote resources.
319 var LayoutView = Falcon.View.extend({
320 	// Generated url: '/template/layout.tmpl'
321 	url: 'layout.tmpl',
322 	
323 	defaults: {
324 		'todo_lists': function() { return new TodoLists }
325 	},
326 	
327 	observables: {
328 		'current_view': null,
329 		'selected_todo_list': null
330 	},
331 	
332 	// This time we'll initialize the class and fetch any todo lists that have
333 	// been saved to the server since our last visit to the application
334 	initialize: function()
335 	{
336 		// The fetch method on a collection will send an ajax GET request to the
337 		// url generated by the 'makeUrl' method.  The collection will
338 		// automatically be populated by the json response from the server. 
339 		// The url that will be generated is:
340 		//
341 		//   /api/todo_list.json
342 		//
343 		// This is generated from the TodoList url attribute and the
344 		// Falcon.baseApiUrl
345 		this.todo_lists.fetch();
346 	},
347 	
348 	// Method to display a TodoListView in the content area of the screen, listen
349 	// for all related events, and select the currently viewing todo list.  All
350 	// the same as the previous tutorial
351 	selectTodoList: function(todo_list) {
352 		var view = new TodoListView( todo_list );
353 		
354 		view.on("edit", function(todo_list) {
355 			this.editTodoList(todo_list);
356 		}, this );
357 		
358 		view.on("delete", function(todo_list) {
359 			this.todo_lists.remove( todo_list )
360 			this.selected_todo_list( null );
361 			this.current_view( null );
362 		}, this);
363 		
364 		this.current_view( view );
365 		this.selected_todo_list( todo_list );
366 	},
367 	
368 	// Method to display a NewTodoListView in the content area of the screen,
369 	// listen for all related events, and select the currently viewing todo list.
370 	// All the same as the previous tutorial
371 	newTodoList: function() {
372 		view = new NewTodoListView();
373 		
374 		view.on("save", function(todo_list) {
375 			this.todo_lists.append( todo_list );
376 			this.selectTodoList( todo_list );
377 		}, this);
378 		
379 		view.on("cancel", function() {
380 			this.current_view( null );
381 		}, this);
382 		
383 		this.current_view( view );
384 		this.selected_todo_list( null );
385 	},
386 	
387 	// Method to display a EditTodoListView in the content area of the screen,
388 	// listen for all related events, and select the currently viewing todo list.
389 	// All the same as the previous tutorial
390 	editTodoList: function(todo_list) {
391 		view = new EditTodoListView( todo_list );
392 		
393 		view.on("save", function() {
394 			this.selectTodoList( todo_list );
395 		}, this);
396 		
397 		view.on("cancel", function() {
398 			this.selectTodoList( todo_list );
399 		}, this);
400 		
401 		this.current_view( view );
402 		this.selected_todo_list( todo_list );
403 	},
404 	
405 	// Method used determine if a specific list is the one that's currently
406 	// being viewed
407 	isSelectedList: function(todo_list) {
408 		return ( todo_list.equals(this.selected_todo_list) );
409 	}
410 });
411 
412 //Initialize our app and the initial view
413 view = new LayoutView
414 //This method is used to apply the bindings to a specific element (in this example
415 //the div with the id 'application'). If an element isn't given, Falcon will default
416 //to the body element.
417 Falcon.apply(view, "#application");
Application HTML
 1 <!DOCTYPE html>
 2 <html>
 3 	<head>
 4 		<script src="knockout-3.1.0.js"></script>
 5 		<script src="falcon.min.js"></script>
 6 		
 7 		<!--
 8 			Allows us to use ajax methods through jQuery and 
 9 			request templates and data from a server.
10 		-->
11 		<script src="jquery-2.1.0.min.js"></script>
12 		<script src="falcon.jquery_adapter.min.js"></script>
13 
14 		<script src="application.js"></script>
15 	</head>
16 	<body>
17 		<div id="application"></div>
18 	</body>
19 </html>
Application HTML