Commit 4b300dc1 authored by Aaron Muir Hamilton's avatar Aaron Muir Hamilton Committed by Sam Saccone

Refactor the Vanilla ES6 example (#1626)

- Simplify the Template module.
 - Simplify the router.
 - Convert all of the View render commands to normal methods, and give them more meaningful names.
 - Convert all of the View bind synthetic events to binding methods, and give them more meaningful names.
 - Normalize the Store API.
 - Collapse the Model abstraction, since it consisted mainly of trampolines to methods on Store.
 - Remove unnecessary dynamic templating for the Clear completed button.
 - Put .footer inside .main since they are always hidden together.
 - Make .filter a div, and unnest the <a>s inside the <li>s to avoid unnecessary extra nodes and CSS.
 - Update the tests to work correctly with the new Vanilla ES6 markup.
 - Remove unnecessary object orientation in app.js.
 - Remove boolean traps from methods.
 - Do not modify the NodeList prototype.
 - Use cross-browser copmatible NodeList iteration in dispatchEvent of $delegate.
 - Fix the existing JSDoc comments.
 - Expand the existing JSDoc comments.
 - JSDoc more of the code.
 - Use Google Closure Compiler instead of Babel. It offers useful warnings and generates better code.
 - Author a learn.json section for the Vanilla ES6 example.
parent 41eaaadf
...@@ -48,6 +48,7 @@ ...@@ -48,6 +48,7 @@
"examples/thorax_lumbar/public/*.js", "examples/thorax_lumbar/public/*.js",
"examples/typescript-*/js/**/*.js", "examples/typescript-*/js/**/*.js",
"examples/vanilladart/**/*.js", "examples/vanilladart/**/*.js",
"examples/vanilla-es6/dist/bundle.js",
], ],
"requireSpaceBeforeBlockStatements": true, "requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true, "requireParenthesesAroundIIFE": true,
......
# Vanilla ES6 (ES2015) • [TodoMVC](http://todomvc.com) # Vanilla ES6 (ES2015) • [TodoMVC](http://todomvc.com)
> An exact port of the [Vanilla JS Example](http://todomvc.com/examples/vanillajs/), but translated into ES6, also known as ES2015. > A port of the [Vanilla JS Example](http://todomvc.com/examples/vanillajs/), but translated into ES6, also known as ES2015.
## Learning ES6 ## Learning ES6
...@@ -34,9 +34,10 @@ npm run compile ...@@ -34,9 +34,10 @@ npm run compile
## Implementation ## Implementation
Uses [Babel JS](https://babeljs.io/) to compile ES6 code to ES5, which is then readable by all browsers. Uses [Google Closure Compiler](https://developers.google.com/closure/compiler/) to compile ES6 code to ES5, which is then readable by all browsers.
## Credit ## Credit
Created by [Luke Edwards](http://www.lukeed.com) Created by [Luke Edwards](http://www.lukeed.com)
Refactored by [Aaron Muir Hamilton](https://github.com/xorgy)
'use strict';
var _controller = require('./controller');
var _controller2 = _interopRequireDefault(_controller);
var _helpers = require('./helpers');
var helpers = _interopRequireWildcard(_helpers);
var _template = require('./template');
var _template2 = _interopRequireDefault(_template);
var _store = require('./store');
var _store2 = _interopRequireDefault(_store);
var _model = require('./model');
var _model2 = _interopRequireDefault(_model);
var _view = require('./view');
var _view2 = _interopRequireDefault(_view);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var $on = helpers.$on;
var setView = function setView() {
return todo.controller.setView(document.location.hash);
};
var Todo =
/**
* Init new Todo List
* @param {string} The name of your list
*/
function Todo(name) {
_classCallCheck(this, Todo);
this.storage = new _store2.default(name);
this.model = new _model2.default(this.storage);
this.template = new _template2.default();
this.view = new _view2.default(this.template);
this.controller = new _controller2.default(this.model, this.view);
};
var todo = new Todo('todos-vanillajs');
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
\ No newline at end of file
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var e,h="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global?global:this;function k(){h.Symbol||(h.Symbol=l);k=function(){}}var m=0;function l(a){return"jscomp_symbol_"+a+m++}function n(){k();h.Symbol.iterator||(h.Symbol.iterator=h.Symbol("iterator"));n=function(){}}function q(a){n();if(a[h.Symbol.iterator])return a[h.Symbol.iterator]();var b=0;return{next:function(){return b==a.length?{done:!0}:{done:!1,value:a[b++]}}}}function r(a,b){return(b||document).querySelector(a)}
'use strict'; function t(a,b,c,d){a.addEventListener(b,c,!!d)}function w(a,b,c,d,f){t(a,c,function(c){for(var f=c.target,u=a.querySelectorAll(b),v=u.length;v--;)if(u[v]===f){d.call(f,c);break}},!!f)}function x(a){return a.replace(/[&<]/g,function(a){return"&"===a?"&amp;":"&lt;"})};var y={};function z(a){return a.reduce(function(a,c){return a+('\n<li data-id="'+c.id+'"'+(c.c?' class="completed"':"")+'>\n\t<input class="toggle" type="checkbox" '+(c.c?"checked":"")+">\n\t<label>"+x(c.title)+'</label>\n\t<button class="destroy"></button>\n</li>')},"")};function A(a){return parseInt(a.parentNode.dataset.id,10)}function B(){this.a=r(".todo-list");this.s=r(".todo-count");this.b=r(".clear-completed");this.o=r(".main");this.h=r(".toggle-all");this.g=r(".new-todo");w(this.a,"li label","dblclick",function(a){a=a.target;var b=a.parentElement;b.classList.add("editing");var c=document.createElement("input");c.className="edit";c.value=a.innerText;b.appendChild(c);c.focus()})}B.prototype.u=function(a){this.a.innerHTML=z(a)};
function C(a,b){var c=r('[data-id="'+a+'"]');c.removeChild(r("input.edit",c));c.classList.remove("editing");r("label",c).textContent=b}function D(a,b){t(a.g,"change",function(a){(a=a.target.value.trim())&&b(a)})}function E(a,b){t(a.h,"click",function(a){b(a.target.checked)})}function F(a,b){w(a.a,".destroy","click",function(a){b(A(a.target))})}function G(a,b){w(a.a,".toggle","click",function(a){a=a.target;b(A(a),a.checked)})}
var _controller = require('./controller'); function H(a,b){w(a.a,"li .edit","blur",function(a){a=a.target;a.dataset.v||b(A(a),a.value.trim())},!0);w(a.a,"li .edit","keypress",function(a){var b=a.target;13===a.keyCode&&b.blur()})}function I(a,b){w(a.a,"li .edit","keyup",function(a){var d=a.target;27===a.keyCode&&(d.dataset.v=!0,d.blur(),b(A(d)))})};function J(a,b){var c=window.localStorage,d;this.a=function(){return d||JSON.parse(c.getItem(a)||"[]")};this.b=function(b){c.setItem(a,JSON.stringify(d=b))};b&&b()}J.prototype.find=function(a,b){var c=this.a(),d;b(c.filter(function(b){for(d in a)if(a[d]!==b[d])return!1;return!0}))};function K(a,b,c){for(var d=b.id,f=a.a(),g=f.length,p;g--;)if(f[g].id===d){for(p in b)f[g][p]=b[p];break}a.b(f);c&&c()}function L(a,b,c){var d=a.a();d.push(b);a.b(d);c&&c()}
J.prototype.remove=function(a,b){var c,d=this.a().filter(function(b){for(c in a)if(a[c]!==b[c])return!0;return!1});this.b(d);b&&b(d)};function M(a,b){a.find(y,function(a){for(var d=a.length,f=d,g=0;f--;)g+=a[f].c;b(d,d-g,g)})};function N(a,b){var c=this;this.a=a;this.b=b;D(b,this.j.bind(this));H(b,this.m.bind(this));I(b,this.l.bind(this));F(b,this.i.bind(this));G(b,function(a,b){O(c,a,b);c.f()});t(b.b,"click",this.w.bind(this));E(b,this.A.bind(this));this.h="";this.g=null}e=N.prototype;e.j=function(a){var b=this;L(this.a,{id:Date.now(),title:a,c:!1},function(){b.b.g.value="";b.f(!0)})};e.m=function(a,b){b.length?K(this.a,{id:a,title:b},function(){C(a,b)}):this.i(a)};e.l=function(a){this.a.find({id:a},function(b){C(a,b[0].title)})};
var _controller2 = _interopRequireDefault(_controller); e.i=function(a){var b=this;this.a.remove({id:a},function(){b.f();var c=r('[data-id="'+a+'"]');c&&b.b.a.removeChild(c)})};e.w=function(){this.a.remove({c:!0},this.f.bind(this))};function O(a,b,c){K(a.a,{id:b,c:c},function(){var a=r('[data-id="'+b+'"]');a&&(a.className=c?"completed":"",r("input",a).checked=c)})}e.A=function(a){var b=this;this.a.find({c:!a},function(c){c=q(c);for(var d=c.next();!d.done;d=c.next())O(b,d.value.id,a)});this.f()};
e.f=function(a){var b=this,c=this.h;(a||""!==this.g||this.g!==c)&&this.a.find({"":y,active:{c:!1},completed:{c:!0}}[c],this.b.u.bind(this.b));M(this.a,function(a,c,g){b.b.s.innerHTML=c+" item"+(1!==c?"s":"")+" left";b.b.b.style.display=g?"block":"none";b.b.h.checked=g===a;b.b.o.style.display=a?"block":"none"});this.g=c};var P=new J("todos-vanilla-es6"),Q=new B,R=new N(P,Q);function S(){var a=document.location.hash.replace(/^#\//,"");R.h=a;R.f();r(".filters>.selected").className="";r('.filters>[href="#/'+a+'"]').className="selected"}t(window,"load",S);t(window,"hashchange",S);
var _helpers = require('./helpers');
var helpers = _interopRequireWildcard(_helpers);
var _template = require('./template');
var _template2 = _interopRequireDefault(_template);
var _store = require('./store');
var _store2 = _interopRequireDefault(_store);
var _model = require('./model');
var _model2 = _interopRequireDefault(_model);
var _view = require('./view');
var _view2 = _interopRequireDefault(_view);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var $on = helpers.$on;
var setView = function setView() {
return todo.controller.setView(document.location.hash);
};
var Todo =
/**
* Init new Todo List
* @param {string} The name of your list
*/
function Todo(name) {
_classCallCheck(this, Todo);
this.storage = new _store2.default(name);
this.model = new _model2.default(this.storage);
this.template = new _template2.default();
this.view = new _view2.default(this.template);
this.controller = new _controller2.default(this.model, this.view);
};
var todo = new Todo('todos-vanillajs');
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
},{"./controller":2,"./helpers":3,"./model":4,"./store":5,"./template":6,"./view":7}],2:[function(require,module,exports){
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Controller = function () {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var _this = this;
_classCallCheck(this, Controller);
this.model = model;
this.view = view;
this.view.bind('newTodo', function (title) {
return _this.addItem(title);
});
this.view.bind('itemEdit', function (item) {
return _this.editItem(item.id);
});
this.view.bind('itemEditDone', function (item) {
return _this.editItemSave(item.id, item.title);
});
this.view.bind('itemEditCancel', function (item) {
return _this.editItemCancel(item.id);
});
this.view.bind('itemRemove', function (item) {
return _this.removeItem(item.id);
});
this.view.bind('itemToggle', function (item) {
return _this.toggleComplete(item.id, item.completed);
});
this.view.bind('removeCompleted', function () {
return _this.removeCompletedItems();
});
this.view.bind('toggleAll', function (status) {
return _this.toggleAll(status.completed);
});
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
_createClass(Controller, [{
key: 'setView',
value: function setView(hash) {
var route = hash.split('/')[1];
var page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
}, {
key: 'showAll',
value: function showAll() {
var _this2 = this;
this.model.read(function (data) {
return _this2.view.render('showEntries', data);
});
}
/**
* Renders all active tasks
*/
}, {
key: 'showActive',
value: function showActive() {
var _this3 = this;
this.model.read({ completed: false }, function (data) {
return _this3.view.render('showEntries', data);
});
}
/**
* Renders all completed tasks
*/
}, {
key: 'showCompleted',
value: function showCompleted() {
var _this4 = this;
this.model.read({ completed: true }, function (data) {
return _this4.view.render('showEntries', data);
});
}
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
}, {
key: 'addItem',
value: function addItem(title) {
var _this5 = this;
if (title.trim() === '') {
return;
}
this.model.create(title, function () {
_this5.view.render('clearNewTodo');
_this5._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
}, {
key: 'editItem',
value: function editItem(id) {
var _this6 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this6.view.render('editItem', { id: id, title: title });
});
}
/*
* Finishes the item editing mode successfully.
*/
}, {
key: 'editItemSave',
value: function editItemSave(id, title) {
var _this7 = this;
title = title.trim();
if (title.length !== 0) {
this.model.update(id, { title: title }, function () {
_this7.view.render('editItemDone', { id: id, title: title });
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
*/
}, {
key: 'editItemCancel',
value: function editItemCancel(id) {
var _this8 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this8.view.render('editItemDone', { id: id, title: title });
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
*/
}, {
key: 'removeItem',
value: function removeItem(id) {
var _this9 = this;
this.model.remove(id, function () {
return _this9.view.render('removeItem', id);
});
this._filter();
}
/**
* Will remove all completed items from the DOM and storage.
*/
}, {
key: 'removeCompletedItems',
value: function removeCompletedItems() {
var _this10 = this;
this.model.read({ completed: true }, function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
_this10.removeItem(item.id);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
this._filter();
}
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
}, {
key: 'toggleComplete',
value: function toggleComplete(id, completed, silent) {
var _this11 = this;
this.model.update(id, { completed: completed }, function () {
_this11.view.render('elementComplete', { id: id, completed: completed });
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
}, {
key: 'toggleAll',
value: function toggleAll(completed) {
var _this12 = this;
this.model.read({ completed: !completed }, function (data) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
_this12.toggleComplete(item.id, completed, true);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
});
this._filter();
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
}, {
key: '_updateCount',
value: function _updateCount() {
var _this13 = this;
this.model.getCount(function (todos) {
var completed = todos.completed;
var visible = completed > 0;
var checked = completed === todos.total;
_this13.view.render('updateElementCount', todos.active);
_this13.view.render('clearCompletedButton', { completed: completed, visible: visible });
_this13.view.render('toggleAll', { checked: checked });
_this13.view.render('contentBlockVisibility', { visible: todos.total > 0 });
});
}
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
}, {
key: '_filter',
value: function _filter(force) {
var active = this._activeRoute;
var activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]()
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
}
this._lastActiveRoute = activeRoute;
}
/**
* Simply updates the filter nav's selected states
*/
}, {
key: '_updateFilter',
value: function _updateFilter(currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.render('setFilter', currentPage);
}
}]);
return Controller;
}();
exports.default = Controller;
},{}],3:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.qs = qs;
exports.qsa = qsa;
exports.$on = $on;
exports.$delegate = $delegate;
exports.$parent = $parent;
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
function $on(target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
}
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
function $delegate(target, selector, type, handler) {
var dispatchEvent = function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = qsa(selector, target);
var hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
},{}],4:[function(require,module,exports){
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
var Model = function () {
function Model(storage) {
_classCallCheck(this, Model);
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
_createClass(Model, [{
key: 'create',
value: function create(title, callback) {
title = title || '';
var newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
}
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func) // Will find the model with an ID of 1
* model.read('1') // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' })
*/
}, {
key: 'read',
value: function read(query, callback) {
var queryType = typeof query === 'undefined' ? 'undefined' : _typeof(query);
if (queryType === 'function') {
this.storage.findAll(query);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({ id: query }, callback);
} else {
this.storage.find(query, callback);
}
}
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
}, {
key: 'update',
value: function update(id, data, callback) {
this.storage.save(data, callback, id);
}
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
}, {
key: 'remove',
value: function remove(id, callback) {
this.storage.remove(id, callback);
}
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
}, {
key: 'removeAll',
value: function removeAll(callback) {
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
}, {
key: 'getCount',
value: function getCount(callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var todo = _step.value;
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
callback(todos);
});
}
}]);
return Model;
}();
exports.default = Model;
},{}],5:[function(require,module,exports){
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/*jshint eqeqeq:false */
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
var Store = function () {
function Store(name, callback) {
_classCallCheck(this, Store);
this._dbName = name;
if (!localStorage[name]) {
var data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
if (callback) {
callback.call(this, JSON.parse(localStorage[name]));
}
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* })
*/
_createClass(Store, [{
key: "find",
value: function find(query, callback) {
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q]) {
return false;
}
}
return true;
}));
}
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
}, {
key: "findAll",
value: function findAll(callback) {
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
}, {
key: "save",
value: function save(updateData, callback, id) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < len; i++) {
if (todos[i].id === id) {
for (var key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, [updateData]);
}
}
}
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
}, {
key: "remove",
value: function remove(id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
}, {
key: "drop",
value: function drop(callback) {
localStorage[this._dbName] = JSON.stringify({ todos: [] });
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
}]);
return Store;
}();
exports.default = Store;
},{}],6:[function(require,module,exports){
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function escape(str) {
return str && reHasUnescapedHtml.test(str) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
};
var escapeHtmlChar = function escapeHtmlChar(chr) {
return htmlEscapes[chr];
};
var Template = function () {
function Template() {
_classCallCheck(this, Template);
this.defaultTemplate = '\n\t\t\t<li data-id="{{id}}" class="{{completed}}">\n\t\t\t\t<div class="view">\n\t\t\t\t\t<input class="toggle" type="checkbox" {{checked}}>\n\t\t\t\t\t<label>{{title}}</label>\n\t\t\t\t\t<button class="destroy"></button>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t';
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* })
*/
_createClass(Template, [{
key: 'show',
value: function show(data) {
var _this = this;
var view = data.map(function (d) {
var template = _this.defaultTemplate;
var completed = d.completed ? 'completed' : '';
var checked = d.completed ? 'checked' : '';
return _this.defaultTemplate.replace('{{id}}', d.id).replace('{{title}}', escape(d.title)).replace('{{completed}}', completed).replace('{{checked}}', checked);
});
return view.join('');
}
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
}, {
key: 'itemCounter',
value: function itemCounter(activeTodos) {
var plural = activeTodos === 1 ? '' : 's';
return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
}
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
}, {
key: 'clearCompletedButton',
value: function clearCompletedButton(completedTodos) {
return completedTodos > 0 ? 'Clear completed' : '';
}
}]);
return Template;
}();
exports.default = Template;
},{}],7:[function(require,module,exports){
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
var _helpers = require('./helpers');
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _itemId = function _itemId(element) {
return parseInt((0, _helpers.$parent)(element, 'li').dataset.id, 10);
};
var _setFilter = function _setFilter(currentPage) {
(0, _helpers.qs)('.filters .selected').className = '';
(0, _helpers.qs)('.filters [href="#/' + currentPage + '"]').className = 'selected';
};
var _elementComplete = function _elementComplete(id, completed) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
(0, _helpers.qs)('input', listItem).checked = completed;
};
var _editItem = function _editItem(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className += ' editing';
var input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
var View = function () {
function View(template) {
var _this = this;
_classCallCheck(this, View);
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = (0, _helpers.qs)('.todo-list');
this.$todoItemCounter = (0, _helpers.qs)('.todo-count');
this.$clearCompleted = (0, _helpers.qs)('.clear-completed');
this.$main = (0, _helpers.qs)('.main');
this.$footer = (0, _helpers.qs)('.footer');
this.$toggleAll = (0, _helpers.qs)('.toggle-all');
this.$newTodo = (0, _helpers.qs)('.new-todo');
this.viewCommands = {
showEntries: function showEntries(parameter) {
return _this.$todoList.innerHTML = _this.template.show(parameter);
},
removeItem: function removeItem(parameter) {
return _this._removeItem(parameter);
},
updateElementCount: function updateElementCount(parameter) {
return _this.$todoItemCounter.innerHTML = _this.template.itemCounter(parameter);
},
clearCompletedButton: function clearCompletedButton(parameter) {
return _this._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function contentBlockVisibility(parameter) {
return _this.$main.style.display = _this.$footer.style.display = parameter.visible ? 'block' : 'none';
},
toggleAll: function toggleAll(parameter) {
return _this.$toggleAll.checked = parameter.checked;
},
setFilter: function setFilter(parameter) {
return _setFilter(parameter);
},
clearNewTodo: function clearNewTodo(parameter) {
return _this.$newTodo.value = '';
},
elementComplete: function elementComplete(parameter) {
return _elementComplete(parameter.id, parameter.completed);
},
editItem: function editItem(parameter) {
return _editItem(parameter.id, parameter.title);
},
editItemDone: function editItemDone(parameter) {
return _this._editItemDone(parameter.id, parameter.title);
}
};
}
_createClass(View, [{
key: '_removeItem',
value: function _removeItem(id) {
var elem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (elem) {
this.$todoList.removeChild(elem);
}
}
}, {
key: '_clearCompletedButton',
value: function _clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
}
}, {
key: '_editItemDone',
value: function _editItemDone(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
var input = (0, _helpers.qs)('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
(0, _helpers.qsa)('label', listItem).forEach(function (label) {
return label.textContent = title;
});
}
}, {
key: 'render',
value: function render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
}
}, {
key: '_bindItemEditDone',
value: function _bindItemEditDone(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: _itemId(this),
title: this.value
});
}
});
// Remove the cursor from the input when you hit enter just like if it were a real form
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
});
}
}, {
key: '_bindItemEditCancel',
value: function _bindItemEditCancel(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
var id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id: id });
}
});
}
}, {
key: 'bind',
value: function bind(event, handler) {
var _this2 = this;
switch (event) {
case 'newTodo':
(0, _helpers.$on)(this.$newTodo, 'change', function () {
return handler(_this2.$newTodo.value);
});
break;
case 'removeCompleted':
(0, _helpers.$on)(this.$clearCompleted, 'click', handler);
break;
case 'toggleAll':
(0, _helpers.$on)(this.$toggleAll, 'click', function () {
handler({ completed: this.checked });
});
break;
case 'itemEdit':
(0, _helpers.$delegate)(this.$todoList, 'li label', 'dblclick', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemRemove':
(0, _helpers.$delegate)(this.$todoList, '.destroy', 'click', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemToggle':
(0, _helpers.$delegate)(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
}
}]);
return View;
}();
exports.default = View;
},{"./helpers":3}]},{},[1]);
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Controller = function () {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var _this = this;
_classCallCheck(this, Controller);
this.model = model;
this.view = view;
this.view.bind('newTodo', function (title) {
return _this.addItem(title);
});
this.view.bind('itemEdit', function (item) {
return _this.editItem(item.id);
});
this.view.bind('itemEditDone', function (item) {
return _this.editItemSave(item.id, item.title);
});
this.view.bind('itemEditCancel', function (item) {
return _this.editItemCancel(item.id);
});
this.view.bind('itemRemove', function (item) {
return _this.removeItem(item.id);
});
this.view.bind('itemToggle', function (item) {
return _this.toggleComplete(item.id, item.completed);
});
this.view.bind('removeCompleted', function () {
return _this.removeCompletedItems();
});
this.view.bind('toggleAll', function (status) {
return _this.toggleAll(status.completed);
});
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
_createClass(Controller, [{
key: 'setView',
value: function setView(hash) {
var route = hash.split('/')[1];
var page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
}, {
key: 'showAll',
value: function showAll() {
var _this2 = this;
this.model.read(function (data) {
return _this2.view.render('showEntries', data);
});
}
/**
* Renders all active tasks
*/
}, {
key: 'showActive',
value: function showActive() {
var _this3 = this;
this.model.read({ completed: false }, function (data) {
return _this3.view.render('showEntries', data);
});
}
/**
* Renders all completed tasks
*/
}, {
key: 'showCompleted',
value: function showCompleted() {
var _this4 = this;
this.model.read({ completed: true }, function (data) {
return _this4.view.render('showEntries', data);
});
}
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
}, {
key: 'addItem',
value: function addItem(title) {
var _this5 = this;
if (title.trim() === '') {
return;
}
this.model.create(title, function () {
_this5.view.render('clearNewTodo');
_this5._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
}, {
key: 'editItem',
value: function editItem(id) {
var _this6 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this6.view.render('editItem', { id: id, title: title });
});
}
/*
* Finishes the item editing mode successfully.
*/
}, {
key: 'editItemSave',
value: function editItemSave(id, title) {
var _this7 = this;
title = title.trim();
if (title.length !== 0) {
this.model.update(id, { title: title }, function () {
_this7.view.render('editItemDone', { id: id, title: title });
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
*/
}, {
key: 'editItemCancel',
value: function editItemCancel(id) {
var _this8 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this8.view.render('editItemDone', { id: id, title: title });
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
*/
}, {
key: 'removeItem',
value: function removeItem(id) {
var _this9 = this;
this.model.remove(id, function () {
return _this9.view.render('removeItem', id);
});
this._filter();
}
/**
* Will remove all completed items from the DOM and storage.
*/
}, {
key: 'removeCompletedItems',
value: function removeCompletedItems() {
var _this10 = this;
this.model.read({ completed: true }, function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
_this10.removeItem(item.id);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
this._filter();
}
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
}, {
key: 'toggleComplete',
value: function toggleComplete(id, completed, silent) {
var _this11 = this;
this.model.update(id, { completed: completed }, function () {
_this11.view.render('elementComplete', { id: id, completed: completed });
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
}, {
key: 'toggleAll',
value: function toggleAll(completed) {
var _this12 = this;
this.model.read({ completed: !completed }, function (data) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
_this12.toggleComplete(item.id, completed, true);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
});
this._filter();
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
}, {
key: '_updateCount',
value: function _updateCount() {
var _this13 = this;
this.model.getCount(function (todos) {
var completed = todos.completed;
var visible = completed > 0;
var checked = completed === todos.total;
_this13.view.render('updateElementCount', todos.active);
_this13.view.render('clearCompletedButton', { completed: completed, visible: visible });
_this13.view.render('toggleAll', { checked: checked });
_this13.view.render('contentBlockVisibility', { visible: todos.total > 0 });
});
}
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
}, {
key: '_filter',
value: function _filter(force) {
var active = this._activeRoute;
var activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]()
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
}
this._lastActiveRoute = activeRoute;
}
/**
* Simply updates the filter nav's selected states
*/
}, {
key: '_updateFilter',
value: function _updateFilter(currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.render('setFilter', currentPage);
}
}]);
return Controller;
}();
exports.default = Controller;
\ No newline at end of file
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.qs = qs;
exports.qsa = qsa;
exports.$on = $on;
exports.$delegate = $delegate;
exports.$parent = $parent;
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
function $on(target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
}
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
function $delegate(target, selector, type, handler) {
var dispatchEvent = function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = qsa(selector, target);
var hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
\ No newline at end of file
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
var Model = function () {
function Model(storage) {
_classCallCheck(this, Model);
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
_createClass(Model, [{
key: 'create',
value: function create(title, callback) {
title = title || '';
var newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
}
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func) // Will find the model with an ID of 1
* model.read('1') // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' })
*/
}, {
key: 'read',
value: function read(query, callback) {
var queryType = typeof query === 'undefined' ? 'undefined' : _typeof(query);
if (queryType === 'function') {
this.storage.findAll(query);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({ id: query }, callback);
} else {
this.storage.find(query, callback);
}
}
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
}, {
key: 'update',
value: function update(id, data, callback) {
this.storage.save(data, callback, id);
}
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
}, {
key: 'remove',
value: function remove(id, callback) {
this.storage.remove(id, callback);
}
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
}, {
key: 'removeAll',
value: function removeAll(callback) {
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
}, {
key: 'getCount',
value: function getCount(callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var todo = _step.value;
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
callback(todos);
});
}
}]);
return Model;
}();
exports.default = Model;
\ No newline at end of file
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/*jshint eqeqeq:false */
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
var Store = function () {
function Store(name, callback) {
_classCallCheck(this, Store);
this._dbName = name;
if (!localStorage[name]) {
var data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
if (callback) {
callback.call(this, JSON.parse(localStorage[name]));
}
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* })
*/
_createClass(Store, [{
key: "find",
value: function find(query, callback) {
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q]) {
return false;
}
}
return true;
}));
}
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
}, {
key: "findAll",
value: function findAll(callback) {
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
}, {
key: "save",
value: function save(updateData, callback, id) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < len; i++) {
if (todos[i].id === id) {
for (var key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, [updateData]);
}
}
}
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
}, {
key: "remove",
value: function remove(id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
}, {
key: "drop",
value: function drop(callback) {
localStorage[this._dbName] = JSON.stringify({ todos: [] });
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
}]);
return Store;
}();
exports.default = Store;
\ No newline at end of file
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function escape(str) {
return str && reHasUnescapedHtml.test(str) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
};
var escapeHtmlChar = function escapeHtmlChar(chr) {
return htmlEscapes[chr];
};
var Template = function () {
function Template() {
_classCallCheck(this, Template);
this.defaultTemplate = '\n\t\t\t<li data-id="{{id}}" class="{{completed}}">\n\t\t\t\t<div class="view">\n\t\t\t\t\t<input class="toggle" type="checkbox" {{checked}}>\n\t\t\t\t\t<label>{{title}}</label>\n\t\t\t\t\t<button class="destroy"></button>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t';
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* })
*/
_createClass(Template, [{
key: 'show',
value: function show(data) {
var _this = this;
var view = data.map(function (d) {
var template = _this.defaultTemplate;
var completed = d.completed ? 'completed' : '';
var checked = d.completed ? 'checked' : '';
return _this.defaultTemplate.replace('{{id}}', d.id).replace('{{title}}', escape(d.title)).replace('{{completed}}', completed).replace('{{checked}}', checked);
});
return view.join('');
}
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
}, {
key: 'itemCounter',
value: function itemCounter(activeTodos) {
var plural = activeTodos === 1 ? '' : 's';
return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
}
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
}, {
key: 'clearCompletedButton',
value: function clearCompletedButton(completedTodos) {
return completedTodos > 0 ? 'Clear completed' : '';
}
}]);
return Template;
}();
exports.default = Template;
\ No newline at end of file
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
var _helpers = require('./helpers');
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _itemId = function _itemId(element) {
return parseInt((0, _helpers.$parent)(element, 'li').dataset.id, 10);
};
var _setFilter = function _setFilter(currentPage) {
(0, _helpers.qs)('.filters .selected').className = '';
(0, _helpers.qs)('.filters [href="#/' + currentPage + '"]').className = 'selected';
};
var _elementComplete = function _elementComplete(id, completed) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
(0, _helpers.qs)('input', listItem).checked = completed;
};
var _editItem = function _editItem(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className += ' editing';
var input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
var View = function () {
function View(template) {
var _this = this;
_classCallCheck(this, View);
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = (0, _helpers.qs)('.todo-list');
this.$todoItemCounter = (0, _helpers.qs)('.todo-count');
this.$clearCompleted = (0, _helpers.qs)('.clear-completed');
this.$main = (0, _helpers.qs)('.main');
this.$footer = (0, _helpers.qs)('.footer');
this.$toggleAll = (0, _helpers.qs)('.toggle-all');
this.$newTodo = (0, _helpers.qs)('.new-todo');
this.viewCommands = {
showEntries: function showEntries(parameter) {
return _this.$todoList.innerHTML = _this.template.show(parameter);
},
removeItem: function removeItem(parameter) {
return _this._removeItem(parameter);
},
updateElementCount: function updateElementCount(parameter) {
return _this.$todoItemCounter.innerHTML = _this.template.itemCounter(parameter);
},
clearCompletedButton: function clearCompletedButton(parameter) {
return _this._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function contentBlockVisibility(parameter) {
return _this.$main.style.display = _this.$footer.style.display = parameter.visible ? 'block' : 'none';
},
toggleAll: function toggleAll(parameter) {
return _this.$toggleAll.checked = parameter.checked;
},
setFilter: function setFilter(parameter) {
return _setFilter(parameter);
},
clearNewTodo: function clearNewTodo(parameter) {
return _this.$newTodo.value = '';
},
elementComplete: function elementComplete(parameter) {
return _elementComplete(parameter.id, parameter.completed);
},
editItem: function editItem(parameter) {
return _editItem(parameter.id, parameter.title);
},
editItemDone: function editItemDone(parameter) {
return _this._editItemDone(parameter.id, parameter.title);
}
};
}
_createClass(View, [{
key: '_removeItem',
value: function _removeItem(id) {
var elem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (elem) {
this.$todoList.removeChild(elem);
}
}
}, {
key: '_clearCompletedButton',
value: function _clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
}
}, {
key: '_editItemDone',
value: function _editItemDone(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
var input = (0, _helpers.qs)('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
(0, _helpers.qsa)('label', listItem).forEach(function (label) {
return label.textContent = title;
});
}
}, {
key: 'render',
value: function render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
}
}, {
key: '_bindItemEditDone',
value: function _bindItemEditDone(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: _itemId(this),
title: this.value
});
}
});
// Remove the cursor from the input when you hit enter just like if it were a real form
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
});
}
}, {
key: '_bindItemEditCancel',
value: function _bindItemEditCancel(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
var id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id: id });
}
});
}
}, {
key: 'bind',
value: function bind(event, handler) {
var _this2 = this;
switch (event) {
case 'newTodo':
(0, _helpers.$on)(this.$newTodo, 'change', function () {
return handler(_this2.$newTodo.value);
});
break;
case 'removeCompleted':
(0, _helpers.$on)(this.$clearCompleted, 'click', handler);
break;
case 'toggleAll':
(0, _helpers.$on)(this.$toggleAll, 'click', function () {
handler({ completed: this.checked });
});
break;
case 'itemEdit':
(0, _helpers.$delegate)(this.$todoList, 'li label', 'dblclick', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemRemove':
(0, _helpers.$delegate)(this.$todoList, '.destroy', 'click', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemToggle':
(0, _helpers.$delegate)(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
}
}]);
return View;
}();
exports.default = View;
\ No newline at end of file
<!DOCTYPE html> <!doctype html>
<html> <html lang="en" data-framework="es6">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>ES6 • TodoMVC</title> <title>Vanilla ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head> </head>
<body> <body>
...@@ -12,31 +11,29 @@ ...@@ -12,31 +11,29 @@
<h1>todos</h1> <h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus> <input class="new-todo" placeholder="What needs to be done?" autofocus>
</header> </header>
<section style="display:none" class="main">
<section class="main">
<input class="toggle-all" type="checkbox"> <input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label> <label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul> <ul class="todo-list"></ul>
<footer class="footer">
<span class="todo-count"></span>
<div class="filters">
<a href="#/" class="selected">All</a>
<a href="#/active">Active</a>
<a href="#/completed">Completed</a>
</div>
<button class="clear-completed">Clear completed</button>
</footer>
</section> </section>
<footer class="footer">
<span class="todo-count"></span>
<ul class="filters">
<li><a href="#/" class="selected">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section> </section>
<footer class="info"> <footer class="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
<br>
<p>Written by <a href="http://twitter.com/lukeed05">Luke Edwards</a></p> <p>Written by <a href="http://twitter.com/lukeed05">Luke Edwards</a></p>
<p>Refactored by <a href="https://github.com/xorgy">Aaron Muir Hamilton</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
<script src="dist/bundle.js"></script> <script src="dist/bundle.js"></script>
<script src="node_modules/todomvc-common/base.js"></script>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
</body> </body>
</html> </html>
...@@ -17,8 +17,7 @@ button { ...@@ -17,8 +17,7 @@ button {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
} }
body { body {
...@@ -30,8 +29,7 @@ body { ...@@ -30,8 +29,7 @@ body {
max-width: 550px; max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
font-weight: 300; font-weight: 300;
} }
...@@ -100,8 +98,7 @@ input[type="checkbox"] { ...@@ -100,8 +98,7 @@ input[type="checkbox"] {
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
} }
.new-todo { .new-todo {
...@@ -163,17 +160,19 @@ label[for='toggle-all'] { ...@@ -163,17 +160,19 @@ label[for='toggle-all'] {
padding: 0; padding: 0;
} }
.todo-list li.editing button,
.todo-list li.editing label,
.todo-list li.editing .toggle{
display: none;
}
.todo-list li.editing .edit { .todo-list li.editing .edit {
display: block; display: block;
width: 506px; width: 506px;
padding: 13px 17px 12px 17px; padding: 12px 16px;
margin: 0 0 0 43px; margin: 0 0 0 43px;
} }
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle { .todo-list li .toggle {
text-align: center; text-align: center;
width: 40px; width: 40px;
...@@ -287,11 +286,7 @@ label[for='toggle-all'] { ...@@ -287,11 +286,7 @@ label[for='toggle-all'] {
left: 0; left: 0;
} }
.filters li { .filters a {
display: inline;
}
.filters li a {
color: inherit; color: inherit;
margin: 3px; margin: 3px;
padding: 3px 7px; padding: 3px 7px;
...@@ -300,12 +295,12 @@ label[for='toggle-all'] { ...@@ -300,12 +295,12 @@ label[for='toggle-all'] {
border-radius: 3px; border-radius: 3px;
} }
.filters li a.selected, .filters a.selected,
.filters li a:hover { .filters a:hover {
border-color: rgba(175, 47, 47, 0.1); border-color: rgba(175, 47, 47, 0.1);
} }
.filters li a.selected { .filters a.selected {
border-color: rgba(175, 47, 47, 0.2); border-color: rgba(175, 47, 47, 0.2);
} }
......
/* global _ */
(function () {
'use strict';
/* jshint ignore:start */
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
}
/* jshint ignore:end */
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base = location.href.indexOf('examples/');
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').dataset.framework;
}
this.template = template;
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append();
}
this.fetchIssueCount();
}
Learn.prototype.append = function (opts) {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
if (opts && opts.backend) {
// Remove demo link
var sourceLinks = aside.querySelector('.source-links');
var heading = sourceLinks.firstElementChild;
var sourceLink = sourceLinks.lastElementChild;
// Correct link path
var href = sourceLink.getAttribute('href');
sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
} else {
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
}
});
}
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect();
getFile('learn.json', Learn);
})();
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"compile": "babel src --presets es2015 --out-dir=dist && browserify dist/app.js > dist/bundle.js", "compile": "java -jar node_modules/google-closure-compiler/compiler.jar -O ADVANCED --language_in=ES6_STRICT --new_type_inf --js_output_file='dist/bundle.js' 'src/**.js' -W VERBOSE",
"prepublish": "npm run compile" "prepublish": "npm run compile"
}, },
"dependencies": { "dependencies": {
...@@ -9,8 +9,6 @@ ...@@ -9,8 +9,6 @@
"todomvc-common": "^1.0.2" "todomvc-common": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.1.0", "google-closure-compiler": "^20160315.2.0"
"babel-preset-es2015": "^6.1.18",
"browserify": "^12.0.1"
} }
} }
import Controller from './controller'; import Controller from './controller';
import * as helpers from './helpers'; import {$on} from './helpers';
import Template from './template'; import Template from './template';
import Store from './store'; import Store from './store';
import Model from './model';
import View from './view'; import View from './view';
const $on = helpers.$on; const store = new Store('todos-vanilla-es6');
const setView = () => todo.controller.setView(document.location.hash);
class Todo { const template = new Template();
/** const view = new View(template);
* Init new Todo List
* @param {string} The name of your list
*/
constructor(name) {
this.storage = new Store(name);
this.model = new Model(this.storage);
this.template = new Template(); /**
this.view = new View(this.template); * @type {Controller}
*/
this.controller = new Controller(this.model, this.view); const controller = new Controller(store, view);
}
}
const todo = new Todo('todos-vanillajs');
const setView = () => controller.setView(document.location.hash);
$on(window, 'load', setView); $on(window, 'load', setView);
$on(window, 'hashchange', setView); $on(window, 'hashchange', setView);
import {emptyItemQuery} from './item';
import Store from './store';
import View from './view';
export default class Controller { export default class Controller {
/** /**
* Take a model & view, then act as controller between them * @param {!Store} store A Store instance
* @param {object} model The model instance * @param {!View} view A View instance
* @param {object} view The view instance
*/ */
constructor(model, view) { constructor(store, view) {
this.model = model; this.store = store;
this.view = view; this.view = view;
this.view.bind('newTodo', title => this.addItem(title)); view.bindAddItem(this.addItem.bind(this));
this.view.bind('itemEdit', item => this.editItem(item.id)); view.bindEditItemSave(this.editItemSave.bind(this));
this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title)); view.bindEditItemCancel(this.editItemCancel.bind(this));
this.view.bind('itemEditCancel', item => this.editItemCancel(item.id)); view.bindRemoveItem(this.removeItem.bind(this));
this.view.bind('itemRemove', item => this.removeItem(item.id)); view.bindToggleItem((id, completed) => {
this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed)); this.toggleCompleted(id, completed);
this.view.bind('removeCompleted', () => this.removeCompletedItems()); this._filter();
this.view.bind('toggleAll', status => this.toggleAll(status.completed)); });
} view.bindRemoveCompleted(this.removeCompletedItems.bind(this));
view.bindToggleAll(this.toggleAll.bind(this));
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
setView(hash) {
const route = hash.split('/')[1];
const page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
showAll() {
this.model.read(data => this.view.render('showEntries', data));
}
/** this._activeRoute = '';
* Renders all active tasks this._lastActiveRoute = null;
*/
showActive() {
this.model.read({completed: false}, data => this.view.render('showEntries', data));
} }
/** /**
* Renders all completed tasks * Set and render the active route.
*
* @param {string} raw '' | '#/' | '#/active' | '#/completed'
*/ */
showCompleted() { setView(raw) {
this.model.read({completed: true}, data => this.view.render('showEntries', data)); const route = raw.replace(/^#\//, '');
this._activeRoute = route;
this._filter();
this.view.updateFilterButtons(route);
} }
/** /**
* An event to fire whenever you want to add an item. Simply pass in the event * Add an Item to the Store and display it in the list.
* object and it'll handle the DOM insertion and saving of the new item. *
* @param {!string} title Title of the new item
*/ */
addItem(title) { addItem(title) {
if (title.trim() === '') { this.store.insert({
return; id: Date.now(),
} title,
completed: false
this.model.create(title, () => { }, () => {
this.view.render('clearNewTodo'); this.view.clearNewTodo();
this._filter(true); this._filter(true);
}); });
} }
/* /**
* Triggers the item editing mode. * Save an Item in edit.
*/ *
editItem(id) { * @param {number} id ID of the Item in edit
this.model.read(id, data => { * @param {!string} title New title for the Item in edit
const title = data[0].title;
this.view.render('editItem', {id, title});
});
}
/*
* Finishes the item editing mode successfully.
*/ */
editItemSave(id, title) { editItemSave(id, title) {
title = title.trim(); if (title.length) {
this.store.update({id, title}, () => {
if (title.length !== 0) { this.view.editItemDone(id, title);
this.model.update(id, {title}, () => {
this.view.render('editItemDone', {id, title});
}); });
} else { } else {
this.removeItem(id); this.removeItem(id);
} }
} }
/* /**
* Cancels the item editing mode. * Cancel the item editing mode.
*
* @param {!number} id ID of the Item in edit
*/ */
editItemCancel(id) { editItemCancel(id) {
this.model.read(id, data => { this.store.find({id}, data => {
const title = data[0].title; const title = data[0].title;
this.view.render('editItemDone', {id, title}); this.view.editItemDone(id, title);
}); });
} }
/** /**
* Find the DOM element with given ID, * Remove the data and elements related to an Item.
* Then remove it from DOM & Storage *
* @param {!number} id Item ID of item to remove
*/ */
removeItem(id) { removeItem(id) {
this.model.remove(id, () => this.view.render('removeItem', id)); this.store.remove({id}, () => {
this._filter(); this._filter();
this.view.removeItem(id);
});
} }
/** /**
* Will remove all completed items from the DOM and storage. * Remove all completed items.
*/ */
removeCompletedItems() { removeCompletedItems() {
this.model.read({completed: true}, data => { this.store.remove({completed: true}, this._filter.bind(this));
for (let item of data) {
this.removeItem(item.id);
}
});
this._filter();
} }
/** /**
* Give it an ID of a model and a checkbox and it will update the item * Update an Item in storage based on the state of completed.
* in storage based on the checkbox's state.
* *
* @param {number} id The ID of the element to complete or uncomplete * @param {!number} id ID of the target Item
* @param {object} checkbox The checkbox to check the state of complete * @param {!boolean} completed Desired completed state
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/ */
toggleComplete(id, completed, silent) { toggleCompleted(id, completed) {
this.model.update(id, {completed}, () => { this.store.update({id, completed}, () => {
this.view.render('elementComplete', {id, completed}); this.view.setItemComplete(id, completed);
}); });
if (!silent) {
this._filter();
}
} }
/** /**
* Will toggle ALL checkboxes' on/off state and completeness of models. * Set all items to complete or active.
* Just pass in the event object. *
* @param {boolean} completed Desired completed state
*/ */
toggleAll(completed) { toggleAll(completed) {
this.model.read({completed: !completed}, data => { this.store.find({completed: !completed}, data => {
for (let item of data) { for (let {id} of data) {
this.toggleComplete(item.id, completed, true); this.toggleCompleted(id, completed);
} }
}); });
...@@ -155,58 +129,31 @@ export default class Controller { ...@@ -155,58 +129,31 @@ export default class Controller {
} }
/** /**
* Updates the pieces of the page which change depending on the remaining * Refresh the list based on the current route.
* number of todos. *
*/ * @param {boolean} [force] Force a re-paint of the list
_updateCount() {
this.model.getCount(todos => {
const completed = todos.completed;
const visible = completed > 0;
const checked = completed === todos.total;
this.view.render('updateElementCount', todos.active);
this.view.render('clearCompletedButton', {completed, visible});
this.view.render('toggleAll', {checked});
this.view.render('contentBlockVisibility', {visible: todos.total > 0});
});
}
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/ */
_filter(force) { _filter(force) {
const active = this._activeRoute; const route = this._activeRoute;
const activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
if (force || this._lastActiveRoute !== '' || this._lastActiveRoute !== route) {
// Update the elements on the page, which change with each completed todo /* jscs:disable disallowQuotedKeysInObjects */
this._updateCount(); this.store.find({
'': emptyItemQuery,
// If the last active route isn't "All", or we're switching routes, we 'active': {completed: false},
// re-create the todo item elements, calling: 'completed': {completed: true}
// this.show[All|Active|Completed]() }[route], this.view.showItems.bind(this.view));
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) { /* jscs:enable disallowQuotedKeysInObjects */
this['show' + activeRoute]();
} }
this._lastActiveRoute = activeRoute; this.store.count((total, active, completed) => {
} this.view.setItemsLeft(active);
this.view.setClearCompletedButtonVisibility(completed);
/**
* Simply updates the filter nav's selected states
*/
_updateFilter(currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') { this.view.setCompleteAllCheckbox(completed === total);
this._activeRoute = 'All'; this.view.setMainVisibility(total);
} });
this._filter();
this.view.render('setFilter', currentPage); this._lastActiveRoute = route;
} }
} }
// Allow for looping on nodes by chaining: /**
// qsa('.foo').forEach(function () {}) * querySelector wrapper
NodeList.prototype.forEach = Array.prototype.forEach; *
* @param {string} selector Selector to query
// Get element(s) by CSS selector: * @param {Element} [scope] Optional scope element for the selector
*/
export function qs(selector, scope) { export function qs(selector, scope) {
return (scope || document).querySelector(selector); return (scope || document).querySelector(selector);
} }
export function qsa(selector, scope) { /**
return (scope || document).querySelectorAll(selector); * addEventListener wrapper
} *
* @param {Element|Window} target Target Element
// addEventListener wrapper: * @param {string} type Event name to bind to
export function $on(target, type, callback, useCapture) { * @param {Function} callback Event callback
target.addEventListener(type, callback, !!useCapture); * @param {boolean} [capture] Capture the event
*/
export function $on(target, type, callback, capture) {
target.addEventListener(type, callback, !!capture);
} }
// Attach a handler to event for all elements that match the selector, /**
// now or in the future, based on a root element * Attach a handler to an event for all elements matching a selector.
export function $delegate(target, selector, type, handler) { *
* @param {Element} target Element which the event must bubble to
* @param {string} selector Selector to match
* @param {string} type Event name
* @param {Function} handler Function called when the event bubbles to target
* from an element matching selector
* @param {boolean} [capture] Capture the event
*/
export function $delegate(target, selector, type, handler, capture) {
const dispatchEvent = event => { const dispatchEvent = event => {
const targetElement = event.target; const targetElement = event.target;
const potentialElements = qsa(selector, target); const potentialElements = target.querySelectorAll(selector);
const hasMatch = Array.from(potentialElements).includes(targetElement); let i = potentialElements.length;
if (hasMatch) { while (i--) {
handler.call(targetElement, event); if (potentialElements[i] === targetElement) {
handler.call(targetElement, event);
break;
}
} }
}; };
// https://developer.mozilla.org/en-US/docs/Web/Events/blur $on(target, type, dispatchEvent, !!capture);
const useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
} }
// Find the element's parent with the given tag name: /**
// $parent(qs('a'), 'div') * Encode less-than and ampersand characters with entity codes to make user-
export function $parent(element, tagName) { * provided text safe to parse as HTML.
if (!element.parentNode) { *
return; * @param {string} s String to escape
} *
* @returns {string} String with unsafe characters escaped with entity codes
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { */
return element.parentNode; export const escapeForHTML = s => s.replace(/[&<]/g, c => c === '&' ? '&amp;' : '&lt;');
}
return $parent(element.parentNode, tagName);
}
/**
* @typedef {!{id: number, completed: boolean, title: string}}
*/
export var Item;
/**
* @typedef {!Array<Item>}
*/
export var ItemList;
/**
* Enum containing a known-empty record type, matching only empty records unlike Object.
*
* @enum {Object}
*/
const Empty = {
Record: {}
};
/**
* Empty ItemQuery type, based on the Empty @enum.
*
* @typedef {Empty}
*/
export var EmptyItemQuery;
/**
* Reference to the only EmptyItemQuery instance.
*
* @type {EmptyItemQuery}
*/
export const emptyItemQuery = Empty.Record;
/**
* @typedef {!({id: number}|{completed: boolean}|EmptyItemQuery)}
*/
export var ItemQuery;
/**
* @typedef {!({id: number, title: string}|{id: number, completed: boolean})}
*/
export var ItemUpdate;
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
export default class Model {
constructor(storage) {
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
create(title, callback){
title = title || '';
const newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
}
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func) // Will find the model with an ID of 1
* model.read('1') // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' })
*/
read(query, callback){
const queryType = typeof query;
if (queryType === 'function') {
this.storage.findAll(query);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({id: query}, callback);
} else {
this.storage.find(query, callback);
}
}
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
update(id, data, callback){
this.storage.save(data, callback, id);
}
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
remove(id, callback){
this.storage.remove(id, callback);
}
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
removeAll(callback){
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
getCount(callback){
const todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(data => {
for (let todo of data) {
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
callback(todos);
});
}
}
/*jshint eqeqeq:false */ import {Item, ItemList, ItemQuery, ItemUpdate, emptyItemQuery} from './item';
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
export default class Store { export default class Store {
/**
* @param {!string} name Database name
* @param {function()} [callback] Called when the Store is ready
*/
constructor(name, callback) { constructor(name, callback) {
this._dbName = name; /**
* @type {Storage}
if (!localStorage[name]) { */
const data = { const localStorage = window.localStorage;
todos: []
}; /**
* @type {ItemList}
localStorage[name] = JSON.stringify(data); */
} let liveTodos;
/**
* Read the local ItemList from localStorage.
*
* @returns {ItemList} Current array of todos
*/
this.getLocalStorage = () => {
return liveTodos || JSON.parse(localStorage.getItem(name) || '[]');
};
/**
* Write the local ItemList to localStorage.
*
* @param {ItemList} todos Array of todos to write
*/
this.setLocalStorage = (todos) => {
localStorage.setItem(name, JSON.stringify(liveTodos = todos));
};
if (callback) { if (callback) {
callback.call(this, JSON.parse(localStorage[name])); callback();
} }
} }
/** /**
* Finds items based on a query given as a JS object * Find items with properties matching those on query.
* *
* @param {object} query The query to match against (i.e. {foo: 'bar'}) * @param {ItemQuery} query Query to match
* @param {function} callback The callback to fire when the query has * @param {function(ItemList)} callback Called when the query is done
* completed running
* *
* @example * @example
* db.find({foo: 'bar', hello: 'world'}, function (data) { * db.find({completed: true}, data => {
* // data will return any items that have foo: bar and * // data shall contain items whose completed properties are true
* // hello: world in their properties * })
* })
*/ */
find(query, callback){ find(query, callback) {
const todos = JSON.parse(localStorage[this._dbName]).todos; const todos = this.getLocalStorage();
let k;
callback.call(this, todos.filter(todo => { callback(todos.filter(todo => {
for (let q in query) { for (k in query) {
if (query[q] !== todo[q]) { if (query[k] !== todo[k]) {
return false; return false;
} }
} }
...@@ -52,93 +65,90 @@ export default class Store { ...@@ -52,93 +65,90 @@ export default class Store {
} }
/** /**
* Will retrieve all data from the collection * Update an item in the Store.
* *
* @param {function} callback The callback to fire upon retrieving data * @param {ItemUpdate} update Record with an id and a property to update
* @param {function()} [callback] Called when partialRecord is applied
*/ */
findAll(callback){ update(update, callback) {
const id = update.id;
const todos = this.getLocalStorage();
let i = todos.length;
let k;
while (i--) {
if (todos[i].id === id) {
for (k in update) {
todos[i][k] = update[k];
}
break;
}
}
this.setLocalStorage(todos);
if (callback) { if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos); callback();
} }
} }
/** /**
* Will save the given data to the DB. If no item exists it will create a new * Insert an item into the Store.
* item, otherwise it'll simply update an existing item's properties
* *
* @param {object} updateData The data to save back into the DB * @param {Item} item Item to insert
* @param {function} callback The callback to fire after saving * @param {function()} [callback] Called when item is inserted
* @param {number} id An optional param to enter an ID of an item to update
*/ */
save(updateData, callback, id){ insert(item, callback) {
const data = JSON.parse(localStorage[this._dbName]); const todos = this.getLocalStorage();
const todos = data.todos; todos.push(item);
const len = todos.length; this.setLocalStorage(todos);
// If an ID was actually given, find the item and update each property
if (id) {
for (let i = 0; i < len; i++) {
if (todos[i].id === id) {
for (let key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data); if (callback) {
callback();
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, [updateData]);
}
} }
} }
/** /**
* Will remove an item from the Store based on its ID * Remove items from the Store based on a query.
* *
* @param {number} id The ID of the item you want to remove * @param {ItemQuery} query Query matching the items to remove
* @param {function} callback The callback to fire after saving * @param {function(ItemList)|function()} [callback] Called when records matching query are removed
*/ */
remove(id, callback){ remove(query, callback) {
const data = JSON.parse(localStorage[this._dbName]); let k;
const todos = data.todos;
const len = todos.length; const todos = this.getLocalStorage().filter(todo => {
for (k in query) {
for (let i = 0; i < todos.length; i++) { if (query[k] !== todo[k]) {
if (todos[i].id == id) { return true;
todos.splice(i, 1); }
break;
} }
} return false;
});
localStorage[this._dbName] = JSON.stringify(data); this.setLocalStorage(todos);
if (callback) { if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos); callback(todos);
} }
} }
/** /**
* Will drop all storage and start fresh * Count total, active, and completed todos.
* *
* @param {function} callback The callback to fire after dropping the data * @param {function(number, number, number)} callback Called when the count is completed
*/ */
drop(callback){ count(callback) {
localStorage[this._dbName] = JSON.stringify({todos: []}); this.find(emptyItemQuery, data => {
const total = data.length;
if (callback) { let i = total;
callback.call(this, JSON.parse(localStorage[this._dbName]).todos); let completed = 0;
}
while (i--) {
completed += data[i].completed;
}
callback(total, total - completed, completed);
});
} }
} }
const htmlEscapes = { import {ItemList} from './item';
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
const reUnescapedHtml = /[&<>"'`]/g; import {escapeForHTML} from './helpers';
const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
const escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
const escapeHtmlChar = chr => htmlEscapes[chr];
export default class Template { export default class Template {
constructor() {
this.defaultTemplate = `
<li data-id="{{id}}" class="{{completed}}">
<div class="view">
<input class="toggle" type="checkbox" {{checked}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
</li>
`;
}
/** /**
* Creates an <li> HTML string and returns it for placement in your app. * Format the contents of a todo list.
* *
* NOTE: In real life you should be using a templating engine such as Mustache * @param {ItemList} items Object containing keys you want to find in the template to replace.
* or Handlebars, however, this is a vanilla JS example. * @returns {!string} Contents for a todo list
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
* *
* @example * @example
* view.show({ * view.show({
* id: 1, * id: 1,
* title: "Hello World", * title: "Hello World",
* completed: 0, * completed: false,
* }) * })
*/ */
show(data){ itemList(items) {
const view = data.map(d => { return items.reduce((a, item) => a + `
const template = this.defaultTemplate; <li data-id="${item.id}"${item.completed ? ' class="completed"' : ''}>
const completed = d.completed ? 'completed' : ''; <input class="toggle" type="checkbox" ${item.completed ? 'checked' : ''}>
const checked = d.completed ? 'checked' : ''; <label>${escapeForHTML(item.title)}</label>
<button class="destroy"></button>
return this.defaultTemplate </li>`, '');
.replace('{{id}}', d.id)
.replace('{{title}}', escape(d.title))
.replace('{{completed}}', completed)
.replace('{{checked}}', checked);
});
return view.join('');
} }
/** /**
* Displays a counter of how many to dos are left to complete * Format the contents of an "items left" indicator.
* *
* @param {number} activeTodos The number of active todos. * @param {number} activeTodos Number of active todos
* @returns {string} String containing the count
*/
itemCounter(activeTodos){
const plural = activeTodos === 1 ? '' : 's';
return `<strong>${activeTodos}</strong> item${plural} left`;
}
/**
* Updates the text within the "Clear completed" button
* *
* @param {[type]} completedTodos The number of completed todos. * @returns {!string} Contents for an "items left" indicator
* @returns {string} String containing the count
*/ */
clearCompletedButton(completedTodos){ itemCounter(activeTodos) {
return (completedTodos > 0) ? 'Clear completed' : ''; return `${activeTodos} item${activeTodos !== 1 ? 's' : ''} left`;
} }
} }
import {qs, qsa, $on, $parent, $delegate} from './helpers'; import {ItemList} from './item';
import {qs, $on, $delegate} from './helpers';
import Template from './template';
const _itemId = element => parseInt($parent(element, 'li').dataset.id, 10); const _itemId = element => parseInt(element.parentNode.dataset.id, 10);
const ENTER_KEY = 13;
const ESCAPE_KEY = 27;
const _setFilter = currentPage => {
qs('.filters .selected').className = '';
qs(`.filters [href="#/${currentPage}"]`).className = 'selected';
};
const _elementComplete = (id, completed) => {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
qs('input', listItem).checked = completed;
};
const _editItem = (id, title) => {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
listItem.className += ' editing';
const input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
export default class View { export default class View {
/**
* @param {!Template} template A Template instance
*/
constructor(template) { constructor(template) {
this.template = template; this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = qs('.todo-list'); this.$todoList = qs('.todo-list');
this.$todoItemCounter = qs('.todo-count'); this.$todoItemCounter = qs('.todo-count');
this.$clearCompleted = qs('.clear-completed'); this.$clearCompleted = qs('.clear-completed');
this.$main = qs('.main'); this.$main = qs('.main');
this.$footer = qs('.footer');
this.$toggleAll = qs('.toggle-all'); this.$toggleAll = qs('.toggle-all');
this.$newTodo = qs('.new-todo'); this.$newTodo = qs('.new-todo');
$delegate(this.$todoList, 'li label', 'dblclick', ({target}) => {
this.editItem(target);
});
}
/**
* Put an item into edit mode.
*
* @param {!Element} target Target Item's label Element
*/
editItem(target) {
const listItem = target.parentElement;
this.viewCommands = { listItem.classList.add('editing');
showEntries: parameter => this.$todoList.innerHTML = this.template.show(parameter),
removeItem: parameter => this._removeItem(parameter), const input = document.createElement('input');
updateElementCount: parameter => this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter), input.className = 'edit';
clearCompletedButton: parameter => this._clearCompletedButton(parameter.completed, parameter.visible),
contentBlockVisibility: parameter => this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none', input.value = target.innerText;
toggleAll: parameter => this.$toggleAll.checked = parameter.checked, listItem.appendChild(input);
setFilter: parameter => _setFilter(parameter), input.focus();
clearNewTodo: parameter => this.$newTodo.value = '', }
elementComplete: parameter => _elementComplete(parameter.id, parameter.completed),
editItem: parameter => _editItem(parameter.id, parameter.title), /**
editItemDone: parameter => this._editItemDone(parameter.id, parameter.title), * Populate the todo list with a list of items.
}; *
} * @param {ItemList} items Array of items to display
*/
_removeItem(id) { showItems(items) {
this.$todoList.innerHTML = this.template.itemList(items);
}
/**
* Remove an item from the view.
*
* @param {number} id Item ID of the item to remove
*/
removeItem(id) {
const elem = qs(`[data-id="${id}"]`); const elem = qs(`[data-id="${id}"]`);
if (elem) { if (elem) {
...@@ -84,108 +64,170 @@ export default class View { ...@@ -84,108 +64,170 @@ export default class View {
} }
} }
_clearCompletedButton(completedCount, visible) { /**
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount); * Set the number in the 'items left' display.
this.$clearCompleted.style.display = visible ? 'block' : 'none'; *
* @param {number} itemsLeft Number of items left
*/
setItemsLeft(itemsLeft) {
this.$todoItemCounter.innerHTML = this.template.itemCounter(itemsLeft);
}
/**
* Set the visibility of the "Clear completed" button.
*
* @param {boolean|number} visible Desired visibility of the button
*/
setClearCompletedButtonVisibility(visible) {
this.$clearCompleted.style.display = !!visible ? 'block' : 'none';
}
/**
* Set the visibility of the main content and footer.
*
* @param {boolean|number} visible Desired visibility
*/
setMainVisibility(visible) {
this.$main.style.display = !!visible ? 'block' : 'none';
}
/**
* Set the checked state of the Complete All checkbox.
*
* @param {boolean|number} checked The desired checked state
*/
setCompleteAllCheckbox(checked) {
this.$toggleAll.checked = !!checked;
}
/**
* Change the appearance of the filter buttons based on the route.
*
* @param {string} route The current route
*/
updateFilterButtons(route) {
qs('.filters>.selected').className = '';
qs(`.filters>[href="#/${route}"]`).className = 'selected';
}
/**
* Clear the new todo input
*/
clearNewTodo() {
this.$newTodo.value = '';
} }
_editItemDone(id, title) { /**
* Render an item as either completed or not.
*
* @param {!number} id Item ID
* @param {!boolean} completed True if the item is completed
*/
setItemComplete(id, completed) {
const listItem = qs(`[data-id="${id}"]`); const listItem = qs(`[data-id="${id}"]`);
if (!listItem) { if (!listItem) {
return; return;
} }
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
qs('input', listItem).checked = completed;
}
/**
* Bring an item out of edit mode.
*
* @param {!number} id Item ID of the item in edit
* @param {!string} title New title for the item in edit
*/
editItemDone(id, title) {
const listItem = qs(`[data-id="${id}"]`);
const input = qs('input.edit', listItem); const input = qs('input.edit', listItem);
listItem.removeChild(input); listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', ''); listItem.classList.remove('editing');
qsa('label', listItem).forEach(label => label.textContent = title); qs('label', listItem).textContent = title;
} }
render(viewCmd, parameter) { /**
this.viewCommands[viewCmd](parameter); * @param {Function} handler Function called on synthetic event.
*/
bindAddItem(handler) {
$on(this.$newTodo, 'change', ({target}) => {
const title = target.value.trim();
if (title) {
handler(title);
}
});
} }
_bindItemEditDone(handler) { /**
const self = this; * @param {Function} handler Function called on synthetic event.
*/
bindRemoveCompleted(handler) {
$on(this.$clearCompleted, 'click', handler);
}
$delegate(self.$todoList, 'li .edit', 'blur', function () { /**
if (!this.dataset.iscanceled) { * @param {Function} handler Function called on synthetic event.
handler({ */
id: _itemId(this), bindToggleAll(handler) {
title: this.value $on(this.$toggleAll, 'click', ({target}) => {
}); handler(target.checked);
}
}); });
}
// Remove the cursor from the input when you hit enter just like if it were a real form /**
$delegate(self.$todoList, 'li .edit', 'keypress', function (event) { * @param {Function} handler Function called on synthetic event.
if (event.keyCode === self.ENTER_KEY) { */
this.blur(); bindRemoveItem(handler) {
} $delegate(this.$todoList, '.destroy', 'click', ({target}) => {
handler(_itemId(target));
}); });
} }
_bindItemEditCancel(handler) { /**
const self = this; * @param {Function} handler Function called on synthetic event.
*/
bindToggleItem(handler) {
$delegate(this.$todoList, '.toggle', 'click', ({target}) => {
handler(_itemId(target), target.checked);
});
}
$delegate(self.$todoList, 'li .edit', 'keyup', function (event) { /**
if (event.keyCode === self.ESCAPE_KEY) { * @param {Function} handler Function called on synthetic event.
const id = _itemId(this); */
this.dataset.iscanceled = true; bindEditItemSave(handler) {
this.blur(); $delegate(this.$todoList, 'li .edit', 'blur', ({target}) => {
if (!target.dataset.iscanceled) {
handler(_itemId(target), target.value.trim());
}
}, true);
handler({ id }); // Remove the cursor from the input when you hit enter just like if it were a real form
$delegate(this.$todoList, 'li .edit', 'keypress', ({target, keyCode}) => {
if (keyCode === ENTER_KEY) {
target.blur();
} }
}); });
} }
bind(event, handler) { /**
switch (event) { * @param {Function} handler Function called on synthetic event.
case 'newTodo': */
$on(this.$newTodo, 'change', () => handler(this.$newTodo.value)); bindEditItemCancel(handler) {
break; $delegate(this.$todoList, 'li .edit', 'keyup', ({target, keyCode}) => {
if (keyCode === ESCAPE_KEY) {
case 'removeCompleted': target.dataset.iscanceled = true;
$on(this.$clearCompleted, 'click', handler); target.blur();
break;
handler(_itemId(target));
case 'toggleAll': }
$on(this.$toggleAll, 'click', function () { });
handler({completed: this.checked});
});
break;
case 'itemEdit':
$delegate(this.$todoList, 'li label', 'dblclick', function () {
handler({id: _itemId(this)});
});
break;
case 'itemRemove':
$delegate(this.$todoList, '.destroy', 'click', function () {
handler({id: _itemId(this)});
});
break;
case 'itemToggle':
$delegate(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
} }
} }
...@@ -326,6 +326,9 @@ ...@@ -326,6 +326,9 @@
<li class="routing"> <li class="routing">
<a href="examples/vanillajs/" data-source="https://developer.mozilla.org/en/JavaScript" data-content="You know JavaScript right? :P">Vanilla JS</a> <a href="examples/vanillajs/" data-source="https://developer.mozilla.org/en/JavaScript" data-content="You know JavaScript right? :P">Vanilla JS</a>
</li> </li>
<li class="routing">
<a href="examples/vanilla-es6/" data-source="https://developer.mozilla.org/en/JavaScript" data-content="Just ECMAScript 6 and DOM APIs. Compiled with Google Closure Compiler.">Vanilla ES6</a>
</li>
<li class="routing"> <li class="routing">
<a href="examples/jquery/" data-source="http://jquery.com" data-content="jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.">jQuery</a> <a href="examples/jquery/" data-source="http://jquery.com" data-content="jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.">jQuery</a>
</li> </li>
......
...@@ -1123,6 +1123,15 @@ ...@@ -1123,6 +1123,15 @@
"url": "examples/vanillajs" "url": "examples/vanillajs"
}] }]
}, },
"es6": {
"name": "ECMAScript 6",
"description": "The ECMAScript 6 (ES2015) standard was ratified in 2015 following years of work standardizing improvements to ECMAScript 3. The committee introduced a wide variety of improvements such as arrow functions, const declarations, and native Promises.",
"homepage": "developer.mozilla.org/en-US/docs/JavaScript",
"examples": [{
"name": "Vanilla ES6 Example",
"url": "examples/vanilla-es6"
}]
},
"js_of_ocaml": { "js_of_ocaml": {
"name": "js_of_ocaml", "name": "js_of_ocaml",
"description": "Js_of_ocaml is a compiler of OCaml bytecode to Javascript. It makes it possible to run Ocaml programs in a Web browser.", "description": "Js_of_ocaml is a compiler of OCaml bytecode to Javascript. It makes it possible to run Ocaml programs in a Web browser.",
......
...@@ -16,7 +16,6 @@ var ELEMENT_MISSING = Object.freeze({}); ...@@ -16,7 +16,6 @@ var ELEMENT_MISSING = Object.freeze({});
var ITEM_HIDDEN_OR_REMOVED = Object.freeze({}); var ITEM_HIDDEN_OR_REMOVED = Object.freeze({});
module.exports = function Page(browser) { module.exports = function Page(browser) {
// CSS ELEMENT SELECTORS // CSS ELEMENT SELECTORS
this.getMainSectionCss = function () { return classOrId + 'main'; }; this.getMainSectionCss = function () { return classOrId + 'main'; };
...@@ -31,7 +30,10 @@ module.exports = function Page(browser) { ...@@ -31,7 +30,10 @@ module.exports = function Page(browser) {
this.getItemCountCss = function () { return 'span' + classOrId + 'todo-count'; }; this.getItemCountCss = function () { return 'span' + classOrId + 'todo-count'; };
this.getFilterCss = function (index) { return classOrId + 'filters li:nth-of-type(' + (index + 1) + ') a'; }; this.getFilterCss = function (index) {
return classOrId + 'filters li:nth-of-type(' + (index + 1) + ') a, ' +
classOrId + 'filters a:nth-of-type(' + (index + 1) + ')';
};
this.getSelectedFilterCss = function (index) { return this.getFilterCss(index) + '.selected'; }; this.getSelectedFilterCss = function (index) { return this.getFilterCss(index) + '.selected'; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment