Skip to content
Permalink
Browse files

Add React Todo Components

  • Loading branch information...
paulftw committed Mar 20, 2019
1 parent 1fbbda8 commit 68a12fbb484db71c1ef988e16ba2febf39ea27a3
Showing with 521 additions and 6 deletions.
  1. +2 −0 package.json
  2. +166 −0 src/TodoApp.js
  3. +54 −0 src/TodoFooter.js
  4. +112 −0 src/TodoItem.js
  5. +72 −0 src/TodoModel.js
  6. +43 −0 src/Utils.js
  7. +5 −0 src/defs.js
  8. +17 −5 src/index.js
  9. +50 −1 yarn.lock
@@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"classnames": "^2.2.6",
"history": "^4.7.2",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-scripts": "2.1.8",
@@ -0,0 +1,166 @@
import React from 'react'
import { createBrowserHistory } from 'history'

import defs from './defs'
import TodoFooter from './TodoFooter'
import TodoItem from './TodoItem'

const ENTER_KEY = 13
const history = createBrowserHistory()

export default class TodoApp extends React.Component {
state = {
nowShowing: defs.ALL_TODOS,
editing: null,
newTodo: '',
}

componentDidMount() {
const setNowShowingFn = nowShowing => () => this.setState({ nowShowing })

const routes = {
'/': setNowShowingFn(defs.ALL_TODOS),
'/active': setNowShowingFn(defs.ACTIVE_TODOS),
'/completed': setNowShowingFn(defs.COMPLETED_TODOS),
}

const processLocationHash = hash => {
if (hash) {
hash = hash.substring(1)
}
const route = routes[hash] || routes['/']
route()
}

processLocationHash(history.location.hash)

history.listen((location, action) =>
processLocationHash(location.hash)
)
}

handleChange = event =>
this.setState({ newTodo: event.target.value })

handleNewTodoKeyDown = event => {
if (event.keyCode !== ENTER_KEY) {
return
}

event.preventDefault()

const val = this.state.newTodo.trim()

if (val) {
this.props.model.addTodo(val)
this.setState({ newTodo: '' })
}
}

toggleAll = event => {
const checked = event.target.checked
this.props.model.toggleAll(checked)
}

toggle = todoToToggle =>
this.props.model.toggle(todoToToggle)

destroy = todo =>
this.props.model.destroy(todo)

edit = todo =>
this.setState({ editing: todo.id })

save = (todoToSave, text) => {
this.props.model.save(todoToSave, text)
this.setState({ editing: null })
}

cancel = () =>
this.setState({ editing: null })

clearCompleted = () =>
this.props.model.clearCompleted()

render() {
const { todos } = this.props.model
const { editing, newTodo } = this.state

const shownTodos = todos.filter(todo => {
switch (this.state.nowShowing) {
case defs.ACTIVE_TODOS:
return !todo.completed
case defs.COMPLETED_TODOS:
return todo.completed
default:
return true
}
})

const todoItems = shownTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => this.toggle(todo)}
onDestroy={() => this.destroy(todo)}
onEdit={() => this.edit(todo)}
editing={editing === todo.id}
onSave={text => this.save(todo, text)}
onCancel={this.cancel}
/>
))

const activeTodoCount = todos.reduce(function (accum, todo) {
return todo.completed ? accum : accum + 1
}, 0)

const completedCount = todos.length - activeTodoCount

const footer = (activeTodoCount || completedCount)
? <TodoFooter
count={activeTodoCount}
completedCount={completedCount}
nowShowing={this.state.nowShowing}
onClearCompleted={this.clearCompleted}
/>
: null

const main = !todos.length
? null
: (
<section className="main">
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
<label
htmlFor="toggle-all"
/>
<ul className="todo-list">
{todoItems}
</ul>
</section>
)

return (
<div>
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
value={newTodo}
onKeyDown={this.handleNewTodoKeyDown}
onChange={this.handleChange}
autoFocus={true}
/>
</header>
{main}
{footer}
</div>
)
}
}
@@ -0,0 +1,54 @@
import React from 'react'
import classNames from 'classnames'

import defs from './defs'
import Utils from './Utils'

export default class TodoFooter extends React.Component {
render() {
const { completedCount, count, nowShowing, onClearCompleted } = this.props
const clearButton = completedCount === 0
? null
: (
<button
className="clear-completed"
onClick={onClearCompleted}>
Clear completed
</button>
)

return (
<footer className="footer">
<span className="todo-count">
<strong>{count}</strong> {Utils.pluralize(count, 'item')} left
</span>
<ul className="filters">
<li>
<a
href="#/"
className={classNames({selected: nowShowing === defs.ALL_TODOS})}>
All
</a>
</li>
{' '}
<li>
<a
href="#/active"
className={classNames({selected: nowShowing === defs.ACTIVE_TODOS})}>
Active
</a>
</li>
{' '}
<li>
<a
href="#/completed"
className={classNames({selected: nowShowing === defs.COMPLETED_TODOS})}>
Completed
</a>
</li>
</ul>
{clearButton}
</footer>
)
}
}
@@ -0,0 +1,112 @@
import React from 'react'
import classNames from 'classnames'

const ESCAPE_KEY = 27
const ENTER_KEY = 13


export default class TodoItem extends React.Component {
constructor(props) {
super(props)

this.state = {
editText: this.props.todo.title
}

this.editField = React.createRef()
}

handleSubmit = event => {
const { onDestroy, onSave } = this.props
var val = this.state.editText.trim()
if (val) {
onSave(val)
this.setState({editText: val})
} else {
onDestroy()
}
}

handleEdit = () => {
const { onEdit, todo } = this.props
onEdit()
this.setState({editText: todo.title})
}

handleKeyDown = event => {
const { onCancel, todo } = this.props
if (event.which === ESCAPE_KEY) {
this.setState({editText: todo.title})
onCancel(event)
} else if (event.which === ENTER_KEY) {
this.handleSubmit(event)
}
}

handleChange = event => {
if (this.props.editing) {
this.setState({editText: event.target.value})
}
}

/**
* 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 = (nextProps, nextState) => {
return (
nextProps.todo !== this.props.todo ||
nextProps.editing !== this.props.editing ||
nextState.editText !== this.state.editText
)
}

/**
* 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 = prevProps => {
if (!prevProps.editing && this.props.editing) {
const node = this.editField.current
node.focus()
node.setSelectionRange(node.value.length, node.value.length)
}
}

render() {
const { editing, onDestroy, onToggle, todo } = this.props
const { editText } = this.state
return (
<li className={classNames({
completed: todo.completed,
editing,
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.completed}
onChange={onToggle}
/>
<label onDoubleClick={this.handleEdit}>
{todo.title}
</label>
<button className="destroy" onClick={onDestroy} />
</div>
<input
ref={this.editField}
className="edit"
value={editText}
onBlur={this.handleSubmit}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</li>
)
}
}
Oops, something went wrong.

0 comments on commit 68a12fb

Please sign in to comment.
You can’t perform that action at this time.