From 9658655fd3a40d68ea9a63a6a66da78a9d19b25c Mon Sep 17 00:00:00 2001 From: Jorin Vogel Date: Sat, 13 Jun 2015 16:59:14 +0700 Subject: [PATCH 1/2] sourcemaps and simple build command - include sourcemaps when built with npm run build-debug - run browserify and uglify with pipe --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5542943..56f77ad 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ "license": "MIT", "private": true, "devDependencies": { + "babelify": "^6.0.2", + "browserify": "^10.0.0", "cyclejs": "0.21.1", + "immutable": "^3.7.3", "todomvc-app-css": "^1.0.0", "todomvc-common": "^1.0.1", - "babel": "^5.1.13", - "babelify": "^6.0.2", - "browserify": "^10.0.0" + "uglifyjs": "^2.4.10" }, "scripts": { - "build-debug": "mkdir -p js && browserify src/app.js -t babelify --outfile js/app.js", - "uglify": "uglifyjs js/app.js -o js/app.min.js", - "build": "npm run build-debug && npm run uglify" + "build-debug": "browserify src/app.js -d -t babelify > js/app.js", + "build": "browserify src/app.js -t babelify | uglifyjs -c > js/app.js" } } From 03f6fdd3379108f88f86360a2c49f4c62674b089 Mon Sep 17 00:00:00 2001 From: Jorin Vogel Date: Sat, 13 Jun 2015 17:02:23 +0700 Subject: [PATCH 2/2] use immutable data structures via immutable.js --- src/models/todos.js | 99 ++++++++++++++++---------------------- src/sinks/local-storage.js | 10 ++-- src/sources/todos.js | 20 ++------ src/views/todos.js | 30 +++++++----- 4 files changed, 66 insertions(+), 93 deletions(-) diff --git a/src/models/todos.js b/src/models/todos.js index b5b6150..879edac 100644 --- a/src/models/todos.js +++ b/src/models/todos.js @@ -1,88 +1,73 @@ import Cycle from 'cyclejs'; +import {Map} from 'immutable'; function getFilterFn(route) { switch (route) { - case '/active': return (task => task.completed === false); - case '/completed': return (task => task.completed === true); + case '/active': return (x => !x.get('completed')); + case '/completed': return (x => x.get('completed')); default: return () => true; // allow anything } } function determineFilter(todosData, route) { - todosData.filter = route.replace('/', '').trim(); - todosData.filterFn = getFilterFn(route); - return todosData; -} - -function searchTodoIndex(todosList, todoid) { - let top = todosList.length; - let bottom = 0; - let pointerId; - let index; - while (true) { // binary search - index = bottom + ((top - bottom) >> 1); - pointerId = todosList[index].id; - if (pointerId === todoid) { - return index; - } else if (pointerId < todoid) { - bottom = index; - } else if (pointerId > todoid) { - top = index; - } - } + return todosData + .set('filter', route.replace('/', '').trim()) + .set('filterFn', getFilterFn(route)); } function makeModification$(intent) { - let clearInputMod$ = intent.clearInput$.map(() => (todosData) => { - todosData.input = ''; - return todosData; + let clearInputMod$ = intent.clearInput$.map(() => todosData => { + return todosData.set('input', ''); }); - let insertTodoMod$ = intent.insertTodo$.map((todoTitle) => (todosData) => { - let lastId = todosData.list.length > 0 ? - todosData.list[todosData.list.length - 1].id : - 0; - todosData.list.push({ + let insertTodoMod$ = intent.insertTodo$.map(todoTitle => todosData => { + let list = todosData.get('list'); + let lastId = list.size ? list.last().get('id') : 0; + + let todo = Map({ id: lastId + 1, title: todoTitle, completed: false }); - todosData.input = ''; - return todosData; + + return todosData.update('list', list => list.push(todo).set('input', '')); }); - let editTodoMod$ = intent.editTodo$.map((evdata) => (todosData) => { - let todoIndex = searchTodoIndex(todosData.list, evdata.id); - todosData.list[todoIndex].title = evdata.content; - return todosData; + let editTodoMod$ = intent.editTodo$.map(evdata => todosData => { + let todoIndex = todosData.get('list') + .findIndex(x => x.get('id') === evdata.id); + + return todosData.update('list', list => + list.update(todoIndex, x => x.set('title', evdata.content)) + ); }); - let toggleTodoMod$ = intent.toggleTodo$.map((todoid) => (todosData) => { - let todoIndex = searchTodoIndex(todosData.list, todoid); - let previousCompleted = todosData.list[todoIndex].completed; - todosData.list[todoIndex].completed = !previousCompleted; - return todosData; + let toggleTodoMod$ = intent.toggleTodo$.map(todoId => todosData => { + let todoIndex = todosData.get('list') + .findIndex(x => x.get('id') === todoId); + + return todosData.update('list', list => + list.update(todoIndex, x => x.set('completed', !x.get('completed'))) + ); }); - let toggleAllMod$ = intent.toggleAll$.map(() => (todosData) => { - let allAreCompleted = todosData.list - .reduce((x, y) => x && y.completed, true); - todosData.list.forEach((todoData) => { - todoData.completed = allAreCompleted ? false : true; - }); - return todosData; + let toggleAllMod$ = intent.toggleAll$.map(() => todosData => { + let state = todosData.get('list').some(x => !x.get('completed')); + return todosData.update('list', list => + list.map(x => x.set('completed', state)) + ); }); - let deleteTodoMod$ = intent.deleteTodo$.map((todoid) => (todosData) => { - let todoIndex = searchTodoIndex(todosData.list, todoid); - todosData.list.splice(todoIndex, 1); - return todosData; + let deleteTodoMod$ = intent.deleteTodo$.map(todoId => todosData => { + return todosData.update('list', list => + list.filter(x => x.get('id') !== todoId) + ); }); - let deleteCompletedsMod$ = intent.deleteCompleteds$.map(() => (todosData) => { - todosData.list = todosData.list - .filter(todoData => todoData.completed === false); - return todosData + let deleteCompletedsMod$ = intent.deleteCompleteds$.map(() => todosData => { + return todosData.update('list', list => + list.filter(x => !x.get('completed')) + ); }); return Cycle.Rx.Observable.merge( diff --git a/src/sinks/local-storage.js b/src/sinks/local-storage.js index d390d9b..c099128 100644 --- a/src/sinks/local-storage.js +++ b/src/sinks/local-storage.js @@ -1,13 +1,9 @@ export default function localStorageSink(todosData) { + // Observe all todos data and save them to localStorage let savedTodosData = { - list: todosData.list.map(todoData => - ({ - title: todoData.title, - completed: todoData.completed, - id: todoData.id - }) - ) + list: todosData.get('list').filter(x => x ).toJS() }; + localStorage.setItem('todos-cycle', JSON.stringify(savedTodosData)) }; diff --git a/src/sources/todos.js b/src/sources/todos.js index 0e6fc20..327ef58 100644 --- a/src/sources/todos.js +++ b/src/sources/todos.js @@ -1,28 +1,16 @@ import Cycle from 'cyclejs'; +import {fromJS} from 'immutable'; -function merge() { - let result = {}; - for (let i = 0; i < arguments.length; i++) { - let object = arguments[i]; - for (let key in object) { - if (object.hasOwnProperty(key)) { - result[key] = object[key]; - } - } - } - return result; -} - -let defaultTodosData = { +let defaultTodosData = fromJS({ list: [], input: '', filter: '', filterFn: () => true // allow anything -}; +}); let storedTodosData = JSON.parse(localStorage.getItem('todos-cycle')) || {}; -let initialTodosData = merge(defaultTodosData, storedTodosData); +let initialTodosData = defaultTodosData.merge(storedTodosData); export default { todosData$: Cycle.Rx.Observable.just(initialTodosData) diff --git a/src/views/todos.js b/src/views/todos.js index b0fc92b..bc6bef3 100644 --- a/src/views/todos.js +++ b/src/views/todos.js @@ -7,7 +7,7 @@ function vrenderHeader(todosData) { h('h1', 'todos'), h('input#new-todo', { type: 'text', - value: propHook(elem => { elem.value = todosData.input; }), + value: propHook(elem => elem.value = todosData.get('input')), attributes: { placeholder: 'What needs to be done?' }, @@ -18,16 +18,19 @@ function vrenderHeader(todosData) { } function vrenderMainSection(todosData) { - let allCompleted = todosData.list.reduce((x, y) => x && y.completed, true); + let list = todosData.get('list'); + let allCompleted = list.every(x => x.get('completed')); + return h('section#main', { - style: {'display': todosData.list.length ? '' : 'none'} + style: {'display': list.size ? '' : 'none'} }, [ h('input#toggle-all', { type: 'checkbox', checked: allCompleted }), - h('ul#todo-list', todosData.list - .filter(todosData.filterFn) + h('ul#todo-list', list + .filter(todosData.get('filterFn')) + .toJS() .map(todoData => h('todo-item.todo-item', { key: todoData.id, @@ -41,12 +44,13 @@ function vrenderMainSection(todosData) { } function vrenderFooter(todosData) { - let amountCompleted = todosData.list - .filter(todoData => todoData.completed) - .length; - let amountActive = todosData.list.length - amountCompleted; + let list = todosData.get('list'); + let filter = todosData.get('filter'); + let amountCompleted = list.count(x => x.get('completed')); + let amountActive = list.size - amountCompleted; + return h('footer#footer', { - style: {'display': todosData.list.length ? '' : 'none'} + style: {'display': list.size ? '' : 'none'} }, [ h('span#todo-count', [ h('strong', String(amountActive)), @@ -54,17 +58,17 @@ function vrenderFooter(todosData) { ]), h('ul#filters', [ h('li', [ - h('a' + (todosData.filter === '' ? '.selected' : ''), { + h('a' + (filter === '' ? '.selected' : ''), { attributes: {'href': '#/'} }, 'All') ]), h('li', [ - h('a' + (todosData.filter === 'active' ? '.selected' : ''), { + h('a' + (filter === 'active' ? '.selected' : ''), { attributes: {'href': '#/active'} }, 'Active') ]), h('li', [ - h('a' + (todosData.filter === 'completed' ? '.selected' : ''), { + h('a' + (filter === 'completed' ? '.selected' : ''), { attributes: {'href': '#/completed'} }, 'Completed') ])