PageRenderTime 67ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/tutorial/backbone.rst

https://github.com/htulipe/resthub.org
ReStructuredText | 1161 lines | 783 code | 378 blank | 0 comment | 0 complexity | 8c1ebe53bd7e0e8925093c7d452a7507 MD5 | raw file
  1. RESThub Backbone tutorial
  2. =========================
  3. This tutorial will help you to get an overview of resthub-backbone-stack.
  4. If you want to use this tutorial in a training mode, `a version without answers is also available <backbone-without-answer.html>`_.
  5. **Code**: you can find the code of the sample application at `<https://github.com/resthub/resthub-backbone-tutorial>`_ (Have a look to branches for each step).
  6. Step 1: Model and View
  7. -----------------------
  8. Find:
  9. +++++
  10. 1. **Backbone documentation and description of Backbone.Events, Model, Collection, Router, Sync, View**
  11. see `<http://backbonejs.org/>`_
  12. 2. **RequireJS documentation:**
  13. - requirejs usage
  14. RequireJS allows to define modules and dependencies between modules in order to load your js files
  15. - how to define a module as function
  16. .. code-block:: javascript
  17. define(['backbone'], function(Backbone) { ... });
  18. - how to use it
  19. .. code-block:: javascript
  20. require(['models/task', 'views/task-view'], function(Task, TaskView) { ... });
  21. - usage for config options shims and path
  22. see `<http://requirejs.org/>`_
  23. 3. Description of resthub-backbone `standard project layout <../backbone-stack.html#project-layout>`_ based on requireJS
  24. Do:
  25. +++
  26. 1. Get an empty resthub-backbone project via an `archetype <../spring-stack.html#bootstrap-your-project>`_ and discover the base application layout
  27. 2. **Create a Task model**
  28. .. code-block:: javascript
  29. define(['backbone'], function(Backbone) {
  30. var Task = Backbone.Model.extend();
  31. return Task;
  32. });
  33. 3. **Instantiate a task in app.js with attributes title and description**
  34. .. code-block:: javascript
  35. var task = new Task({
  36. title: 'Learn Backbone',
  37. description: 'To write great Rich Internet Applications.'
  38. });
  39. 4. **Try to see your task object in the console. Make it work**
  40. attach task to window with ``window.task = new Task(...)``
  41. 5. **Try to access to title and description. Is task.title working?**
  42. task.title does not work.
  43. 6. **Inspect task and find where attributes are stored**
  44. In *attributes*.
  45. 7. **Access title attribute value**
  46. .. code-block:: javascript
  47. task.get("title")
  48. 8. **Change description attribute. What operation does backbone perform whena a model attrbute is modified?**
  49. .. code-block:: javascript
  50. task.set("description", "newDesc");
  51. Backbone raise events on attribute modification ("change", etc.) so we have to use getters / setters to manipulate attributes
  52. 9. **Create a TaskView and implement render with a function that simply logs "rendered"**
  53. .. code-block:: javascript
  54. define(['backbone'], function(Backbone) {
  55. var TaskView = Backbone.View.extend({
  56. render: function() {
  57. console.log("rendered");
  58. return this;
  59. }
  60. });
  61. return TaskView;
  62. });
  63. 10. **Instantiate view in app and render it. Verify that "rendered" is logged. Try to render view multiple times in console**
  64. .. code-block:: javascript
  65. window.taskView = new TaskView();
  66. taskView.render();
  67. **Output:**
  68. .. code-block:: javascript
  69. rendered
  70. >>> taskView.render()
  71. rendered
  72. Object { cid="view1", options={...}, $el=[1], more...}
  73. >>> taskView.render()
  74. rendered
  75. Object { cid="view1", options={...}, $el=[1], more...}
  76. 11. **Instantiate the view with a task model in app. Modify TaskView render to log the title of the task. No other modification should be made on TaskView**
  77. app.js:
  78. .. code-block:: javascript
  79. window.task = new Task({
  80. title: 'Learn Backbone',
  81. description: 'To write great Rich Internet Applications.'
  82. });
  83. window.taskView = new TaskView({model: task});
  84. taskView.render();
  85. views/task.js:
  86. .. code-block:: javascript
  87. render: function() {
  88. console.log(this.model.get("title"));
  89. return this;
  90. }
  91. **Output:**
  92. .. code-block:: javascript
  93. Learn Backbone
  94. >>> taskView.render()
  95. Learn Backbone
  96. Object { cid="view1", options={...}, $el=[1], more...}
  97. Write in DOM
  98. ++++++++++++
  99. View rendering is done in view relative el element that could be attached anywhere in DOM with jQuery DOM insertion API
  100. Find:
  101. #####
  102. 1. **backbone view's DOM element documentation**
  103. see `<http://backbonejs.org/#View-el>`_
  104. 2. **jquery documentation and search for $(), html(), append() methods**
  105. see `<http://api.jquery.com/category/manipulation/dom-insertion-inside/>`_
  106. Do:
  107. ###
  108. 1. **Modify render to display a task inside a div with class='task' containing title in a h1 and description in a p**
  109. .. code-block:: javascript
  110. render: function() {
  111. this.$el.html("<div class='task'><h1>" + this.model.get("title") + "</h1><p>" + this.model.get("description") + "</p></div>");
  112. return this;
  113. }
  114. 2. **render the view and attach $el to the DOM 'tasks' element (in app.js)**
  115. .. code-block:: javascript
  116. $('#tasks').html(taskView.render().el);
  117. Templating
  118. ++++++++++
  119. Let's render our task in DOM with a template engine: Handlebars
  120. Find:
  121. ######
  122. 1. **Handlebars documentation**
  123. see `<http://handlebarsjs.com/>`_
  124. 2. **How to pass a full model instance as render context in backbone**
  125. see `<http://backbonejs.org/#View-render>`_
  126. Do:
  127. ####
  128. 1. **Create Task handlebars template to display task. Template should start with a div with class='task'**
  129. .. code-block:: html
  130. <div class="task">
  131. <h1>{{title}}</h1>
  132. <p>{{description}}</p>
  133. </div>
  134. 2. **Load (with requirejs text plugin), compile template in view and render it (pass all model to template)**
  135. .. code-block:: javascript
  136. define(['backbone', 'text!template/task', 'handlebars'], function(Backbone, taskTemplate, Handlebars) {
  137. var TaskView = Backbone.View.extend({
  138. template: Handlebars.compile(taskTemplate),
  139. render: function() {
  140. this.$el.html(this.template(this.model.toJSON()));
  141. return this;
  142. }
  143. });
  144. return TaskView;
  145. });
  146. 3. Resthub comes with a `hbs RequireJS extension <../backbone-stack.html#templating>`_ to replace Handlebars.compile.
  147. **Change TaskView to use this extension. Remove Handlebars requirement**
  148. .. code-block:: javascript
  149. define(['backbone', 'hbs!template/task'], function(Backbone, taskTemplate) {
  150. var TaskView = Backbone.View.extend({
  151. render: function() {
  152. this.$el.html(taskTemplate(this.model.toJSON()));
  153. return this;
  154. }
  155. });
  156. return TaskView;
  157. });
  158. Model events
  159. ++++++++++++
  160. Find:
  161. ######
  162. 1. **Backbone events documentation and model events catalog**
  163. see `<http://backbonejs.org/#Events>`_ and `<http://backbonejs.org/#Events-catalog>`_
  164. Do:
  165. ####
  166. 1. **Update task in the console -> does not update the HTML**
  167. 2. **Bind model's change event in the view to render. Update task in console: HTML is magically updated!**
  168. .. code-block:: javascript
  169. var TaskView = Backbone.View.extend({
  170. initialize: function() {
  171. this.model.on('change', this.render, this);
  172. },
  173. render: function() {
  174. this.$el.html(taskTemplate(this.model.toJSON()));
  175. return this;
  176. }
  177. });
  178. Step 2: Collections
  179. -------------------
  180. 1. **Create a Tasks collection in** ``collection`` **directory**
  181. .. code-block:: javascript
  182. define(['backbone'], function(Backbone) {
  183. var Tasks = Backbone.Collection.extend();
  184. return Tasks;
  185. });
  186. 2. **Create a TasksView** in ``views`` **and a tasks template in** ``templates``.
  187. 3. **Implement rendering in TasksView**
  188. 4. **Pass the collection as context**
  189. 5. **Iterate through the items in the collection in the template**. **Template should start with an** ``ul``
  190. **element with class='task-list'**
  191. .. code-block:: javascript
  192. // view
  193. define(['backbone', 'hbs!template/tasks'], function(Backbone, tasksTemplate) {
  194. var TasksView = Backbone.View.extend({
  195. render: function() {
  196. this.$el.html(tasksTemplate(this.collection.toJSON()));
  197. return this;
  198. }
  199. });
  200. return TasksView;
  201. });
  202. .. code-block:: html
  203. // template
  204. <ul class="task-list">
  205. {{#each this}}
  206. <li class="task">{{title}}</li>
  207. {{/each}}
  208. </ul>
  209. 6. **In app: instanciate two task and add them into a new tasks collections. Instantiate View and render it and attach $el to '#tasks' div**
  210. .. code-block:: javascript
  211. require(['models/task', 'collections/tasks', 'views/tasks'], function(Task, Tasks, TasksView) {
  212. var tasks = new Tasks();
  213. var task1 = new Task({
  214. title: 'Learn Backbone',
  215. description: 'To write great Rich Internet Applications.'
  216. });
  217. var task2 = new Task({
  218. title: 'Learn RESThub',
  219. description: 'Use rethub.org.'
  220. });
  221. tasks.add(task1);
  222. tasks.add(task2);
  223. var tasksView = new TasksView({collection: tasks});
  224. $('#tasks').html(tasksView.render().el);
  225. });
  226. 7. **try adding an item to the collection in the console**
  227. .. code-block:: javascript
  228. require(['models/task', 'collections/tasks', 'views/tasks'], function(Task, Tasks, TasksView) {
  229. window.Task = Task;
  230. window.tasks = new Tasks();
  231. ...
  232. });
  233. **Output:**
  234. .. code-block:: javascript
  235. >>> task3 = new Task()
  236. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  237. >>> task3.set("title", "Learn again");
  238. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  239. >>> task3.set("description", "A new learning");
  240. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  241. >>> tasks.add(task3);
  242. Object { length=3, models=[3], _byId={...}, more...}
  243. HTML was not updated.
  244. 8. **Bind collection's add event in the view to render**
  245. .. code-block:: javascript
  246. define(['backbone', 'hbs!template/tasks'], function(Backbone, tasksTemplate) {
  247. var TasksView = Backbone.View.extend({
  248. initialize: function() {
  249. this.collection.on('add', this.render, this);
  250. },
  251. render: function() {
  252. this.$el.html(tasksTemplate(this.collection.toJSON()));
  253. return this;
  254. }
  255. });
  256. return TasksView;
  257. });
  258. **Output:**
  259. .. code-block:: javascript
  260. >>> task3 = new Task()
  261. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  262. >>> task3.set("title", "Learn again");
  263. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  264. >>> task3.set("description", "A new learning");
  265. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  266. >>> tasks.add(task3);
  267. Object { length=3, models=[3], _byId={...}, more...}
  268. HTML is updated with the new task in collection.
  269. 9. **Add a nice fade effect**
  270. .. code-block:: javascript
  271. define(['backbone', 'hbs!template/tasks'], function(Backbone, tasksTemplate) {
  272. var TasksView = Backbone.View.extend({
  273. initialize: function() {
  274. this.collection.on('add', this.render, this);
  275. },
  276. render: function() {
  277. this.$el.fadeOut(function() {
  278. this.$el.html(tasksTemplate(this.collection.toJSON()));
  279. this.$el.fadeIn();
  280. }.bind(this));
  281. return this;
  282. }
  283. });
  284. return TasksView;
  285. });
  286. 10. **Add a task to the collection in the console** -> the *whole* collection in rerendered.
  287. .. code-block:: javascript
  288. >>> task3 = new Task()
  289. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  290. >>> task3.set("title", "Learn again");
  291. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  292. >>> task3.set("description", "A new learning");
  293. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  294. >>> tasks.add(task3);
  295. Object { length=3, models=[3], _byId={...}, more...}
  296. Step 3: Nested Views
  297. --------------------
  298. 1. Remove the each block in template.
  299. .. code-block:: html
  300. <ul class="task-list"></ul>
  301. 2. Use TaskView in TasksView to render each tasks.
  302. .. code-block:: javascript
  303. // views/tasks.js
  304. render: function() {
  305. this.$el.fadeOut(function() {
  306. this.$el.html(tasksTemplate(this.collection.toJSON()));
  307. this.collection.forEach(this.add, this);
  308. this.$el.fadeIn();
  309. }.bind(this));
  310. return this;
  311. },
  312. 3. Update a task in the console -> the HTML for the task is automatically updated.
  313. .. code-block:: javascript
  314. // app.js
  315. ...
  316. window.task1 = new Task({
  317. title: 'Learn Backbone',
  318. description: 'To write great Rich Internet Applications.'
  319. });
  320. **output:**
  321. .. code-block:: javascript
  322. >>> task1.set("title", "new Title");
  323. Object { attributes={...}, _escapedAttributes={...}, cid="c0", more...}
  324. 4. Add tasks to the collection in the console -> the *whole* list is still rerendered.
  325. .. code-block:: javascript
  326. >>> task3 = new Task()
  327. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  328. >>> task3.set("title", "Learn again");
  329. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  330. >>> task3.set("description", "A new learning");
  331. Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...}
  332. >>> tasks.add(task3);
  333. Object { length=3, models=[3], _byId={...}, more...}
  334. 5. Update TasksView to only append one task when added to the collection instead of rendering the whole list again.
  335. .. code-block:: javascript
  336. initialize: function() {
  337. this.collection.on('add', this.add, this);
  338. },
  339. 6. Add a nice fade effect to TaskView.
  340. .. code-block:: javascript
  341. // view/task.js
  342. render: function() {
  343. this.$el.fadeOut(function() {
  344. this.$el.html(taskTemplate(this.model.toJSON()));
  345. this.$el.fadeIn();
  346. }.bind(this));
  347. return this;
  348. }
  349. 7. Test in the console.
  350. 8. Remove automatic generated divs and replace them with lis
  351. goal is to have:
  352. .. code-block:: html
  353. <ul>
  354. <li class='task'></li>
  355. <li class='task'></li>
  356. </ul>
  357. instead of:
  358. .. code-block:: html
  359. <ul>
  360. <div><li class='task'></li></div>
  361. <div><li class='task'></li></div>
  362. </ul>
  363. example:
  364. .. code-block:: javascript
  365. // views/task.js
  366. var TaskView = Backbone.View.extend({
  367. tagName:'li',
  368. className: 'task',
  369. ...
  370. 9. Manage click in TaskView to toggle task's details visibility.
  371. .. code-block:: javascript
  372. events: {
  373. click: 'toggleDetails'
  374. },
  375. ...
  376. toggleDetails: function() {
  377. this.$('p').slideToggle();
  378. }
  379. Step 4: Rendering strategy
  380. --------------------------
  381. Find:
  382. +++++
  383. 1. **Resthub documentation for default rendering strategy**
  384. see `<../backbone-stack.html#root-attribute>`_
  385. Do:
  386. +++
  387. 1. **Use Resthub.View for managing rendering in TaskView. Remove render method in TaskView and modify add method in TasksView to set root element**
  388. .. code-block:: javascript
  389. // views/task.js
  390. define(['backbone', 'resthub', hbs!template/task'], function(Backbone, Resthub, taskTemplate) {
  391. var TaskView = Resthub.View.extend({
  392. template: taskTemplate,
  393. tagName: 'li',
  394. className: 'task',
  395. strategy: 'append',
  396. events: {
  397. click: 'toggleDetails'
  398. },
  399. initialize: function() {
  400. this.model.on('change', this.render, this);
  401. },
  402. toggleDetails: function() {
  403. this.$('p').slideToggle();
  404. }
  405. });
  406. return TaskView;
  407. });
  408. // views/tasks.js
  409. ...
  410. add: function(task) {
  411. var taskView = new TaskView({root: this.$('.task-list'), model: task});
  412. taskView.render();
  413. }
  414. ...
  415. 2. **Re-implement render to get back the fade effect by extending it calling parent function**
  416. .. code-block:: javascript
  417. render: function() {
  418. this.$el.fadeOut(function() {
  419. TaskView.__super__.render.apply(this);
  420. this.$el.fadeIn();
  421. }.bind(this));
  422. return this;
  423. },
  424. 3. **Use Resthub.View for managing rendering in TasksView. Call the parent render function.**
  425. .. code-block:: javascript
  426. define(['backbone', 'resthub', 'view/task-view', 'hbs!template/tasks'], function(Backbone, Resthub, TaskView, tasksTemplate) {
  427. var TasksView = Resthub.View.extend({
  428. template: tasksTemplate,
  429. initialize: function() {
  430. this.collection.on('add', this.add, this);
  431. },
  432. render: function() {
  433. TasksView.__super__.render.apply(this);
  434. this.collection.forEach(this.add, this);
  435. return this;
  436. },
  437. add: function(task) {
  438. var taskView = new TaskView({root: this.$('.task-list'), model: task});
  439. taskView.render();
  440. }
  441. });
  442. return TasksView;
  443. });
  444. 4. **In the console try adding a Task: thanks to the effect we can see that only one more Task is rendered and not the entirely list**
  445. .. code-block:: javascript
  446. >>> task3 = new Task()
  447. Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...}
  448. >>> task3.set("title", "Learn again");
  449. Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...}
  450. >>> task3.set("description", "A new learning");
  451. Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...}
  452. >>> tasks.add(task3);
  453. Object { length=3, models=[3], _byId={...}, more...}
  454. 5. **In the console, update an existing Task: thanks to the effect we can see that just this task is updated**
  455. .. code-block:: javascript
  456. >>> task3.set("title", "new Title");
  457. Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...}
  458. Step 5: Forms
  459. -------------
  460. Do:
  461. +++
  462. 1. **Create TaskFormView which is rendered in place when double clicking on a TaskView. Wrap your each form field in a div with** ``class='control-group'`` **. Add**
  463. ``class='btn btn-success'`` **on your input submit**
  464. .. code-block:: javascript
  465. // views/task.js
  466. define(['backbone', 'resthub', 'view/taskform-view', 'hbs!template/task'], function(Backbone, Resthub, TaskFormView, taskTemplate) {
  467. var TaskView = Resthub.View.extend({
  468. ...
  469. events: {
  470. click: 'toggleDetails',
  471. dblclick: 'edit'
  472. },
  473. ...
  474. edit: function() {
  475. var taskFormView = new TaskFormView({root: this.$el, model: this.model});
  476. taskFormView.render();
  477. },
  478. ...
  479. });
  480. return TaskView;
  481. });
  482. // views/taskform.js
  483. define(['backbone', 'resthub', 'hbs!template/taskform'], function(Backbone, Resthub, ,taskFormTemplate) {
  484. var TaskFormView = Resthub.View.extend({
  485. template: taskFormTemplate,
  486. tagName: 'form',
  487. });
  488. return TaskFormView;
  489. });
  490. .. code-block:: html
  491. <div class="control-group">
  492. <input class="title" type="text" placeholder="Title" value="{{model.title}}" />
  493. </div>
  494. <div class="control-group">
  495. <textarea class="description" rows="3" placeholder="Description">{{model.description}}</textarea>
  496. </div>
  497. <input type="submit" class="btn btn-success" value="Save" />
  498. 2. **When the form is submitted, update the task with the changes and display it
  499. again -> note that the change event is not triggered when there was no
  500. changes at all.**
  501. .. code-block:: javascript
  502. // views/taskform.js
  503. ...
  504. save: function() {
  505. this.model.set({
  506. title: this.$('.title').val(),
  507. description: this.$('.description').val(),
  508. });
  509. return false;
  510. }
  511. ...
  512. 3. **Force change event to be raised once and only once**
  513. .. code-block:: javascript
  514. // views/taskform.js
  515. ...
  516. save: function() {
  517. this.model.set({
  518. title: this.$('.title').val(),
  519. description: this.$('.description').val(),
  520. }, {silent: true});
  521. this.model.trigger('change', this.model);
  522. return false;
  523. }
  524. ...
  525. 4. **Add a button to create a new empty task. In TasksView, bind its click event
  526. to a create method which instantiate a new empty task with a TaskView which
  527. is directly editable. Add** ``class="btn btn-primary"`` **to this button**
  528. .. code-block:: html
  529. <!-- template/tasks.hbs -->
  530. <ul class="task-list"></ul>
  531. <p>
  532. <button id="create" class="btn btn-primary" type="button">New Task</button>
  533. </p>
  534. .. code-block:: javascript
  535. var TasksView = Resthub.View.extend({
  536. template: tasksTemplate,
  537. events: {
  538. 'click #create': 'create'
  539. },
  540. ...
  541. create: function() {
  542. var taskView = new TaskView({root: this.$('.task-list'), model: new Task()});
  543. taskView.edit();
  544. }
  545. });
  546. 5. **Note that you have to add the task to the collection otherwise when you
  547. render the whole collection again, the created tasks disappear. Try by attach
  548. tasksView to windows and call render() from console**
  549. .. code-block:: javascript
  550. create: function() {
  551. var task = new Task();
  552. this.collection.add(task, {silent: true});
  553. var taskView = new TaskView({root: this.$('.task-list'), model: task});
  554. taskView.edit();
  555. }
  556. 6. **Add a cancel button in TaskFormView to cancel task edition. Add a**
  557. ``class="btn cancel"`` **to this button**
  558. .. code-block:: html
  559. <!-- templates/taskform.hbs -->
  560. ...
  561. <input type="button" class="btn cancel" value="Cancel" />
  562. .. code-block:: javascript
  563. var TaskFormView = Resthub.View.extend({
  564. ...
  565. events: {
  566. submit: 'save',
  567. 'click .cancel': 'cancel'
  568. },
  569. ...
  570. cancel: function() {
  571. this.model.trigger('change');
  572. }
  573. });
  574. 7. **Add a delete button which delete a task. Add** ``class="btn btn-danger delete"``
  575. **to this button. Remove the view associated to this task on delete click and remove
  576. the task from the collection**
  577. Note that we can't directly remove it from the collection cause the
  578. TaskFormView is not responsible for the collection management and does not
  579. have access to this one.
  580. **Then use the model's destroy method and note that Backbone will automatically
  581. remove the destroyed object from the collection on a destroy event**
  582. .. code-block:: javascript
  583. // views/taskform.js
  584. var TaskFormView = Resthub.View.extend({
  585. ...
  586. events: {
  587. submit: 'save',
  588. 'click .cancel': 'cancel',
  589. 'click .delete': 'delete'
  590. },
  591. ...
  592. delete: function() {
  593. this.model.destroy();
  594. }
  595. });
  596. // views/task.js
  597. ...
  598. initialize: function() {
  599. this.model.on('change', this.render, this);
  600. this.model.on('destroy', this.remove, this);
  601. },
  602. ...
  603. **output:**
  604. .. code-block:: javascript
  605. // no click on delete
  606. >>> tasks
  607. Object { length=2, models=[2], _byId={...}, more...}
  608. // on click on delete
  609. >>> tasks
  610. Object { length=1, models=[1], _byId={...}, more...}
  611. // two clicks on delete
  612. >>> tasks
  613. Object { length=0, models=[0], _byId={...}, more...}
  614. 8. **Note in the console that when removing a task manually in the collection, it
  615. does not disappear**
  616. .. code-block:: javascript
  617. >>> tasks
  618. Object { length=2, models=[2], _byId={...}, more...}
  619. >>> tasks.remove(tasks.models[0]);
  620. Object { length=1, models=[1], _byId={...}, more...}
  621. But task is still displayed
  622. 9. **Bind remove event on the collection to call** ``task.destroy()`` **in TasksView**
  623. .. code-block:: javascript
  624. ...
  625. initialize: function() {
  626. this.collection.on('add', this.add, this);
  627. this.collection.on('remove', this.destroyTask, this);
  628. },
  629. ...
  630. destroyTask: function(task) {
  631. task.destroy();
  632. }
  633. 10. **Test again in the console**
  634. .. code-block:: javascript
  635. >>> tasks
  636. Object { length=2, models=[2], _byId={...}, more...}
  637. >>> tasks.remove(tasks.models[0]);
  638. Object { length=1, models=[1], _byId={...}, more...}
  639. And task disapeared
  640. Step 6: Validation
  641. ------------------
  642. Find:
  643. +++++
  644. 1. **Backbone documentation about model validation**
  645. see `<http://backbonejs.org/#Model-validate>`_
  646. 2. **Resthub documentation for populateModel**
  647. see `<../backbone-stack.html#automatic-population-of-view-model-from-a-form>`_
  648. Do:
  649. +++
  650. 1. **Implement validate function in Task model: make sure that the title is not
  651. blank**
  652. .. code-block:: javascript
  653. define(['backbone'], function(Backbone) {
  654. var Task = Backbone.Model.extend({
  655. validate: function(attrs) {
  656. if (/^\s*$/.test(attrs.title)) {
  657. return 'Title cannot be blank.';
  658. }
  659. }
  660. });
  661. return Task;
  662. });
  663. 2. **In TaskFormView, on save method, get the result of set method call on attributes and
  664. trigger "change" event only if validation passes**
  665. .. code-block:: javascript
  666. save: function() {
  667. var success = this.model.set({
  668. title: this.$('.title').val(),
  669. description: this.$('.desc').val(),
  670. });
  671. // If validation passed, manually force trigger
  672. // change event even if there were no actual
  673. // changes to the fields.
  674. if (success) {
  675. this.model.trigger('change');
  676. }
  677. return false;
  678. },
  679. 3. **Update TaskForm template to add a span with class** ``help-inline`` **immediately after title input**
  680. .. code-block:: html
  681. <div class="control-group">
  682. <input class="title" type="text" placeholder="Title" value="{{model.title}}" />
  683. <span class="help-inline"></span>
  684. </div>
  685. 4. **In TaskFormView bind model's error event on a function which renders
  686. validation errors. On error, add class "error" on title input and display error in span "help-inline"**
  687. .. code-block:: javascript
  688. initialize: function() {
  689. this.model.on('error', this.error, this);
  690. },
  691. ...
  692. error: function(model, error) {
  693. this.$('.control-group:first-child').addClass('error');
  694. this.$('.help-inline').html(error);
  695. }
  696. 5. **Use Backbone.Validation for easy validation management**
  697. .. code-block:: javascript
  698. // models/task.js
  699. define(['backbone'], function(Backbone) {
  700. var Task = Backbone.Model.extend({
  701. validation: {
  702. title: {
  703. required: true,
  704. msg: 'A title is required.'
  705. }
  706. }
  707. });
  708. return Task;
  709. });
  710. // views/taskform.js
  711. define(['backbone', 'hbs!template/taskform'], function(Backbone, taskFormTemplate) {
  712. ...
  713. initialize: function() {
  714. this.model.on('error', this.error, this);
  715. Backbone.Validation.bind(this);
  716. },
  717. ...
  718. });
  719. 6. **Note that Backbone.Validation can handle for you error displaying in your
  720. views: remove error bindings and method and ensure that you form input have
  721. a name attribute equals to the model attribute name**
  722. .. code-block:: html
  723. <div class="control-group">
  724. <input class="title" type="text" name="title" placeholder="Title" value="{{model.title}}" />
  725. <span class="help-inline"></span>
  726. </div>
  727. <div class="control-group">
  728. <textarea class="description" rows="3" name="description" placeholder="Description">{{model.description}}</textarea>
  729. </div>
  730. .. code-block:: javascript
  731. // views/taskform.js
  732. ...
  733. initialize: function() {
  734. Backbone.Validation.bind(this);
  735. },
  736. ...
  737. 7. **Rewrite save method using resthub** ``populateModel`` and backbone ``isValid``
  738. .. code-block:: javascript
  739. save: function() {
  740. this.populateModel(this.$el);
  741. // If validation passed, manually force trigger
  742. // change event even if there were no actual
  743. // changes to the fields.
  744. if (this.model.isValid()) {
  745. this.model.trigger('change');
  746. }
  747. return false;
  748. },
  749. Step 7: Persist & Sync
  750. ----------------------
  751. * Our data are not persisted, after a refresh, our task collection will be
  752. reinitialized.
  753. * Use Backbone local storage extension to persist our tasks into the local
  754. storage.
  755. * Bind the collection's reset event on TasksView.render to render the
  756. collection once synced with the local storage.
  757. * Warning: you need to specify the model attribute in the Tasks collection to
  758. tell the collection which model object is gonna be used internally.
  759. Otherwise, when fetching, the returned JSON object will be added directly to
  760. the collection without instantiating a Task. As a consequence every specific
  761. attributes (like validation hash), would be unavailable in the model. At this
  762. step, if validation does not work anymore after fetching the tasks through
  763. Backbone.sync, check that the model attribute is correctly set in the
  764. collection.
  765. Step 8
  766. ------
  767. * Download `RESThub Spring tutorial sample project <https://github.com/resthub/resthub-spring-tutorial/zipball/step5-solution>`_ and extract it
  768. * Create jpa-webservice/src/main/webapp directory, and move your JS application into it
  769. * Run the jpa-webservice webapp thanks to Maven Jetty plugin
  770. * Remove backbone-localstorage.js file and usage in JS application
  771. * Make your application retreiving tasks from api/task?page=no URL
  772. .. code-block:: javascript
  773. // collections/tasks.js
  774. define(['backbone', 'models/task'], function(Backbone, Task) {
  775. var Tasks = Backbone.Collection.extend({
  776. url: 'api/task',
  777. model: Task
  778. });
  779. return Tasks;
  780. });
  781. // app.js
  782. tasks.fetch({ data: { page: 'no'} });
  783. * Validate that retreive, delete, create and update actions work as expected with this whole new jpa-webservice backend