From 7aeab76dc96119e9486f918b445a8a923b53640b Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 4 Apr 2016 21:30:28 -0600 Subject: [PATCH] Make things nice --- .babelrc | 10 + .eslintignore | 3 + .eslintrc | 14 + js/app.js | 42 +-- js/controller.js | 414 ++++++++++++++-------------- js/helpers.js | 47 ++-- js/model.js | 166 ++++++------ js/store.js | 207 +++++++------- js/template.js | 132 ++++----- js/view.js | 312 +++++++++++----------- karma.conf.js | 103 ++----- package.json | 14 +- test/.eslintrc | 9 + test/ControllerSpec.js | 579 ++++++++++++++++++++-------------------- webpack.config.babel.js | 15 ++ webpack.config.js | 62 ----- 16 files changed, 1036 insertions(+), 1093 deletions(-) create mode 100644 .babelrc create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 test/.eslintrc create mode 100644 webpack.config.babel.js delete mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..f3683c9 --- /dev/null +++ b/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": ["es2015-webpack", "stage-2"], + "env": { + "test": { + "plugins": [ + ["__coverage__", {"ignore": "test"}] + ] + } + } +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..12b9cf1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +coverage/ +node_modules/ +bundle.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8f58d93 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,14 @@ +{ + "extends": ["kentcdodds/es-next"], + "rules": { + // these are only here because I didn't + // want to update the entire codebase ¯\_(ツ)_/¯ + "func-names": 0, + "no-var": 0, + "func-style": 0, + "comma-dangle": 0, + "valid-jsdoc": 0, + "vars-on-top": 0, + "complexity": [2, 6], + } +} diff --git a/js/app.js b/js/app.js index 8cddfa7..f5a7c0d 100755 --- a/js/app.js +++ b/js/app.js @@ -1,12 +1,14 @@ -import 'todomvc-common/base.css'; -import 'todomvc-app-css/index.css'; +import 'todomvc-common/base.css' +import 'todomvc-app-css/index.css' -import View from './view'; -import {$on} from './helpers'; -import Controller from './controller'; -import Model from './model'; -import Store from './store'; -import Template from './template'; +import View from './view' +import {$on} from './helpers' +import Controller from './controller' +import Model from './model' +import Store from './store' +import Template from './template' + +var todo /** * Sets up a brand new Todo list. @@ -14,21 +16,19 @@ import Template from './template'; * @param {string} name The name of your new to do list. */ function Todo(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); + 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) } -var todo; - function setView() { - todo.controller.setView(document.location.hash); + todo.controller.setView(document.location.hash) } -$on(window, 'load', function () { - todo = new Todo('todos-vanillajs'); - setView(); -}); -$on(window, 'hashchange', setView); +$on(window, 'load', function() { + todo = new Todo('todos-vanillajs') + setView() +}) +$on(window, 'hashchange', setView) diff --git a/js/controller.js b/js/controller.js index 3d5c174..c126991 100755 --- a/js/controller.js +++ b/js/controller.js @@ -1,261 +1,261 @@ -export default Controller; +export default Controller /** - * Takes a model and view and acts as the controller between them - * - * @constructor - * @param {object} model The model instance - * @param {object} view The view instance - */ +* Takes a model and view and acts as the controller between them +* +* @constructor +* @param {object} model The model instance +* @param {object} view The view instance +*/ function Controller(model, view) { - var that = this; - that.model = model; - that.view = view; + var that = this + that.model = model + that.view = view - that.view.bind('newTodo', function (title) { - that.addItem(title); - }); + that.view.bind('newTodo', function(title) { + that.addItem(title) + }) - that.view.bind('itemEdit', function (item) { - that.editItem(item.id); - }); + that.view.bind('itemEdit', function(item) { + that.editItem(item.id) + }) - that.view.bind('itemEditDone', function (item) { - that.editItemSave(item.id, item.title); - }); + that.view.bind('itemEditDone', function(item) { + that.editItemSave(item.id, item.title) + }) - that.view.bind('itemEditCancel', function (item) { - that.editItemCancel(item.id); - }); + that.view.bind('itemEditCancel', function(item) { + that.editItemCancel(item.id) + }) - that.view.bind('itemRemove', function (item) { - that.removeItem(item.id); - }); + that.view.bind('itemRemove', function(item) { + that.removeItem(item.id) + }) - that.view.bind('itemToggle', function (item) { - that.toggleComplete(item.id, item.completed); - }); + that.view.bind('itemToggle', function(item) { + that.toggleComplete(item.id, item.completed) + }) - that.view.bind('removeCompleted', function () { - that.removeCompletedItems(); - }); + that.view.bind('removeCompleted', function() { + that.removeCompletedItems() + }) - that.view.bind('toggleAll', function (status) { - that.toggleAll(status.completed); - }); + that.view.bind('toggleAll', function(status) { + that.toggleAll(status.completed) + }) } /** - * Loads and initialises the view - * - * @param {string} '' | 'active' | 'completed' - */ -Controller.prototype.setView = function (locationHash) { - var route = locationHash.split('/')[1]; - var page = route || ''; - this._updateFilterState(page); -}; +* Loads and initialises the view +* +* @param {string} '' | 'active' | 'completed' +*/ +Controller.prototype.setView = function(locationHash) { + var route = locationHash.split('/')[1] + var page = route || '' + this._updateFilterState(page) +} /** - * An event to fire on load. Will get all items and display them in the - * todo-list - */ -Controller.prototype.showAll = function () { - var that = this; - that.model.read(function (data) { - that.view.render('showEntries', data); - }); -}; +* An event to fire on load. Will get all items and display them in the +* todo-list +*/ +Controller.prototype.showAll = function() { + var that = this + that.model.read(function(data) { + that.view.render('showEntries', data) + }) +} /** - * Renders all active tasks - */ -Controller.prototype.showActive = function () { - var that = this; - that.model.read({completed: false}, function (data) { - that.view.render('showEntries', data); - }); -}; +* Renders all active tasks +*/ +Controller.prototype.showActive = function() { + var that = this + that.model.read({completed: false}, function(data) { + that.view.render('showEntries', data) + }) +} /** - * Renders all completed tasks - */ -Controller.prototype.showCompleted = function () { - var that = this; - that.model.read({completed: true}, function (data) { - that.view.render('showEntries', data); - }); -}; +* Renders all completed tasks +*/ +Controller.prototype.showCompleted = function() { + var that = this + that.model.read({completed: true}, function(data) { + that.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. - */ -Controller.prototype.addItem = function (title) { - var that = this; +* 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. +*/ +Controller.prototype.addItem = function(title) { + var that = this - if (title.trim() === '') { - return; - } + if (title.trim() === '') { + return + } - that.model.create(title, function () { - that.view.render('clearNewTodo'); - that._filter(true); - }); -}; + that.model.create(title, function() { + that.view.render('clearNewTodo') + that._filter(true) + }) +} /* - * Triggers the item editing mode. - */ -Controller.prototype.editItem = function (id) { - var that = this; - that.model.read(id, function (data) { - that.view.render('editItem', {id: id, title: data[0].title}); - }); -}; +* Triggers the item editing mode. +*/ +Controller.prototype.editItem = function(id) { + var that = this + that.model.read(id, function(data) { + that.view.render('editItem', {id, title: data[0].title}) + }) +} /* - * Finishes the item editing mode successfully. - */ -Controller.prototype.editItemSave = function (id, title) { - var that = this; - if (title.trim()) { - that.model.update(id, {title: title}, function () { - that.view.render('editItemDone', {id: id, title: title}); - }); - } else { - that.removeItem(id); - } -}; +* Finishes the item editing mode successfully. +*/ +Controller.prototype.editItemSave = function(id, title) { + var that = this + if (title.trim()) { + that.model.update(id, {title}, function() { + that.view.render('editItemDone', {id, title}) + }) + } else { + that.removeItem(id) + } +} /* - * Cancels the item editing mode. - */ -Controller.prototype.editItemCancel = function (id) { - var that = this; - that.model.read(id, function (data) { - that.view.render('editItemDone', {id: id, title: data[0].title}); - }); -}; +* Cancels the item editing mode. +*/ +Controller.prototype.editItemCancel = function(id) { + var that = this + that.model.read(id, function(data) { + that.view.render('editItemDone', {id, title: data[0].title}) + }) +} /** - * By giving it an ID it'll find the DOM element matching that ID, - * remove it from the DOM and also remove it from storage. - * - * @param {number} id The ID of the item to remove from the DOM and - * storage - */ -Controller.prototype.removeItem = function (id) { - var that = this; - that.model.remove(id, function () { - that.view.render('removeItem', id); - }); +* By giving it an ID it'll find the DOM element matching that ID, +* remove it from the DOM and also remove it from storage. +* +* @param {number} id The ID of the item to remove from the DOM and +* storage +*/ +Controller.prototype.removeItem = function(id) { + var that = this + that.model.remove(id, function() { + that.view.render('removeItem', id) + }) - that._filter(); -}; + that._filter() +} /** - * Will remove all completed items from the DOM and storage. - */ -Controller.prototype.removeCompletedItems = function () { - var that = this; - that.model.read({completed: true}, function (data) { - data.forEach(function (item) { - that.removeItem(item.id); - }); - }); +* Will remove all completed items from the DOM and storage. +*/ +Controller.prototype.removeCompletedItems = function() { + var that = this + that.model.read({completed: true}, function(data) { + data.forEach(function(item) { + that.removeItem(item.id) + }) + }) - that._filter(); -}; + that._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 - */ -Controller.prototype.toggleComplete = function (id, completed, silent) { - var that = this; - that.model.update(id, {completed: completed}, function () { - that.view.render('elementComplete', { - id: id, - completed: completed - }); - }); +* 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 +*/ +Controller.prototype.toggleComplete = function(id, completed, silent) { + var that = this + that.model.update(id, {completed}, function() { + that.view.render('elementComplete', { + id, + completed, + }) + }) - if (!silent) { - that._filter(); - } -}; + if (!silent) { + that._filter() + } +} /** - * Will toggle ALL checkboxes' on/off state and completeness of models. - * Just pass in the event object. - */ -Controller.prototype.toggleAll = function (completed) { - var that = this; - that.model.read({completed: !completed}, function (data) { - data.forEach(function (item) { - that.toggleComplete(item.id, completed, true); - }); - }); +* Will toggle ALL checkboxes' on/off state and completeness of models. +* Just pass in the event object. +*/ +Controller.prototype.toggleAll = function(completed) { + var that = this + that.model.read({completed: !completed}, function(data) { + data.forEach(function(item) { + that.toggleComplete(item.id, completed, true) + }) + }) - that._filter(); -}; + that._filter() +} /** - * Updates the pieces of the page which change depending on the remaining - * number of todos. - */ -Controller.prototype._updateCount = function () { - var that = this; - that.model.getCount(function (todos) { - that.view.render('updateElementCount', todos.active); - that.view.render('clearCompletedButton', { - completed: todos.completed, - visible: todos.completed > 0 - }); +* Updates the pieces of the page which change depending on the remaining +* number of todos. +*/ +Controller.prototype._updateCount = function() { + var that = this + that.model.getCount(function(todos) { + that.view.render('updateElementCount', todos.active) + that.view.render('clearCompletedButton', { + completed: todos.completed, + visible: todos.completed > 0 + }) - that.view.render('toggleAll', {checked: todos.completed === todos.total}); - that.view.render('contentBlockVisibility', {visible: todos.total > 0}); - }); -}; + that.view.render('toggleAll', {checked: todos.completed === todos.total}) + that.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. - */ -Controller.prototype._filter = function (force) { - var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1); +* Re-filters the todo items, based on the active route. +* @param {boolean|undefined} force forces a re-painting of todo items. +*/ +Controller.prototype._filter = function(force) { + var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1) - // Update the elements on the page, which change with each completed todo - this._updateCount(); + // 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](); - } + // 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; -}; + this._lastActiveRoute = activeRoute +} /** - * Simply updates the filter nav's selected states - */ -Controller.prototype._updateFilterState = function (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; +* Simply updates the filter nav's selected states +*/ +Controller.prototype._updateFilterState = function(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'; - } + if (currentPage === '') { + this._activeRoute = 'All' + } - this._filter(); + this._filter() - this.view.render('setFilter', currentPage); -}; + this.view.render('setFilter', currentPage) +} diff --git a/js/helpers.js b/js/helpers.js index c50c04a..be36b04 100755 --- a/js/helpers.js +++ b/js/helpers.js @@ -1,50 +1,49 @@ /*global NodeList */ -export {qs, qsa, $on, $delegate, $parent}; +export {qs, qsa, $on, $delegate, $parent} // Get element(s) by CSS selector: function qs(selector, scope) { - return (scope || document).querySelector(selector); + return (scope || document).querySelector(selector) } function qsa(selector, scope) { - return (scope || document).querySelectorAll(selector); + return (scope || document).querySelectorAll(selector) } // addEventListener wrapper: function $on(target, type, callback, useCapture) { - target.addEventListener(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) { - function dispatchEvent(event) { - var targetElement = event.target; - var potentialElements = qsa(selector, target); - var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0; + // https://developer.mozilla.org/en-US/docs/Web/Events/blur + var useCapture = type === 'blur' || type === 'focus' + $on(target, type, dispatchEvent, useCapture) - if (hasMatch) { - handler.call(targetElement, event); - } - } + function dispatchEvent(event) { + var targetElement = event.target + var potentialElements = qsa(selector, target) + var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0 - // https://developer.mozilla.org/en-US/docs/Web/Events/blur - var useCapture = type === 'blur' || type === 'focus'; - - $on(target, type, dispatchEvent, useCapture); + if (hasMatch) { + handler.call(targetElement, event) + } + } } // 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); + if (!element.parentNode) { + return undefined + } + if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { + return element.parentNode + } + return $parent(element.parentNode, tagName) } // Allow for looping on nodes by chaining: // qsa('.foo').forEach(function () {}) -NodeList.prototype.forEach = Array.prototype.forEach; +NodeList.prototype.forEach = Array.prototype.forEach diff --git a/js/model.js b/js/model.js index 241cbf8..06fa58a 100755 --- a/js/model.js +++ b/js/model.js @@ -1,39 +1,38 @@ -export default Model; +export default Model /** - * Creates a new Model instance and hooks up the storage. - * - * @constructor - * @param {object} storage A reference to the client side storage class - */ +* Creates a new Model instance and hooks up the storage. +* +* @constructor +* @param {object} storage A reference to the client side storage class +*/ function Model(storage) { - this.storage = 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 - */ -Model.prototype.create = function (title, callback) { - title = title || ''; - callback = callback || function () { - }; +* 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 +*/ +Model.prototype.create = function(title, callback) { + title = title || '' + callback = callback || function() { + } - var newItem = { - title: title.trim(), - completed: false - }; + var newItem = { + title: title.trim(), + completed: false + } - this.storage.save(newItem, callback); -}; + 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. + * 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 @@ -44,73 +43,74 @@ Model.prototype.create = function (title, callback) { * //Below will find a model with foo equalling bar and hello equalling world. * model.read({ foo: 'bar', hello: 'world' }); */ -Model.prototype.read = function (query, callback) { - var queryType = typeof query; - callback = callback || function () { - }; +Model.prototype.read = function(query, callback) { + var queryType = typeof query + callback = callback || function() { + } - if (queryType === 'function') { - callback = query; - return this.storage.findAll(callback); - } else if (queryType === 'string' || queryType === 'number') { - query = parseInt(query, 10); - this.storage.find({id: query}, callback); - } else { - this.storage.find(query, callback); - } -}; + if (queryType === 'function') { + callback = query + return this.storage.findAll(callback) + } else if (queryType === 'string' || queryType === 'number') { + query = parseInt(query, 10) + this.storage.find({id: query}, callback) + } else { + this.storage.find(query, callback) + } + return undefined +} /** - * 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. - */ -Model.prototype.update = function (id, data, callback) { - this.storage.save(data, callback, id); -}; +* 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. +*/ +Model.prototype.update = function(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. - */ -Model.prototype.remove = function (id, callback) { - this.storage.remove(id, callback); -}; +* 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. +*/ +Model.prototype.remove = function(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. - */ -Model.prototype.removeAll = function (callback) { - this.storage.drop(callback); -}; +* WARNING: Will remove ALL data from storage. +* +* @param {function} callback The callback to fire when the storage is wiped. +*/ +Model.prototype.removeAll = function(callback) { + this.storage.drop(callback) +} /** - * Returns a count of all todos - */ -Model.prototype.getCount = function (callback) { - var todos = { - active: 0, - completed: 0, - total: 0 - }; +* Returns a count of all todos +*/ +Model.prototype.getCount = function(callback) { + var todos = { + active: 0, + completed: 0, + total: 0 + } - this.storage.findAll(function (data) { - data.forEach(function (todo) { - if (todo.completed) { - todos.completed++; - } else { - todos.active++; - } + this.storage.findAll(function(data) { + data.forEach(function(todo) { + if (todo.completed) { + todos.completed++ + } else { + todos.active++ + } - todos.total++; - }); - callback(todos); - }); -}; + todos.total++ + }) + callback(todos) + }) +} diff --git a/js/store.js b/js/store.js index e661b83..d086f8e 100755 --- a/js/store.js +++ b/js/store.js @@ -1,5 +1,4 @@ -/*jshint eqeqeq:false */ -export default Store; +export default Store /** * Creates a new client side storage object and will create an empty * collection if no collection already exists. @@ -9,128 +8,128 @@ export default Store; * real life you probably would be making AJAX calls */ function Store(name, callback) { - callback = callback || function () { - }; + callback = callback || function() { + } - this._dbName = name; + this._dbName = name - if (!localStorage[name]) { - var data = { - todos: [] - }; + if (!localStorage[name]) { + var data = { + todos: [] + } - localStorage[name] = JSON.stringify(data); - } + localStorage[name] = JSON.stringify(data) + } - callback.call(this, JSON.parse(localStorage[name])); + 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 - * }); - */ -Store.prototype.find = function (query, callback) { - if (!callback) { - return; - } +* 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 +* }); +*/ +Store.prototype.find = function(query, callback) { + if (!callback) { + return + } - var todos = JSON.parse(localStorage[this._dbName]).todos; + 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; - })); -}; + 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 - */ -Store.prototype.findAll = function (callback) { - callback = callback || function () { - }; - callback.call(this, JSON.parse(localStorage[this._dbName]).todos); -}; +* Will retrieve all data from the collection +* +* @param {function} callback The callback to fire upon retrieving data +*/ +Store.prototype.findAll = function(callback) { + callback = callback || function() { + } + 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 - */ -Store.prototype.save = function (updateData, callback, id) { - var data = JSON.parse(localStorage[this._dbName]); - var todos = data.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 +*/ +Store.prototype.save = function(updateData, callback, id) { + var data = JSON.parse(localStorage[this._dbName]) + var todos = data.todos - callback = callback || function () { - }; + callback = callback || function() { + } - // If an ID was actually given, find the item and update each property - if (id) { - for (var i = 0; i < todos.length; i++) { - if (todos[i].id === id) { - for (var key in updateData) { - todos[i][key] = updateData[key]; - } - break; - } - } + // If an ID was actually given, find the item and update each property + if (id) { + for (var i = 0; i < todos.length; i++) { + if (todos[i].id === id) { + for (var key in updateData) { // eslint-disable-line guard-for-in + todos[i][key] = updateData[key] + } + break + } + } - localStorage[this._dbName] = JSON.stringify(data); - callback.call(this, JSON.parse(localStorage[this._dbName]).todos); - } else { - // Generate an ID - updateData.id = new Date().getTime(); + localStorage[this._dbName] = JSON.stringify(data) + 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); - callback.call(this, [updateData]); - } -}; + todos.push(updateData) + localStorage[this._dbName] = JSON.stringify(data) + 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 - */ -Store.prototype.remove = function (id, callback) { - var data = JSON.parse(localStorage[this._dbName]); - var todos = data.todos; +* 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 +*/ +Store.prototype.remove = function(id, callback) { + var data = JSON.parse(localStorage[this._dbName]) + var todos = data.todos - for (var i = 0; i < todos.length; i++) { - if (todos[i].id == id) { - todos.splice(i, 1); - break; - } - } + for (var i = 0; i < todos.length; i++) { + if (todos[i].id === id) { + todos.splice(i, 1) + break + } + } - localStorage[this._dbName] = JSON.stringify(data); - callback.call(this, JSON.parse(localStorage[this._dbName]).todos); -}; + localStorage[this._dbName] = JSON.stringify(data) + 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 - */ -Store.prototype.drop = function (callback) { - localStorage[this._dbName] = JSON.stringify({todos: []}); - 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 +*/ +Store.prototype.drop = function(callback) { + localStorage[this._dbName] = JSON.stringify({todos: []}) + callback.call(this, JSON.parse(localStorage[this._dbName]).todos) +} diff --git a/js/template.js b/js/template.js index a7a4f82..60c4f6f 100755 --- a/js/template.js +++ b/js/template.js @@ -1,42 +1,44 @@ -/*jshint laxbreak:true */ -export default Template; +export default Template var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '`': '`' -}; + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '`': '`' +} -var escapeHtmlChar = function (chr) { - return htmlEscapes[chr]; -}; +var escapeHtmlChar = function(chr) { + return htmlEscapes[chr] +} -var reUnescapedHtml = /[&<>"'`]/g, - reHasUnescapedHtml = new RegExp(reUnescapedHtml.source); +var reUnescapedHtml = /[&<>"'`]/g +var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source) -var escape = function (string) { - return (string && reHasUnescapedHtml.test(string)) - ? string.replace(reUnescapedHtml, escapeHtmlChar) - : string; -}; +var escape = function(string) { + if (string && reHasUnescapedHtml.test(string)) { + return string.replace(reUnescapedHtml, escapeHtmlChar) + } else { + return string + } +} /** - * Sets up defaults for all the Template methods such as a default template - * - * @constructor - */ +* Sets up defaults for all the Template methods such as a default template +* +* @constructor +*/ function Template() { - this.defaultTemplate - = '
  • ' - + '
    ' - + '' - + '' - + '' - + '
    ' - + '
  • '; + this.defaultTemplate = ` +
  • +
    + + + +
    +
  • + ` } /** @@ -51,35 +53,35 @@ function Template() { * * @example * view.show({ - * id: 1, - * title: "Hello World", - * completed: 0, - * }); + * id: 1, + * title: "Hello World", + * completed: 0, + * }); */ -Template.prototype.show = function (data) { - var i, l; - var view = ''; +Template.prototype.show = function(data) { + var i, l + var view = '' - for (i = 0, l = data.length; i < l; i++) { - var template = this.defaultTemplate; - var completed = ''; - var checked = ''; + for (i = 0, l = data.length; i < l; i++) { + var template = this.defaultTemplate + var completed = '' + var checked = '' - if (data[i].completed) { - completed = 'completed'; - checked = 'checked'; - } + if (data[i].completed) { + completed = 'completed' + checked = 'checked' + } - template = template.replace('{{id}}', data[i].id); - template = template.replace('{{title}}', escape(data[i].title)); - template = template.replace('{{completed}}', completed); - template = template.replace('{{checked}}', checked); + template = template.replace('{{id}}', data[i].id) + template = template.replace('{{title}}', escape(data[i].title)) + template = template.replace('{{completed}}', completed) + template = template.replace('{{checked}}', checked) - view = view + template; - } + view = view + template + } - return view; -}; + return view +} /** * Displays a counter of how many to dos are left to complete @@ -87,11 +89,11 @@ Template.prototype.show = function (data) { * @param {number} activeTodos The number of active todos. * @returns {string} String containing the count */ -Template.prototype.itemCounter = function (activeTodos) { - var plural = activeTodos === 1 ? '' : 's'; +Template.prototype.itemCounter = function(activeTodos) { + var plural = activeTodos === 1 ? '' : 's' - return '' + activeTodos + ' item' + plural + ' left'; -}; + return '' + activeTodos + ' item' + plural + ' left' +} /** * Updates the text within the "Clear completed" button @@ -99,10 +101,10 @@ Template.prototype.itemCounter = function (activeTodos) { * @param {[type]} completedTodos The number of completed todos. * @returns {string} String containing the count */ -Template.prototype.clearCompletedButton = function (completedTodos) { - if (completedTodos > 0) { - return 'Clear completed'; - } else { - return ''; - } -}; +Template.prototype.clearCompletedButton = function(completedTodos) { + if (completedTodos > 0) { + return 'Clear completed' + } else { + return '' + } +} diff --git a/js/view.js b/js/view.js index 6015ed5..6f6fddd 100755 --- a/js/view.js +++ b/js/view.js @@ -1,4 +1,4 @@ -import {qs, qsa, $on, $parent, $delegate} from './helpers'; +import {qs, qsa, $on, $parent, $delegate} from './helpers' /** * View that abstracts away the browser's DOM completely. @@ -10,199 +10,195 @@ import {qs, qsa, $on, $parent, $delegate} from './helpers'; * Renders the given command with the options */ class View { - constructor(template) { - this.template = template; + constructor(template) { + this.template = template - this.ENTER_KEY = 13; - this.ESCAPE_KEY = 27; + 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'); - } - _removeItem(id) { - var elem = qs('[data-id="' + id + '"]'); + 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') + } + _removeItem(id) { + var 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) { - var listItem = 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) { + var listItem = qs('[data-id="' + id + '"]') - if (!listItem) { - return; - } + if (!listItem) { + return + } - var input = qs('input.edit', listItem); - listItem.removeChild(input); + var input = qs('input.edit', listItem) + listItem.removeChild(input) - listItem.className = listItem.className.replace('editing', ''); + listItem.className = listItem.className.replace('editing', '') - qsa('label', listItem).forEach(function (label) { - label.textContent = title; - }); - } - render(viewCmd, parameter) { - var that = this; - var viewCommands = { - showEntries: function () { - that.$todoList.innerHTML = that.template.show(parameter); - }, - removeItem: function () { - that._removeItem(parameter); - }, - updateElementCount: function () { - that.$todoItemCounter.innerHTML = that.template.itemCounter(parameter); - }, - clearCompletedButton: function () { - that._clearCompletedButton(parameter.completed, parameter.visible); - }, - contentBlockVisibility: function () { - that.$main.style.display = that.$footer.style.display = parameter.visible ? 'block' : 'none'; - }, - toggleAll: function () { - that.$toggleAll.checked = parameter.checked; - }, - setFilter: function () { - _setFilter(parameter); - }, - clearNewTodo: function () { - that.$newTodo.value = ''; - }, - elementComplete: function () { - _elementComplete(parameter.id, parameter.completed); - }, - editItem: function () { - _editItem(parameter.id, parameter.title); - }, - editItemDone: function () { - that._editItemDone(parameter.id, parameter.title); - } - }; + qsa('label', listItem).forEach(function(label) { + label.textContent = title + }) + } + render(viewCmd, parameter) { + var viewCommands = { + showEntries: () => { + this.$todoList.innerHTML = this.template.show(parameter) + }, + removeItem: () => { + this._removeItem(parameter) + }, + updateElementCount: () => { + this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter) + }, + clearCompletedButton: () => { + this._clearCompletedButton(parameter.completed, parameter.visible) + }, + contentBlockVisibility: () => { + this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none' + }, + toggleAll: () => { + this.$toggleAll.checked = parameter.checked + }, + setFilter: () => { + _setFilter(parameter) + }, + clearNewTodo: () => { + this.$newTodo.value = '' + }, + elementComplete: () => { + _elementComplete(parameter.id, parameter.completed) + }, + editItem: () => { + _editItem(parameter.id, parameter.title) + }, + editItemDone: () => { + this._editItemDone(parameter.id, parameter.title) + } + } - viewCommands[viewCmd](); - } - _bindItemEditDone(handler) { - var that = this; - $delegate(that.$todoList, 'li .edit', 'blur', function () { - if (!this.dataset.iscanceled) { - handler({ - id: _itemId(this), - title: this.value - }); - } - }); + viewCommands[viewCmd]() + } + _bindItemEditDone(handler) { + $delegate(this.$todoList, 'li .edit', 'blur', () => { + if (!this.dataset.iscanceled) { + handler({ + id: _itemId(this), + title: this.value + }) + } + }) - $delegate(that.$todoList, 'li .edit', 'keypress', function (event) { - if (event.keyCode === that.ENTER_KEY) { - // Remove the cursor from the input when you hit enter just like if it - // were a real form - this.blur(); - } - }); - } - _bindItemEditCancel(handler) { - var that = this; - $delegate(that.$todoList, 'li .edit', 'keyup', function (event) { - if (event.keyCode === that.ESCAPE_KEY) { - this.dataset.iscanceled = true; - this.blur(); + $delegate(this.$todoList, 'li .edit', 'keypress', (event) => { + if (event.keyCode === this.ENTER_KEY) { + // Remove the cursor from the input when you hit enter just like if it + // were a real form + this.blur() + } + }) + } + _bindItemEditCancel(handler) { + $delegate(this.$todoList, 'li .edit', 'keyup', (event) => { + if (event.keyCode === this.ESCAPE_KEY) { + this.dataset.iscanceled = true + this.blur() - handler({id: _itemId(this)}); - } - }); - } - bind(event, handler) { - var that = this; - if (event === 'newTodo') { - $on(that.$newTodo, 'change', function () { - handler(that.$newTodo.value); - }); + handler({id: _itemId(this)}) + } + }) + } + bind(event, handler) { // eslint-disable-line complexity + if (event === 'newTodo') { + $on(this.$newTodo, 'change', () => { + handler(this.$newTodo.value) + }) - } else if (event === 'removeCompleted') { - $on(that.$clearCompleted, 'click', function () { - handler(); - }); + } else if (event === 'removeCompleted') { + $on(this.$clearCompleted, 'click', () => { + handler() + }) - } else if (event === 'toggleAll') { - $on(that.$toggleAll, 'click', function () { - handler({completed: this.checked}); - }); + } else if (event === 'toggleAll') { + $on(this.$toggleAll, 'click', () => { + handler({completed: this.checked}) + }) - } else if (event === 'itemEdit') { - $delegate(that.$todoList, 'li label', 'dblclick', function () { - handler({id: _itemId(this)}); - }); + } else if (event === 'itemEdit') { + $delegate(this.$todoList, 'li label', 'dblclick', () => { + handler({id: _itemId(this)}) + }) - } else if (event === 'itemRemove') { - $delegate(that.$todoList, '.destroy', 'click', function () { - handler({id: _itemId(this)}); - }); + } else if (event === 'itemRemove') { + $delegate(this.$todoList, '.destroy', 'click', () => { + handler({id: _itemId(this)}) + }) - } else if (event === 'itemToggle') { - $delegate(that.$todoList, '.toggle', 'click', function () { - handler({ - id: _itemId(this), - completed: this.checked - }); - }); + } else if (event === 'itemToggle') { + $delegate(this.$todoList, '.toggle', 'click', () => { + handler({ + id: _itemId(this), + completed: this.checked + }) + }) - } else if (event === 'itemEditDone') { - that._bindItemEditDone(handler); + } else if (event === 'itemEditDone') { + this._bindItemEditDone(handler) - } else if (event === 'itemEditCancel') { - that._bindItemEditCancel(handler); - } - } + } else if (event === 'itemEditCancel') { + this._bindItemEditCancel(handler) + } + } } function _setFilter(currentPage) { - qs('.filters .selected').className = ''; - qs('.filters [href="#/' + currentPage + '"]').className = 'selected'; + qs('.filters .selected').className = '' + qs('.filters [href="#/' + currentPage + '"]').className = 'selected' } function _elementComplete(id, completed) { - var listItem = qs('[data-id="' + id + '"]'); + var listItem = qs('[data-id="' + id + '"]') - if (!listItem) { - return; - } + if (!listItem) { + return + } - listItem.className = completed ? 'completed' : ''; + listItem.className = completed ? 'completed' : '' - // In case it was toggled from an event and not by clicking the checkbox - qs('input', listItem).checked = completed; + // In case it was toggled from an event and not by clicking the checkbox + qs('input', listItem).checked = completed } function _editItem(id, title) { - var listItem = qs('[data-id="' + id + '"]'); + var listItem = qs('[data-id="' + id + '"]') - if (!listItem) { - return; - } + if (!listItem) { + return + } - listItem.className = listItem.className + ' editing'; + listItem.className = listItem.className + ' editing' - var input = document.createElement('input'); - input.className = 'edit'; + var input = document.createElement('input') + input.className = 'edit' - listItem.appendChild(input); - input.focus(); - input.value = title; + listItem.appendChild(input) + input.focus() + input.value = title } function _itemId(element) { - var li = $parent(element, 'li'); - return parseInt(li.dataset.id, 10); + var li = $parent(element, 'li') + return parseInt(li.dataset.id, 10) } export default View diff --git a/karma.conf.js b/karma.conf.js index 1bea3a2..702e340 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,76 +1,29 @@ -var path = require('path'); -var webpackConfig = require('./webpack.config'); -var entry = path.resolve(webpackConfig.context, webpackConfig.entry); -var preprocessors = {}; -preprocessors[entry] = ['webpack']; +const webpackConfig = require('./webpack.config.babel') +require('babel-register') -// Karma configuration -// Generated on Mon Aug 10 2015 05:47:13 GMT-0600 (MDT) - -module.exports = function (config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], - - - // list of files / patterns to load in the browser - files: [entry], - - - // list of files to exclude - exclude: [], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: preprocessors, - webpack: webpackConfig, - webpackMiddleware: {noInfo: true}, - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', 'coverage'], - - coverageReporter: { - reporters: [ - {type: 'lcov', dir: 'coverage/', subdir: '.'}, - {type: 'json', dir: 'coverage/', subdir: '.'}, - {type: 'text-summary'} - ] - }, - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Firefox'], - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: false, - }); -}; +module.exports = function setKarmaConfig(config) { + config.set({ + basePath: '', + frameworks: ['jasmine'], + files: ['test/**/*Spec.js'], + exclude: [], + preprocessors: { + 'test/**/*Spec.js': ['webpack'], + }, + webpack: webpackConfig, + webpackMiddleware: {noInfo: true}, + reporters: ['progress', 'coverage'], + coverageReporter: { + reporters: [ + {type: 'lcov', dir: 'coverage/', subdir: '.'}, + {type: 'json', dir: 'coverage/', subdir: '.'}, + {type: 'text-summary'}, + ], + }, + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + browsers: ['Firefox'], + singleRun: true, + }) +} diff --git a/package.json b/package.json index d526c22..c062b95 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,16 @@ "devDependencies": { "babel": "6.5.2", "babel-core": "6.7.4", + "babel-eslint": "6.0.2", "babel-loader": "6.2.4", - "babel-plugin-__coverage__": "0.1111.1", + "babel-plugin-__coverage__": "0.11111.0", "babel-preset-es2015-webpack": "6.4.0", "babel-preset-stage-2": "6.5.0", + "cross-env": "1.0.7", "css-loader": "0.23.1", + "eslint": "2.7.0", + "eslint-config-kentcdodds": "6.1.1", + "eslint-loader": "1.3.0", "jasmine-core": "2.4.1", "karma": "0.13.22", "karma-chrome-launcher": "0.2.3", @@ -24,9 +29,10 @@ "webpack-dev-server": "2.0.0-beta" }, "scripts": { - "test": "NODE_ENV=test karma start", - "test:single": "NODE_ENV=test karma start --single-run", + "test": "cross-env NODE_ENV=test karma start", + "watch:test": "cross-env NODE_ENV=test karma start --auto-watch --no-single-run", "start": "webpack-dev-server", - "build": "webpack" + "build": "webpack", + "lint": "eslint ." } } diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..1185edc --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,9 @@ +{ + "globals": { + "jasmine": false, + "describe": false, + "it": false, + "beforeEach": false, + "expect": false, + } +} diff --git a/test/ControllerSpec.js b/test/ControllerSpec.js index 8eccfe4..f5a38dd 100755 --- a/test/ControllerSpec.js +++ b/test/ControllerSpec.js @@ -1,405 +1,404 @@ -/*global app, jasmine, describe, it, beforeEach, expect */ -import Controller from '../js/controller'; +import Controller from '../js/controller' -describe('controller', function () { - var subject, model, view; +describe('controller', () => { + var subject, model, view - var setUpModel = function (todos) { - model.read.and.callFake(function (query, callback) { - callback = callback || query; - callback(todos); - }); + var setUpModel = function(todos) { + model.read.and.callFake(function(query, callback) { + callback = callback || query + callback(todos) + }) - model.getCount.and.callFake(function (callback) { + model.getCount.and.callFake(function(callback) { - var todoCounts = { - active: todos.filter(function (todo) { - return !todo.completed; - }).length, - completed: todos.filter(function (todo) { - return !!todo.completed; - }).length, - total: todos.length - }; + var todoCounts = { + active: todos.filter(function(todo) { + return !todo.completed + }).length, + completed: todos.filter(function(todo) { + return !!todo.completed + }).length, + total: todos.length + } - callback(todoCounts); - }); + callback(todoCounts) + }) - model.remove.and.callFake(function (id, callback) { - callback(); - }); + model.remove.and.callFake(function(id, callback) { + callback() + }) - model.create.and.callFake(function (title, callback) { - callback(); - }); + model.create.and.callFake(function(title, callback) { + callback() + }) - model.update.and.callFake(function (id, updateData, callback) { - callback(); - }); - }; + model.update.and.callFake(function(id, updateData, callback) { + callback() + }) + } - var createViewStub = function () { - var eventRegistry = {}; - return { - render: jasmine.createSpy('render'), - bind: function (event, handler) { - eventRegistry[event] = handler; - }, - trigger: function (event, parameter) { - eventRegistry[event](parameter); - } - }; - }; + var createViewStub = () => { + var eventRegistry = {} + return { + render: jasmine.createSpy('render'), + bind: function(event, handler) { + eventRegistry[event] = handler + }, + trigger: function(event, parameter) { + eventRegistry[event](parameter) + } + } + } - beforeEach(function () { - model = jasmine.createSpyObj('model', ['read', 'getCount', 'remove', 'create', 'update']); - view = createViewStub(); - subject = new Controller(model, view); - }); + beforeEach(() => { + model = jasmine.createSpyObj('model', ['read', 'getCount', 'remove', 'create', 'update']) + view = createViewStub() + subject = new Controller(model, view) + }) - it('should show entries on start-up', function () { - setUpModel([]); + it('should show entries on start-up', () => { + setUpModel([]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('showEntries', []); - }); + expect(view.render).toHaveBeenCalledWith('showEntries', []) + }) - describe('routing', function () { + describe('routing', () => { - it('should show all entries without a route', function () { - var todo = {title: 'my todo'}; - setUpModel([todo]); + it('should show all entries without a route', () => { + var todo = {title: 'my todo'} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('showEntries', [todo]); - }); + expect(view.render).toHaveBeenCalledWith('showEntries', [todo]) + }) - it('should show all entries without "all" route', function () { - var todo = {title: 'my todo'}; - setUpModel([todo]); + it('should show all entries without "all" route', () => { + var todo = {title: 'my todo'} + setUpModel([todo]) - subject.setView('#/'); + subject.setView('#/') - expect(view.render).toHaveBeenCalledWith('showEntries', [todo]); - }); + expect(view.render).toHaveBeenCalledWith('showEntries', [todo]) + }) - it('should show active entries', function () { - var todo = {title: 'my todo', completed: false}; - setUpModel([todo]); + it('should show active entries', () => { + var todo = {title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView('#/active'); + subject.setView('#/active') - expect(model.read).toHaveBeenCalledWith({completed: false}, jasmine.any(Function)); - expect(view.render).toHaveBeenCalledWith('showEntries', [todo]); - }); + expect(model.read).toHaveBeenCalledWith({completed: false}, jasmine.any(Function)) + expect(view.render).toHaveBeenCalledWith('showEntries', [todo]) + }) - it('should show completed entries', function () { - var todo = {title: 'my todo', completed: true}; - setUpModel([todo]); + it('should show completed entries', () => { + var todo = {title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView('#/completed'); + subject.setView('#/completed') - expect(model.read).toHaveBeenCalledWith({completed: true}, jasmine.any(Function)); - expect(view.render).toHaveBeenCalledWith('showEntries', [todo]); - }); - }); + expect(model.read).toHaveBeenCalledWith({completed: true}, jasmine.any(Function)) + expect(view.render).toHaveBeenCalledWith('showEntries', [todo]) + }) + }) - it('should show the content block when todos exists', function () { - setUpModel([{title: 'my todo', completed: true}]); + it('should show the content block when todos exists', () => { + setUpModel([{title: 'my todo', completed: true}]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('contentBlockVisibility', { - visible: true - }); - }); + expect(view.render).toHaveBeenCalledWith('contentBlockVisibility', { + visible: true + }) + }) - it('should hide the content block when no todos exists', function () { - setUpModel([]); + it('should hide the content block when no todos exists', () => { + setUpModel([]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('contentBlockVisibility', { - visible: false - }); - }); + expect(view.render).toHaveBeenCalledWith('contentBlockVisibility', { + visible: false + }) + }) - it('should check the toggle all button, if all todos are completed', function () { - setUpModel([{title: 'my todo', completed: true}]); + it('should check the toggle all button, if all todos are completed', () => { + setUpModel([{title: 'my todo', completed: true}]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('toggleAll', { - checked: true - }); - }); + expect(view.render).toHaveBeenCalledWith('toggleAll', { + checked: true + }) + }) - it('should set the "clear completed" button', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); + it('should set the "clear completed" button', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('clearCompletedButton', { - completed: 1, - visible: true - }); - }); + expect(view.render).toHaveBeenCalledWith('clearCompletedButton', { + completed: 1, + visible: true + }) + }) - it('should highlight "All" filter by default', function () { - setUpModel([]); + it('should highlight "All" filter by default', () => { + setUpModel([]) - subject.setView(''); + subject.setView('') - expect(view.render).toHaveBeenCalledWith('setFilter', ''); - }); + expect(view.render).toHaveBeenCalledWith('setFilter', '') + }) - it('should highlight "Active" filter when switching to active view', function () { - setUpModel([]); + it('should highlight "Active" filter when switching to active view', () => { + setUpModel([]) - subject.setView('#/active'); + subject.setView('#/active') - expect(view.render).toHaveBeenCalledWith('setFilter', 'active'); - }); + expect(view.render).toHaveBeenCalledWith('setFilter', 'active') + }) - describe('toggle all', function () { - it('should toggle all todos to completed', function () { - var todos = [{ - id: 42, - title: 'my todo', - completed: false - }, { - id: 21, - title: 'another todo', - completed: false - }]; + describe('toggle all', () => { + it('should toggle all todos to completed', () => { + var todos = [{ + id: 42, + title: 'my todo', + completed: false + }, { + id: 21, + title: 'another todo', + completed: false + }] - setUpModel(todos); - subject.setView(''); + setUpModel(todos) + subject.setView('') - view.trigger('toggleAll', {completed: true}); + view.trigger('toggleAll', {completed: true}) - expect(model.update).toHaveBeenCalledWith(42, {completed: true}, jasmine.any(Function)); - expect(model.update).toHaveBeenCalledWith(21, {completed: true}, jasmine.any(Function)); - }); + expect(model.update).toHaveBeenCalledWith(42, {completed: true}, jasmine.any(Function)) + expect(model.update).toHaveBeenCalledWith(21, {completed: true}, jasmine.any(Function)) + }) - it('should update the view', function () { - var todos = [{ - id: 42, - title: 'my todo', - completed: true - }]; + it('should update the view', () => { + var todos = [{ + id: 42, + title: 'my todo', + completed: true + }] - setUpModel(todos); - subject.setView(''); + setUpModel(todos) + subject.setView('') - view.trigger('toggleAll', {completed: false}); + view.trigger('toggleAll', {completed: false}) - expect(view.render).toHaveBeenCalledWith('elementComplete', {id : 42, completed : false}); - }); - }); + expect(view.render).toHaveBeenCalledWith('elementComplete', {id: 42, completed: false}) + }) + }) - describe('new todo', function () { - it('should add a new todo to the model', function () { - setUpModel([]); + describe('new todo', () => { + it('should add a new todo to the model', () => { + setUpModel([]) - subject.setView(''); + subject.setView('') - view.trigger('newTodo', 'a new todo'); + view.trigger('newTodo', 'a new todo') - expect(model.create).toHaveBeenCalledWith('a new todo', jasmine.any(Function)); - }); + expect(model.create).toHaveBeenCalledWith('a new todo', jasmine.any(Function)) + }) - it('should add a new todo to the view', function () { - setUpModel([]); + it('should add a new todo to the view', () => { + setUpModel([]) - subject.setView(''); + subject.setView('') - view.render.calls.reset(); - model.read.calls.reset(); - model.read.and.callFake(function (callback) { - callback([{ - title: 'a new todo', - completed: false - }]); - }); + view.render.calls.reset() + model.read.calls.reset() + model.read.and.callFake(function(callback) { + callback([{ + title: 'a new todo', + completed: false + }]) + }) - view.trigger('newTodo', 'a new todo'); + view.trigger('newTodo', 'a new todo') - expect(model.read).toHaveBeenCalled(); + expect(model.read).toHaveBeenCalled() - expect(view.render).toHaveBeenCalledWith('showEntries', [{ - title: 'a new todo', - completed: false - }]); - }); + expect(view.render).toHaveBeenCalledWith('showEntries', [{ + title: 'a new todo', + completed: false + }]) + }) - it('should clear the input field when a new todo is added', function () { - setUpModel([]); + it('should clear the input field when a new todo is added', () => { + setUpModel([]) - subject.setView(''); + subject.setView('') - view.trigger('newTodo', 'a new todo'); + view.trigger('newTodo', 'a new todo') - expect(view.render).toHaveBeenCalledWith('clearNewTodo'); - }); - }); + expect(view.render).toHaveBeenCalledWith('clearNewTodo') + }) + }) - describe('element removal', function () { - it('should remove an entry from the model', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); + describe('element removal', () => { + it('should remove an entry from the model', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView(''); - view.trigger('itemRemove', {id: 42}); + subject.setView('') + view.trigger('itemRemove', {id: 42}) - expect(model.remove).toHaveBeenCalledWith(42, jasmine.any(Function)); - }); + expect(model.remove).toHaveBeenCalledWith(42, jasmine.any(Function)) + }) - it('should remove an entry from the view', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); + it('should remove an entry from the view', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView(''); - view.trigger('itemRemove', {id: 42}); + subject.setView('') + view.trigger('itemRemove', {id: 42}) - expect(view.render).toHaveBeenCalledWith('removeItem', 42); - }); + expect(view.render).toHaveBeenCalledWith('removeItem', 42) + }) - it('should update the element count', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); + it('should update the element count', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView(''); - view.trigger('itemRemove', {id: 42}); + subject.setView('') + view.trigger('itemRemove', {id: 42}) - expect(view.render).toHaveBeenCalledWith('updateElementCount', 0); - }); - }); + expect(view.render).toHaveBeenCalledWith('updateElementCount', 0) + }) + }) - describe('remove completed', function () { - it('should remove a completed entry from the model', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); + describe('remove completed', () => { + it('should remove a completed entry from the model', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView(''); - view.trigger('removeCompleted'); + subject.setView('') + view.trigger('removeCompleted') - expect(model.read).toHaveBeenCalledWith({completed: true}, jasmine.any(Function)); - expect(model.remove).toHaveBeenCalledWith(42, jasmine.any(Function)); - }); + expect(model.read).toHaveBeenCalledWith({completed: true}, jasmine.any(Function)) + expect(model.remove).toHaveBeenCalledWith(42, jasmine.any(Function)) + }) - it('should remove a completed entry from the view', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); + it('should remove a completed entry from the view', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) - subject.setView(''); - view.trigger('removeCompleted'); + subject.setView('') + view.trigger('removeCompleted') - expect(view.render).toHaveBeenCalledWith('removeItem', 42); - }); - }); + expect(view.render).toHaveBeenCalledWith('removeItem', 42) + }) + }) - describe('element complete toggle', function () { - it('should update the model', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); - subject.setView(''); + describe('element complete toggle', () => { + it('should update the model', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) + subject.setView('') - view.trigger('itemToggle', {id: 21, completed: true}); + view.trigger('itemToggle', {id: 21, completed: true}) - expect(model.update).toHaveBeenCalledWith(21, {completed: true}, jasmine.any(Function)); - }); + expect(model.update).toHaveBeenCalledWith(21, {completed: true}, jasmine.any(Function)) + }) - it('should update the view', function () { - var todo = {id: 42, title: 'my todo', completed: true}; - setUpModel([todo]); - subject.setView(''); + it('should update the view', () => { + var todo = {id: 42, title: 'my todo', completed: true} + setUpModel([todo]) + subject.setView('') - view.trigger('itemToggle', {id: 42, completed: false}); + view.trigger('itemToggle', {id: 42, completed: false}) - expect(view.render).toHaveBeenCalledWith('elementComplete', {id: 42, completed: false}); - }); - }); + expect(view.render).toHaveBeenCalledWith('elementComplete', {id: 42, completed: false}) + }) + }) - describe('edit item', function () { - it('should switch to edit mode', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + describe('edit item', () => { + it('should switch to edit mode', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEdit', {id: 21}); + view.trigger('itemEdit', {id: 21}) - expect(view.render).toHaveBeenCalledWith('editItem', {id: 21, title: 'my todo'}); - }); + expect(view.render).toHaveBeenCalledWith('editItem', {id: 21, title: 'my todo'}) + }) - it('should leave edit mode on done', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + it('should leave edit mode on done', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEditDone', {id: 21, title: 'new title'}); + view.trigger('itemEditDone', {id: 21, title: 'new title'}) - expect(view.render).toHaveBeenCalledWith('editItemDone', {id: 21, title: 'new title'}); - }); + expect(view.render).toHaveBeenCalledWith('editItemDone', {id: 21, title: 'new title'}) + }) - it('should persist the changes on done', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + it('should persist the changes on done', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEditDone', {id: 21, title: 'new title'}); + view.trigger('itemEditDone', {id: 21, title: 'new title'}) - expect(model.update).toHaveBeenCalledWith(21, {title: 'new title'}, jasmine.any(Function)); - }); + expect(model.update).toHaveBeenCalledWith(21, {title: 'new title'}, jasmine.any(Function)) + }) - it('should remove the element from the model when persisting an empty title', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + it('should remove the element from the model when persisting an empty title', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEditDone', {id: 21, title: ''}); + view.trigger('itemEditDone', {id: 21, title: ''}) - expect(model.remove).toHaveBeenCalledWith(21, jasmine.any(Function)); - }); + expect(model.remove).toHaveBeenCalledWith(21, jasmine.any(Function)) + }) - it('should remove the element from the view when persisting an empty title', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + it('should remove the element from the view when persisting an empty title', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEditDone', {id: 21, title: ''}); + view.trigger('itemEditDone', {id: 21, title: ''}) - expect(view.render).toHaveBeenCalledWith('removeItem', 21); - }); + expect(view.render).toHaveBeenCalledWith('removeItem', 21) + }) - it('should leave edit mode on cancel', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + it('should leave edit mode on cancel', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEditCancel', {id: 21}); + view.trigger('itemEditCancel', {id: 21}) - expect(view.render).toHaveBeenCalledWith('editItemDone', {id: 21, title: 'my todo'}); - }); + expect(view.render).toHaveBeenCalledWith('editItemDone', {id: 21, title: 'my todo'}) + }) - it('should not persist the changes on cancel', function () { - var todo = {id: 21, title: 'my todo', completed: false}; - setUpModel([todo]); + it('should not persist the changes on cancel', () => { + var todo = {id: 21, title: 'my todo', completed: false} + setUpModel([todo]) - subject.setView(''); + subject.setView('') - view.trigger('itemEditCancel', {id: 21}); + view.trigger('itemEditCancel', {id: 21}) - expect(model.update).not.toHaveBeenCalled(); - }); - }); -}); + expect(model.update).not.toHaveBeenCalled() + }) + }) +}) diff --git a/webpack.config.babel.js b/webpack.config.babel.js new file mode 100644 index 0000000..816372f --- /dev/null +++ b/webpack.config.babel.js @@ -0,0 +1,15 @@ +module.exports = { + entry: './js/app.js', + output: { + filename: 'bundle.js', + path: __dirname, + }, + context: __dirname, + devtool: 'eval', + module: { + loaders: [ + {test: /\.js$/, loader: 'babel!eslint', exclude: /node_modules/}, + {test: /\.css$/, loader: 'style!css'}, + ], + }, +} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 695d37e..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,62 +0,0 @@ -var path = require('path'); - -var config = { - entry: './app.js', - output: { - filename: 'bundle.js', - path: here() - }, - context: here('js'), - devtool: 'eval', - module: { - loaders: [ - {test: /\.css$/, loader: 'style!css'} - ] - } -}; - -config.module.loaders = getJSLoaders().concat(config.module.loaders); - -if (process.env.NODE_ENV === 'test') { - config.entry = './ControllerSpec.js'; - config.context = here('test'); -} - -module.exports = config; - -function getJSLoaders() { - var jsLoaders = []; - var presets = ['es2015-webpack', 'stage-2'] - var test = process.env.NODE_ENV === 'test' - if (test) { - jsLoaders.push({ - test: /js\/.*\.js$/, - loader: 'babel', - exclude: /node_modules/, - query: { - presets: presets, - plugins: ['__coverage__'], - }, - }); - jsLoaders.push({ - test: /test\/.*\.js$/, - loader: 'babel', - exclude: /node_modules/, - query: { - presets: presets, - }, - }); - } else { - jsLoaders.push({ - test: /\.js$/, - loader: 'babel', - exclude: /node_modules/, - }); - } - return jsLoaders; -} - -function here(d) { - return d ? path.join(__dirname, d) : __dirname; -} -