diff --git a/src/app.js b/src/app.js index bea3f58..e17fa0f 100644 --- a/src/app.js +++ b/src/app.js @@ -1,17 +1,17 @@ -require('todomvc-app-css/index.css') +import 'todomvc-app-css/index.css' -var View = require('./view') -var helpers = require('./helpers') -var Controller = require('./controller') -var Model = require('./model') -var Store = require('./store') -var Template = require('./template') +import View from './view' +import {log} from './helpers' +import Controller from './controller' +import Model from './model' +import Store from './store' +import Template from './template' /** -* Sets up a brand new Todo list. -* -* @param {string} name The name of your new to do list. -*/ + * Sets up a brand new Todo list. + * + * @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) @@ -20,8 +20,8 @@ function Todo(name) { this.controller = new Controller(this.model, this.view) } -module.exports.onLoad = function onLoad() { - var todo = new Todo('todos-vanillajs') +export function onLoad() { // eslint-disable-line import/prefer-default-export + const todo = new Todo('todos-vanillajs') todo.controller.setView(document.location.hash) - helpers.log('view set') + log('view set') } diff --git a/src/bootstrap.js b/src/bootstrap.js index 4572f23..5b32a1b 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -1,6 +1,6 @@ /* eslint no-console:0 */ -var app = require('./app') -var helpers = require('./helpers') +import {onLoad} from './app' +import {$on} from './helpers' // this is only relevant when using `hot` mode with webpack // special thanks to Eric Clemmons: https://github.com/ericclemmons/webpack-hot-server-example @@ -11,7 +11,7 @@ if (module.hot) { }) if (reloading) { console.log('🔁 HMR Reloading.') - app.onLoad() + onLoad() } else { console.info('✅ HMR Enabled.') bootstrap() @@ -22,6 +22,6 @@ if (module.hot) { } function bootstrap() { - helpers.$on(window, 'load', app.onLoad) - helpers.$on(window, 'hashchange', app.onLoad) + $on(window, 'load', onLoad) + $on(window, 'hashchange', onLoad) } diff --git a/src/controller.js b/src/controller.js index 292e640..cb7a70c 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,4 +1,4 @@ -module.exports = Controller +export default Controller /** * Takes a model and view and acts as the controller between them @@ -110,7 +110,7 @@ Controller.prototype.addItem = function(title) { Controller.prototype.editItem = function(id) { var that = this that.model.read(id, function(data) { - that.view.render('editItem', {id: id, title: data[0].title}) + that.view.render('editItem', {id, title: data[0].title}) }) } @@ -120,8 +120,8 @@ Controller.prototype.editItem = function(id) { 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}) + that.model.update(id, {title}, function() { + that.view.render('editItemDone', {id, title}) }) } else { that.removeItem(id) @@ -134,7 +134,7 @@ Controller.prototype.editItemSave = function(id, title) { 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.render('editItemDone', {id, title: data[0].title}) }) } @@ -179,10 +179,10 @@ Controller.prototype.removeCompletedItems = function() { */ Controller.prototype.toggleComplete = function(id, completed, silent) { var that = this - that.model.update(id, {completed: completed}, function() { + that.model.update(id, {completed}, function() { that.view.render('elementComplete', { - id: id, - completed: completed + id, + completed, }) }) diff --git a/src/controller.test.js b/src/controller.test.js index ba67dc9..b349559 100644 --- a/src/controller.test.js +++ b/src/controller.test.js @@ -1,4 +1,4 @@ -var Controller = require('./controller') +import Controller from './controller' describe('controller', () => { it('exists', () => { diff --git a/src/helpers.js b/src/helpers.js index b8eb212..3830387 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,4 +1,4 @@ -module.exports = {qs, qsa, log, $on, $delegate, $parent, remove, leftPad} +export {qs, qsa, log, $on, $delegate, $parent, remove, leftPad} // Get element(s) by CSS selector: function qs(selector, scope) { @@ -9,9 +9,9 @@ function qsa(selector, scope) { return (scope || document).querySelectorAll(selector) } -function log() { +function log(...args) { if (window.console && window.console.log) { - window.console.log.apply(window.console, arguments) // eslint-disable-line + window.console.log(...args) } } @@ -23,6 +23,10 @@ function $on(target, 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) { + // https://developer.mozilla.org/en-US/docs/Web/Events/blur + var useCapture = type === 'blur' || type === 'focus' + $on(target, type, dispatchEvent, useCapture) + function dispatchEvent(event) { var targetElement = event.target var potentialElements = qsa(selector, target) @@ -32,18 +36,13 @@ function $delegate(target, selector, type, handler) { handler.call(targetElement, event) } } - - // https://developer.mozilla.org/en-US/docs/Web/Events/blur - var useCapture = type === 'blur' || type === 'focus' - - $on(target, type, dispatchEvent, useCapture) } // Find the element's parent with the given tag name: // $parent(qs('a'), 'div'); function $parent(element, tagName) { if (!element.parentNode) { - return + return undefined } if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { return element.parentNode diff --git a/src/model.js b/src/model.js index 19ba010..06fa58a 100644 --- a/src/model.js +++ b/src/model.js @@ -1,4 +1,4 @@ -module.exports = Model +export default Model /** * Creates a new Model instance and hooks up the storage. @@ -30,20 +30,19 @@ Model.prototype.create = function(title, 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' }); -*/ + * 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() { @@ -58,6 +57,7 @@ Model.prototype.read = function(query, callback) { } else { this.storage.find(query, callback) } + return undefined } /** diff --git a/src/store.js b/src/store.js index b012cb8..0278006 100644 --- a/src/store.js +++ b/src/store.js @@ -1,13 +1,13 @@ -module.exports = Store +export default Store /** -* 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 -*/ + * 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() { } @@ -34,8 +34,8 @@ function Store(name, callback) { * * @example * db.find({foo: 'bar', hello: 'world'}, function (data) { -* // data will return any items that have foo: bar and -* // hello: world in their properties +* // data will return any items that have foo: bar and +* // hello: world in their properties * }); */ Store.prototype.find = function(query, callback) { @@ -85,10 +85,8 @@ Store.prototype.save = function(updateData, callback, id) { 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] - } + for (var key in updateData) { // eslint-disable-line guard-for-in + todos[i][key] = updateData[key] } break } @@ -117,7 +115,7 @@ Store.prototype.remove = function(id, callback) { var todos = data.todos for (var i = 0; i < todos.length; i++) { - if (todos[i].id == id) { // eslint-disable-line + if (todos[i].id === id) { todos.splice(i, 1) break } diff --git a/src/template.js b/src/template.js index cf482d1..60c4f6f 100644 --- a/src/template.js +++ b/src/template.js @@ -1,4 +1,4 @@ -module.exports = Template +export default Template var htmlEscapes = { '&': '&', @@ -17,9 +17,11 @@ var reUnescapedHtml = /[&<>"'`]/g var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source) var escape = function(string) { - return (string && reHasUnescapedHtml.test(string)) ? - string.replace(reUnescapedHtml, escapeHtmlChar) : - string + if (string && reHasUnescapedHtml.test(string)) { + return string.replace(reUnescapedHtml, escapeHtmlChar) + } else { + return string + } } /** @@ -28,32 +30,34 @@ var escape = function(string) { * @constructor */ function Template() { - this.defaultTemplate = '
  • ' + - '
    ' + - '' + - '' + - '' + - '
    ' + - '
  • ' + 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, -* }); -*/ + * 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 = '' @@ -80,11 +84,11 @@ Template.prototype.show = function(data) { } /** -* 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 -*/ + * 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' @@ -92,11 +96,11 @@ Template.prototype.itemCounter = function(activeTodos) { } /** -* Updates the text within the "Clear completed" button -* -* @param {[type]} completedTodos The number of completed todos. -* @returns {string} String containing the count -*/ + * 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' diff --git a/src/view.js b/src/view.js index 722d4fd..315ed5c 100644 --- a/src/view.js +++ b/src/view.js @@ -1,57 +1,184 @@ -/* eslint no-invalid-this: 0 */ - -var helpers = require('./helpers') -var qs = helpers.qs -var qsa = helpers.qsa -var $on = helpers.$on -var $parent = helpers.$parent -var $delegate = helpers.$delegate - -module.exports = View +/* eslint no-invalid-this: 0, complexity:[2, 9] */ +import {qs, qsa, $on, $parent, $delegate} from './helpers' /** -* 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 + */ +export default class View { + 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') -} + 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 + '"]') + _removeItem(id) { + var elem = qs('[data-id="' + id + '"]') - if (elem) { - this.$todoList.removeChild(elem) + 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 + } + + var input = qs('input.edit', listItem) + listItem.removeChild(input) + + 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) + } + } + + 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 + }) + } + }) + + $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() + + handler({id: _itemId(this)}) + } + }) + } + + bind(event, handler) { + 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: _itemId(this)}) + }) + + } else if (event === 'itemRemove') { + $delegate(that.$todoList, '.destroy', 'click', function() { + handler({id: _itemId(this)}) + }) + + } else if (event === 'itemToggle') { + $delegate(that.$todoList, '.toggle', 'click', function() { + handler({ + id: _itemId(this), + completed: this.checked + }) + }) + + } else if (event === 'itemEditDone') { + that._bindItemEditDone(handler) + + } else if (event === 'itemEditCancel') { + that._bindItemEditCancel(handler) + } } } -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) { +function _setFilter(currentPage) { qs('.filters .selected').className = '' qs('.filters [href="#/' + currentPage + '"]').className = 'selected' } -View.prototype._elementComplete = function(id, completed) { +function _elementComplete(id, completed) { var listItem = qs('[data-id="' + id + '"]') if (!listItem) { @@ -64,7 +191,7 @@ View.prototype._elementComplete = function(id, completed) { qs('input', listItem).checked = completed } -View.prototype._editItem = function(id, title) { +function _editItem(id, title) { var listItem = qs('[data-id="' + id + '"]') if (!listItem) { @@ -81,140 +208,7 @@ View.prototype._editItem = function(id, title) { 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) - } - } - - viewCommands[viewCmd]() -} - -View.prototype._itemId = function(element) { +function _itemId(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 - }) - } - }) - - $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() - } - }) -} - -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)}) - } - }) -} - -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) - } -}