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).
-
on() - Method used to listen to a specific event being fired on an object. For example, if a view triggered a custom 'edit' event, a different view could listen for the 'edit' event by using the 'on' method like so:
view.on('edit', function(){ ... callback ... })
The first argument of the 'on' method is that event that you would like to listen for and the second argument is the callback method that should be triggered whenever a specific event is encountered. An optional third argument can be passed to this method which defined the context (the value of 'this') in which the callback method should be fire with. - trigger() - Method used to send a custom event to any listeners (who used the 'on' method described above). This method takes N number of arguments, where the first argument is the event that we would like to trigger and any additional arguments are variables that should be passed into any of the listener's callback methods.
- view binding - The view binding is a new knockout binding added by Falcon that allows us to inject a view within another view and is core to building diverse applications. This binding takes in a Falcon View or a Knockout observable and injects the rendered view within the current view's rendered template. If an observable is given (like in our example), the child view will dynamically change whenever the observables is updated with a new view. An example of how this is used can be seen in the example's html tab within the '#layout_template' element. This binding is useful for dynamic apps that have common widgets spread throughout them or with apps that have multiple hierarchies of views (such as with a todo list manager that has a layout with multiple pages and edit forms).
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");
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>