Commit 5a3c81c5 authored by Olivier Scherrer's avatar Olivier Scherrer Committed by Arthur Verschaeve

Olives: update to olives and emily 3.x, remove requirejs

* Update to the latest version of the Olives framework.
* Update to the latest version of Emily
* Remove requirejs in favor of browserify
   - also implemented npm scripts and updated the readme

As a result of updating dependencies, this fixes the issue in #1029.

Ref #1029
Close #1242
parent 65a8ecfa
......@@ -40,13 +40,6 @@
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="node_modules/todomvc-common/base.js"></script>
<script src="node_modules/requirejs/require.js"></script>
<script src="node_modules/emily/build/Emily.js"></script>
<script src="node_modules/olives/build/Olives.js"></script>
<script src="js/lib/Tools.js"></script>
<script src="js/uis/Input.js"></script>
<script src="js/uis/List.js"></script>
<script src="js/uis/Controls.js"></script>
<script src="js/app.js"></script>
<script src="olives-todo.js"></script>
</body>
</html>
/*global require*/
(function () {
'use strict';
'use strict';
var input = require('./uis/input');
var list = require('./uis/list');
var controls = require('./uis/controls');
// These are the UIs that compose the Todo application
require([
'Todos/Input',
'Todos/List',
'Todos/Controls',
'node_modules/olives/build/src/LocalStore',
'Store'
],
var LocalStore = require('olives').LocalStore;
var Store = require('emily').Store;
// The application
function Todos(Input, List, Controls, LocalStore, Store) {
// The tasks Store is told to init on an array
// so tasks are indexed by a number
// This store is shared among several UIs of this application
// that's why it's created here
var tasks = new LocalStore([]);
// The tasks Store is told to init on an array
// so tasks are indexed by a number
// This store is shared among several UIs of this application
// that's why it's created here
var tasks = new LocalStore([]);
// Also create a shared stats store
var stats = new Store({
nbItems: 0,
nbLeft: 0,
nbCompleted: 0,
plural: 'items'
});
// Also create a shared stats store
var stats = new Store({
nbItems: 0,
nbLeft: 0,
nbCompleted: 0,
plural: 'items'
});
// Synchronize the store on 'todos-olives' localStorage
tasks.sync('todos-olives');
// Synchronize the store on 'todos-olives' localStorage
tasks.sync('todos-olives');
// Initialize Input UI by giving it a view and a model.
Input(document.querySelector('#header input'), tasks);
// Initialize Input UI by giving it a view and a model.
input(document.querySelector('#header input'), tasks);
// Init the List UI the same way, pass it the stats store too
List(document.querySelector('#main'), tasks, stats);
// Init the List UI the same way, pass it the stats store too
list(document.querySelector('#main'), tasks, stats);
// Same goes for the control UI
Controls(document.querySelector('#footer'), tasks, stats);
});
})();
// Same goes for the control UI
controls(document.querySelector('#footer'), tasks, stats);
/*global define*/
(function () {
'use strict';
'use strict';
/*
* A set of commonly used functions.
* They're useful for several UIs in the app.
* They could also be reused in other projects
*/
define('Todos/Tools', {
// className is set to the 'this' dom node according to the value's truthiness
'toggleClass': function (value, className) {
if (value) {
this.classList.add(className);
} else {
this.classList.remove(className);
}
/*
* A set of commonly used functions.
* They're useful for several UIs in the app.
* They could also be reused in other projects
*/
module.exports = {
// className is set to the 'this' dom node according to the value's truthiness
toggleClass: function (value, className) {
if (value) {
this.classList.add(className);
} else {
this.classList.remove(className);
}
});
})();
}
};
/*global define*/
(function () {
'use strict';
define('Todos/Controls', [
'node_modules/olives/build/src/OObject',
'node_modules/olives/build/src/Event.plugin',
'node_modules/olives/build/src/Bind.plugin',
'LocalStore',
'Todos/Tools'
],
// The Controls UI
function Controls(OObject, EventPlugin, ModelPlugin, Store, Tools) {
return function ControlsInit(view, model, stats) {
// The OObject (the controller) inits with a default model which is a simple store
// But it can be init'ed with any other store, like the LocalStore
var controls = new OObject(model);
// A function to get the completed tasks
var getCompleted = function () {
var completed = [];
model.loop(function (value, id) {
if (value.completed) {
completed.push(id);
}
});
return completed;
};
// Update all stats
var updateStats = function () {
var nbCompleted = getCompleted().length;
stats.set('nbItems', model.getNbItems());
stats.set('nbLeft', stats.get('nbItems') - nbCompleted);
stats.set('nbCompleted', nbCompleted);
stats.set('plural', stats.get('nbLeft') === 1 ? 'item' : 'items');
};
// Add plugins to the UI.
controls.plugins.addAll({
'event': new EventPlugin(controls),
'stats': new ModelPlugin(stats, {
'toggleClass': Tools.toggleClass
})
});
// Alive applies the plugins to the HTML view
controls.alive(view);
// Delete all tasks
controls.delAll = function () {
model.delAll(getCompleted());
};
// Update stats when the tasks list is modified
model.watch('added', updateStats);
model.watch('deleted', updateStats);
model.watch('updated', updateStats);
// I could either update stats at init or save them in a localStore
updateStats();
};
});
})();
/*global define*/
(function () {
'use strict';
// It's going to be called Input
define('Todos/Input', [
// It uses the Olives' OObject and the Event Plugin to listen to dom events and connect them to methods
'node_modules/olives/build/src/OObject',
'node_modules/olives/build/src/Event.plugin'
],
// The Input UI
function Input(OObject, EventPlugin) {
// It returns an init function
return function InputInit(view, model) {
// The OObject (the controller) inits with a default model which is a simple store
// But it can be init'ed with any other store, like the LocalStore
var input = new OObject(model);
var ENTER_KEY = 13;
// The event plugin that is added to the OObject
// We have to tell it where to find the methods
input.plugins.add('event', new EventPlugin(input));
// The method to add a new taks
input.addTask = function addTask(event, node) {
if (event.keyCode === ENTER_KEY && node.value.trim()) {
model.alter('push', {
title: node.value.trim(),
completed: false
});
node.value = '';
}
};
// Alive applies the plugins to the HTML view
input.alive(view);
};
});
})();
/*global define*/
(function () {
'use strict';
define('Todos/List', [
'node_modules/olives/build/src/OObject',
'node_modules/olives/build/src/Event.plugin',
'node_modules/olives/build/src/Bind.plugin',
'Todos/Tools'
],
// The List UI
function List(OObject, EventPlugin, ModelPlugin, Tools) {
return function ListInit(view, model, stats) {
// The OObject (the controller) inits with a default model which is a simple store
// But it can be init'ed with any other store, like the LocalStore
var list = new OObject(model);
var ENTER_KEY = 13;
// The plugins
list.plugins.addAll({
'event': new EventPlugin(list),
'model': new ModelPlugin(model, {
'toggleClass': Tools.toggleClass
}),
'stats': new ModelPlugin(stats, {
'toggleClass': Tools.toggleClass,
'toggleCheck': function (value) {
this.checked = model.getNbItems() === value ? 'on' : '';
}
})
});
// Remove the completed task
list.remove = function remove(event, node) {
model.del(node.getAttribute('data-model_id'));
};
// Un/check all tasks
list.toggleAll = function toggleAll(event, node) {
var checked = !!node.checked;
model.loop(function (value, idx) {
this.update(idx, 'completed', checked);
}, model);
};
// Enter edit mode
list.startEdit = function (event, node) {
var taskId = node.getAttribute('data-model_id');
Tools.toggleClass.call(view.querySelector('li[data-model_id="' + taskId + '"]'), true, 'editing');
view.querySelector('input.edit[data-model_id="' + taskId + '"]').focus();
};
// Leave edit mode
list.stopEdit = function (event, node) {
var taskId = node.getAttribute('data-model_id');
var value;
if (event.keyCode === ENTER_KEY) {
value = node.value.trim();
if (value) {
model.update(taskId, 'title', value);
} else {
model.del(taskId);
}
// When task #n is removed, #n+1 becomes #n, the dom node is updated to the new value, so editing mode should exit anyway
if (model.has(taskId)) {
Tools.toggleClass.call(view.querySelector('li[data-model_id="' + taskId + '"]'), false, 'editing');
}
} else if (event.type === 'blur') {
Tools.toggleClass.call(view.querySelector('li[data-model_id="' + taskId + '"]'), false, 'editing');
}
};
// Alive applies the plugins to the HTML view
list.alive(view);
};
});
})();
'use strict';
var OObject = require('olives').OObject;
var EventPlugin = require('olives')['Event.plugin'];
var BindPlugin = require('olives')['Bind.plugin'];
var Tools = require('../lib/Tools');
module.exports = function controlsInit(view, model, stats) {
// The OObject (the controller) inits with a default model which is a simple store
// But it can be init'ed with any other store, like the LocalStore
var controls = new OObject(model);
// A function to get the completed tasks
var getCompleted = function () {
var completed = [];
model.loop(function (value, id) {
if (value.completed) {
completed.push(id);
}
});
return completed;
};
// Update all stats
var updateStats = function () {
var nbCompleted = getCompleted().length;
stats.set('nbItems', model.count());
stats.set('nbLeft', stats.get('nbItems') - nbCompleted);
stats.set('nbCompleted', nbCompleted);
stats.set('plural', stats.get('nbLeft') === 1 ? 'item' : 'items');
};
// Add plugins to the UI.
controls.seam.addAll({
event: new EventPlugin(controls),
stats: new BindPlugin(stats, {
toggleClass: Tools.toggleClass
})
});
// Alive applies the plugins to the HTML view
controls.alive(view);
// Delete all tasks
controls.delAll = function () {
model.delAll(getCompleted());
};
// Update stats when the tasks list is modified
model.watch('added', updateStats);
model.watch('deleted', updateStats);
model.watch('updated', updateStats);
// I could either update stats at init or save them in a localStore
updateStats();
};
'use strict';
var OObject = require('olives').OObject;
var EventPlugin = require('olives')['Event.plugin'];
// It returns an init function
module.exports = function inputInit(view, model) {
// The OObject (the controller) inits with a default model which is a simple store
// But it can be init'ed with any other store, like the LocalStore
var input = new OObject(model);
var ENTER_KEY = 13;
// The event plugin that is added to the OObject
// We have to tell it where to find the methods
input.seam.add('event', new EventPlugin(input));
// The method to add a new taks
input.addTask = function addTask(event, node) {
if (event.keyCode === ENTER_KEY && node.value.trim()) {
model.alter('push', {
title: node.value.trim(),
completed: false
});
node.value = '';
}
};
// Alive applies the plugins to the HTML view
input.alive(view);
};
'use strict';
var OObject = require('olives').OObject;
var EventPlugin = require('olives')['Event.plugin'];
var BindPlugin = require('olives')['Bind.plugin'];
var Tools = require('../lib/Tools');
module.exports = function listInit(view, model, stats) {
// The OObject (the controller) inits with a default model which is a simple store
// But it can be init'ed with any other store, like the LocalStore
var list = new OObject(model);
var ENTER_KEY = 13;
// The plugins
list.seam.addAll({
event: new EventPlugin(list),
model: new BindPlugin(model, {
toggleClass: Tools.toggleClass
}),
stats: new BindPlugin(stats, {
toggleClass: Tools.toggleClass,
toggleCheck: function (value) {
this.checked = model.count() === value ? 'on' : '';
}
})
});
// Remove the completed task
list.remove = function remove(event, node) {
model.del(node.getAttribute('data-model_id'));
};
// Un/check all tasks
list.toggleAll = function toggleAll(event, node) {
var checked = !!node.checked;
model.loop(function (value, idx) {
this.update(idx, 'completed', checked);
}, model);
};
// Enter edit mode
list.startEdit = function (event, node) {
var taskId = node.getAttribute('data-model_id');
Tools.toggleClass.call(view.querySelector('li[data-model_id="' + taskId + '"]'), true, 'editing');
view.querySelector('input.edit[data-model_id="' + taskId + '"]').focus();
};
// Leave edit mode
list.stopEdit = function (event, node) {
var taskId = node.getAttribute('data-model_id');
var value;
if (event.keyCode === ENTER_KEY) {
value = node.value.trim();
if (value) {
model.update(taskId, 'title', value);
} else {
model.del(taskId);
}
// When task #n is removed, #n+1 becomes #n, the dom node is updated to the new value, so editing mode should exit anyway
if (model.has(taskId)) {
Tools.toggleClass.call(view.querySelector('li[data-model_id="' + taskId + '"]'), false, 'editing');
}
} else if (event.type === 'blur') {
Tools.toggleClass.call(view.querySelector('li[data-model_id="' + taskId + '"]'), false, 'editing');
}
};
// Alive applies the plugins to the HTML view
list.alive(view);
};
This diff is collapsed.
This diff is collapsed.
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define(["DomUtils"],
/**
* @class
* Event plugin adds events listeners to DOM nodes.
* It can also delegate the event handling to a parent dom node
* @requires Utils
*/
function EventPlugin(Utils) {
"use strict";
/**
* The event plugin constructor.
* ex: new EventPlugin({method: function(){} ...}, false);
* @param {Object} the object that has the event handling methods
* @param {Boolean} $isMobile if the event handler has to map with touch events
*/
return function EventPluginConstructor($parent, $isMobile) {
/**
* The parent callback
* @private
*/
var _parent = null,
/**
* The mapping object.
* @private
*/
_map = {
"mousedown" : "touchstart",
"mouseup" : "touchend",
"mousemove" : "touchmove"
},
/**
* Is touch device.
* @private
*/
_isMobile = !!$isMobile;
/**
* Add mapped event listener (for testing purpose).
* @private
*/
this.addEventListener = function addEventListener(node, event, callback, useCapture) {
node.addEventListener(this.map(event), callback, !!useCapture);
};
/**
* Listen to DOM events.
* @param {Object} node DOM node
* @param {String} name event's name
* @param {String} listener callback's name
* @param {String} useCapture string
*/
this.listen = function listen(node, name, listener, useCapture) {
this.addEventListener(node, name, function(e){
_parent[listener].call(_parent, e, node);
}, !!useCapture);
};
/**
* Delegate the event handling to a parent DOM element
* @param {Object} node DOM node
* @param {String} selector CSS3 selector to the element that listens to the event
* @param {String} name event's name
* @param {String} listener callback's name
* @param {String} useCapture string
*/
this.delegate = function delegate(node, selector, name, listener, useCapture) {
this.addEventListener(node, name, function(event){
if (Utils.matches(node, selector, event.target)) {
_parent[listener].call(_parent, event, node);
}
}, !!useCapture);
};
/**
* Get the parent object.
* @return {Object} the parent object
*/
this.getParent = function getParent() {
return _parent;
};
/**
* Set the parent object.
* The parent object is an object which the functions are called by node listeners.
* @param {Object} the parent object
* @return true if object has been set
*/
this.setParent = function setParent(parent) {
if (parent instanceof Object){
_parent = parent;
return true;
}
return false;
};
/**
* Get event mapping.
* @param {String} event's name
* @return the mapped event's name
*/
this.map = function map(name) {
return _isMobile ? (_map[name] || name) : name;
};
/**
* Set event mapping.
* @param {String} event's name
* @param {String} event's value
* @return true if mapped
*/
this.setMap = function setMap(name, value) {
if (typeof name == "string" &&
typeof value == "string") {
_map[name] = value;
return true;
}
return false;
};
//init
this.setParent($parent);
};
});
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define(["Store", "Tools"],
/**
* @class
* LocalStore is an Emily's Store that can be synchronized with localStorage
* Synchronize the store, reload your page/browser and resynchronize it with the same value
* and it gets restored.
* Only valid JSON data will be stored
*/
function LocalStore(Store, Tools) {
"use strict";
function LocalStoreConstructor() {
/**
* The name of the property in which to store the data
* @private
*/
var _name = null,
/**
* The localStorage
* @private
*/
_localStorage = localStorage,
/**
* Saves the current values in localStorage
* @private
*/
setLocalStorage = function setLocalStorage() {
_localStorage.setItem(_name, this.toJSON());
};
/**
* Override default localStorage with a new one
* @param local$torage the new localStorage
* @returns {Boolean} true if success
* @private
*/
this.setLocalStorage = function setLocalStorage(local$torage) {
if (local$torage && local$torage.setItem instanceof Function) {
_localStorage = local$torage;
return true;
} else {
return false;
}
};
/**
* Get the current localStorage
* @returns localStorage
* @private
*/
this.getLocalStorage = function getLocalStorage() {
return _localStorage;
};
/**
* Synchronize the store with localStorage
* @param {String} name the name in which to save the data
* @returns {Boolean} true if the param is a string
*/
this.sync = function sync(name) {
var json;
if (typeof name == "string") {
_name = name;
json = JSON.parse(_localStorage.getItem(name));
Tools.loop(json, function (value, idx) {
if (!this.has(idx)) {
this.set(idx, value);
}
}, this);
setLocalStorage.call(this);
// Watch for modifications to update localStorage
this.watch("added", setLocalStorage, this);
this.watch("updated", setLocalStorage, this);
this.watch("deleted", setLocalStorage, this);
return true;
} else {
return false;
}
};
}
return function LocalStoreFactory(init) {
LocalStoreConstructor.prototype = new Store(init);
return new LocalStoreConstructor();
};
});
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define(["StateMachine", "Store", "Plugins", "DomUtils", "Tools"],
/**
* @class
* OObject is a container for dom elements. It will also bind
* the dom to additional plugins like Data binding
* @requires StateMachine
*/
function OObject(StateMachine, Store, Plugins, DomUtils, Tools) {
"use strict";
return function OObjectConstructor(otherStore) {
/**
* This function creates the dom of the UI from its template
* It then queries the dom for data- attributes
* It can't be executed if the template is not set
* @private
*/
var render = function render(UI) {
// The place where the template will be created
// is either the currentPlace where the node is placed
// or a temporary div
var baseNode = _currentPlace || document.createElement("div");
// If the template is set
if (UI.template) {
// In this function, the thisObject is the UI's prototype
// UI is the UI that has OObject as prototype
if (typeof UI.template == "string") {
// Let the browser do the parsing, can't be faster & easier.
baseNode.innerHTML = UI.template.trim();
} else if (DomUtils.isAcceptedType(UI.template)) {
// If it's already an HTML element
baseNode.appendChild(UI.template);
}
// The UI must be placed in a unique dom node
// If not, there can't be multiple UIs placed in the same parentNode
// as it wouldn't be possible to know which node would belong to which UI
// This is probably a DOM limitation.
if (baseNode.childNodes.length > 1) {
throw new Error("UI.template should have only one parent node");
} else {
UI.dom = baseNode.childNodes[0];
}
UI.plugins.apply(UI.dom);
} else {
// An explicit message I hope
throw new Error("UI.template must be set prior to render");
}
},
/**
* This function appends the dom tree to the given dom node.
* This dom node should be somewhere in the dom of the application
* @private
*/
place = function place(UI, DOMplace, beforeNode) {
if (DOMplace) {
// IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this.
if (beforeNode) {
DOMplace.insertBefore(UI.dom, beforeNode);
} else {
DOMplace.appendChild(UI.dom);
}
// Also save the new place, so next renderings
// will be made inside it
_currentPlace = DOMplace;
}
},
/**
* Does rendering & placing in one function
* @private
*/
renderNPlace = function renderNPlace(UI, dom) {
render(UI);
place.apply(null, Tools.toArray(arguments));
},
/**
* This stores the current place
* If this is set, this is the place where new templates
* will be appended
* @private
*/
_currentPlace = null,
/**
* The UI's stateMachine.
* Much better than if(stuff) do(stuff) else if (!stuff and stuff but not stouff) do (otherstuff)
* Please open an issue if you want to propose a better one
* @private
*/
_stateMachine = new StateMachine("Init", {
"Init": [["render", render, this, "Rendered"],
["place", renderNPlace, this, "Rendered"]],
"Rendered": [["place", place, this],
["render", render, this]]
});
/**
* The UI's Store
* It has set/get/del/has/watch/unwatch methods
* @see Emily's doc for more info on how it works.
*/
this.model = otherStore instanceof Store ? otherStore : new Store();
/**
* The module that will manage the plugins for this UI
* @see Olives/Plugins' doc for more info on how it works.
*/
this.plugins = new Plugins();
/**
* Describes the template, can either be like "&lt;p&gt;&lt;/p&gt;" or HTMLElements
* @type string or HTMLElement|SVGElement
*/
this.template = null;
/**
* This will hold the dom nodes built from the template.
*/
this.dom = null;
/**
* Place the UI in a given dom node
* @param node the node on which to append the UI
* @param beforeNode the dom before which to append the UI
*/
this.place = function place(node, beforeNode) {
_stateMachine.event("place", this, node, beforeNode);
};
/**
* Renders the template to dom nodes and applies the plugins on it
* It requires the template to be set first
*/
this.render = function render() {
_stateMachine.event("render", this);
};
/**
* Set the UI's template from a DOM element
* @param {HTMLElement|SVGElement} dom the dom element that'll become the template of the UI
* @returns true if dom is an HTMLElement|SVGElement
*/
this.setTemplateFromDom = function setTemplateFromDom(dom) {
if (DomUtils.isAcceptedType(dom)) {
this.template = dom;
return true;
} else {
return false;
}
};
/**
* Transforms dom nodes into a UI.
* It basically does a setTemplateFromDOM, then a place
* It's a helper function
* @param {HTMLElement|SVGElement} node the dom to transform to a UI
* @returns true if dom is an HTMLElement|SVGElement
*/
this.alive = function alive(dom) {
if (DomUtils.isAcceptedType(dom)) {
this.setTemplateFromDom(dom);
this.place(dom.parentNode, dom.nextElementSibling);
return true;
} else {
return false;
}
};
/**
* Get the current dom node where the UI is placed.
* for debugging purpose
* @private
* @return {HTMLElement} node the dom where the UI is placed.
*/
this.getCurrentPlace = function(){
return _currentPlace;
};
};
});
This diff is collapsed.
This diff is collapsed.
{
"private": true,
"dependencies": {
"olives": "^1.6.0",
"emily": "^1.8.1",
"requirejs": "^2.1.5",
"todomvc-common": "^1.0.1",
"todomvc-app-css": "^1.0.1"
"emily": "^3.0.3",
"olives": "^3.0.5",
"todomvc-app-css": "^1.0.1",
"todomvc-common": "^1.0.1"
},
"scripts": {
"build": "browserify ./js/app.js -o olives-todo.js",
"watch": "watchify ./js/app.js -o olives-todo.js"
},
"devDependencies": {
"browserify": "^9.0.4",
"watchify": "^3.1.0"
}
}
......@@ -16,3 +16,23 @@ Here are some links you may find helpful:
* [Olives.js on GitHub](https://github.com/flams/olives)
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Building the app
As this application is using node's module system, `browserify` or similar tool is required to package it for the browser. To build the app, simply do:
```
npm run build
```
To automatically rebuild the application as you make changes to the source code, you can do:
```
npm run watch
```
Make sure that you've installed all the dependencies first:
```
npm install
```
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment