This example demonstrates how to create a todo list app using the Model, Model List, and View components. The example also includes a custom sync layer that stores the todo items in localStorage
(in browsers that support it).
Disclaimer
Ironically, this example complexifies a very simple application in order to demonstrate concepts you might actually use to simplify a complex application. In reality, this little todo app is so conceptually simple that it probably doesn't need to be broken into discrete components, but its inherent conceptual simplicity makes it convenient for explaining how models, model lists, and views work.
In other words, this example is not meant to be a recommendation for how to implement a todo app; it's meant to be a demonstration of how models and views fit together to define application logic. Also, our lawyers advised us that we were legally required to provide a todo example, so we had no choice in the matter. I swear.
For a slightly more realistic use case demonstrating how the App Framework can make a complex task simpler instead of a simple task more complex, take a look at the GitHub Contributors example.
HTML
First we'll create the HTML shell for our todo app.
<!-- This is the main container and "shell" for the todo app. --> <div id="todo-app"> <label class="todo-label" for="new-todo">What do you want to do today?</label> <input type="text" id="new-todo" class="todo-input" placeholder="buy milk"> <ul id="todo-list"></ul> <div id="todo-stats"></div> </div>
We'll also add some invisible HTML templates to the page, which will be used to render our views later.
By putting this template HTML inside a <script>
element with type="text/x-template"
, we ensure that the browser will ignore it because it doesn't recognize the text/x-template
type. We can then retrieve the contents of the element to get a convenient template string.
This is generally a more maintainable way of embedding templates than storing them in JavaScript strings, but there's no requirement that you use this technique.
<!-- This template HTML will be used to render each todo item. --> <script type="text/x-template" id="todo-item-template"> <div class="todo-view"> <input type="checkbox" class="todo-checkbox" {checked}> <span class="todo-content" tabindex="0">{text}</span> </div> <div class="todo-edit"> <input type="text" class="todo-input" value="{text}"> </div> <a href="#" class="todo-remove" title="Remove this task"> <span class="todo-remove-icon"></span> </a> </script> <!-- This template HTML will be used to render the statistics at the bottom of the todo list. --> <script type="text/x-template" id="todo-stats-template"> <span class="todo-count"> <span class="todo-remaining">{numRemaining}</span> <span class="todo-remaining-label">{remainingLabel}</span> left. </span> <a href="#" class="todo-clear"> Clear <span class="todo-done">{numDone}</span> completed <span class="todo-done-label">{doneLabel}</span> </a> </script>
CSS
Next, some CSS to make the todo list look pretty.
<style> #todo-app { margin: 1em; text-align: center; } #todo-list, #todo-stats { margin: 1em auto; text-align: left; width: 450px; } #todo-list { list-style: none; padding: 0; } #todo-stats, .todo-clear { color: #777; } .todo-clear { float: right; } .todo-done .todo-content { color: #666; text-decoration: line-through; } .todo-edit, .editing .todo-view { display: none; } .editing .todo-edit { display: block; } .todo-input { display: block; font-family: Helvetica, sans-serif; font-size: 20px; line-height: normal; margin: 5px auto 0; padding: 6px; width: 420px; } .todo-item { border-bottom: 1px dotted #cfcfcf; font-size: 20px; padding: 6px; position: relative; } .todo-label { color: #444; font-size: 20px; font-weight: bold; text-align: center; } .todo-remaining { color: #333; font-weight: bold; } .todo-remove { position: absolute; right: 0; top: 12px; } .todo-remove-icon { /* Delete icon courtesy of The Noun Project: http://thenounproject.com/noun/delete/ */ background: url(../assets/app/remove.png) no-repeat; display: block; height: 16px; opacity: 0.6; visibility: hidden; width: 23px; } .todo-remove:hover .todo-remove-icon { opacity: 1.0; } .todo-hover .todo-remove-icon, .todo-remove:focus .todo-remove-icon { visibility: visible; } .editing .todo-remove-icon { visibility: hidden; } </style>
JavaScript
Our todo app will consist of five main parts:
A
TodoModel
class, which extendsY.Model
. Each instance of this class will represent the data for a single todo item.A
TodoList
class, which extendsY.ModelList
. There will be a single instance of this class to contain all theTodoModel
instances in the todo list.A
TodoAppView
class that extendsY.View
and acts as a view controller for the entire app.A
TodoView
class, which extendsY.View
. Each instance of this class will represent the visual content and control the interactions for a single todo item.A
localStorage
-based sync layer that will be used by theTodoModel
andTodoList
classes to save and load todo items usinglocalStorage
.
All of these things will live inside the following YUI instance:
<!-- Include YUI on the page if you haven't already. --> <script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script> <script> YUI().use('event-focus', 'json', 'model', 'model-list', 'view', function (Y) { var TodoAppView, TodoList, TodoModel, TodoView; // ... Add the code from the following sections here! ... }); </script>
TodoModel
The TodoModel
class extends Y.Model
and customizes it to use a localStorage
sync provider (the source for that is in the LocalStorageSync section below) and to provide attributes and methods useful for todo items.
TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], { // This tells the Model to use a localStorage sync provider (which we'll // create below) to save and load information about a todo item. sync: LocalStorageSync('todo'), // This method will toggle the `done` attribute from `true` to `false`, or // vice versa. toggleDone: function () { this.set('done', !this.get('done')).save(); } }, { ATTRS: { // Indicates whether or not this todo item has been completed. done: {value: false}, // Contains the text of the todo item. text: {value: ''} } });
TodoList
The TodoList
class extends Y.ModelList
and customizes it to hold a list of TodoModel
instances, and to provide some convenience methods for getting information about the todo items in the list.
TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], { // This tells the list that it will hold instances of the TodoModel class. model: TodoModel, // This tells the list to use a localStorage sync provider (which we'll // create below) to load the list of todo items. sync: LocalStorageSync('todo'), // Returns an array of all models in this list with the `done` attribute // set to `true`. done: function () { return this.filter(function (model) { return model.get('done'); }); }, // Returns an array of all models in this list with the `done` attribute // set to `false`. remaining: function () { return this.filter(function (model) { return !model.get('done'); }); } });
TodoAppView
The TodoAppView
class extends Y.View
and customizes it to represent the main shell of the application, including the new item input field and the list of todo items.
This class also takes care of initializing a TodoList
instance and creating and rendering a TodoView
instance for each todo item when the list is initially loaded or reset.
TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], { // This is where we attach DOM events for the view. The `events` object is a // mapping of selectors to an object containing one or more events to attach // to the node(s) matching each selector. events: { // Handle <enter> keypresses on the "new todo" input field. '#new-todo': {keypress: 'createTodo'}, // Clear all completed items from the list when the "Clear" link is // clicked. '.todo-clear': {click: 'clearDone'}, // Add and remove hover states on todo items. '.todo-item': { mouseover: 'hoverOn', mouseout : 'hoverOff' } }, // The `template` property is a convenience property for holding a // template for this view. In this case, we'll use it to store the // contents of the #todo-stats-template element, which will serve as the // template for the statistics displayed at the bottom of the list. template: Y.one('#todo-stats-template').getHTML(), // The initializer runs when a TodoAppView instance is created, and gives // us an opportunity to set up the view. initializer: function () { // Create a new TodoList instance to hold the todo items. var list = this.todoList = new TodoList(); // Update the display when a new item is added to the list, or when the // entire list is reset. list.after('add', this.add, this); list.after('reset', this.reset, this); // Re-render the stats in the footer whenever an item is added, removed // or changed, or when the entire list is reset. list.after(['add', 'reset', 'remove', 'todoModel:doneChange'], this.render, this); // Load saved items from localStorage, if available. list.load(); }, // The render function is called whenever a todo item is added, removed, or // changed, thanks to the list event handler we attached in the initializer // above. render: function () { var todoList = this.todoList, stats = this.get('container').one('#todo-stats'), numRemaining, numDone; // If there are no todo items, then clear the stats. if (todoList.isEmpty()) { stats.empty(); return this; } // Figure out how many todo items are completed and how many remain. numDone = todoList.done().length; numRemaining = todoList.remaining().length; // Update the statistics. stats.setHTML(Y.Lang.sub(this.template, { numDone : numDone, numRemaining : numRemaining, doneLabel : numDone === 1 ? 'task' : 'tasks', remainingLabel: numRemaining === 1 ? 'task' : 'tasks' })); // If there are no completed todo items, don't show the "Clear // completed items" link. if (!numDone) { stats.one('.todo-clear').remove(); } return this; }, // -- Event Handlers ------------------------------------------------------- // Creates a new TodoView instance and renders it into the list whenever a // todo item is added to the list. add: function (e) { var view = new TodoView({model: e.model}); this.get('container').one('#todo-list').append( view.render().get('container') ); }, // Removes all finished todo items from the list. clearDone: function (e) { var done = this.todoList.done(); e.preventDefault(); // Remove all finished items from the list, but do it silently so as not // to re-render the app view after each item is removed. this.todoList.remove(done, {silent: true}); // Destroy each removed TodoModel instance. Y.Array.each(done, function (todo) { // Passing {remove: true} to the todo model's `destroy()` method // tells it to delete itself from localStorage as well. todo.destroy({remove: true}); }); // Finally, re-render the app view. this.render(); }, // Creates a new todo item when the enter key is pressed in the new todo // input field. createTodo: function (e) { var inputNode, value; if (e.keyCode === 13) { // enter key inputNode = this.get('inputNode'); value = Y.Lang.trim(inputNode.get('value')); if (!value) { return; } // This tells the list to create a new TodoModel instance with the // specified text and automatically save it to localStorage in a // single step. this.todoList.create({text: value}); inputNode.set('value', ''); } }, // Turns off the hover state on a todo item. hoverOff: function (e) { e.currentTarget.removeClass('todo-hover'); }, // Turns on the hover state on a todo item. hoverOn: function (e) { e.currentTarget.addClass('todo-hover'); }, // Creates and renders views for every todo item in the list when the entire // list is reset. reset: function (e) { var fragment = Y.one(Y.config.doc.createDocumentFragment()); Y.Array.each(e.models, function (model) { var view = new TodoView({model: model}); fragment.append(view.render().get('container')); }); this.get('container').one('#todo-list').setHTML(fragment); } }, { ATTRS: { // The container node is the wrapper for this view. All the view's // events will be delegated from the container. In this case, the // #todo-app node already exists on the page, so we don't need to create // it. container: { valueFn: function () { return '#todo-app'; } }, // This is a custom attribute that we'll use to hold a reference to the // "new todo" input field. inputNode: { valueFn: function () { return Y.one('#new-todo'); } } } });
TodoView
The TodoView
class extends Y.View
and customizes it to represent the content of a single todo item in the list. It also handles DOM events on the item to allow it to be edited and removed from the list.
TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], { // This customizes the HTML used for this view's container node. containerTemplate: '<li class="todo-item"/>', // Delegated DOM events to handle this view's interactions. events: { // Toggle the "done" state of this todo item when the checkbox is // clicked. '.todo-checkbox': {click: 'toggleDone'}, // When the text of this todo item is clicked or focused, switch to edit // mode to allow editing. '.todo-content': { click: 'edit', focus: 'edit' }, // On the edit field, when enter is pressed or the field loses focus, // save the current value and switch out of edit mode. '.todo-input' : { blur : 'save', keypress: 'enter' }, // When the remove icon is clicked, delete this todo item. '.todo-remove': {click: 'remove'} }, // The template property holds the contents of the #todo-item-template // element, which will be used as the HTML template for each todo item. template: Y.one('#todo-item-template').getHTML(), initializer: function () { // The model property is set to a TodoModel instance by TodoAppView when // it instantiates this TodoView. var model = this.get('model'); // Re-render this view when the model changes, and destroy this view // when the model is destroyed. model.after('change', this.render, this); model.after('destroy', function () { this.destroy({remove: true}); }, this); }, render: function () { var container = this.get('container'), model = this.get('model'), done = model.get('done'); container.setHTML(Y.Lang.sub(this.template, { checked: done ? 'checked' : '', text : model.getAsHTML('text') })); container[done ? 'addClass' : 'removeClass']('todo-done'); this.set('inputNode', container.one('.todo-input')); return this; }, // -- Event Handlers ------------------------------------------------------- // Toggles this item into edit mode. edit: function () { this.get('container').addClass('editing'); this.get('inputNode').focus(); }, // When the enter key is pressed, focus the new todo input field. This // causes a blur event on the current edit field, which calls the save() // handler below. enter: function (e) { if (e.keyCode === 13) { // enter key Y.one('#new-todo').focus(); } }, // Removes this item from the list. remove: function (e) { e.preventDefault(); this.constructor.superclass.remove.call(this); this.get('model').destroy({'delete': true}); }, // Toggles this item out of edit mode and saves it. save: function () { this.get('container').removeClass('editing'); this.get('model').set('text', this.get('inputNode').get('value')).save(); }, // Toggles the `done` state on this item's model. toggleDone: function () { this.get('model').toggleDone(); } });
LocalStorageSync
This is a simple factory function that returns a sync()
function that can be used as a sync layer for either a Model or a ModelList instance. The TodoModel
and TodoList
instances above use it to save and load items.
function LocalStorageSync(key) { var localStorage; if (!key) { Y.error('No storage key specified.'); } if (Y.config.win.localStorage) { localStorage = Y.config.win.localStorage; } // Try to retrieve existing data from localStorage, if there is any. // Otherwise, initialize `data` to an empty object. var data = Y.JSON.parse((localStorage && localStorage.getItem(key)) || '{}'); // Delete a model with the specified id. function destroy(id) { var modelHash; if ((modelHash = data[id])) { delete data[id]; save(); } return modelHash; } // Generate a unique id to assign to a newly-created model. function generateId() { var id = '', i = 4; while (i--) { id += (((1 + Math.random()) * 0x10000) | 0) .toString(16).substring(1); } return id; } // Loads a model with the specified id. This method is a little tricky, // since it handles loading for both individual models and for an entire // model list. // // If an id is specified, then it loads a single model. If no id is // specified then it loads an array of all models. This allows the same sync // layer to be used for both the TodoModel and TodoList classes. function get(id) { return id ? data[id] : Y.Object.values(data); } // Saves the entire `data` object to localStorage. function save() { localStorage && localStorage.setItem(key, Y.JSON.stringify(data)); } // Sets the id attribute of the specified model (generating a new id if // necessary), then saves it to localStorage. function set(model) { var hash = model.toJSON(), idAttribute = model.idAttribute; if (!Y.Lang.isValue(hash[idAttribute])) { hash[idAttribute] = generateId(); } data[hash[idAttribute]] = hash; save(); return hash; } // Returns a `sync()` function that can be used with either a Model or a // ModelList instance. return function (action, options, callback) { // `this` refers to the Model or ModelList instance to which this sync // method is attached. var isModel = Y.Model && this instanceof Y.Model; switch (action) { case 'create': // intentional fallthru case 'update': callback(null, set(this)); return; case 'read': callback(null, get(isModel && this.get('id'))); return; case 'delete': callback(null, destroy(isModel && this.get('id'))); return; } }; }
Initializing the App
Finally, all we have to do is instantiate a new TodoAppView
to set everything in motion and bring our todo list into existence.
new TodoAppView();
Complete Example Source
<style> #todo-app { margin: 1em; text-align: center; } #todo-list, #todo-stats { margin: 1em auto; text-align: left; width: 450px; } #todo-list { list-style: none; padding: 0; } #todo-stats, .todo-clear { color: #777; } .todo-clear { float: right; } .todo-done .todo-content { color: #666; text-decoration: line-through; } .todo-edit, .editing .todo-view { display: none; } .editing .todo-edit { display: block; } .todo-input { display: block; font-family: Helvetica, sans-serif; font-size: 20px; line-height: normal; margin: 5px auto 0; padding: 6px; width: 420px; } .todo-item { border-bottom: 1px dotted #cfcfcf; font-size: 20px; padding: 6px; position: relative; } .todo-label { color: #444; font-size: 20px; font-weight: bold; text-align: center; } .todo-remaining { color: #333; font-weight: bold; } .todo-remove { position: absolute; right: 0; top: 12px; } .todo-remove-icon { /* Delete icon courtesy of The Noun Project: http://thenounproject.com/noun/delete/ */ background: url(../assets/app/remove.png) no-repeat; display: block; height: 16px; opacity: 0.6; visibility: hidden; width: 23px; } .todo-remove:hover .todo-remove-icon { opacity: 1.0; } .todo-hover .todo-remove-icon, .todo-remove:focus .todo-remove-icon { visibility: visible; } .editing .todo-remove-icon { visibility: hidden; } </style> <!-- This is the main container and "shell" for the todo app. --> <div id="todo-app"> <label class="todo-label" for="new-todo">What do you want to do today?</label> <input type="text" id="new-todo" class="todo-input" placeholder="buy milk"> <ul id="todo-list"></ul> <div id="todo-stats"></div> </div> <!-- This template HTML will be used to render each todo item. --> <script type="text/x-template" id="todo-item-template"> <div class="todo-view"> <input type="checkbox" class="todo-checkbox" {checked}> <span class="todo-content" tabindex="0">{text}</span> </div> <div class="todo-edit"> <input type="text" class="todo-input" value="{text}"> </div> <a href="#" class="todo-remove" title="Remove this task"> <span class="todo-remove-icon"></span> </a> </script> <!-- This template HTML will be used to render the statistics at the bottom of the todo list. --> <script type="text/x-template" id="todo-stats-template"> <span class="todo-count"> <span class="todo-remaining">{numRemaining}</span> <span class="todo-remaining-label">{remainingLabel}</span> left. </span> <a href="#" class="todo-clear"> Clear <span class="todo-done">{numDone}</span> completed <span class="todo-done-label">{doneLabel}</span> </a> </script> <!-- Include YUI on the page if you haven't already. --> <script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script> <script> YUI().use('event-focus', 'json', 'model', 'model-list', 'view', function (Y) { var TodoAppView, TodoList, TodoModel, TodoView; // -- Model -------------------------------------------------------------------- // The TodoModel class extends Y.Model and customizes it to use a localStorage // sync provider (the source for that is further below) and to provide // attributes and methods useful for todo items. TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], { // This tells the Model to use a localStorage sync provider (which we'll // create below) to save and load information about a todo item. sync: LocalStorageSync('todo'), // This method will toggle the `done` attribute from `true` to `false`, or // vice versa. toggleDone: function () { this.set('done', !this.get('done')).save(); } }, { ATTRS: { // Indicates whether or not this todo item has been completed. done: {value: false}, // Contains the text of the todo item. text: {value: ''} } }); // -- ModelList ---------------------------------------------------------------- // The TodoList class extends Y.ModelList and customizes it to hold a list of // TodoModel instances, and to provide some convenience methods for getting // information about the todo items in the list. TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], { // This tells the list that it will hold instances of the TodoModel class. model: TodoModel, // This tells the list to use a localStorage sync provider (which we'll // create below) to load the list of todo items. sync: LocalStorageSync('todo'), // Returns an array of all models in this list with the `done` attribute // set to `true`. done: function () { return this.filter(function (model) { return model.get('done'); }); }, // Returns an array of all models in this list with the `done` attribute // set to `false`. remaining: function () { return this.filter(function (model) { return !model.get('done'); }); } }); // -- Todo App View ------------------------------------------------------------ // The TodoAppView class extends Y.View and customizes it to represent the // main shell of the application, including the new item input field and the // list of todo items. // // This class also takes care of initializing a TodoList instance and creating // and rendering a TodoView instance for each todo item when the list is // initially loaded or reset. TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], { // This is where we attach DOM events for the view. The `events` object is a // mapping of selectors to an object containing one or more events to attach // to the node(s) matching each selector. events: { // Handle <enter> keypresses on the "new todo" input field. '#new-todo': {keypress: 'createTodo'}, // Clear all completed items from the list when the "Clear" link is // clicked. '.todo-clear': {click: 'clearDone'}, // Add and remove hover states on todo items. '.todo-item': { mouseover: 'hoverOn', mouseout : 'hoverOff' } }, // The `template` property is a convenience property for holding a // template for this view. In this case, we'll use it to store the // contents of the #todo-stats-template element, which will serve as the // template for the statistics displayed at the bottom of the list. template: Y.one('#todo-stats-template').getHTML(), // The initializer runs when a TodoAppView instance is created, and gives // us an opportunity to set up the view. initializer: function () { // Create a new TodoList instance to hold the todo items. var list = this.todoList = new TodoList(); // Update the display when a new item is added to the list, or when the // entire list is reset. list.after('add', this.add, this); list.after('reset', this.reset, this); // Re-render the stats in the footer whenever an item is added, removed // or changed, or when the entire list is reset. list.after(['add', 'reset', 'remove', 'todoModel:doneChange'], this.render, this); // Load saved items from localStorage, if available. list.load(); }, // The render function is called whenever a todo item is added, removed, or // changed, thanks to the list event handler we attached in the initializer // above. render: function () { var todoList = this.todoList, stats = this.get('container').one('#todo-stats'), numRemaining, numDone; // If there are no todo items, then clear the stats. if (todoList.isEmpty()) { stats.empty(); return this; } // Figure out how many todo items are completed and how many remain. numDone = todoList.done().length; numRemaining = todoList.remaining().length; // Update the statistics. stats.setHTML(Y.Lang.sub(this.template, { numDone : numDone, numRemaining : numRemaining, doneLabel : numDone === 1 ? 'task' : 'tasks', remainingLabel: numRemaining === 1 ? 'task' : 'tasks' })); // If there are no completed todo items, don't show the "Clear // completed items" link. if (!numDone) { stats.one('.todo-clear').remove(); } return this; }, // -- Event Handlers ------------------------------------------------------- // Creates a new TodoView instance and renders it into the list whenever a // todo item is added to the list. add: function (e) { var view = new TodoView({model: e.model}); this.get('container').one('#todo-list').append( view.render().get('container') ); }, // Removes all finished todo items from the list. clearDone: function (e) { var done = this.todoList.done(); e.preventDefault(); // Remove all finished items from the list, but do it silently so as not // to re-render the app view after each item is removed. this.todoList.remove(done, {silent: true}); // Destroy each removed TodoModel instance. Y.Array.each(done, function (todo) { // Passing {remove: true} to the todo model's `destroy()` method // tells it to delete itself from localStorage as well. todo.destroy({remove: true}); }); // Finally, re-render the app view. this.render(); }, // Creates a new todo item when the enter key is pressed in the new todo // input field. createTodo: function (e) { var inputNode, value; if (e.keyCode === 13) { // enter key inputNode = this.get('inputNode'); value = Y.Lang.trim(inputNode.get('value')); if (!value) { return; } // This tells the list to create a new TodoModel instance with the // specified text and automatically save it to localStorage in a // single step. this.todoList.create({text: value}); inputNode.set('value', ''); } }, // Turns off the hover state on a todo item. hoverOff: function (e) { e.currentTarget.removeClass('todo-hover'); }, // Turns on the hover state on a todo item. hoverOn: function (e) { e.currentTarget.addClass('todo-hover'); }, // Creates and renders views for every todo item in the list when the entire // list is reset. reset: function (e) { var fragment = Y.one(Y.config.doc.createDocumentFragment()); Y.Array.each(e.models, function (model) { var view = new TodoView({model: model}); fragment.append(view.render().get('container')); }); this.get('container').one('#todo-list').setHTML(fragment); } }, { ATTRS: { // The container node is the wrapper for this view. All the view's // events will be delegated from the container. In this case, the // #todo-app node already exists on the page, so we don't need to create // it. container: { valueFn: function () { return '#todo-app'; } }, // This is a custom attribute that we'll use to hold a reference to the // "new todo" input field. inputNode: { valueFn: function () { return Y.one('#new-todo'); } } } }); // -- Todo item view ----------------------------------------------------------- // The TodoView class extends Y.View and customizes it to represent the content // of a single todo item in the list. It also handles DOM events on the item to // allow it to be edited and removed from the list. TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], { // This customizes the HTML used for this view's container node. containerTemplate: '<li class="todo-item"/>', // Delegated DOM events to handle this view's interactions. events: { // Toggle the "done" state of this todo item when the checkbox is // clicked. '.todo-checkbox': {click: 'toggleDone'}, // When the text of this todo item is clicked or focused, switch to edit // mode to allow editing. '.todo-content': { click: 'edit', focus: 'edit' }, // On the edit field, when enter is pressed or the field loses focus, // save the current value and switch out of edit mode. '.todo-input' : { blur : 'save', keypress: 'enter' }, // When the remove icon is clicked, delete this todo item. '.todo-remove': {click: 'remove'} }, // The template property holds the contents of the #todo-item-template // element, which will be used as the HTML template for each todo item. template: Y.one('#todo-item-template').getHTML(), initializer: function () { // The model property is set to a TodoModel instance by TodoAppView when // it instantiates this TodoView. var model = this.get('model'); // Re-render this view when the model changes, and destroy this view // when the model is destroyed. model.after('change', this.render, this); model.after('destroy', function () { this.destroy({remove: true}); }, this); }, render: function () { var container = this.get('container'), model = this.get('model'), done = model.get('done'); container.setHTML(Y.Lang.sub(this.template, { checked: done ? 'checked' : '', text : model.getAsHTML('text') })); container[done ? 'addClass' : 'removeClass']('todo-done'); this.set('inputNode', container.one('.todo-input')); return this; }, // -- Event Handlers ------------------------------------------------------- // Toggles this item into edit mode. edit: function () { this.get('container').addClass('editing'); this.get('inputNode').focus(); }, // When the enter key is pressed, focus the new todo input field. This // causes a blur event on the current edit field, which calls the save() // handler below. enter: function (e) { if (e.keyCode === 13) { // enter key Y.one('#new-todo').focus(); } }, // Removes this item from the list. remove: function (e) { e.preventDefault(); this.constructor.superclass.remove.call(this); this.get('model').destroy({'delete': true}); }, // Toggles this item out of edit mode and saves it. save: function () { this.get('container').removeClass('editing'); this.get('model').set('text', this.get('inputNode').get('value')).save(); }, // Toggles the `done` state on this item's model. toggleDone: function () { this.get('model').toggleDone(); } }); // -- localStorage Sync Implementation ----------------------------------------- // This is a simple factory function that returns a `sync()` function that can // be used as a sync layer for either a Model or a ModelList instance. The // TodoModel and TodoList instances above use it to save and load items. function LocalStorageSync(key) { var localStorage; if (!key) { Y.error('No storage key specified.'); } if (Y.config.win.localStorage) { localStorage = Y.config.win.localStorage; } // Try to retrieve existing data from localStorage, if there is any. // Otherwise, initialize `data` to an empty object. var data = Y.JSON.parse((localStorage && localStorage.getItem(key)) || '{}'); // Delete a model with the specified id. function destroy(id) { var modelHash; if ((modelHash = data[id])) { delete data[id]; save(); } return modelHash; } // Generate a unique id to assign to a newly-created model. function generateId() { var id = '', i = 4; while (i--) { id += (((1 + Math.random()) * 0x10000) | 0) .toString(16).substring(1); } return id; } // Loads a model with the specified id. This method is a little tricky, // since it handles loading for both individual models and for an entire // model list. // // If an id is specified, then it loads a single model. If no id is // specified then it loads an array of all models. This allows the same sync // layer to be used for both the TodoModel and TodoList classes. function get(id) { return id ? data[id] : Y.Object.values(data); } // Saves the entire `data` object to localStorage. function save() { localStorage && localStorage.setItem(key, Y.JSON.stringify(data)); } // Sets the id attribute of the specified model (generating a new id if // necessary), then saves it to localStorage. function set(model) { var hash = model.toJSON(), idAttribute = model.idAttribute; if (!Y.Lang.isValue(hash[idAttribute])) { hash[idAttribute] = generateId(); } data[hash[idAttribute]] = hash; save(); return hash; } // Returns a `sync()` function that can be used with either a Model or a // ModelList instance. return function (action, options, callback) { // `this` refers to the Model or ModelList instance to which this sync // method is attached. var isModel = Y.Model && this instanceof Y.Model; switch (action) { case 'create': // intentional fallthru case 'update': callback(null, set(this)); return; case 'read': callback(null, get(isModel && this.get('id'))); return; case 'delete': callback(null, destroy(isModel && this.get('id'))); return; } }; } // -- Start your engines! ------------------------------------------------------ // Finally, all we have to do is instantiate a new TodoAppView to set everything // in motion and bring our todo list into existence. new TodoAppView(); }); </script>