From e6f149763f6fc551b5871a1de0ea73dad99c1296 Mon Sep 17 00:00:00 2001 From: Oscar Godson Date: Sat, 24 Nov 2012 22:04:54 -0800 Subject: [PATCH] TodoMVC app in vanilla JS. No, that's not a framework. --- vanilla-examples/vanillajs/component.json | 8 + .../components/director/build/director.js | 712 ++++++++++++++++++ .../components/todomvc-common/base.css | 414 ++++++++++ .../components/todomvc-common/base.js | 38 + .../components/todomvc-common/bg.png | Bin 0 -> 2126 bytes vanilla-examples/vanillajs/index.html | 28 +- vanilla-examples/vanillajs/js/app.js | 346 ++------- vanilla-examples/vanillajs/js/controller.js | 344 +++++++++ vanilla-examples/vanillajs/js/helpers.js | 19 + vanilla-examples/vanillajs/js/model.js | 119 +++ vanilla-examples/vanillajs/js/store.js | 153 ++++ vanilla-examples/vanillajs/js/view.js | 91 +++ 12 files changed, 1971 insertions(+), 301 deletions(-) create mode 100644 vanilla-examples/vanillajs/component.json create mode 100644 vanilla-examples/vanillajs/components/director/build/director.js create mode 100644 vanilla-examples/vanillajs/components/todomvc-common/base.css create mode 100644 vanilla-examples/vanillajs/components/todomvc-common/base.js create mode 100644 vanilla-examples/vanillajs/components/todomvc-common/bg.png create mode 100644 vanilla-examples/vanillajs/js/controller.js create mode 100644 vanilla-examples/vanillajs/js/helpers.js create mode 100644 vanilla-examples/vanillajs/js/model.js create mode 100644 vanilla-examples/vanillajs/js/store.js create mode 100644 vanilla-examples/vanillajs/js/view.js diff --git a/vanilla-examples/vanillajs/component.json b/vanilla-examples/vanillajs/component.json new file mode 100644 index 0000000000..70819266ad --- /dev/null +++ b/vanilla-examples/vanillajs/component.json @@ -0,0 +1,8 @@ +{ + "name": "todomvc-vanillajs", + "version": "0.0.0", + "dependencies": { + "todomvc-common": "~0.1.4", + "director": "~1.2.0" + } +} diff --git a/vanilla-examples/vanillajs/components/director/build/director.js b/vanilla-examples/vanillajs/components/director/build/director.js new file mode 100644 index 0000000000..0befbe0751 --- /dev/null +++ b/vanilla-examples/vanillajs/components/director/build/director.js @@ -0,0 +1,712 @@ + + +// +// Generated on Sun Dec 16 2012 22:47:05 GMT-0500 (EST) by Nodejitsu, Inc (Using Codesurgeon). +// Version 1.1.9 +// + +(function (exports) { + + +/* + * browser.js: Browser specific functionality for director. + * + * (C) 2011, Nodejitsu Inc. + * MIT LICENSE + * + */ + +if (!Array.prototype.filter) { + Array.prototype.filter = function(filter, that) { + var other = [], v; + for (var i = 0, n = this.length; i < n; i++) { + if (i in this && filter.call(that, v = this[i], i, this)) { + other.push(v); + } + } + return other; + }; +} + +if (!Array.isArray){ + Array.isArray = function(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; +} + +var dloc = document.location; + +function dlocHashEmpty() { + // Non-IE browsers return '' when the address bar shows '#'; Director's logic + // assumes both mean empty. + return dloc.hash === '' || dloc.hash === '#'; +} + +var listener = { + mode: 'modern', + hash: dloc.hash, + history: false, + + check: function () { + var h = dloc.hash; + if (h != this.hash) { + this.hash = h; + this.onHashChanged(); + } + }, + + fire: function () { + if (this.mode === 'modern') { + this.history === true ? window.onpopstate() : window.onhashchange(); + } + else { + this.onHashChanged(); + } + }, + + init: function (fn, history) { + var self = this; + this.history = history; + + if (!Router.listeners) { + Router.listeners = []; + } + + function onchange(onChangeEvent) { + for (var i = 0, l = Router.listeners.length; i < l; i++) { + Router.listeners[i](onChangeEvent); + } + } + + //note IE8 is being counted as 'modern' because it has the hashchange event + if ('onhashchange' in window && (document.documentMode === undefined + || document.documentMode > 7)) { + // At least for now HTML5 history is available for 'modern' browsers only + if (this.history === true) { + // There is an old bug in Chrome that causes onpopstate to fire even + // upon initial page load. Since the handler is run manually in init(), + // this would cause Chrome to run it twise. Currently the only + // workaround seems to be to set the handler after the initial page load + // http://code.google.com/p/chromium/issues/detail?id=63040 + setTimeout(function() { + window.onpopstate = onchange; + }, 500); + } + else { + window.onhashchange = onchange; + } + this.mode = 'modern'; + } + else { + // + // IE support, based on a concept by Erik Arvidson ... + // + var frame = document.createElement('iframe'); + frame.id = 'state-frame'; + frame.style.display = 'none'; + document.body.appendChild(frame); + this.writeFrame(''); + + if ('onpropertychange' in document && 'attachEvent' in document) { + document.attachEvent('onpropertychange', function () { + if (event.propertyName === 'location') { + self.check(); + } + }); + } + + window.setInterval(function () { self.check(); }, 50); + + this.onHashChanged = onchange; + this.mode = 'legacy'; + } + + Router.listeners.push(fn); + + return this.mode; + }, + + destroy: function (fn) { + if (!Router || !Router.listeners) { + return; + } + + var listeners = Router.listeners; + + for (var i = listeners.length - 1; i >= 0; i--) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + } + } + }, + + setHash: function (s) { + // Mozilla always adds an entry to the history + if (this.mode === 'legacy') { + this.writeFrame(s); + } + + if (this.history === true) { + window.history.pushState({}, document.title, s); + // Fire an onpopstate event manually since pushing does not obviously + // trigger the pop event. + this.fire(); + } else { + dloc.hash = (s[0] === '/') ? s : '/' + s; + } + return this; + }, + + writeFrame: function (s) { + // IE support... + var f = document.getElementById('state-frame'); + var d = f.contentDocument || f.contentWindow.document; + d.open(); + d.write("
@@ -22,16 +19,31 @@

