diff --git a/index.html b/index.html index c17e1c5..0797a44 100755 --- a/index.html +++ b/index.html @@ -39,13 +39,6 @@

Ported to ES6 by Kent C. Dodds

Part of TodoMVC

- - - - - - - diff --git a/src/app.js b/src/app.js index 4549b15..5e6ab34 100644 --- a/src/app.js +++ b/src/app.js @@ -1,28 +1,32 @@ /* global app, log */ -(function(window) { - 'use strict' +'use strict' - /** - * Sets up a brand new Todo list. - * - * @param {string} name The name of your new to do list. - */ - function Todo(name) { - this.storage = new app.Store(name) - this.model = new app.Model(this.storage) - this.template = new app.Template() - this.view = new app.View(this.template) - this.controller = new app.Controller(this.model, this.view) - } +require('./view') +require('./helpers') +require('./controller') +require('./model') +require('./store') +require('./template') - function onLoad() { - var todo = new Todo('todos-vanillajs') - todo.controller.setView(document.location.hash) - log('view set') - } +/** +* Sets up a brand new Todo list. +* +* @param {string} name The name of your new to do list. +*/ +function Todo(name) { + this.storage = new app.Store(name) + this.model = new app.Model(this.storage) + this.template = new app.Template() + this.view = new app.View(this.template) + this.controller = new app.Controller(this.model, this.view) +} +function onLoad() { + var todo = new Todo('todos-vanillajs') + todo.controller.setView(document.location.hash) + log('view set') +} - // Export to window - window.app = window.app || {} - window.app.onLoad = onLoad -})(window) +// Export to window +window.app = window.app || {} +window.app.onLoad = onLoad diff --git a/src/bootstrap.js b/src/bootstrap.js index e11a041..4dac1c4 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -1,7 +1,8 @@ /* global app, $on */ -(function(window) { - 'use strict' +'use strict' - $on(window, 'load', app.onLoad) - $on(window, 'hashchange', app.onLoad) -})(window) +require('./app') +require('./helpers') + +$on(window, 'load', app.onLoad) +$on(window, 'hashchange', app.onLoad) diff --git a/src/controller.js b/src/controller.js index 134e371..e657b89 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,268 +1,266 @@ -(function(window) { - 'use strict' +'use strict' - /** - * 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 +/** +* 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 - 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('itemEditDone', function(item) { + that.editItemSave(item.id, item.title) + }) + + that.view.bind('itemEditCancel', function(item) { + that.editItemCancel(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('removeCompleted', function() { + that.removeCompletedItems() + }) + + 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) +} + +/** +* 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 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 + + if (title.trim() === '') { + return + } + + 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}) + }) +} + +/* +* 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) + } +} - that.view.bind('itemEdit', function(item) { - that.editItem(item.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}) + }) +} - that.view.bind('itemEditDone', function(item) { - that.editItemSave(item.id, item.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) + }) - that.view.bind('itemEditCancel', function(item) { - that.editItemCancel(item.id) - }) + that._filter() +} - that.view.bind('itemRemove', function(item) { +/** +* 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.view.bind('itemToggle', function(item) { - that.toggleComplete(item.id, item.completed) - }) - - that.view.bind('removeCompleted', function() { - that.removeCompletedItems() - }) - - 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) - } - - /** - * 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 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 - - if (title.trim() === '') { - return - } - - 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}) - }) - } - - /* - * 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) - } - } - - /* - * 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}) - }) - } - - /** - * 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() +} + +/** +* 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 }) + }) + if (!silent) { 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 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() +} + +/** +* 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._filter() + 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) + + // 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]() } - /** - * 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 - }) - }) + this._lastActiveRoute = activeRoute +} - if (!silent) { - that._filter() - } +/** +* 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' } - /** - * 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) - }) - }) + this._filter() - that._filter() - } + this.view.render('setFilter', currentPage) +} - /** - * 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}) - }) - } - - /** - * 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() - - // 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 - */ - 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' - } - - this._filter() - - this.view.render('setFilter', currentPage) - } - - // Export to window - window.app = window.app || {} - window.app.Controller = Controller -})(window) +// Export to window +window.app = window.app || {} +window.app.Controller = Controller diff --git a/src/helpers.js b/src/helpers.js index 61c9e4f..8465b3a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,82 +1,78 @@ -/*global NodeList */ -(function(window) { - 'use strict' +'use strict' - // Get element(s) by CSS selector: - window.qs = function(selector, scope) { - return (scope || document).querySelector(selector) +// Get element(s) by CSS selector: +window.qs = function(selector, scope) { + return (scope || document).querySelector(selector) +} + +window.qsa = function(selector, scope) { + return (scope || document).querySelectorAll(selector) +} + +window.log = function log() { + if (window.console && window.console.log) { + window.console.log.apply(window.console, arguments) // eslint-disable-line } +} - window.qsa = function(selector, scope) { - return (scope || document).querySelectorAll(selector) - } +// addEventListener wrapper: +window.$on = function(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 +window.$delegate = function(target, selector, type, handler) { + function dispatchEvent(event) { + var targetElement = event.target + var potentialElements = window.qsa(selector, target) + var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0 - window.log = function log() { - if (window.console && window.console.log) { - window.console.log.apply(window.console, arguments) // eslint-disable-line + if (hasMatch) { + handler.call(targetElement, event) } } - // addEventListener wrapper: - window.$on = function(target, type, callback, useCapture) { - target.addEventListener(type, callback, !!useCapture) + // https://developer.mozilla.org/en-US/docs/Web/Events/blur + var useCapture = type === 'blur' || type === 'focus' + + window.$on(target, type, dispatchEvent, useCapture) +} + +// Find the element's parent with the given tag name: +// $parent(qs('a'), 'div'); +window.$parent = function(element, tagName) { + if (!element.parentNode) { + return } - - // Attach a handler to event for all elements that match the selector, - // now or in the future, based on a root element - window.$delegate = function(target, selector, type, handler) { - function dispatchEvent(event) { - var targetElement = event.target - var potentialElements = window.qsa(selector, target) - var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0 - - if (hasMatch) { - handler.call(targetElement, event) - } - } - - // https://developer.mozilla.org/en-US/docs/Web/Events/blur - var useCapture = type === 'blur' || type === 'focus' - - window.$on(target, type, dispatchEvent, useCapture) + if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { + return element.parentNode } + return window.$parent(element.parentNode, tagName) +} - // Find the element's parent with the given tag name: - // $parent(qs('a'), 'div'); - window.$parent = function(element, tagName) { - if (!element.parentNode) { - return - } - if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { - return element.parentNode - } - return window.$parent(element.parentNode, tagName) +// removes an element from an array +// const x = [1,2,3] +// remove(x, 2) +// x ~== [1,3] +window.remove = function remove(array, thing) { + const index = array.indexOf(thing) + if (index === -1) { + return array } + array.splice(index, 1) +} - // removes an element from an array - // const x = [1,2,3] - // remove(x, 2) - // x ~== [1,3] - window.remove = function remove(array, thing) { - const index = array.indexOf(thing) - if (index === -1) { - return array - } - array.splice(index, 1) +// pad the left of the given string by the given size with the given character +// leftPad('10', 3, '0') -> 010 +window.leftPad = function leftPad(str, size, padWith) { + if (size <= str.length) { + return str + } else { + return Array(size - str.length + 1).join(padWith || '0') + str } +} - // pad the left of the given string by the given size with the given character - // leftPad('10', 3, '0') -> 010 - window.leftPad = function leftPad(str, size, padWith) { - if (size <= str.length) { - return str - } else { - return Array(size - str.length + 1).join(padWith || '0') + str - } - } - - // Allow for looping on nodes by chaining: - // qsa('.foo').forEach(function () {}) - NodeList.prototype.forEach = Array.prototype.forEach -})(window) +// Allow for looping on nodes by chaining: +// qsa('.foo').forEach(function () {}) +NodeList.prototype.forEach = Array.prototype.forEach diff --git a/src/model.js b/src/model.js index ec7b934..12d160f 100644 --- a/src/model.js +++ b/src/model.js @@ -1,122 +1,120 @@ -(function(window) { - 'use strict' +'use strict' - /** - * 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 +/** +* 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 +} + +/** +* 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 - } - - this.storage.save(newItem, callback) + var newItem = { + title: title.trim(), + completed: false } - /** - * 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' }); - */ - Model.prototype.read = function(query, callback) { - var queryType = typeof query - callback = callback || function() { - } + this.storage.save(newItem, 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) - } +/** +* 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' }); +*/ +Model.prototype.read = function(query, callback) { + var queryType = typeof query + callback = callback || function() { } - /** - * 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) + 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) + } +} + +/** +* 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) +} + +/** +* 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 } - /** - * 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) - } + this.storage.findAll(function(data) { + data.forEach(function(todo) { + if (todo.completed) { + todos.completed++ + } else { + todos.active++ + } - /** - * 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 - } - - 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) + }) +} - // Export to window - window.app = window.app || {} - window.app.Model = Model -})(window) +// Export to window +window.app = window.app || {} +window.app.Model = Model diff --git a/src/store.js b/src/store.js index 7f09d7c..727e859 100644 --- a/src/store.js +++ b/src/store.js @@ -1,144 +1,142 @@ -(function(window) { - 'use strict' +'use strict' - /** - * 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 - */ - function Store(name, callback) { - callback = callback || function() { +/** +* 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 +*/ +function Store(name, callback) { + callback = callback || function() { + } + + this._dbName = name + + if (!localStorage[name]) { + var data = { + todos: [] } - this._dbName = name + localStorage[name] = JSON.stringify(data) + } - if (!localStorage[name]) { - var data = { - todos: [] + 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 + } + + 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 } - - localStorage[name] = JSON.stringify(data) } + return true + })) +} - callback.call(this, JSON.parse(localStorage[name])) +/** +* 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 + + callback = callback || function() { } - /** - * 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 - - 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 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() { - } - - // 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) { - if (updateData.hasOwnProperty(key)) { - 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() - - 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 - + // 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) { // eslint-disable-line - todos.splice(i, 1) + if (todos[i].id === id) { + for (var key in updateData) { + if (updateData.hasOwnProperty(key)) { + 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() + + 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 + + for (var i = 0; i < todos.length; i++) { + if (todos[i].id == id) { // eslint-disable-line + todos.splice(i, 1) + break + } } - /** - * 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) - } + localStorage[this._dbName] = JSON.stringify(data) + callback.call(this, JSON.parse(localStorage[this._dbName]).todos) +} - // Export to window - window.app = window.app || {} - window.app.Store = Store -})(window) +/** +* 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) +} + +// Export to window +window.app = window.app || {} +window.app.Store = Store diff --git a/src/template.js b/src/template.js index 15abe7d..27e32d0 100644 --- a/src/template.js +++ b/src/template.js @@ -1,112 +1,110 @@ -(function(window) { - 'use strict' +'use strict' - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '`': '`' - } +var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '`': '`' +} - var escapeHtmlChar = function(chr) { - return htmlEscapes[chr] - } +var escapeHtmlChar = function(chr) { + return htmlEscapes[chr] +} - var reUnescapedHtml = /[&<>"'`]/g - var 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) { + return (string && reHasUnescapedHtml.test(string)) ? + string.replace(reUnescapedHtml, escapeHtmlChar) : + string +} - /** - * Sets up defaults for all the Template methods such as a default template - * - * @constructor - */ - function Template() { - this.defaultTemplate = '
  • ' + - '
    ' + - '' + - '' + - '' + - '
    ' + - '
  • ' - } +/** +* Sets up defaults for all the Template methods such as a default template +* +* @constructor +*/ +function Template() { + this.defaultTemplate = '
  • ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
  • ' +} - /** - * Creates an
  • 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
  • element - * - * @example - * view.show({ - * id: 1, - * title: "Hello World", - * completed: 0, - * }); - */ - Template.prototype.show = function(data) { - var i, l - var view = '' +/** +* Creates an
  • 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
  • element +* +* @example +* view.show({ +* id: 1, +* title: "Hello World", +* completed: 0, +* }); +*/ +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' - } - - 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 + if (data[i].completed) { + completed = 'completed' + checked = 'checked' } - return view + 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 } - /** - * 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 - */ - Template.prototype.itemCounter = function(activeTodos) { - var plural = activeTodos === 1 ? '' : 's' + return view +} - return '' + activeTodos + ' item' + plural + ' left' +/** +* 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 +*/ +Template.prototype.itemCounter = function(activeTodos) { + var plural = activeTodos === 1 ? '' : 's' + + return '' + activeTodos + ' 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 +*/ +Template.prototype.clearCompletedButton = function(completedTodos) { + if (completedTodos > 0) { + return 'Clear completed' + } else { + return '' } +} - /** - * Updates the text within the "Clear completed" button - * - * @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 '' - } - } - - // Export to window - window.app = window.app || {} - window.app.Template = Template -})(window) +// Export to window +window.app = window.app || {} +window.app.Template = Template diff --git a/src/view.js b/src/view.js index 8908adf..2984e36 100644 --- a/src/view.js +++ b/src/view.js @@ -1,220 +1,217 @@ /*global qs, qsa, $on, $parent, $delegate */ /* eslint no-invalid-this: 0 */ +'use strict' -(function(window) { - 'use strict' +/** +* 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 +*/ +function View(template) { + this.template = template - /** - * 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 - */ - function View(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') +} - 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') +View.prototype._removeItem = function(id) { + var elem = qs('[data-id="' + id + '"]') + + if (elem) { + this.$todoList.removeChild(elem) + } +} + +View.prototype._clearCompletedButton = function(completedCount, visible) { + this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount) + this.$clearCompleted.style.display = visible ? 'block' : 'none' +} + +View.prototype._setFilter = function(currentPage) { + qs('.filters .selected').className = '' + qs('.filters [href="#/' + currentPage + '"]').className = 'selected' +} + +View.prototype._elementComplete = function(id, completed) { + var listItem = qs('[data-id="' + id + '"]') + + if (!listItem) { + return } - View.prototype._removeItem = function(id) { - var elem = qs('[data-id="' + id + '"]') + listItem.className = completed ? 'completed' : '' - if (elem) { - this.$todoList.removeChild(elem) + // In case it was toggled from an event and not by clicking the checkbox + qs('input', listItem).checked = completed +} + +View.prototype._editItem = function(id, title) { + var listItem = qs('[data-id="' + id + '"]') + + if (!listItem) { + return + } + + listItem.className = listItem.className + ' editing' + + var input = document.createElement('input') + input.className = 'edit' + + listItem.appendChild(input) + input.focus() + input.value = title +} + +View.prototype._editItemDone = function(id, title) { + var listItem = qs('[data-id="' + id + '"]') + + if (!listItem) { + return + } + + var input = qs('input.edit', listItem) + listItem.removeChild(input) + + listItem.className = listItem.className.replace('editing', '') + + qsa('label', listItem).forEach(function(label) { + label.textContent = title + }) +} + +View.prototype.render = function(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() { + that._setFilter(parameter) + }, + clearNewTodo: function() { + that.$newTodo.value = '' + }, + elementComplete: function() { + that._elementComplete(parameter.id, parameter.completed) + }, + editItem: function() { + that._editItem(parameter.id, parameter.title) + }, + editItemDone: function() { + that._editItemDone(parameter.id, parameter.title) } } - View.prototype._clearCompletedButton = function(completedCount, visible) { - this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount) - this.$clearCompleted.style.display = visible ? 'block' : 'none' - } + viewCommands[viewCmd]() +} - View.prototype._setFilter = function(currentPage) { - qs('.filters .selected').className = '' - qs('.filters [href="#/' + currentPage + '"]').className = 'selected' - } +View.prototype._itemId = function(element) { + var li = $parent(element, 'li') + return parseInt(li.dataset.id, 10) +} - View.prototype._elementComplete = function(id, completed) { - var listItem = qs('[data-id="' + id + '"]') - - if (!listItem) { - return +View.prototype._bindItemEditDone = function(handler) { + var that = this + $delegate(that.$todoList, 'li .edit', 'blur', function() { + if (!this.dataset.iscanceled) { + handler({ + id: that._itemId(this), + title: this.value + }) } + }) - listItem.className = completed ? 'completed' : '' - - // In case it was toggled from an event and not by clicking the checkbox - qs('input', listItem).checked = completed - } - - View.prototype._editItem = function(id, title) { - var listItem = qs('[data-id="' + id + '"]') - - if (!listItem) { - return + $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() } + }) +} - listItem.className = listItem.className + ' editing' +View.prototype._bindItemEditCancel = function(handler) { + var that = this + $delegate(that.$todoList, 'li .edit', 'keyup', function(event) { + if (event.keyCode === that.ESCAPE_KEY) { + this.dataset.iscanceled = true + this.blur() - var input = document.createElement('input') - input.className = 'edit' - - listItem.appendChild(input) - input.focus() - input.value = title - } - - View.prototype._editItemDone = function(id, title) { - var listItem = qs('[data-id="' + id + '"]') - - if (!listItem) { - return + handler({id: that._itemId(this)}) } + }) +} - var input = qs('input.edit', listItem) - listItem.removeChild(input) - - listItem.className = listItem.className.replace('editing', '') - - qsa('label', listItem).forEach(function(label) { - label.textContent = title - }) - } - - View.prototype.render = function(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() { - that._setFilter(parameter) - }, - clearNewTodo: function() { - that.$newTodo.value = '' - }, - elementComplete: function() { - that._elementComplete(parameter.id, parameter.completed) - }, - editItem: function() { - that._editItem(parameter.id, parameter.title) - }, - editItemDone: function() { - that._editItemDone(parameter.id, parameter.title) - } - } - - viewCommands[viewCmd]() - } - - View.prototype._itemId = function(element) { - var li = $parent(element, 'li') - return parseInt(li.dataset.id, 10) - } - - View.prototype._bindItemEditDone = function(handler) { - var that = this - $delegate(that.$todoList, 'li .edit', 'blur', function() { - if (!this.dataset.iscanceled) { - handler({ - id: that._itemId(this), - title: this.value - }) - } +View.prototype.bind = function(event, handler) { // eslint-disable-line + var that = this + if (event === 'newTodo') { + $on(that.$newTodo, 'change', function() { + handler(that.$newTodo.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() - } + } else if (event === 'removeCompleted') { + $on(that.$clearCompleted, 'click', function() { + handler() }) - } - View.prototype._bindItemEditCancel = function(handler) { - var that = this - $delegate(that.$todoList, 'li .edit', 'keyup', function(event) { - if (event.keyCode === that.ESCAPE_KEY) { - this.dataset.iscanceled = true - this.blur() - - handler({id: that._itemId(this)}) - } + } else if (event === 'toggleAll') { + $on(that.$toggleAll, 'click', function() { + handler({completed: this.checked}) }) + + } else if (event === 'itemEdit') { + $delegate(that.$todoList, 'li label', 'dblclick', function() { + handler({id: that._itemId(this)}) + }) + + } else if (event === 'itemRemove') { + $delegate(that.$todoList, '.destroy', 'click', function() { + handler({id: that._itemId(this)}) + }) + + } else if (event === 'itemToggle') { + $delegate(that.$todoList, '.toggle', 'click', function() { + handler({ + id: that._itemId(this), + completed: this.checked + }) + }) + + } else if (event === 'itemEditDone') { + that._bindItemEditDone(handler) + + } else if (event === 'itemEditCancel') { + that._bindItemEditCancel(handler) } +} - View.prototype.bind = function(event, handler) { // eslint-disable-line - var that = this - if (event === 'newTodo') { - $on(that.$newTodo, 'change', function() { - handler(that.$newTodo.value) - }) - - } else if (event === 'removeCompleted') { - $on(that.$clearCompleted, 'click', function() { - handler() - }) - - } else if (event === 'toggleAll') { - $on(that.$toggleAll, 'click', function() { - handler({completed: this.checked}) - }) - - } else if (event === 'itemEdit') { - $delegate(that.$todoList, 'li label', 'dblclick', function() { - handler({id: that._itemId(this)}) - }) - - } else if (event === 'itemRemove') { - $delegate(that.$todoList, '.destroy', 'click', function() { - handler({id: that._itemId(this)}) - }) - - } else if (event === 'itemToggle') { - $delegate(that.$todoList, '.toggle', 'click', function() { - handler({ - id: that._itemId(this), - completed: this.checked - }) - }) - - } else if (event === 'itemEditDone') { - that._bindItemEditDone(handler) - - } else if (event === 'itemEditCancel') { - that._bindItemEditCancel(handler) - } - } - - // Export to window - window.app = window.app || {} - window.app.View = View -})(window) +// Export to window +window.app = window.app || {} +window.app.View = View diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 3af2c98..bc8ca13 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -4,7 +4,7 @@ const webpackValidator = require('webpack-validator') const {getIfUtils} = require('webpack-config-utils') module.exports = env => { - const {ifProd} = getIfUtils(env) + const {ifProd, ifNotProd} = getIfUtils(env) const config = webpackValidator({ context: resolve('src'), entry: './bootstrap.js', @@ -12,6 +12,7 @@ module.exports = env => { filename: 'bundle.js', path: resolve('dist'), publicPath: '/dist/', + pathinfo: ifNotProd(), }, devtool: ifProd('source-map', 'eval'), })