Skip to content
This repository was archived by the owner on Jul 16, 2024. It is now read-only.

Commit 68a12fb

Browse files
author
Paul Korzhyk
committed
Add React Todo Components
1 parent 1fbbda8 commit 68a12fb

File tree

9 files changed

+521
-6
lines changed

9 files changed

+521
-6
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"classnames": "^2.2.6",
7+
"history": "^4.7.2",
68
"react": "^16.8.4",
79
"react-dom": "^16.8.4",
810
"react-scripts": "2.1.8",

src/TodoApp.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import React from 'react'
2+
import { createBrowserHistory } from 'history'
3+
4+
import defs from './defs'
5+
import TodoFooter from './TodoFooter'
6+
import TodoItem from './TodoItem'
7+
8+
const ENTER_KEY = 13
9+
const history = createBrowserHistory()
10+
11+
export default class TodoApp extends React.Component {
12+
state = {
13+
nowShowing: defs.ALL_TODOS,
14+
editing: null,
15+
newTodo: '',
16+
}
17+
18+
componentDidMount() {
19+
const setNowShowingFn = nowShowing => () => this.setState({ nowShowing })
20+
21+
const routes = {
22+
'/': setNowShowingFn(defs.ALL_TODOS),
23+
'/active': setNowShowingFn(defs.ACTIVE_TODOS),
24+
'/completed': setNowShowingFn(defs.COMPLETED_TODOS),
25+
}
26+
27+
const processLocationHash = hash => {
28+
if (hash) {
29+
hash = hash.substring(1)
30+
}
31+
const route = routes[hash] || routes['/']
32+
route()
33+
}
34+
35+
processLocationHash(history.location.hash)
36+
37+
history.listen((location, action) =>
38+
processLocationHash(location.hash)
39+
)
40+
}
41+
42+
handleChange = event =>
43+
this.setState({ newTodo: event.target.value })
44+
45+
handleNewTodoKeyDown = event => {
46+
if (event.keyCode !== ENTER_KEY) {
47+
return
48+
}
49+
50+
event.preventDefault()
51+
52+
const val = this.state.newTodo.trim()
53+
54+
if (val) {
55+
this.props.model.addTodo(val)
56+
this.setState({ newTodo: '' })
57+
}
58+
}
59+
60+
toggleAll = event => {
61+
const checked = event.target.checked
62+
this.props.model.toggleAll(checked)
63+
}
64+
65+
toggle = todoToToggle =>
66+
this.props.model.toggle(todoToToggle)
67+
68+
destroy = todo =>
69+
this.props.model.destroy(todo)
70+
71+
edit = todo =>
72+
this.setState({ editing: todo.id })
73+
74+
save = (todoToSave, text) => {
75+
this.props.model.save(todoToSave, text)
76+
this.setState({ editing: null })
77+
}
78+
79+
cancel = () =>
80+
this.setState({ editing: null })
81+
82+
clearCompleted = () =>
83+
this.props.model.clearCompleted()
84+
85+
render() {
86+
const { todos } = this.props.model
87+
const { editing, newTodo } = this.state
88+
89+
const shownTodos = todos.filter(todo => {
90+
switch (this.state.nowShowing) {
91+
case defs.ACTIVE_TODOS:
92+
return !todo.completed
93+
case defs.COMPLETED_TODOS:
94+
return todo.completed
95+
default:
96+
return true
97+
}
98+
})
99+
100+
const todoItems = shownTodos.map(todo => (
101+
<TodoItem
102+
key={todo.id}
103+
todo={todo}
104+
onToggle={() => this.toggle(todo)}
105+
onDestroy={() => this.destroy(todo)}
106+
onEdit={() => this.edit(todo)}
107+
editing={editing === todo.id}
108+
onSave={text => this.save(todo, text)}
109+
onCancel={this.cancel}
110+
/>
111+
))
112+
113+
const activeTodoCount = todos.reduce(function (accum, todo) {
114+
return todo.completed ? accum : accum + 1
115+
}, 0)
116+
117+
const completedCount = todos.length - activeTodoCount
118+
119+
const footer = (activeTodoCount || completedCount)
120+
? <TodoFooter
121+
count={activeTodoCount}
122+
completedCount={completedCount}
123+
nowShowing={this.state.nowShowing}
124+
onClearCompleted={this.clearCompleted}
125+
/>
126+
: null
127+
128+
const main = !todos.length
129+
? null
130+
: (
131+
<section className="main">
132+
<input
133+
id="toggle-all"
134+
className="toggle-all"
135+
type="checkbox"
136+
onChange={this.toggleAll}
137+
checked={activeTodoCount === 0}
138+
/>
139+
<label
140+
htmlFor="toggle-all"
141+
/>
142+
<ul className="todo-list">
143+
{todoItems}
144+
</ul>
145+
</section>
146+
)
147+
148+
return (
149+
<div>
150+
<header className="header">
151+
<h1>todos</h1>
152+
<input
153+
className="new-todo"
154+
placeholder="What needs to be done?"
155+
value={newTodo}
156+
onKeyDown={this.handleNewTodoKeyDown}
157+
onChange={this.handleChange}
158+
autoFocus={true}
159+
/>
160+
</header>
161+
{main}
162+
{footer}
163+
</div>
164+
)
165+
}
166+
}

