I am new to React (I'm used to working with Angular) and I am currently working on filtering my Todo list app based on a category selection.
I cloned the Todo list app from http://todomvc.com/examples/react/#/ . I added a 'category' input, which works, but now I am trying to filter by category once the list is shown.
I currently don't have any search function for categories and am looking for some guidance as to where to start. I'll post the code below but here is the link to my repo if you want to clone it: https://github.com/aenser/todo-react
app.jsx
var app = app || {};
(function () {
'use strict';
app.ALL_TODOS = 'all';
app.ACTIVE_TODOS = 'active';
app.COMPLETED_TODOS = 'completed';
var TodoFooter = app.TodoFooter;
var TodoItem = app.TodoItem;
var ENTER_KEY = 13;
var TodoApp = React.createClass({
getInitialState: function () {
return {
nowShowing: app.ALL_TODOS,
editing: null,
newTodo: '',
newCategory: ''
};
},
componentDidMount: function () {
var setState = this.setState;
var router = Router({
'/': setState.bind(this, {nowShowing: app.ALL_TODOS}),
'/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),
'/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS})
});
router.init('/');
},
handleChange: function (event) {
this.setState({newTodo: event.target.value});
},
handleCategoryChange: function (event) {
this.setState({newCategory: event.target.value});
},
handleNewTodoKeyDown: function (event) {
if (event.keyCode !== ENTER_KEY) {
return;
}
event.preventDefault();
var val = this.state.newTodo.trim();
var cat = this.state.newCategory.trim();
if (val, cat) {
this.props.model.addTodo(val, cat);
this.setState({newTodo: '', newCategory: ''});
}
},
toggleAll: function (event) {
var checked = event.target.checked;
this.props.model.toggleAll(checked);
},
toggle: function (todoToToggle) {
this.props.model.toggle(todoToToggle);
},
destroy: function (todo) {
this.props.model.destroy(todo);
},
edit: function (todo) {
this.setState({editing: todo.id});
},
save: function (todoToSave, text, cat) {
this.props.model.save(todoToSave, text, cat);
this.setState({editing: null});
},
cancel: function () {
this.setState({editing: null});
},
clearCompleted: function () {
this.props.model.clearCompleted();
},
render: function () {
var footer;
var main;
var todos = this.props.model.todos;
var shownTodos = todos.filter(function (todo) {
switch (this.state.nowShowing) {
case app.ACTIVE_TODOS:
return !todo.completed;
case app.COMPLETED_TODOS:
return todo.completed;
default:
return true;
}
}, this);
var todoItems = shownTodos.map(function (todo) {
return (
<TodoItem
key={todo.id}
todo={todo}
onToggle={this.toggle.bind(this, todo)}
onDestroy={this.destroy.bind(this, todo)}
onEdit={this.edit.bind(this, todo)}
editing={this.state.editing === todo.id}
onSave={this.save.bind(this, todo)}
onCancel={this.cancel}
/>
);
}, this);
var activeTodoCount = todos.reduce(function (accum, todo) {
return todo.completed ? accum : accum + 1;
}, 0);
var completedCount = todos.length - activeTodoCount;
if (activeTodoCount || completedCount) {
footer =
<TodoFooter
count={activeTodoCount}
completedCount={completedCount}
nowShowing={this.state.nowShowing}
onClearCompleted={this.clearCompleted}
/>;
}
if (todos.length) {
main = (
<section className="main">
<input
className="toggle-all"
type="checkbox"
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
<ul className="todo-list">
{todoItems}
</ul>
</section>
);
}
return (
<div>
<header className="header">
<h1>todos</h1>
<form onKeyDown={this.handleNewTodoKeyDown}>
<input
placeholder="What needs to be done?"
value={this.state.newTodo}
autoFocus={true}
className="new-todo"
onChange={this.handleChange}
/>
<select value={this.state.newCategory} className="new-todo"
onChange={this.handleCategoryChange}>
<option value="">Select a Category</option>
<option value="Urgent">Urgent</option>
<option value="Soon">Soon</option>
<option value="Anytime">Anytime</option>
</select>
</form>
</header>
{main}
{footer}
</div>
);
}
});
var model = new app.TodoModel('react-todos');
function render() {
React.render(
<TodoApp model={model}/>,
document.getElementsByClassName('todoapp')[0]
);
}
model.subscribe(render);
render();
})();
todoModel.js
var app = app || {};
(function () {
'use strict';
var Utils = app.Utils;
// Generic "model" object. You can use whatever
// framework you want. For this application it
// may not even be worth separating this logic
// out, but we do this to demonstrate one way to
// separate out parts of your application.
app.TodoModel = function (key) {
this.key = key;
this.todos = Utils.store(key);
this.onChanges = [];
};
app.TodoModel.prototype.subscribe = function (onChange) {
this.onChanges.push(onChange);
};
app.TodoModel.prototype.inform = function () {
Utils.store(this.key, this.todos);
this.onChanges.forEach(function (cb) { cb(); });
};
app.TodoModel.prototype.addTodo = function (title, category) {
this.todos = this.todos.concat({
id: Utils.uuid(),
title: title,
category: category,
completed: false
});
this.inform();
};
app.TodoModel.prototype.toggleAll = function (checked) {
// Note: it's usually better to use immutable data structures since they're
// easier to reason about and React works very well with them. That's why
// we use map() and filter() everywhere instead of mutating the array or
// todo items themselves.
this.todos = this.todos.map(function (todo) {
return Utils.extend({}, todo, {completed: checked});
});
this.inform();
};
app.TodoModel.prototype.filterAll = function () {
this.todos = this.todos.map(function (todo) {
return Utils.extend({}, todo);
});
this.inform();
};
app.TodoModel.prototype.toggle = function (todoToToggle) {
this.todos = this.todos.map(function (todo) {
return todo !== todoToToggle ?
todo :
Utils.extend({}, todo, {completed: !todo.completed});
});
this.inform();
};
app.TodoModel.prototype.destroy = function (todo) {
this.todos = this.todos.filter(function (candidate) {
return candidate !== todo;
});
this.inform();
};
app.TodoModel.prototype.save = function (todoToSave, text, cat) {
this.todos = this.todos.map(function (todo) {
return todo !== todoToSave ? todo : Utils.extend({}, todo, {title: text}, {category: cat});
});
this.inform();
};
app.TodoModel.prototype.clearCompleted = function () {
this.todos = this.todos.filter(function (todo) {
return !todo.completed;
});
this.inform();
};
})();
todoItem.jsx
var app = app || {};
(function () {
'use strict';
var ESCAPE_KEY = 27;
var ENTER_KEY = 13;
app.TodoItem = React.createClass({
handleSubmit: function (event) {
var val = this.state.editText.trim();
var cat = this.state.editCategoryText.trim();
if (val || cat) {
this.props.onSave(val, cat);
this.setState({editText: this.props.todo.title, editCategoryText: this.props.todo.category});
} else {
this.props.onDestroy();
}
},
handleEdit: function (event) {
this.props.onEdit();
this.setState({editText: this.props.todo.title, editCategoryText: this.props.todo.category});
},
handleKeyDown: function (event) {
if (event.which === ESCAPE_KEY) {
this.setState({editText: this.props.todo.title});
this.props.onCancel(event);
} else if (event.which === ENTER_KEY) {
this.handleSubmit(event);
}
},
handleChange: function (event) {
if (this.props.editing) {
this.setState({editText: event.target.value});
}
},
handleCategoryChange: function (event) {
if (this.props.editing) {
this.setState({editCategoryText: event.target.value});
}
},
getInitialState: function () {
return {editText: this.props.todo.title, editCategoryText: this.props.todo.category};
},
/**
* This is a completely optional performance enhancement that you can
* implement on any React component. If you were to delete this method
* the app would still work correctly (and still be very performant!), we
* just use it as an example of how little code it takes to get an order
* of magnitude performance improvement.
*/
shouldComponentUpdate: function (nextProps, nextState) {
return (
nextProps.todo !== this.props.todo ||
nextProps.editing !== this.props.editing ||
nextState.editText !== this.state.editText ||
nextState.editCategoryText !== this.state.editCategoryText
);
},
/**
* Safely manipulate the DOM after updating the state when invoking
* `this.props.onEdit()` in the `handleEdit` method above.
* For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
* and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
*/
componentDidUpdate: function (prevProps) {
if (!prevProps.editing && this.props.editing) {
var node = React.findDOMNode(this.refs.editField);
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}
},
render: function () {
return (
<li className={classNames({
completed: this.props.todo.completed,
editing: this.props.editing
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={this.props.todo.completed}
onChange={this.props.onToggle}
/>
<label onDoubleClick={this.handleEdit}>
{this.props.todo.title}
</label>
<label onDoubleClick={this.handleEdit}>
{this.props.todo.category}
</label>
<button className="destroy" onClick={this.props.onDestroy} />
</div>
<input
ref="editField"
value={this.state.editText}
className="edit"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
<select value={this.state.EditCategoryText} className="edit" onChange={this.handleCategoryChange} defaultValue={this.props.todo.category} onKeyDown={this.handleKeyDown}>
<option value="Urgent">Urgent</option>
<option value="Soon">Soon</option>
<option value="Anytime">Anytime</option>
</select>
</li>
);
}
});
})();
Thank You for taking the time to help me figure out how to filter my search based on category selection.
Your interface is a little confusing as you seem to use the same input select for both assigning categories to todos and filtering, I'll get to that at the end of the answer, but for now, I just used the category selector for both entering data and filtering by category.
The answer to your question is extremely simple. You just filter by the category as well as by the completed state. Like this:
I would add some more buttons along the bottom for the categorical currently on display. You would also add a new set of statuses like
nowShowing
for the category likenowShowingCategory
. The buttons would set this to the 3 values of category, and you would use that variable in the above filter instead ofnewCategory
from my example.