Commit 4033761f authored by Tim Branyen's avatar Tim Branyen Committed by Sam Saccone

Request for adding diffHTML to TodoMVC

Hi TasteJS folks!

This is a PR that contains a snapshot of:
https://github.com/tbranyen/todomvc to be included as part of TodoMVC's
collection.

This utilizes a tool I've been working on called
[diffHTML](http://www.diffhtml.org) that helps write components and
applications with declarative *real* HTML and hook into Virtual DOM
changes with transition hooks.

It also utilizes Redux to handle all state, since it pairs so nicely
with diffHTML it made sense to include over a vanilla approach.

Let me know what you think, thanks for your consideration!
parent 083b718c
# Names should be added to this file with this pattern:
#
# For individuals:
# Name <email address>
#
# For organizations:
# Organization <fnmatch pattern>
#
Tim Branyen <tim@tabdeveloper.com>
TodoMVC
-------
This is an experimental project to make a TodoMVC with Custom Elements and
diffHTML to handle Virtual DOM operations.
# diffHTML TodoMVC Example
> diffHTML is an experimental library for building components and structuring applications with a virtual DOM and declarative HTML interface.
> _[diffHTML - www.diffhtml.org](http://www.diffhtml.org/)_
## Learning diffHTML
The [diffHTML website](http://www.diffhtml.org) is a great resource for getting started.
Get help from diffHTML devs and users:
* Find us on Gitter -
[https://gitter.im/tbranyen/diffhtml](https://gitter.im/tbranyen/diffhtml)
## Implementation
The diffHTML implementation of TodoMVC has a few key differences from other
implementations:
* [Custom Elements](http://w3c.github.io/webcomponents/explainer/) allow you to
create new HTML elements that are reusable, composable, and encapsulated.
* [Transitions](https://github.com/tbranyen/diffhtml#add-a-transition-state-callback)
are added since they are trivial to write with diffHTML. They provide a
smoother and more pleasing experience by adding animations that hook into the
rendering engine.
## Compatibility
This example should run in all major browsers, although the Web Animations
specification will only work in browsers that support it.
## Running this sample
1. Start an HTTP server in the root directory of `todomvc/` and navigate to
`/diffhtml` to run this sample.
Hint: if you have python installed, you can just run: `python -m SimpleHTTPServer`
## Building this sample
1. Install [Node.js](https://www.nodejs.org) (contains NPM for package management)
2. From the `todomvc/` folder, run `npm install` followed by an `npm run build`
3. Run the http server from the above "Running this sample" section
4. If you're continually updating you can run `npm run watch` to monitor
changes and re-build automatically
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<title>diffHTML • TodoMVC</title>
<style>todo-app footer.info { opacity: 0; }</style>
</head>
<body>
<todo-app data-reducer="todoApp">
<footer class="info">
<p>Double-click to edit a todo</p>
<p>
Created by <a href="http://github.com/tbranyen">Tim Branyen</a>
using <a href="http://diffhtml.org">diffHTML</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</todo-app>
<script src="node_modules/todomvc-common/base.js" async></script>
<script src="dist/todomvc.js" async></script>
</body>
</html>
import { html, innerHTML } from 'diffhtml';
import store from '../redux/store';
import * as todoAppActions from '../redux/actions/todo-app';
import renderTodoList from './todo-list';
export default class TodoApp {
static create(mount) { return new TodoApp(mount); }
constructor(mount) {
this.mount = mount;
this.existingMarkup = this.mount.innerHTML;
this.unsubscribeStore = store.subscribe(() => this.render());
this.render();
}
addTodo(ev) {
if (!ev.target.matches('.add-todo')) { return; }
ev.preventDefault();
const newTodo = ev.target.querySelector('.new-todo');
store.dispatch(todoAppActions.addTodo(newTodo.value));
newTodo.value = '';
}
removeTodo(ev) {
if (!ev.target.matches('.destroy')) { return; }
const li = ev.target.parentNode.parentNode;
const index = Array.from(li.parentNode.children).indexOf(li);
store.dispatch(todoAppActions.removeTodo(index));
}
toggleCompletion(ev) {
if (!ev.target.matches('.toggle')) { return; }
const li = ev.target.parentNode.parentNode;
const index = Array.from(li.parentNode.children).indexOf(li);
store.dispatch(todoAppActions.toggleCompletion(index, ev.target.checked));
}
startEditing(ev) {
if (!ev.target.matches('label')) { return; }
const li = ev.target.parentNode.parentNode;
const index = Array.from(li.parentNode.children).indexOf(li);
store.dispatch(todoAppActions.startEditing(index));
li.querySelector('form input').focus();
}
stopEditing(ev) {
ev.preventDefault();
const parentNode = ev.target.parentNode;
const nodeName = parentNode.nodeName.toLowerCase();
const li = nodeName === 'li' ? parentNode : parentNode.parentNode;
const index = Array.from(li.parentNode.children).indexOf(li);
const editTodo = li.querySelector('.edit');
const text = editTodo.value.trim();
if (text) {
store.dispatch(todoAppActions.stopEditing(index, text));
} else {
store.dispatch(todoAppActions.removeTodo(index));
}
}
clearCompleted(ev) {
if (!ev.target.matches('.clear-completed')) { return; }
store.dispatch(todoAppActions.clearCompleted());
}
toggleAll(ev) {
if (!ev.target.matches('.toggle-all')) { return; }
store.dispatch(todoAppActions.toggleAll(ev.target.checked));
}
handleKeyDown(ev) {
if (!ev.target.matches('.edit')) { return; }
const todoApp = store.getState()[this.mount.dataset.reducer];
const li = ev.target.parentNode.parentNode;
const index = Array.from(li.parentNode.children).indexOf(li);
switch (ev.keyCode) {
case 27: {
ev.target.value = todoApp.todos[index].title;
this.stopEditing(ev);
}
}
}
getTodoClassNames(todo) {
return [
todo.completed ? 'completed' : '',
todo.editing ? 'editing' : ''
].filter(Boolean).join(' ');
}
setCheckedState() {
const todoApp = store.getState()[this.mount.dataset.reducer];
const notChecked = todoApp.todos.filter(todo => !todo.completed).length;
return notChecked ? '' : 'checked';
}
onSubmitHandler(ev) {
ev.preventDefault();
if (ev.target.matches('.add-todo')) {
this.addTodo(ev);
} else if (ev.target.matches('.edit-todo')) {
this.stopEditing(ev);
}
}
onClickHandler(ev) {
if (ev.target.matches('.destroy')) {
this.removeTodo(ev);
} else if (ev.target.matches('.toggle-all')) {
this.toggleAll(ev);
} else if (ev.target.matches('.clear-completed')) {
this.clearCompleted(ev);
}
}
getNavClass(name) {
const state = store.getState();
const path = state.url.path;
return path === name ? 'selected' : undefined;
}
render() {
const state = store.getState();
const todoApp = state[this.mount.dataset.reducer];
const status = state.url.path.slice(1);
const allTodos = todoApp.todos;
const todos = todoApp.getByStatus(status);
const activeTodos = todoApp.getByStatus('active');
const completedTodos = todoApp.getByStatus('completed');
localStorage['diffhtml-todos'] = JSON.stringify(allTodos);
innerHTML(this.mount, html`
<section class="todoapp"
onsubmit=${this.onSubmitHandler.bind(this)}
onclick=${this.onClickHandler.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
ondblclick=${this.startEditing.bind(this)}
onchange=${this.toggleCompletion.bind(this)}>
<header class="header">
<h1>todos</h1>
<form class="add-todo">
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus="">
</form>
</header>
${allTodos.length ? html`
<section class="main">
<input class="toggle-all" type="checkbox" ${this.setCheckedState()}>
<ul class="todo-list">${
renderTodoList.call(this, {
stopEditing: this.stopEditing.bind(this),
todos
})
}</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>${activeTodos.length}</strong>
${activeTodos.length == 1 ? 'item' : 'items'} left
</span>
<ul class="filters">
<li>
<a href="#/" class=${this.getNavClass('/')}>
All
</a>
</li>
<li>
<a href="#/active" class=${this.getNavClass('/active')}>
Active
</a>
</li>
<li>
<a href="#/completed" class=${this.getNavClass('/completed')}>
Completed
</a>
</li>
</ul>
${completedTodos.length ? html`
<button class="clear-completed" onclick=${this.clearCompleted.bind(this)}>
Clear completed
</button>
` : ''}
</footer>
` : ''}
</section>
${this.existingMarkup}
`);
}
}
import { html } from 'diffhtml';
function encode(str) {
return str.replace(/["&'<>`]/g, match => `&#${match.charCodeAt(0)};`);
}
export default function renderTodoList(props) {
return props.todos.map(todo => html`
<li key="${todo.key}" class="${this.getTodoClassNames(todo)}">
<div class="view">
<input class="toggle" type="checkbox" ${todo.completed ? 'checked' : ''}>
<label>${encode(todo.title)}</label>
<button class="destroy"></button>
</div>
<form class="edit-todo">
<input
onblur=${props.stopEditing}
value="${encode(todo.title)}"
class="edit">
</form>
</li>
`);
}
import TodoApp from './components/todo-app';
import store from './redux/store';
import * as urlActions from './redux/actions/url';
const setHashState = hash => store.dispatch(urlActions.setHashState(hash));
// Create the application and mount.
TodoApp.create(document.querySelector('todo-app'));
// Set URL state.
setHashState(location.hash);
// Set URL state when hash changes.
window.onhashchange = e => setHashState(location.hash);
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const TOGGLE_COMPLETION = 'TOGGLE_COMPLETION';
export const START_EDITING = 'START_EDITING';
export const STOP_EDITING = 'STOP_EDITING';
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
export const TOGGLE_ALL = 'TOGGLE_ALL';
export function addTodo(title) {
return {
type: ADD_TODO,
title
};
}
export function removeTodo(index) {
return {
type: REMOVE_TODO,
index
};
}
export function toggleCompletion(index, completed) {
return {
type: TOGGLE_COMPLETION,
index,
completed
};
}
export function startEditing(index) {
return {
type: START_EDITING,
index
};
}
export function stopEditing(index, title) {
return {
type: STOP_EDITING,
index,
title
};
}
export function clearCompleted() {
return {
type: CLEAR_COMPLETED
};
}
export function toggleAll(completed) {
return {
type: TOGGLE_ALL,
completed
};
}
export const SET_HASH_STATE = 'SET_HASH_STATE';
export function setHashState(hash) {
const path = hash.slice(1) || '/';
return {
type: SET_HASH_STATE,
path
};
}
import * as todoAppActions from '../actions/todo-app';
const initialState = {
todos: JSON.parse(localStorage['diffhtml-todos'] || '[]'),
getByStatus(type) {
return this.todos.filter(todo => {
switch (type) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
}
return true;
})
}
};
export default function todoApp(state = initialState, action) {
switch (action.type) {
case todoAppActions.ADD_TODO: {
if (!action.title) { return state; }
return Object.assign({}, state, {
todos: state.todos.concat({
completed: false,
editing: false,
title: action.title.trim(),
key: Date.now() + state.todos.length
})
});
}
case todoAppActions.REMOVE_TODO: {
state.todos.splice(action.index, 1);
return Object.assign({}, state, {
todos: [].concat(state.todos)
});
}
case todoAppActions.TOGGLE_COMPLETION: {
const todo = state.todos[action.index];
state.todos[action.index] = Object.assign({}, todo, {
completed: action.completed
});
return Object.assign({}, state, {
todos: [].concat(state.todos)
});
}
case todoAppActions.START_EDITING: {
const todo = state.todos[action.index];
state.todos[action.index] = Object.assign({}, todo, {
editing: true
});
return Object.assign({}, state, {
todos: [].concat(state.todos)
});
}
case todoAppActions.STOP_EDITING: {
const todo = state.todos[action.index];
state.todos[action.index] = Object.assign({}, todo, {
title: action.title,
editing: false
});
return Object.assign({}, state, {
todos: [].concat(state.todos)
});
}
case todoAppActions.CLEAR_COMPLETED: {
return Object.assign({}, state, {
todos: state.todos.filter(todo => todo.completed === false)
});
}
case todoAppActions.TOGGLE_ALL: {
return Object.assign({}, state, {
todos: state.todos.map(todo => Object.assign({}, todo, {
completed: action.completed
}))
});
}
default: {
return state;
}
}
}
import * as urlActions from '../actions/url';
const initialState = {
path: location.hash.slice(1) || '/'
};
export default function url(state = initialState, action) {
switch (action.type) {
case urlActions.SET_HASH_STATE: {
return Object.assign({}, state, {
path: action.path
});
}
default: {
return state;
}
}
}
import { compose, combineReducers, createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import todoApp from './reducers/todo-app';
import url from './reducers/url';
// Makes a reusable function to create a store. Currently not exported, but
// could be in the future for testing purposes.
const createStoreWithMiddleware = compose(
// Adds in store middleware, such as async thunk and logging.
applyMiddleware(createLogger()),
// Hook devtools into our store.
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
// Compose the root reducer from modular reducers.
export default createStoreWithMiddleware(combineReducers({
// Encapsulates all TodoApp state.
todoApp,
// Manage the URL state.
url,
// Store the last action taken.
lastAction: (state, action) => action
}), {});
{
"name": "todomvc",
"version": "1.0.0",
"description": "TodoMVC",
"main": "lib/index.js",
"scripts": {
"test": "mocha",
"build": "mkdir -p dist ; browserify -t [ babelify --presets [ es2015 ] ] lib/index.js | derequire > dist/todomvc.js",
"watch": "watchify -t [ babelify --presets [ es2015 ] ] lib/index.js -o 'derequire > dist/todomvc.js' -v"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tbranyen/todomvc.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/tbranyen/todomvc/issues"
},
"homepage": "https://github.com/tbranyen/todomvc#readme",
"devDependencies": {
"babel-preset-es2015": "^6.6.0",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"derequire": "^2.0.3",
"mocha": "^2.4.5",
"watchify": "^3.7.0"
},
"dependencies": {
"diffhtml": "^0.8.2",
"redux": "^3.3.1",
"redux-logger": "^2.6.1",
"todomvc-app-css": "^2.0.6",
"todomvc-common": "^1.0.2"
}
}
......@@ -117,6 +117,9 @@
<li class="routing">
<a href="examples/flight/" data-source="https://flightjs.github.io/" data-content="Flight is a lightweight, component-based JavaScript framework that maps behavior to DOM nodes. Twitter uses it for their web applications.">Flight</a>
</li>
<li class="routing">
<a href="examples/diffhtml/" data-source="http://diffhtml.org/" data-content="diffHTML is a small experimental library that polyfills Custom Elements, provides a Virtual DOM, and has transition hooks. Netflix has used it on an internal application.">diffHTML</a>
</li>
<li class="routing">
<a href="examples/vue/" data-source="http://vuejs.org" data-content="Vue.js provides the benefits of MVVM data binding and a composable component system with an extremely simple and flexible API.">Vue.js</a>
</li>
......
{
{
"angularjs": {
"name": "AngularJS",
"description": "HTML is great for declaring static documents, but it falters when we try to use it for declaring dynamic views in web-applications. AngularJS lets you extend HTML vocabulary for your application. The resulting environment is extraordinarily expressive, readable, and quick to develop.",
......@@ -658,6 +658,31 @@
}]
}]
},
"diffhtml": {
"name": "diffHTML",
"description": "diffHTML provides an easy way to swap out markup with an intelligent virtual dom, optionally exposes a Custom Element's polyfill, and supports transition hooks for element, attribute, and text modifications. These transitions can delay future renders until an animation/transition Promise has resolved.",
"homepage": "diffhtml.org",
"examples": [{
"name": "Example",
"url": "examples/diffhtml"
}],
"link_groups": [{
"heading": "Official Resources",
"links": [{
"name": "Documentation",
"url": "http://diffhtml.org/"
}, {
"name": "diffHTML on GitHub",
"url": "https://github.com/tbranyen/diffhtml"
}]
}, {
"heading": "Community",
"links": [{
"name": "diffHTML on Twitter",
"url": "http://twitter.com/diffhtml"
}]
}]
},
"dijon": {
"name": "Dijon",
"description": "An IOC/DI framework in Javascript, inspired by Robotlegs and Swiftsuspenders.",
......
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