Commit c2c75ee6 authored by Addy Osmani's avatar Addy Osmani

Merge pull request #1515 from lukeed/vanilla-es6

Vanilla ES6, take 2
parents 999d4f4c e6f4264e
...@@ -38,7 +38,8 @@ ...@@ -38,7 +38,8 @@
"examples/thorax/js/lib/backbone-localstorage.js", "examples/thorax/js/lib/backbone-localstorage.js",
"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/**/*.js"
], ],
"requireSpaceBeforeBlockStatements": true, "requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true, "requireParenthesesAroundIIFE": true,
......
node_modules/.bin
node_modules/babel-core
node_modules/babel-preset-es2015
node_modules/browserify
node_modules/todomvc-app-css/*
!node_modules/todomvc-app-css/index.css
node_modules/todomvc-common/*
!node_modules/todomvc-common/base.css
# 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.
## Learning ES6
- [ES6 Features](https://github.com/lukehoban/es6features)
- [Learning Resources](https://github.com/ericdouglas/ES6-Learning)
- [Babel's ES6 Guide](https://babeljs.io/docs/learn-es2015/)
- [Babel Compiler](https://babeljs.io/)
## Installation
To get started with this example, navigate into the example folder and install the NPM modules.
```bash
cd todomvc/examples/vanilla-es6
npm install
```
## Compiling ES6 to ES5
After NPM modules have been installed, use the pre-defined Babel script to convert the `src` files. Browserify is also used so that `module.exports` and `require()` can be run in your browser.
```bash
npm run compile
```
## Support
- [Twitter](http://twitter.com/lukeed05)
*Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.*
## Implementation
Uses [Babel JS](https://babeljs.io/) to compile ES6 code to ES5, which is then readable by all browsers.
## Credit
Created by [Luke Edwards](http://www.lukeed.com)
'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
This diff is collapsed.
'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 _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 _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; }
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>
<html>
<head>
<meta charset="utf-8">
<title>ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section class="main">
<input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
</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>
<footer class="info">
<p>Double-click to edit a todo</p>
<br>
<p>Written by <a href="http://twitter.com/lukeed05">Luke Edwards</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="dist/bundle.js"></script>
</body>
</html>
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
{
"private": true,
"scripts": {
"compile": "babel src --presets es2015 --out-dir=dist && browserify dist/app.js > dist/bundle.js",
"prepublish": "npm run compile"
},
"dependencies": {
"todomvc-app-css": "^2.0.1",
"todomvc-common": "^1.0.2"
},
"devDependencies": {
"babel-core": "^6.1.0",
"babel-preset-es2015": "^6.1.18",
"browserify": "^12.0.1"
}
}
import Controller from './controller';
import * as helpers from './helpers';
import Template from './template';
import Store from './store';
import Model from './model';
import View from './view';
const $on = helpers.$on;
const setView = () => todo.controller.setView(document.location.hash);
class Todo {
/**
* 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);
this.controller = new Controller(this.model, this.view);
}
}
const todo = new Todo('todos-vanillajs');
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
export default class Controller {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
*/
constructor(model, view) {
this.model = model;
this.view = view;
this.view.bind('newTodo', title => this.addItem(title));
this.view.bind('itemEdit', item => this.editItem(item.id));
this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title));
this.view.bind('itemEditCancel', item => this.editItemCancel(item.id));
this.view.bind('itemRemove', item => this.removeItem(item.id));
this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed));
this.view.bind('removeCompleted', () => this.removeCompletedItems());
this.view.bind('toggleAll', status => this.toggleAll(status.completed));
}
/**
* 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));
}
/**
* Renders all active tasks
*/
showActive() {
this.model.read({completed: false}, data => this.view.render('showEntries', data));
}
/**
* Renders all completed tasks
*/
showCompleted() {
this.model.read({completed: true}, data => this.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.
*/
addItem(title) {
if (title.trim() === '') {
return;
}
this.model.create(title, () => {
this.view.render('clearNewTodo');
this._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
editItem(id) {
this.model.read(id, data => {
const title = data[0].title;
this.view.render('editItem', {id, title});
});
}
/*
* Finishes the item editing mode successfully.
*/
editItemSave(id, title) {
title = title.trim();
if (title.length !== 0) {
this.model.update(id, {title}, () => {
this.view.render('editItemDone', {id, title});
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
*/
editItemCancel(id) {
this.model.read(id, data => {
const title = data[0].title;
this.view.render('editItemDone', {id, title});
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
*/
removeItem(id) {
this.model.remove(id, () => this.view.render('removeItem', id));
this._filter();
}
/**
* Will remove all completed items from the DOM and storage.
*/
removeCompletedItems() {
this.model.read({completed: true}, data => {
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
* 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
*/
toggleComplete(id, completed, silent) {
this.model.update(id, {completed}, () => {
this.view.render('elementComplete', {id, completed});
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
toggleAll(completed) {
this.model.read({completed: !completed}, data => {
for (let item of data) {
this.toggleComplete(item.id, completed, true);
}
});
this._filter();
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
_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) {
let active = this._activeRoute;
const 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
*/
_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);
}
}
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
export function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
export function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
export 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
export function $delegate(target, selector, type, handler) {
let dispatchEvent = event => {
const targetElement = event.target;
const potentialElements = qsa(selector, target);
const hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
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')
export function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
/**
* 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 || '';
let 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){
let 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 */
/**
* 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 {
constructor(name, callback) {
this._dbName = name;
if (!localStorage[name]) {
const 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
* })
*/
find(query, callback){
let todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(todo => {
for (let 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
*/
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
*/
save(updateData, callback, id){
const data = JSON.parse(localStorage[this._dbName]);
let todos = data.todos;
const len = todos.length;
// 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.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
*/
remove(id, callback){
const data = JSON.parse(localStorage[this._dbName]);
let todos = data.todos;
const len = todos.length;
for (let 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
*/
drop(callback){
localStorage[this._dbName] = JSON.stringify({todos: []});
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
}
const htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
const reUnescapedHtml = /[&<>"'`]/g;
const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
let escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
let escapeHtmlChar = chr => htmlEscapes[chr];
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.
*
* 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,
* })
*/
show(data){
let view = data.map(d => {
let template = this.defaultTemplate;
let completed = d.completed ? 'completed' : '';
let 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
*/
itemCounter(activeTodos){
let 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
*/
clearCompletedButton(completedTodos){
return (completedTodos > 0) ? 'Clear completed' : '';
}
}
import {qs, qsa, $on, $parent, $delegate} from './helpers';
let _itemId = element => parseInt($parent(element, 'li').dataset.id, 10);
let _setFilter = currentPage => {
qs('.filters .selected').className = '';
qs(`.filters [href="#/${currentPage}"]`).className = 'selected';
};
let _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;
};
let _editItem = (id, title) => {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
listItem.className += ' editing';
let 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 {
constructor(template) {
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = qs('.todo-list');
this.$todoItemCounter = qs('.todo-count');
this.$clearCompleted = qs('.clear-completed');
this.$main = qs('.main');
this.$footer = qs('.footer');
this.$toggleAll = qs('.toggle-all');
this.$newTodo = qs('.new-todo');
this.viewCommands = {
showEntries: parameter => this.$todoList.innerHTML = this.template.show(parameter),
removeItem: parameter => this._removeItem(parameter),
updateElementCount: parameter => this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter),
clearCompletedButton: parameter => this._clearCompletedButton(parameter.completed, parameter.visible),
contentBlockVisibility: parameter => this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none',
toggleAll: parameter => this.$toggleAll.checked = parameter.checked,
setFilter: parameter => _setFilter(parameter),
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),
};
}
_removeItem(id) {
const elem = qs(`[data-id="${id}"]`);
if (elem) {
this.$todoList.removeChild(elem);
}
}
_clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
}
_editItemDone(id, title) {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
const input = qs('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
qsa('label', listItem).forEach(label => label.textContent = title);
}
render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
}
_bindItemEditDone(handler) {
let self = this;
$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
$delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
});
}
_bindItemEditCancel(handler) {
let self = this;
$delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
let id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id });
}
});
}
bind(event, handler) {
switch (event) {
case 'newTodo':
$on(this.$newTodo, 'change', () => handler(this.$newTodo.value));
break;
case 'removeCompleted':
$on(this.$clearCompleted, 'click', handler);
break;
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;
}
}
}
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