PageRenderTime 52ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/public/javascripts/todos.js

https://github.com/hackingbeauty/base-node-heroku-app
JavaScript | 418 lines | 285 code | 74 blank | 59 comment | 33 complexity | 03d7b4d727cf7647b12ef9c892fab843 MD5 | raw file
  1. $(function ($, _, Backbone, io) {
  2. "use strict";
  3. var Todo, TodoList, Todos, TodoView, AppView, App, socket;
  4. socket = io.connect();
  5. // Todo Model
  6. // ----------
  7. // Our basic **Todo** model has `title`, `order`, and `done` attributes.
  8. Todo = Backbone.Model.extend({
  9. // MongoDB uses _id as default primary key
  10. idAttribute: "_id",
  11. noIoBind: false,
  12. socket: socket,
  13. url: function () {
  14. return "/todo" + ((this.id) ? '/' + this.id : '');
  15. },
  16. // Default attributes for the todo item.
  17. defaults: function () {
  18. return {
  19. title: "empty todo...",
  20. order: Todos.nextOrder(),
  21. done: false
  22. };
  23. },
  24. // Ensure that each todo created has `title`.
  25. initialize: function () {
  26. if (!this.get("title")) {
  27. this.set({"title": this.defaults.title});
  28. }
  29. this.on('serverChange', this.serverChange, this);
  30. this.on('serverDelete', this.serverDelete, this);
  31. this.on('modelCleanup', this.modelCleanup, this);
  32. if (!this.noIoBind) {
  33. this.ioBind('update', this.serverChange, this);
  34. this.ioBind('delete', this.serverDelete, this);
  35. this.ioBind('lock', this.serverLock, this);
  36. this.ioBind('unlock', this.serverUnlock, this);
  37. }
  38. },
  39. // Toggle the `done` state of this todo item.
  40. toggle: function () {
  41. this.save({done: !this.get("done")});
  42. },
  43. // Remove this Todo and delete its view.
  44. clear: function (options) {
  45. this.destroy(options);
  46. this.modelCleanup();
  47. },
  48. serverChange: function (data) {
  49. data.fromServer = true;
  50. this.set(data);
  51. },
  52. serverDelete: function (data) {
  53. if (typeof this.collection === 'object') {
  54. this.collection.remove(this);
  55. } else {
  56. this.trigger('remove', this);
  57. }
  58. },
  59. serverLock: function (success) {
  60. if (success) {
  61. this.locked = true;
  62. //this.trigger('lock', this);
  63. }
  64. },
  65. serverUnlock: function (success) {
  66. if (success) {
  67. this.locked = false;
  68. }
  69. },
  70. modelCleanup: function () {
  71. this.ioUnbindAll();
  72. return this;
  73. },
  74. locked: false,
  75. lock: function (options) {
  76. if (!this._locked) {
  77. options = options ? _.clone(options) : {};
  78. var model = this
  79. , success = options.success;
  80. options.success = function (resp, status, xhr) {
  81. model.locked = true;
  82. if (success) {
  83. success(model, resp);
  84. } else {
  85. model.trigger('lock', model, resp, options);
  86. }
  87. };
  88. options.error = Backbone.wrapError(options.error, model, options);
  89. return (this.sync || Backbone.sync).call(this, 'lock', this, options);
  90. }
  91. },
  92. unlock: function (options) {
  93. if (this.locked) {
  94. options = options ? _.clone(options) : {};
  95. var model = this
  96. , success = options.success;
  97. options.success = function (resp, status, xhr) {
  98. model._locked = false;
  99. if (success) {
  100. success(model, resp);
  101. } else {
  102. model.trigger('unlock', model, resp, options);
  103. }
  104. };
  105. options.error = Backbone.wrapError(options.error, model, options);
  106. return (this.sync || Backbone.sync).call(this, 'unlock', this, options);
  107. }
  108. }
  109. });
  110. // Todo Collection
  111. // ---------------
  112. TodoList = Backbone.Collection.extend({
  113. // Reference to this collection's model.
  114. model: Todo,
  115. socket: socket,
  116. // Returns the relative URL where the model's resource would be
  117. // located on the server. If your models are located somewhere else,
  118. // override this method with the correct logic. Generates URLs of the
  119. // form: "/[collection.url]/[id]", falling back to "/[urlRoot]/id" if
  120. // the model is not part of a collection.
  121. // Note that url may also be defined as a function.
  122. url: function () {
  123. return "/todo" + ((this.id) ? '/' + this.id : '');
  124. },
  125. initialize: function () {
  126. this.on('collectionCleanup', this.collectionCleanup, this);
  127. socket.on('/todo:create', this.serverCreate, this);
  128. },
  129. serverCreate: function (data) {
  130. if (data) {
  131. // make sure no duplicates, just in case
  132. var todo = Todos.get(data._id);
  133. if (typeof todo === 'undefined') {
  134. Todos.add(data);
  135. } else {
  136. data.fromServer = true;
  137. todo.set(data);
  138. }
  139. }
  140. },
  141. collectionCleanup: function (callback) {
  142. this.ioUnbindAll();
  143. this.each(function (model) {
  144. model.modelCleanup();
  145. });
  146. return this;
  147. },
  148. // Filter down the list of all todo items that are finished.
  149. done: function () {
  150. return this.filter(function (todo) { return todo.get('done'); });
  151. },
  152. // Filter down the list to only todo items that are still not finished.
  153. remaining: function () {
  154. return this.without.apply(this, this.done());
  155. },
  156. // We keep the Todos in sequential order, despite being saved by unordered
  157. // GUID in the database. This generates the next order number for new items.
  158. nextOrder: function () {
  159. if (!this.length) { return 1; }
  160. return this.last().get('order') + 1;
  161. },
  162. // Todos are sorted by their original insertion order.
  163. comparator: function (todo) {
  164. return todo.get('order');
  165. }
  166. });
  167. // Create our global collection of **Todos**.
  168. Todos = new TodoList();
  169. // Todo Item View
  170. // --------------
  171. // The DOM element for a todo item...
  172. TodoView = Backbone.View.extend({
  173. //... is a list tag.
  174. tagName: "li",
  175. // Cache the template function for a single item.
  176. template: _.template($('#item-template').html()),
  177. // The DOM events specific to an item.
  178. events: {
  179. "click .toggle" : "toggleDone",
  180. "dblclick .view" : "edit",
  181. "click a.destroy" : "clear",
  182. "keypress .edit" : "updateOnEnter",
  183. "blur .edit" : "close"
  184. },
  185. // The TodoView listens for changes to its model, re-rendering. Since there's
  186. // a one-to-one correspondence between a **Todo** and a **TodoView** in this
  187. // app, we set a direct reference on the model for convenience.
  188. initialize: function () {
  189. this.model.on('change', this.render, this);
  190. this.model.on('lock', this.serverLock, this);
  191. this.model.on('unlock', this.serverUnlock, this);
  192. Todos.on('remove', this.serverDelete, this);
  193. },
  194. // Re-render the titles of the todo item.
  195. render: function () {
  196. this.$el.html(this.template(this.model.toJSON()));
  197. this.$el.toggleClass('done', this.model.get('done'));
  198. this.input = this.$('.edit');
  199. return this;
  200. },
  201. // Toggle the `"done"` state of the model.
  202. toggleDone: function () {
  203. this.model.toggle();
  204. },
  205. // Switch this view into `"editing"` mode, displaying the input field.
  206. edit: function () {
  207. if (!this.model.locked) {
  208. this.$el.addClass("editing");
  209. this.input.focus();
  210. this.model.lock();
  211. }
  212. },
  213. // Close the `"editing"` mode, saving changes to the todo.
  214. close: function () {
  215. var value = this.input.val();
  216. if (!value) {
  217. this.clear();
  218. }
  219. this.model.save({title: value});
  220. this.$el.removeClass("editing");
  221. this.model.unlock();
  222. },
  223. // If you hit `enter`, we're through editing the item.
  224. updateOnEnter: function (e) {
  225. if (e.keyCode === 13) {
  226. this.close();
  227. }
  228. },
  229. // Remove the item, destroy the model.
  230. clear: function () {
  231. if (!this.model.locked) {
  232. this.model.clear();
  233. }
  234. },
  235. serverDelete: function (data) {
  236. if (data.id === this.model.id) {
  237. this.model.clear({silent: true});
  238. this.$el.remove();
  239. }
  240. },
  241. serverLock: function () {
  242. if (!this.$el.hasClass("editing") && this.model.locked) {
  243. this.$el.addClass('locked');
  244. this.$('.toggle').attr('disabled', true);
  245. }
  246. },
  247. serverUnlock: function () {
  248. this.$el.removeClass('locked');
  249. this.$('.toggle').attr('disabled', false);
  250. }
  251. });
  252. // The Application
  253. // ---------------
  254. // Our overall **AppView** is the top-level piece of UI.
  255. AppView = Backbone.View.extend({
  256. // Instead of generating a new element, bind to the existing skeleton of
  257. // the App already present in the HTML.
  258. el: $("#todoapp"),
  259. // Our template for the line of statistics at the bottom of the app.
  260. statsTemplate: _.template($('#stats-template').html()),
  261. // Delegated events for creating new items, and clearing completed ones.
  262. events: {
  263. "keypress #new-todo": "createOnEnter",
  264. "click #clear-completed": "clearCompleted",
  265. "click #toggle-all": "toggleAllComplete"
  266. },
  267. // At initialization we bind to the relevant events on the `Todos`
  268. // collection, when items are added or changed. Kick things off by
  269. // loading any preexisting todos.
  270. initialize: function (initalData) {
  271. this.input = this.$("#new-todo");
  272. this.allCheckbox = this.$("#toggle-all")[0];
  273. Todos.on('add', this.addOne, this);
  274. Todos.on('reset', this.addAll, this);
  275. Todos.on('all', this.render, this);
  276. this.footer = this.$("footer");
  277. this.main = $("#main");
  278. Todos.fetch({
  279. success: function (todos, models) {
  280. var data = initalData.todo
  281. , locks = ((data && data.locks) ? data.locks : [])
  282. , model;
  283. _.each(locks, function (lock) {
  284. model = todos.get(lock);
  285. if (model) {
  286. model.lock();
  287. }
  288. });
  289. }
  290. });
  291. },
  292. // Re-rendering the App just means refreshing the statistics -- the rest
  293. // of the app doesn't change.
  294. render: function () {
  295. var done = Todos.done().length,
  296. remaining = Todos.remaining().length;
  297. if (Todos.length) {
  298. this.main.show();
  299. this.footer.show();
  300. this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
  301. } else {
  302. this.main.hide();
  303. this.footer.hide();
  304. }
  305. this.allCheckbox.checked = !remaining;
  306. },
  307. // Add a single todo item to the list by creating a view for it, and
  308. // appending its element to the `<ul>`.
  309. addOne: function (todo) {
  310. var view = new TodoView({model: todo});
  311. $("#todo-list").append(view.render().el);
  312. },
  313. // Add all items in the **Todos** collection at once.
  314. addAll: function () {
  315. Todos.each(this.addOne);
  316. },
  317. // If you hit return in the main input field, create new **Todo** model
  318. createOnEnter: function (e) {
  319. if (e.keyCode !== 13) { return; }
  320. if (!this.input.val()) { return; }
  321. var t = new Todo({title: this.input.val()});
  322. t.save();
  323. //Todos.create({title: this.input.val()});
  324. this.input.val('');
  325. },
  326. // Clear all done todo items, destroying their models.
  327. clearCompleted: function () {
  328. _.each(Todos.done(), function (todo) { todo.clear(); });
  329. return false;
  330. },
  331. toggleAllComplete: function () {
  332. var done = this.allCheckbox.checked;
  333. Todos.each(function (todo) { todo.save({'done': done}); });
  334. }
  335. });
  336. // Finally, we kick things off by creating the **App** on successful socket connection
  337. socket.emit('connect', ['todo'], function (err, data) {
  338. if (err) {
  339. console.log('Unable to connect.');
  340. } else {
  341. App = new AppView(data);
  342. }
  343. });
  344. }(jQuery, _, Backbone, io));