src/TodoFooter.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react'
2+
import classNames from 'classnames'
3+
4+
import defs from './defs'
5+
import Utils from './Utils'
6+
7+
export default class TodoFooter extends React.Component {
8+
render() {
9+
const { completedCount, count, nowShowing, onClearCompleted } = this.props
10+
const clearButton = completedCount === 0
11+
? null
12+
: (
13+
<button
14+
className="clear-completed"
15+
onClick={onClearCompleted}>
16+
Clear completed
17+
</button>
18+
)
19+
20+
return (
21+
<footer className="footer">
22+
<span className="todo-count">
23+
<strong>{count}</strong> {Utils.pluralize(count, 'item')} left
24+
</span>
25+
<ul className="filters">
26+
<li>
27+
<a
28+
href="#/"
29+
className={classNames({selected: nowShowing === defs.ALL_TODOS})}>
30+
All
31+
</a>
32+
</li>
33+
{' '}
34+
<li>
35+
<a
36+
href="#/active"
37+
className={classNames({selected: nowShowing === defs.ACTIVE_TODOS})}>
38+
Active
39+
</a>
40+
</li>
41+
{' '}
42+
<li>
43+
<a
44+
href="#/completed"
45+
className={classNames({selected: nowShowing === defs.COMPLETED_TODOS})}>
46+
Completed
47+
</a>
48+
</li>
49+
</ul>
50+
{clearButton}
51+
</footer>
52+
)
53+
}
54+
}

src/TodoItem.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react'
2+
import classNames from 'classnames'
3+
4+
const ESCAPE_KEY = 27
5+
const ENTER_KEY = 13
6+
7+
8+
export default class TodoItem extends React.Component {
9+
constructor(props) {
10+
super(props)
11+
12+
this.state = {
13+
editText: this.props.todo.title
14+
}
15+
16+
this.editField = React.createRef()
17+
}
18+
19+
handleSubmit = event => {
20+
const { onDestroy, onSave } = this.props
21+
var val = this.state.editText.trim()
22+
if (val) {
23+
onSave(val)
24+
this.setState({editText: val})
25+
} else {
26+
onDestroy()
27+
}
28+
}
29+
30+
handleEdit = () => {
31+
const { onEdit, todo } = this.props
32+
onEdit()
33+
this.setState({editText: todo.title})
34+
}
35+
36+
handleKeyDown = event => {
37+
const { onCancel, todo } = this.props
38+
if (event.which === ESCAPE_KEY) {
39+
this.setState({editText: todo.title})
40+
onCancel(event)
41+
} else if (event.which === ENTER_KEY) {
42+
this.handleSubmit(event)
43+
}
44+
}
45+
46+
handleChange = event => {
47+
if (this.props.editing) {
48+
this.setState({editText: event.target.value})
49+
}
50+
}
51+
52+
/**
53+
* This is a completely optional performance enhancement that you can
54+
* implement on any React component. If you were to delete this method
55+
* the app would still work correctly (and still be very performant!), we
56+
* just use it as an example of how little code it takes to get an order
57+
* of magnitude performance improvement.
58+
*/
59+
shouldComponentUpdate = (nextProps, nextState) => {
60+
return (
61+
nextProps.todo !== this.props.todo ||
62+
nextProps.editing !== this.props.editing ||
63+
nextState.editText !== this.state.editText
64+
)
65+
}
66+
67+
/**
68+
* Safely manipulate the DOM after updating the state when invoking
69+
* `this.props.onEdit()` in the `handleEdit` method above.
70+
* For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
71+
* and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
72+
*/
73+
componentDidUpdate = prevProps => {
74+
if (!prevProps.editing && this.props.editing) {
75+
const node = this.editField.current
76+
node.focus()
77+
node.setSelectionRange(node.value.length, node.value.length)
78+
}
79+
}
80+
81+
render() {
82+
const { editing, onDestroy, onToggle, todo } = this.props
83+
const { editText } = this.state
84+
return (
85+
<li className={classNames({
86+
completed: todo.completed,
87+
editing,
88+
})}>
89+
<div className="view">
90+
<input
91+
className="toggle"
92+
type="checkbox"
93+
checked={todo.completed}
94+
onChange={onToggle}
95+
/>
96+
<label onDoubleClick={this.handleEdit}>
97+
{todo.title}
98+
</label>
99+
<button className="destroy" onClick={onDestroy} />
100+
</div>
101+
<input
102+
ref={this.editField}
103+
className="edit"
104+
value={editText}
105+
onBlur={this.handleSubmit}
106+
onChange={this.handleChange}
107+
onKeyDown={this.handleKeyDown}
108+
/>
109+
</li>
110+
)
111+
}
112+
}

0 commit comments

Comments
 (0)