todos

- + + + + + + + diff --git a/vanilla-examples/vanillajs/js/app.js b/vanilla-examples/vanillajs/js/app.js index daff48ecc2..9f013b687f 100644 --- a/vanilla-examples/vanillajs/js/app.js +++ b/vanilla-examples/vanillajs/js/app.js @@ -1,312 +1,72 @@ +/*global Store, Model, View, Controller, $$ */ (function () { 'use strict'; - var todos = [], - stat = {}, - ENTER_KEY = 13; - - window.addEventListener('load', windowLoadHandler, false); - - function Todo(title, completed) { - this.id = getUuid(); - this.title = title; - this.completed = completed; - } - - function Stat() { - this.todoLeft = 0; - this.todoCompleted = 0; - this.totalTodo = 0; - } - - function windowLoadHandler() { - loadTodos(); - refreshData(); - addEventListeners(); - } - - function addEventListeners() { - document.getElementById('new-todo').addEventListener('keypress', newTodoKeyPressHandler, false); - document.getElementById('toggle-all').addEventListener('change', toggleAllChangeHandler, false); - } - - function inputEditTodoKeyPressHandler(event) { - var inputEditTodo = event.target, - trimmedText = inputEditTodo.value.trim(), - todoId = event.target.id.slice(6); - - if (trimmedText) { - if (event.keyCode === ENTER_KEY) { - editTodo(todoId, trimmedText); - } - } else { - removeTodoById(todoId); - refreshData(); - } - } - - function inputEditTodoBlurHandler(event) { - var inputEditTodo = event.target, - todoId = event.target.id.slice(6); - - editTodo(todoId, inputEditTodo.value); - } - - function newTodoKeyPressHandler(event) { - if (event.keyCode === ENTER_KEY) { - addTodo(document.getElementById('new-todo').value); - } - } - - function toggleAllChangeHandler(event) { - for (var i in todos) { - todos[i].completed = event.target.checked; - } - - refreshData(); - } - - function spanDeleteClickHandler(event) { - removeTodoById(event.target.getAttribute('data-todo-id')); - refreshData(); - } - - function hrefClearClickHandler() { - removeTodosCompleted(); - refreshData(); - } - - function todoContentHandler(event) { - var todoId = event.target.getAttribute('data-todo-id'), - div = document.getElementById('li_' + todoId), - inputEditTodo = document.getElementById('input_' + todoId); - - div.className = 'editing'; - inputEditTodo.focus(); - } - - function checkboxChangeHandler(event) { - var checkbox = event.target, - todo = getTodoById(checkbox.getAttribute('data-todo-id')); - - todo.completed = checkbox.checked; - refreshData(); - } - - function loadTodos() { - if (!localStorage.getItem('todos-vanillajs')) { - localStorage.setItem('todos-vanillajs', JSON.stringify([])); - } - - todos = JSON.parse(localStorage.getItem('todos-vanillajs')); - } - - function addTodo(text) { - var trimmedText = text.trim(); - - if (trimmedText) { - var todo = new Todo(trimmedText, false); - todos.push(todo); - refreshData(); - } - } - - function editTodo(todoId, text) { - var i, l; - - for (i = 0, l = todos.length; i < l; i++) { - if (todos[i].id === todoId) { - todos[i].title = text; - } - } - - refreshData(); - } - - function removeTodoById(id) { - var i = todos.length; - - while (i--) { - if (todos[i].id === id) { - todos.splice(i, 1); - } + /** + * 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); + this.view = new View(); + this.controller = new Controller(this.model, this.view); + } + + var todo = new Todo('todos-vanillajs'); + + /** + * Finds the model ID of the clicked DOM element + * + * @param {object} target The starting point in the DOM for it to try to find + * the ID of the model. + */ + function lookupId(target) { + var lookup = target; + + while (lookup.nodeName !== 'LI') { + lookup = lookup.parentNode; } - } - - function removeTodosCompleted() { - var i = todos.length; - - while (i--) { - console.log(i); - if (todos[i].completed) { - todos.splice(i, 1); - } - } - } - - function getTodoById(id) { - var i, l; - for (i = 0, l = todos.length; i < l; i++) { - if (todos[i].id === id) { - return todos[i]; - } - } - } - - function refreshData() { - saveTodos(); - computeStats(); - redrawTodosUI(); - redrawStatsUI(); - changeToggleAllCheckboxState(); + return lookup.dataset.id; } - function saveTodos() { - localStorage.setItem('todos-vanillajs', JSON.stringify(todos)); - } - - function computeStats() { - var i, l; + // When the enter key is pressed fire the addItem method. + $$('#new-todo').addEventListener('keypress', function (e) { + todo.controller.addItem(e); + }); - stat = new Stat(); - stat.totalTodo = todos.length; + // A delegation event. Will check what item was clicked whenever you click on any + // part of a list item. + $$('#todo-list').addEventListener('click', function (e) { + var target = e.target; - for (i = 0, l = todos.length; i < l; i++) { - if (todos[i].completed) { - stat.todoCompleted++; - } + // If you click a destroy button + if (target.className.indexOf('destroy') > -1) { + todo.controller.removeItem(lookupId(target)); } - stat.todoLeft = stat.totalTodo - stat.todoCompleted; - } - - - function redrawTodosUI() { - - var todo, checkbox, label, deleteLink, divDisplay, inputEditTodo, li, i, l, - ul = document.getElementById('todo-list'); - - document.getElementById('main').style.display = todos.length ? 'block' : 'none'; - - ul.innerHTML = ''; - document.getElementById('new-todo').value = ''; - - for (i = 0, l = todos.length; i < l; i++) { - todo = todos[i]; - - // create checkbox - checkbox = document.createElement('input'); - checkbox.className = 'toggle'; - checkbox.setAttribute('data-todo-id', todo.id); - checkbox.type = 'checkbox'; - checkbox.addEventListener('change', checkboxChangeHandler); - - // create div text - label = document.createElement('label'); - label.setAttribute('data-todo-id', todo.id); - label.appendChild(document.createTextNode(todo.title)); - label.addEventListener('dblclick', todoContentHandler); - - - // create delete button - deleteLink = document.createElement('button'); - deleteLink.className = 'destroy'; - deleteLink.setAttribute('data-todo-id', todo.id); - deleteLink.addEventListener('click', spanDeleteClickHandler); - - // create divDisplay - divDisplay = document.createElement('div'); - divDisplay.className = 'view'; - divDisplay.setAttribute('data-todo-id', todo.id); - divDisplay.appendChild(checkbox); - divDisplay.appendChild(label); - divDisplay.appendChild(deleteLink); - - // create todo input - inputEditTodo = document.createElement('input'); - inputEditTodo.id = 'input_' + todo.id; - inputEditTodo.className = 'edit'; - inputEditTodo.value = todo.title; - inputEditTodo.addEventListener('keypress', inputEditTodoKeyPressHandler); - inputEditTodo.addEventListener('blur', inputEditTodoBlurHandler); - - - // create li - li = document.createElement('li'); - li.id = 'li_' + todo.id; - li.appendChild(divDisplay); - li.appendChild(inputEditTodo); - - - if (todo.completed) { - li.className += 'completed'; - checkbox.checked = true; - } - - ul.appendChild(li); + // If you click the checkmark + if (target.className.indexOf('toggle') > -1) { + todo.controller.toggleComplete(lookupId(target), target); } - } - function changeToggleAllCheckboxState() { - var toggleAll = document.getElementById('toggle-all'); + }); - toggleAll.checked = stat.todoCompleted === todos.length; - } - - function redrawStatsUI() { - removeChildren(document.getElementsByTagName('footer')[0]); - document.getElementById('footer').style.display = todos.length ? 'block' : 'none'; - - if (stat.todoCompleted) { - drawTodoClear(); - } + $$('#todo-list').addEventListener('dblclick', function (e) { + var target = e.target; - if (stat.totalTodo) { - drawTodoCount(); + if (target.nodeName === 'LABEL') { + todo.controller.editItem(lookupId(target), target); } - } - - function drawTodoCount() { - var number = document.createElement('strong'), - remaining = document.createElement('span'), - text = ' ' + (stat.todoLeft === 1 ? 'item' : 'items') + ' left'; + }); - // create remaining count - number.innerHTML = stat.todoLeft; + $$('#toggle-all').addEventListener('click', function (e) { + todo.controller.toggleAll(e); + }); - remaining.id = 'todo-count'; - remaining.appendChild(number); - remaining.appendChild(document.createTextNode(text)); - - document.getElementsByTagName('footer')[0].appendChild(remaining); - } - - function drawTodoClear() { - var buttonClear = document.createElement('button'); - - buttonClear.id = 'clear-completed'; - buttonClear.addEventListener('click', hrefClearClickHandler); - buttonClear.innerHTML = 'Clear completed (' + stat.todoCompleted + ')'; - - document.getElementsByTagName('footer')[0].appendChild(buttonClear); - } - - function removeChildren(node) { - node.innerHTML = ''; - } - - function getUuid() { - var i, random, - uuid = ''; - - for (i = 0; i < 32; i++) { - random = Math.random() * 16 | 0; - if (i === 8 || i === 12 || i === 16 || i === 20) { - uuid += '-'; - } - uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16); - } - return uuid; - } + $$('#clear-completed').addEventListener('click', function () { + todo.controller.removeCompletedItems(); + }); })(); diff --git a/vanilla-examples/vanillajs/js/controller.js b/vanilla-examples/vanillajs/js/controller.js new file mode 100644 index 0000000000..de55108fb7 --- /dev/null +++ b/vanilla-examples/vanillajs/js/controller.js @@ -0,0 +1,344 @@ +/*global Router, $$, $ */ +(function (window) { + 'use strict'; + + /** + * Takes a model and view and acts as the controller between them + * + * @constructor + * @param {object} model The model constructor + * @param {object} view The view constructor + */ + function Controller(model, view) { + this.model = model; + this.view = view; + + this.ENTER_KEY = 13; + this.ESCAPE_KEY = 27; + + this.$main = $$('#main'); + this.$toggleAll = $$('#toggle-all'); + this.$todoList = $$('#todo-list'); + this.$todoItemCounter = $$('#todo-count'); + this.$clearCompleted = $$('#clear-completed'); + this.$footer = $$('#footer'); + + this.router = new Router(); + this.router.init(); + + window.addEventListener('load', function () { + this._updateFilterState(); + }.bind(this)); + + // Couldn't figure out how to get flatiron to run some code on all pages. I + // tried '*', but then it overwrites ALL handlers for all the other pages + // and only runs this. + window.addEventListener('hashchange', function () { + this._updateFilterState(); + }.bind(this)); + + // Make sure on page load we start with a hash to trigger the flatiron and + // onhashchange routes + if (window.location.href.indexOf('#') === -1) { + window.location.hash = '#/'; + } + } + + /** + * An event to fire on load. Will get all items and display them in the + * todo-list + */ + Controller.prototype.showAll = function () { + this.model.read(function (data) { + this.$todoList.innerHTML = this.view.show(data); + }.bind(this)); + }; + + /** + * Renders all active tasks + */ + Controller.prototype.showActive = function () { + this.model.read({ completed: 0 }, function (data) { + this.$todoList.innerHTML = this.view.show(data); + }.bind(this)); + }; + + /** + * Renders all completed tasks + */ + Controller.prototype.showCompleted = function () { + this.model.read({ completed: 1 }, function (data) { + this.$todoList.innerHTML = this.view.show(data); + }.bind(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. + * + * @param {object} e The event object + */ + Controller.prototype.addItem = function (e) { + var input = $$('#new-todo'); + var title = title || ''; + + if (e.keyCode === this.ENTER_KEY) { + this.model.create(e.target.value, function (data) { + // We want to make sure we don't add incomplete + // items to the completed tab when you go to + // add an item and you're viewing the completed + // items + if (this._getCurrentPage() !== 'completed') { + this.$todoList.innerHTML = this.$todoList.innerHTML + this.view.show(data); + } + input.value = ''; + }.bind(this)); + } + + this._filter(); + }; + + /** + * Hides the label text and creates an input to edit the title of the item. + * When you hit enter or blur out of the input it saves it and updates the UI + * with the new name. + * + * @param {number} id The id of the item to edit + * @param {object} label The label you want to edit the text of + */ + Controller.prototype.editItem = function (id, label) { + var li = label; + + // This finds the