Skip to content

Instantly share code, notes, and snippets.

@curran
Last active March 25, 2017 13:12
Show Gist options
  • Save curran/d8639546697c7ae3ab46c2544683d53a to your computer and use it in GitHub Desktop.
Save curran/d8639546697c7ae3ab46c2544683d53a to your computer and use it in GitHub Desktop.
Todos
license: mit

A todo app that demonstrates usage of d3-component. Inspired by Redux Todos Example.

This implementation has a few rough edges compared to the original with React and Redux, but it's also less code and does implement all the same features. This example shows that that d3-component can in fact be used to construct moderately complex user interfaces, and it also shows some of its limitations.

One strong point of d3-component is its support for transitions. I wonder what it would it look like to implement the same functionality in React?

forked from curran's block: Counter

Built with blockbuilder.org


<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/d3@4"></script>
<script src="https://unpkg.com/d3-component@3"></script>
<script src="https://unpkg.com/redux@3/dist/redux.min.js"></script>
<style>
body {
background-color: #f2f2f2;
}
.app {
margin-top: 50px;
margin-left: 150px;
font-family: sans;
}
.app * {
font-size: 40px;
}
.todo {
cursor: pointer;
user-select: none;
}
.todo.completed {
text-decoration: line-through;
}
</style>
</head>
<body>
<script>
// A generic button component.
var button = d3.component("button")
.render(function (selection, d){
selection
.text(d.text)
.on("click", d.onClick);
});
// A generic text input component.
var input = d3.component("input");
// Displays the form that lets you add a todo.
var addTodo = d3.component("form")
.render(function (selection, d){
var inputNode = input(selection).node();
selection.call(button, {
text:"Add Todo",
onClick: function (e){
d.actions.addTodo(inputNode.value);
inputNode.value = "";
}
});
});
// Displays a single entry in the todo list.
var todo = d3.component("li", "todo")
.create(function (selection){
selection
.style("font-size", "0px")
.transition().duration(600)
.style("font-size", "40px");
})
.render(function (selection, d){
selection
.text(d.text)
.classed("completed", d.completed)
.on("click", function (){
d3.event.preventDefault(); // Prevent navigation.
d.actions.toggleTodo(d.id);
});
})
.destroy(function (selection){
return selection
.transition().duration(600)
.style("font-size", "0px");
})
.key(function (d){ return d.id; });
// An empty <li> element with zero size.
// This makes the ul element fill out its size,
// so that the enter and exit transitions for a single <li>
// are smooth, and don't include an abrupt change
// in the position of the footer.
var spaceFiller = d3.component("li")
.create(function (selection){
selection.style("font-size", "0px");
});
// Displays a list of todos.
var todoList = d3.component("ul")
.render(function (selection, d){
var visibleTodos = d.todos.filter(function (_){
switch (d.currentFilter) {
case 'SHOW_ALL':
return true;
case 'SHOW_COMPLETED':
return _.completed;
case 'SHOW_ACTIVE':
return !_.completed;
}
});
selection
.call(todo, visibleTodos, d)
.call(spaceFiller);
});
// Displays one of the options in the footer.
// This is one area where the JSX solution
// in the Redux example is much cleaner.
var filterLink = (function (){
var a = d3.component("a")
.render(function (selection, d){
selection
.attr("href", "#")
.text(d.text)
.on("click", function (){
d.onClick();
});
}),
span = d3.component("span")
.render(function (selection, d){
selection.text(d.text);
}),
comma = d3.component("span", "comma")
.create(function (selection){
selection.text(", ");
})
return d3.component("span")
.render(function (selection, d){
selection
.call((d.filter === d.currentFilter ? span : a), {
text: d.text,
onClick: function (){
d.actions.setVisibilityFilter(d.filter);
}
})
.call(comma, d.useComma || []);
});
}());
// Displays the visibility filter controls.
var footer = (function (){
var data = [
{ text: "All", filter: "SHOW_ALL", useComma: true },
{ text: "Active", filter: "SHOW_ACTIVE", useComma: true },
{ text: "Completed", filter: "SHOW_COMPLETED" }
];
return d3.component("span")
.render(function (selection, d){
selection
.text("Show: ")
.call(filterLink, data, d);
});
}());
// The top-level app component.
var app = d3.component("div")
.create(function (selection){
selection.attr("class", "app");
})
.render(function (selection, d){
selection
.call(addTodo, d)
.call(todoList, d)
.call(footer, d);
});
function main(){
var store = Redux.createStore(reducer),
actions = actionsFromDispatch(store.dispatch);
render();
store.subscribe(render);
function reducer (state, action){
state = state || {
todos: [],
currentFilter: "SHOW_ALL",
};
switch (action.type) {
case "ADD_TODO":
return Object.assign({}, state, {
todos: state.todos.concat({
text: action.text,
id: action.id,
completed: false
})
});
case "TOGGLE_TODO":
return Object.assign({}, state, {
todos: state.todos.map(function (d){
if(d.id === action.id){
return Object.assign({}, d, {
completed: !d.completed
});
}
return d;
})
});
case "SET_VISIBILITY_FILTER":
return Object.assign({}, state, {
currentFilter: action.filter
});
default:
return state;
}
}
function actionsFromDispatch(dispatch){
var nextTodoId = 0;
return {
addTodo: function (text){
dispatch({
type: "ADD_TODO",
id: nextTodoId++,
text: text
});
},
toggleTodo: function (id){
dispatch({
type: "TOGGLE_TODO",
id: id
});
},
setVisibilityFilter: function (filter){
dispatch({
type: "SET_VISIBILITY_FILTER",
filter: filter
});
}
}
}
function render(){
d3.select("body").call(app, store.getState(), {
actions: actions
});
}
}
main();
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment