From a28d785b7f2be5e5484743216e17f34274be41b5 Mon Sep 17 00:00:00 2001 From: Rogerio Chaves Date: Sat, 17 Sep 2016 13:40:58 -0300 Subject: [PATCH] Add cycle.js --- implementations/cyclejs-4.1.1/.babelrc | 4 + implementations/cyclejs-4.1.1/.editorconfig | 7 + implementations/cyclejs-4.1.1/.gitignore | 5 + implementations/cyclejs-4.1.1/LICENSE | 22 +++ implementations/cyclejs-4.1.1/README.md | 17 ++ implementations/cyclejs-4.1.1/index.html | 19 +++ implementations/cyclejs-4.1.1/package.json | 51 ++++++ implementations/cyclejs-4.1.1/src/app.js | 28 ++++ .../src/components/Task/index.js | 19 +++ .../src/components/Task/intent.js | 36 +++++ .../src/components/Task/model.js | 27 ++++ .../cyclejs-4.1.1/src/components/Task/view.js | 34 ++++ .../src/components/TaskList/index.js | 99 ++++++++++++ .../src/components/TaskList/intent.js | 66 ++++++++ .../src/components/TaskList/model.js | 149 ++++++++++++++++++ .../src/components/TaskList/storage-sink.js | 15 ++ .../src/components/TaskList/storage-source.js | 30 ++++ .../src/components/TaskList/view.js | 83 ++++++++++ implementations/cyclejs-4.1.1/src/utils.js | 4 + index.html | 1 + 20 files changed, 716 insertions(+) create mode 100644 implementations/cyclejs-4.1.1/.babelrc create mode 100644 implementations/cyclejs-4.1.1/.editorconfig create mode 100644 implementations/cyclejs-4.1.1/.gitignore create mode 100644 implementations/cyclejs-4.1.1/LICENSE create mode 100644 implementations/cyclejs-4.1.1/README.md create mode 100644 implementations/cyclejs-4.1.1/index.html create mode 100644 implementations/cyclejs-4.1.1/package.json create mode 100644 implementations/cyclejs-4.1.1/src/app.js create mode 100644 implementations/cyclejs-4.1.1/src/components/Task/index.js create mode 100644 implementations/cyclejs-4.1.1/src/components/Task/intent.js create mode 100644 implementations/cyclejs-4.1.1/src/components/Task/model.js create mode 100644 implementations/cyclejs-4.1.1/src/components/Task/view.js create mode 100644 implementations/cyclejs-4.1.1/src/components/TaskList/index.js create mode 100644 implementations/cyclejs-4.1.1/src/components/TaskList/intent.js create mode 100644 implementations/cyclejs-4.1.1/src/components/TaskList/model.js create mode 100644 implementations/cyclejs-4.1.1/src/components/TaskList/storage-sink.js create mode 100644 implementations/cyclejs-4.1.1/src/components/TaskList/storage-source.js create mode 100644 implementations/cyclejs-4.1.1/src/components/TaskList/view.js create mode 100644 implementations/cyclejs-4.1.1/src/utils.js diff --git a/implementations/cyclejs-4.1.1/.babelrc b/implementations/cyclejs-4.1.1/.babelrc new file mode 100644 index 000000000..831f20a8e --- /dev/null +++ b/implementations/cyclejs-4.1.1/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015"], + "plugins": ["transform-object-rest-spread"] +} diff --git a/implementations/cyclejs-4.1.1/.editorconfig b/implementations/cyclejs-4.1.1/.editorconfig new file mode 100644 index 000000000..442d28d0c --- /dev/null +++ b/implementations/cyclejs-4.1.1/.editorconfig @@ -0,0 +1,7 @@ +; editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = LF \ No newline at end of file diff --git a/implementations/cyclejs-4.1.1/.gitignore b/implementations/cyclejs-4.1.1/.gitignore new file mode 100644 index 000000000..bbba82a23 --- /dev/null +++ b/implementations/cyclejs-4.1.1/.gitignore @@ -0,0 +1,5 @@ +.idea/ +ignore/ +node_modules/ +npm-debug.log +js diff --git a/implementations/cyclejs-4.1.1/LICENSE b/implementations/cyclejs-4.1.1/LICENSE new file mode 100644 index 000000000..76dc4b525 --- /dev/null +++ b/implementations/cyclejs-4.1.1/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 André Staltz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/implementations/cyclejs-4.1.1/README.md b/implementations/cyclejs-4.1.1/README.md new file mode 100644 index 000000000..0063fcd8a --- /dev/null +++ b/implementations/cyclejs-4.1.1/README.md @@ -0,0 +1,17 @@ +TodoMVC in Cycle.js +=================== + +To build the app run: + +``` +npm install +npm run build +``` + +TodoMVC example implemented in [Cycle.js](http://cycle.js.org). + +[Open the app]( http://cycle.js.org/todomvc-cycle/ ) + +- - - + +To see a version of this codebase using Immutable.js, [click here](https://github.com/cyclejs/todomvc-cycle/pull/9/files). diff --git a/implementations/cyclejs-4.1.1/index.html b/implementations/cyclejs-4.1.1/index.html new file mode 100644 index 000000000..5be41b1ad --- /dev/null +++ b/implementations/cyclejs-4.1.1/index.html @@ -0,0 +1,19 @@ + + + + + Cycle • TodoMVC + + + + +
+ + + + + diff --git a/implementations/cyclejs-4.1.1/package.json b/implementations/cyclejs-4.1.1/package.json new file mode 100644 index 000000000..3d337ecac --- /dev/null +++ b/implementations/cyclejs-4.1.1/package.json @@ -0,0 +1,51 @@ +{ + "name": "todomvc-cycle", + "version": "0.0.0", + "author": "Andre Staltz", + "repository": { + "type": "git", + "url": "git@github.com:staltz/todomvc-cycle.git" + }, + "license": "MIT", + "private": true, + "contributors": [ + { + "name": "Frederik Krautwald" + }, + { + "name": "Kahlil Lechelt", + "email": "hello@kahlil.info" + } + ], + "dependencies": { + "@cycle/dom": "12.2.5", + "@cycle/history": "^4.0.0", + "@cycle/isolate": "1.4.x", + "@cycle/storage": "3.0.0-rc3", + "@cycle/xstream-run": "3.1.0", + "history": "^3.0.0", + "todomvc-app-css": "2.0.3", + "todomvc-common": "1.0.1", + "xstream": "5.2.1" + }, + "devDependencies": { + "babel-plugin-transform-object-rest-spread": "^6.6.5", + "babel-preset-es2015": "^6.3.13", + "babel-register": "^6.4.3", + "babelify": "^7.2.0", + "browserify": "12.0.1", + "live-server": "^0.9.0", + "mkdirp": "^0.5.1", + "npm-run-all": "^1.4.0", + "uglify-js": "2.6.1", + "watchify": "^3.6.1" + }, + "scripts": { + "build-debug": "mkdirp js && browserify src/app.js -t babelify --outfile js/app.js", + "watch:js": "mkdirp js && watchify src/app.js -t babelify --outfile js/app.js -dv", + "serve": "live-server ./", + "uglify": "uglifyjs js/app.js -o js/app.min.js", + "build": "npm run build-debug && npm run uglify", + "start": "npm-run-all --parallel watch:js serve" + } +} diff --git a/implementations/cyclejs-4.1.1/src/app.js b/implementations/cyclejs-4.1.1/src/app.js new file mode 100644 index 000000000..e72554961 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/app.js @@ -0,0 +1,28 @@ +import {run} from '@cycle/xstream-run'; +import {makeDOMDriver} from '@cycle/dom'; +import {makeHistoryDriver} from '@cycle/history' +import {createHistory} from 'history'; +import storageDriver from '@cycle/storage'; +// THE MAIN FUNCTION +// This is the todo list component. +import TaskList from './components/TaskList/index'; + +const main = TaskList; + +// THE ENTRY POINT +// This is where the whole story starts. +// `run` receives a main function and an object +// with the drivers. +run(main, { + // THE DOM DRIVER + // `makeDOMDriver(container)` from Cycle DOM returns a + // driver function to interact with the DOM. + DOM: makeDOMDriver('.todoapp', {transposition: true}), + // THE HISTORY DRIVER + // A driver to interact with browser history + History: makeHistoryDriver(createHistory(), {capture: true}), + // THE STORAGE DRIVER + // The storage driver which can be used to access values for + // local- and sessionStorage keys as streams. + storage: storageDriver +}); diff --git a/implementations/cyclejs-4.1.1/src/components/Task/index.js b/implementations/cyclejs-4.1.1/src/components/Task/index.js new file mode 100644 index 000000000..eb2979ff1 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/Task/index.js @@ -0,0 +1,19 @@ +import intent from './intent'; +import model from './model'; +import view from './view'; + +// THE TODO ITEM FUNCTION +// This is a simple todo item component, +// structured with the MVI-pattern. +function Task({DOM, props$}) { + let action$ = intent(DOM); + let state$ = model(props$, action$); + let vtree$ = view(state$); + + return { + DOM: vtree$, + action$, + }; +} + +export default Task; diff --git a/implementations/cyclejs-4.1.1/src/components/Task/intent.js b/implementations/cyclejs-4.1.1/src/components/Task/intent.js new file mode 100644 index 000000000..1666f0e9c --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/Task/intent.js @@ -0,0 +1,36 @@ +import xs from 'xstream'; +import {ENTER_KEY, ESC_KEY} from '../../utils'; + +// THE TODO ITEM INTENT +// This intent function returns a stream of all the different, +// actions that can be taken on a todo. +function intent(DOMSource) { + // THE INTENT MERGE + // Merge all actions into one stream. + return xs.merge( + // THE DESTROY ACTION STREAM + DOMSource.select('.destroy').events('click') + .mapTo({type: 'destroy'}), + + // THE TOGGLE ACTION STREAM + DOMSource.select('.toggle').events('change') + .mapTo({type: 'toggle'}), + + // THE START EDIT ACTION STREAM + DOMSource.select('label').events('dblclick') + .mapTo({type: 'startEdit'}), + + // THE ESC KEY ACTION STREAM + DOMSource.select('.edit').events('keyup') + .filter(ev => ev.keyCode === ESC_KEY) + .mapTo({type: 'cancelEdit'}), + + // THE ENTER KEY ACTION STREAM + DOMSource.select('.edit').events('keyup') + .filter(ev => ev.keyCode === ENTER_KEY) + .compose(s => xs.merge(s, DOMSource.select('.edit').events('blur', true))) + .map(ev => ({title: ev.target.value, type: 'doneEdit'})) + ); +} + +export default intent; diff --git a/implementations/cyclejs-4.1.1/src/components/Task/model.js b/implementations/cyclejs-4.1.1/src/components/Task/model.js new file mode 100644 index 000000000..513f6e053 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/Task/model.js @@ -0,0 +1,27 @@ +import xs from 'xstream'; + +function model(props$, action$) { + // THE SANITIZED PROPERTIES + // If the list item has no data set it as empty and not completed. + let sanitizedProps$ = props$.startWith({title: '', completed: false}); + + // THE EDITING STREAM + // Create a stream that emits booleans that represent the + // "is editing" state. + let editing$ = + xs.merge( + action$.filter(a => a.type === 'startEdit').mapTo(true), + action$.filter(a => a.type === 'doneEdit').mapTo(false), + action$.filter(a => a.type === 'cancelEdit').mapTo(false) + ) + .startWith(false); + + return xs.combine(sanitizedProps$, editing$) + .map(([{title, completed}, editing]) => ({ + title, + isCompleted: completed, + isEditing: editing, + })); +} + +export default model; diff --git a/implementations/cyclejs-4.1.1/src/components/Task/view.js b/implementations/cyclejs-4.1.1/src/components/Task/view.js new file mode 100644 index 000000000..c2895857e --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/Task/view.js @@ -0,0 +1,34 @@ +import {button, div, input, label, li} from '@cycle/dom'; + +function view(state$) { + return state$.map(({title, isCompleted, isEditing}) => { + let todoRootClasses = { + completed: isCompleted, + editing: isEditing, + }; + + return li('.todoRoot', {class: todoRootClasses}, [ + div('.view', [ + input('.toggle', { + props: {type: 'checkbox', checked: isCompleted}, + }), + label(title), + button('.destroy') + ]), + input('.edit', { + props: {type: 'text'}, + hook: { + update: (oldVNode, {elm}) => { + elm.value = title; + if (isEditing) { + elm.focus(); + elm.selectionStart = elm.value.length; + } + } + } + }) + ]); + }); +} + +export default view; diff --git a/implementations/cyclejs-4.1.1/src/components/TaskList/index.js b/implementations/cyclejs-4.1.1/src/components/TaskList/index.js new file mode 100644 index 000000000..94f763b2f --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/TaskList/index.js @@ -0,0 +1,99 @@ +import xs from 'xstream'; +import isolate from '@cycle/isolate' +import intent from './intent'; +import model from './model'; +import view from './view'; +import deserialize from './storage-source'; +import serialize from './storage-sink'; +import Task from '../Task/index'; + +// AMEND STATE WITH CHILDREN +// This function creates the projection function +// for the map function below. +function amendStateWithChildren(DOMSource) { + return function (todosData) { + return { + ...todosData, + // The list property is the only one being amended. + // We map over the array in the list property to + // enhance them with the actual todo item data flow components. + list: todosData.list.map(data => { + // Turn the data item into an Observable + let props$ = xs.of(data); + // Create scoped todo item dataflow component. + let todoItem = isolate(Task)({DOM: DOMSource, props$}); + debugger; + + // Return the new data item for the list property array. + return { + ...data, + // This is a new property containing the DOM- and action stream of + // the todo item. + todoItem: { + DOM: todoItem.DOM, + action$: todoItem.action$.map(ev => ({...ev, id: data.id})) + } + }; + }), + }; + }; +} + +// THE TASKLIST COMPONENT +// This is the TaskList component which is being exported below. +function TaskList(sources) { + // THE LOCALSTORAGE STREAM + // Here we create a localStorage stream that only streams + // the first value read from localStorage in order to + // supply the application with initial state. + let localStorage$ = sources.storage.local.getItem('todos-cycle').take(1); + // THE INITIAL TODO DATA + // The `deserialize` function takes the serialized JSON stored in localStorage + // and turns it into a stream sending a JSON object. + let sourceTodosData$ = deserialize(localStorage$); + // THE PROXY ITEM ACTION STREAM + // We create a stream as a proxy for all the actions from each task. + let proxyItemAction$ = xs.create(); + // THE INTENT (MVI PATTERN) + // Pass relevant sources to the intent function, which set up + // streams that model the users actions. + let action$ = intent(sources.DOM, sources.History, proxyItemAction$); + // THE MODEL (MVI PATTERN) + // Actions get passed to the model function which transforms the data + // coming through and prepares the data for the view. + let state$ = model(action$, sourceTodosData$); + // AMEND STATE WITH CHILDREN + let amendedState$ = state$ + .map(amendStateWithChildren(sources.DOM)) + .remember(); + // A STREAM OF ALL ACTIONS ON ALL TASKS + // Each todo item has an action stream. All those action streams are being + // merged into a stream of all actions. Below this stream is passed into + // the proxyItemAction$ that we passed to the intent function above. + // This is how the intent on all the todo items flows back through the intent + // function of the list and can be handled in the model function of the list. + let itemAction$ = amendedState$ + .map(({list}) => xs.merge(...list.map(i => i.todoItem.action$))) + .flatten(); + // PASS REAL ITEM ACTIONS TO PROXY + // The item actions are passed to the proxy object. + proxyItemAction$.imitate(itemAction$); + // THE VIEW (MVI PATTERN) + // We render state as markup for the DOM. + let vdom$ = view(amendedState$); + // WRITE TO LOCALSTORAGE + // The latest state is written to localStorage. + let storage$ = serialize(state$).map((state) => ({ + key: 'todos-cycle', value: state + })); + // COMPLETE THE CYCLE + // Write the virtual dom stream to the DOM and write the + // storage stream to localStorage. + let sinks = { + DOM: vdom$, + storage: storage$, + }; + return sinks; +} + +export default TaskList; diff --git a/implementations/cyclejs-4.1.1/src/components/TaskList/intent.js b/implementations/cyclejs-4.1.1/src/components/TaskList/intent.js new file mode 100644 index 000000000..e180f27e8 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/TaskList/intent.js @@ -0,0 +1,66 @@ +import xs from 'xstream'; +import dropRepeats from 'xstream/extra/dropRepeats'; +import {ENTER_KEY, ESC_KEY} from '../../utils'; + +// THE INTENT FOR THE LIST +export default function intent(DOMSource, History, itemAction$) { + return xs.merge( + // THE ROUTE STREAM + // A stream that provides the path whenever the route changes. + History + .startWith({pathname: '/'}) + .map(location => location.pathname) + .compose(dropRepeats()) + .map(payload => ({type: 'changeRoute', payload})), + + // THE URL STREAM + // A stream of URL clicks in the app + DOMSource.select('a').events('click') + .map(event => event.target.hash.replace('#', '')) + .map(payload => ({type: 'url', payload})), + + // CLEAR INPUT STREAM + // A stream of ESC key strokes in the `.new-todo` field. + DOMSource.select('.new-todo').events('keydown') + .filter(ev => ev.keyCode === ESC_KEY) + .map(payload => ({type: 'clearInput', payload})), + + // ENTER KEY STREAM + // A stream of ENTER key strokes in the `.new-todo` field. + DOMSource.select('.new-todo').events('keydown') + // Trim value and only let the data through when there + // is anything but whitespace in the field and the ENTER key was hit. + .filter(ev => { + let trimmedVal = String(ev.target.value).trim(); + return ev.keyCode === ENTER_KEY && trimmedVal; + }) + // Return the trimmed value. + .map(ev => String(ev.target.value).trim()) + .map(payload => ({type: 'insertTodo', payload})), + + // TOGGLE STREAM + // Create a stream out of all the toggle actions on the todo items. + itemAction$.filter(action => action.type === 'toggle') + .map(action => ({...action, type: 'toggleTodo'})), + + // DELETE STREAM + // Create a stream out of all the destroy actions on the todo items. + itemAction$.filter(action => action.type === 'destroy') + .map(action => ({...action, type: 'deleteTodo'})), + + // EDIT STREAM + // Create a stream out of all the doneEdit actions on the todo items. + itemAction$.filter(action => action.type === 'doneEdit') + .map(action => ({...action, type: 'editTodo'})), + + // TOGGLE ALL STREAM + // Create a stream out of the clicks on the `.toggle-all` button. + DOMSource.select('.toggle-all').events('click') + .mapTo({type: 'toggleAll'}), + + // DELETE COMPLETED TODOS STREAM + // A stream of click events on the `.clear-completed` element. + DOMSource.select('.clear-completed').events('click') + .mapTo({type: 'deleteCompleteds'}) + ); +}; diff --git a/implementations/cyclejs-4.1.1/src/components/TaskList/model.js b/implementations/cyclejs-4.1.1/src/components/TaskList/model.js new file mode 100644 index 000000000..1dda2d5b0 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/TaskList/model.js @@ -0,0 +1,149 @@ +import xs from 'xstream'; +import concat from 'xstream/extra/concat'; + +// A helper function that provides filter functions +// depending on the route value. +function getFilterFn(route) { + switch (route) { + case '/active': return (task => task.completed === false); + case '/completed': return (task => task.completed === true); + default: return () => true; // allow anything + } +} + +// This search function is used in the `makeReducer$` +// function below to retrieve the index of a todo in the todosList +// in order to make a modification to the todo data. +function searchTodoIndex(todosList, todoid) { + let pointerId; + let index; + let top = todosList.length; + let bottom = 0; + for (let i = todosList.length - 1; i >= 0; i--) { // 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 null; +} + +// MAKE REDUCER STREAM +// A function that takes the actions on the todo list +// and returns a stream of "reducers": functions that expect the current +// todosData (the state) and return a new version of todosData. +function makeReducer$(action$) { + let clearInputReducer$ = action$ + .filter(a => a.type === 'clearInput') + .mapTo(function clearInputReducer(todosData) { + return todosData; + }); + + let insertTodoReducer$ = action$ + .filter(a => a.type === 'insertTodo') + .map(action => function insertTodoReducer(todosData) { + let lastId = todosData.list.length > 0 ? + todosData.list[todosData.list.length - 1].id : + 0; + todosData.list.push({ + id: lastId + 1, + title: action.payload, + completed: false + }); + return todosData; + }); + + let editTodoReducer$ = action$ + .filter(a => a.type === 'editTodo') + .map(action => function editTodoReducer(todosData) { + let todoIndex = searchTodoIndex(todosData.list, action.id); + todosData.list[todoIndex].title = action.title; + return todosData; + }); + + let toggleTodoReducer$ = action$ + .filter(a => a.type === 'toggleTodo') + .map(action => function toggleTodoReducer(todosData) { + let todoIndex = searchTodoIndex(todosData.list, action.id); + let previousCompleted = todosData.list[todoIndex].completed; + todosData.list[todoIndex].completed = !previousCompleted; + return todosData; + }); + + let toggleAllReducer$ = action$ + .filter(a => a.type === 'toggleAll') + .mapTo(function toggleAllReducer(todosData) { + let allAreCompleted = todosData.list + .reduce((x, y) => x && y.completed, true); + todosData.list.forEach((todoData) => { + todoData.completed = allAreCompleted ? false : true; + }); + return todosData; + }); + + let deleteTodoReducer$ = action$ + .filter(a => a.type === 'deleteTodo') + .map(action => function deleteTodoReducer(todosData) { + let todoIndex = searchTodoIndex(todosData.list, action.id); + todosData.list.splice(todoIndex, 1); + return todosData; + }); + + let deleteCompletedsReducer$ = action$ + .filter(a => a.type === 'deleteCompleteds') + .mapTo(function deleteCompletedsReducer(todosData) { + todosData.list = todosData.list + .filter(todoData => todoData.completed === false); + return todosData; + }); + + let changeRouteReducer$ = action$ + .filter(a => a.type === 'changeRoute') + .map(a => a.payload) + .startWith('/') + .map(path => { + let filterFn = getFilterFn(path); + return function changeRouteReducer(todosData) { + todosData.filter = path.replace('/', '').trim(); + todosData.filterFn = filterFn; + return todosData; + }; + }); + + return xs.merge( + clearInputReducer$, + insertTodoReducer$, + editTodoReducer$, + toggleTodoReducer$, + toggleAllReducer$, + deleteTodoReducer$, + deleteCompletedsReducer$, + changeRouteReducer$ + ); +} + +// THIS IS THE MODEL FUNCTION +// It expects the actions coming in from the todo items and +// the initial localStorage data. +function model(action$, sourceTodosData$) { + // THE BUSINESS LOGIC + // Actions are passed to the `makeReducer$` function + // which creates a stream of reducer functions that needs + // to be applied on the todoData when an action happens. + let reducer$ = makeReducer$(action$); + + // RETURN THE MODEL DATA + return sourceTodosData$.map(sourceTodosData => + reducer$.fold((todosData, reducer) => reducer(todosData), sourceTodosData) + ).flatten() + // Make this remember its latest event, so late listeners + // will be updated with the latest state. + .remember(); +} + +export default model; diff --git a/implementations/cyclejs-4.1.1/src/components/TaskList/storage-sink.js b/implementations/cyclejs-4.1.1/src/components/TaskList/storage-sink.js new file mode 100644 index 000000000..b002a31e9 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/TaskList/storage-sink.js @@ -0,0 +1,15 @@ +// Turn the data object that contains +// the todos into a string for localStorage. +export default function serialize(todos$) { + return todos$.map(todosData => JSON.stringify( + { + list: todosData.list.map(todoData => + ({ + title: todoData.title, + completed: todoData.completed, + id: todoData.id + }) + ) + } + )); +}; diff --git a/implementations/cyclejs-4.1.1/src/components/TaskList/storage-source.js b/implementations/cyclejs-4.1.1/src/components/TaskList/storage-source.js new file mode 100644 index 000000000..674355364 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/TaskList/storage-source.js @@ -0,0 +1,30 @@ +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 safeJSONParse = str => JSON.parse(str) || {}; + +let mergeWithDefaultTodosData = todosData => { + return merge({ + list: [], + filter: '', + filterFn: () => true, // allow anything + }, todosData); +} + +// Take localStorage todoData stream and transform into +// a JavaScript object. Set default data. +export default function deserialize(localStorageValue$) { + return localStorageValue$ + .map(safeJSONParse) + .map(mergeWithDefaultTodosData); +}; diff --git a/implementations/cyclejs-4.1.1/src/components/TaskList/view.js b/implementations/cyclejs-4.1.1/src/components/TaskList/view.js new file mode 100644 index 000000000..3a1c13d03 --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/components/TaskList/view.js @@ -0,0 +1,83 @@ +import {a, button, div, footer, h1, header, input, li, + section, span, strong, ul} from '@cycle/dom'; + +function renderHeader() { + return header('.header', [ + h1('todos'), + input('.new-todo', { + props: { + type: 'text', + placeholder: 'What needs to be done?', + autofocus: true, + name: 'newTodo' + }, + hook: { + update: (oldVNode, {elm}) => { + elm.value = ''; + }, + }, + }) + ]); +} + +function renderMainSection(todosData) { + let allCompleted = todosData.list.reduce((x, y) => x && y.completed, true); + let sectionStyle = {'display': todosData.list.length ? '' : 'none'}; + + return section('.main', {style: sectionStyle}, [ + input('.toggle-all', { + props: {type: 'checkbox', checked: allCompleted}, + }), + ul('.todo-list', todosData.list + .filter(todosData.filterFn) + .map(data => data.todoItem.DOM) + ) + ]); +} + +function renderFilterButton(todosData, filterTag, path, label) { + return li([ + todosData.filter === filterTag ? + a('.selected', {props: {href: path}}, label) : + a({props: {href: path}}, label) + ]); +} + +function renderFooter(todosData) { + let amountCompleted = todosData.list + .filter(todoData => todoData.completed) + .length; + let amountActive = todosData.list.length - amountCompleted; + let footerStyle = {'display': todosData.list.length ? '' : 'none'}; + + return footer('.footer', {style: footerStyle}, [ + span('.todo-count', [ + strong(String(amountActive)), + ' item' + (amountActive !== 1 ? 's' : '') + ' left' + ]), + ul('.filters', [ + renderFilterButton(todosData, '', '/', 'All'), + renderFilterButton(todosData, 'active', '/active', 'Active'), + renderFilterButton(todosData, 'completed', '/completed', 'Completed'), + ]), + (amountCompleted > 0 ? + button('.clear-completed', 'Clear completed (' + amountCompleted + ')') + : null + ) + ]) +} + +// THE VIEW +// This function expects the stream of todosData +// from the model function and turns it into a +// virtual DOM stream that is then ultimately returned into +// the DOM sink in the index.js. +export default function view(todos$) { + return todos$.map(todos => + div([ + renderHeader(), + renderMainSection(todos), + renderFooter(todos) + ]) + ); +}; diff --git a/implementations/cyclejs-4.1.1/src/utils.js b/implementations/cyclejs-4.1.1/src/utils.js new file mode 100644 index 000000000..68033e65e --- /dev/null +++ b/implementations/cyclejs-4.1.1/src/utils.js @@ -0,0 +1,4 @@ +const ENTER_KEY = 13; +const ESC_KEY = 27; + +export {ENTER_KEY, ESC_KEY}; diff --git a/index.html b/index.html index a5a6fec8e..4a140fbad 100644 --- a/index.html +++ b/index.html @@ -44,6 +44,7 @@

Methodology Notes

impl('React', '15.3.1', false, 'react-15.3.1/index.html'), impl('Angular', '1.5.8', false, 'angular-1.5.8/index.html'), impl('Angular', '2', false, 'angular-2/index.html'), + impl('Cycle.js', '4.1.1', false, 'cyclejs-4.1.1/index.html'), impl('Elm', '0.16', false, 'elm-0.16/index.html'), impl('Elm', '0.17', false, 'elm-0.17/index.html'), impl('React', '15.3.1', true, 'react-15.3.1-optimized/index.html'),