🧩 Andre Madarang - React Todo App (Jun 3, 2018)
Link: https://www.youtube.com/playlist?list=PLEhEHUEU3x5qz5OYDXamBmYnLvwboYTdH
Description
In this series, we'll build a Todo App in React. Although a todo app isn't very exciting, it gives a solid foundation on the basics of a framework and how to perform CRUD actions. We go over the basics, look at several ways to communicate between components, take a look at state management and eventually hook it up to a Laravel API backend for persistence.
- AndreMadarang-ReactTodoAppWithMobX-20180603
- 1 - Basics (Vue Dev POV) - Part 1
- 1.1 - Overview of the Application
- 1.2 - Project Setup
- 1.3 - Get Codes from Vue TodoApp
- 1.4 - Adding State
- 1.5 - V-for ⇒ Map Function
- 1.6 - @keyup.enter ⇒ onKeyUp
- 1.7 - v-model in React
- 1.8 - Adding todos in the display of todos (setState method)
- 1.9 - Delete Todo
- 1.10 - onChange
- 1.11 - Editing a Todo
- 1.12 - Done Editing Todo
- 2 - Basics (Vue Dev POV) - Part 2
- 3 - Extracting Components
- 4 - MobX State Management
- 5 - API Integration
Goal: To remake this app using react and mobx:
npx create-react-app andremadarang-reacttodoappwithmobx-20180603
App.css
* {
box-sizing: border-box;
}
.App {
/* text-align: center; */
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 24px;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
text-align: center;
/* height: 150px; */
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.Todo-container {
max-width: 600px;
margin: 40px auto;
}
/* @keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
} */
.todo-input {
width: 100%;
padding: 10px 18px;
font-size: 18px;
margin-bottom: 16px;
}
.todo-input:focus {
outline: 0;
}
.todo-item {
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
animation-duration: 0.3s;
}
.remove-item {
cursor: pointer;
margin-left: 14px;
}
.remove-item:hover {
color: black;
}
.todo-item-left {
display: flex;
align-items: center;
}
.todo-item-label {
padding: 10px;
border: 1px solid white;
margin-left: 12px;
}
.todo-item-edit {
font-size: 24px;
color: #2c3e50;
margin-left: 12px;
width: 100%;
padding: 10px;
border: 1px solid #ccc;
font-family: "Avenir", Helvetica, Arial, sans-serif;
}
.todo-item-edit:focus {
outline: none;
}
.completed {
text-decoration: line-through;
color: grey;
}
.extra-container {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 16px;
border-top: 1px solid lightgrey;
padding-top: 14px;
margin-bottom: 14px;
}
button {
font-size: 14px;
background-color: white;
appearance: none;
margin: 2px;
}
button:hover {
background: lightgreen;
}
button:focus {
outline: none;
}
.active {
background: lightgreen;
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity 300ms ease-in;
}
.fade-leave {
opacity: 1;
}
.fade-leave-active {
opacity: 0;
transition: opacity 300ms ease-out;
}
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
// START: TEMPLATE
render() {
return (
<div className="App">
{/* START: HEADER */}
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
{/* END: HEADER */}
{/* START: TODO CONTAINER */}
<div className="Todo-container">
<input type="text" className="todo-input" placeholder="What needs to be done" />
{/* START: Sample Todo */}
<div className="todo-item">
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">Todo Item 1</div>
</div>
<div className="remove-item">
×
</div>
</div>
{/* End: Sample Todo */}
{/* START: Select All and Item Remaining Counter */}
<div class="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>remaining items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
{/* START: Extra Buttons */}
<div class="extra-container">
<div>
<button >All</button>
<button>Active</button>
<button >Completed</button>
</div>
<div>
<transition name="fade">
<button >Clear Completed</button>
</transition>
</div>
</div>
{/* END: Extra Buttons */}
</div>
{/* END: TODO CONTAINER */}
</div >
);
}
// END: TEMPLATE
}
export default App;
since this is done in vue way, the guy places the states below the render template of the class component like this:
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
// START: TEMPLATE
render() {
return (
<div className="App">
{/* START: HEADER */}
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
{/* END: HEADER */}
{/* START: TODO CONTAINER */}
<div className="Todo-container">
<input type="text" className="todo-input" placeholder="What needs to be done" />
{/* START: Sample Todo */}
<div className="todo-item">
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">Todo Item 1</div>
</div>
<div className="remove-item">
×
</div>
</div>
{/* End: Sample Todo */}
{/* START: Select All and Item Remaining Counter */}
<div class="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>remaining items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
{/* START: Extra Buttons */}
<div class="extra-container">
<div>
<button >All</button>
<button>Active</button>
<button >Completed</button>
</div>
<div>
<transition name="fade">
<button >Clear Completed</button>
</transition>
</div>
</div>
{/* END: Extra Buttons */}
</div>
{/* END: TODO CONTAINER */}
</div >
);
}
// END: TEMPLATE
// START: STATES
state = {
todos: [
{
'id': 1,
'title': 'Title1',
'completed': false,
'editing': false,
},
{
'id': 2,
'title': 'Title2',
'completed': false,
'editing': false,
},
]
}
// END: STATES
}
export default App;
App.js
{/* Start: Display Todos */}
{this.state.todos.map((todo, index) => {
return (
<div className="todo-item" key={todo.id }>
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">{todo.title}</div>
</div>
<div className="remove-item">
×
</div>
</div>
)
})}
{/* End: Display Todos */}
Overview of App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
// START: TEMPLATE
render() {
return (
<div className="App">
{/* START: HEADER */}
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
{/* END: HEADER */}
{/* START: TODO CONTAINER */}
<div className="Todo-container">
<input type="text" className="todo-input" placeholder="What needs to be done" />
{/* Start: Display Todos */}
{this.state.todos.map((todo, index) => {
return (
<div className="todo-item" key={todo.id }>
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">{todo.title}</div>
</div>
<div className="remove-item">
×
</div>
</div>
)
})}
{/* End: Display Todos */}
{/* START: Select All and Item Remaining Counter */}
<div class="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>remaining items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
{/* START: Extra Buttons */}
<div class="extra-container">
<div>
<button >All</button>
<button>Active</button>
<button >Completed</button>
</div>
<div>
<transition name="fade">
<button >Clear Completed</button>
</transition>
</div>
</div>
{/* END: Extra Buttons */}
</div>
{/* END: TODO CONTAINER */}
</div >
);
}
// END: TEMPLATE
// START: STATES
state = {
todos: [
{
'id': 1,
'title': 'Title1',
'completed': false,
'editing': false,
},
{
'id': 2,
'title': 'Title2',
'completed': false,
'editing': false,
},
]
}
// END: STATES
}
export default App;
App.js: onKeyup
<input type="text" className="todo-input" placeholder="What needs to be done" onKeyUp={this.addTodo} />
then adding method under states
// START: METHODS
addTodo(event) {
console.log('working')
}
// END: METHODS
if you try checking ur browser console and inputting stuff in the input, it works
// START: METHODS
addTodo(event) {
if (event.key === 'Enter') {
console.log('working')
}
}
// END: METHODS
now what we did here was if the user presses enter after inputting value in the input, instead for every key presses, will print to console.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
// START: TEMPLATE
render() {
return (
<div className="App">
{/* START: HEADER */}
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
{/* END: HEADER */}
{/* START: TODO CONTAINER */}
<div className="Todo-container">
<input type="text" className="todo-input" placeholder="What needs to be done" onKeyUp={this.addTodo} />
{/* Start: Display Todos */}
{this.state.todos.map((todo, index) => {
return (
<div className="todo-item" key={todo.id}>
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">{todo.title}</div>
</div>
<div className="remove-item">
×
</div>
</div>
)
})}
{/* End: Display Todos */}
{/* START: Select All and Item Remaining Counter */}
<div class="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>remaining items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
{/* START: Extra Buttons */}
<div class="extra-container">
<div>
<button >All</button>
<button>Active</button>
<button >Completed</button>
</div>
<div>
<transition name="fade">
<button >Clear Completed</button>
</transition>
</div>
</div>
{/* END: Extra Buttons */}
</div>
{/* END: TODO CONTAINER */}
</div >
);
}
// END: TEMPLATE
// START: STATES
state = {
todos: [
{
'id': 1,
'title': 'Title1',
'completed': false,
'editing': false,
},
{
'id': 2,
'title': 'Title2',
'completed': false,
'editing': false,
},
]
}
// END: STATES
// START: METHODS
addTodo(event) {
if (event.key === 'Enter') {
console.log('working')
}
}
// END: METHODS
}
export default App;
under state object in App.js
todoInput = React.createRef();
afterwards, you add a ref attribute to this.todoInput in the input tag
<input type="text" className="todo-input" placeholder="What needs to be done" onKeyUp={this.addTodo} ref={this.todoInput} />
then in your addTodo method in the methods section, we should change our custom method into an arrow function instead of just a normal function in order to use "this".
// START: METHODS
addTodo = (event) => {
if (event.key === 'Enter') {
const todoInput = (this.todoInput.current.value);
console.log(todoInput);
}
}
// END: METHODS
try it again and it works now with whatever you had in the input
add new field in state object then in addTodo method implement setState method like this:
App.js
// START: STATES
state = {
todos: [
{
'id': 1,
'title': 'Title1',
'completed': false,
'editing': false,
},
{
'id': 2,
'title': 'Title2',
'completed': false,
'editing': false,
},
],
todoId: 3
}
todoInput = React.createRef();
// END: STATES
// START: METHODS
addTodo = (event) => {
if (event.key === 'Enter') {
const todoInput = this.todoInput.current.value;
//Start: if input value is empty, do not add
if (todoInput.trim().length === 0) {
return;
}
//End--
// Start: Add new todo to todos for display
this.setState((prevState, props) => {
console.log(todoInput);
let newTodoId = prevState.todoId++;
let newTodos = [
...prevState.todos,
{
id: newTodoId,
title: todoInput,
completed: false,
}
]
return {
todos: newTodos
}
})
// End: --
this.todoInput.current.value = ""
}
}
// END: METHODS
add onClick: deleteTodo Method at App.js
{/* Start: Display Todos */}
{this.state.todos.map((todo, index) => {
return (
<div className="todo-item" key={todo.id}>
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">{todo.title}</div>
</div>
<div className="remove-item" onClick={() => this.deleteTodo(index)}>
×
</div>
</div>
)
})}
{/* End: Display Todos */}
then define deleteMethod in methods section below
/**
* START:
* @name: deleteTodo()
* @description: deletes todo
* @param index
*/
deleteTodo = index => {
// Start: Deletes todo
this.setState((prevState, props) => {
let todos = prevState.todos;
//have the index, and just remove that 1.
todos.splice(index, 1);
return {
todos: todos
}
})
// End: Deletes todo
}
// END: deleteTodo()
Overview of App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
// START: TEMPLATE
render() {
return (
<div className="App">
{/* START: HEADER */}
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
{/* END: HEADER */}
{/* START: TODO CONTAINER */}
<div className="Todo-container">
<input type="text" className="todo-input" placeholder="What needs to be done" onKeyUp={this.addTodo} ref={this.todoInput} />
{/* Start: Display Todos */}
{this.state.todos.map((todo, index) => {
return (
<div className="todo-item" key={todo.id}>
<div className="todo-item-left">
<input type="checkbox" />
<div className="todo-item-label">{todo.title}</div>
</div>
<div className="remove-item" onClick={() => this.deleteTodo(index)}>
×
</div>
</div>
)
})}
{/* End: Display Todos */}
{/* START: Select All and Item Remaining Counter */}
<div class="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>remaining items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
{/* START: Extra Buttons */}
<div class="extra-container">
<div>
<button >All</button>
<button>Active</button>
<button >Completed</button>
</div>
<div>
<transition name="fade">
<button >Clear Completed</button>
</transition>
</div>
</div>
{/* END: Extra Buttons */}
</div>
{/* END: TODO CONTAINER */}
</div >
);
}
// END: TEMPLATE
// START: STATES
state = {
todos: [
{
'id': 1,
'title': 'Title1',
'completed': false,
'editing': false,
},
{
'id': 2,
'title': 'Title2',
'completed': false,
'editing': false,
},
],
todoId: 3
}
todoInput = React.createRef();
// END: STATES
// START: METHODS
/**
* START:
* @name: addTodo()
* @description: adds new todo
* @param event
*/
addTodo = (event) => {
if (event.key === 'Enter') {
const todoInput = this.todoInput.current.value;
//Start: if input value is empty, do not add
if (todoInput.trim().length === 0) {
return;
}
//End--
// Start: Add new todo to todos for display
this.setState((prevState, props) => {
console.log(todoInput);
let newTodoId = prevState.todoId++;
let newTodos = [
...prevState.todos,
{
id: newTodoId,
title: todoInput,
completed: false,
}
]
return { todos: newTodos }
})
// End: Add new todo to todos for display
this.todoInput.current.value = ""
}
}
// END: addTodo()
/**
* START:
* @name: deleteTodo()
* @description: deletes todo
* @param index
*/
deleteTodo = index => {
// Start: Deletes todo
this.setState((prevState, props) => {
let todos = prevState.todos;
//have the index, and just remove that 1.
todos.splice(index, 1);
return {
todos: todos
}
})
// End: Deletes todo
}
// END: deleteTodo()
// END: METHODS
}
export default App;
added checkTodo method, onClick on checkbox, then conditional classes at div tag displaying todo title below the input.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
// START: TEMPLATE
render() {
return (
<div className="App">
{/* START: HEADER */}
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
{/* END: HEADER */}
{/* START: TODO CONTAINER */}
<div className="Todo-container">
<input type="text" className="todo-input" placeholder="What needs to be done" onKeyUp={this.addTodo} ref={this.todoInput} />
{/* Start: Display Todos */}
{this.state.todos.map((todo, index) => {
return (
<div className="todo-item" key={todo.id}>
<div className="todo-item-left">
<input type="checkbox" onChange={(event) => this.checkTodo(todo, index, event)} />
<div className={'todo-item-label' + (todo.completed ? ' completed' : '')}>{todo.title} </div>
</div>
<div className="remove-item" onClick={() => this.deleteTodo(index)}>
×
</div>
</div>
)
})}
{/* End: Display Todos */}
{/* START: Select All and Item Remaining Counter */}
<div className="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>remaining items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
{/* START: Extra Buttons */}
<div className="extra-container">
<div>
<button >All</button>
<button>Active</button>
<button >Completed</button>
</div>
<div>
{/* <transition name="fade"> */}
<button >Clear Completed</button>
{/* </transition> */}
</div>
</div>
{/* END: Extra Buttons */}
</div>
{/* END: TODO CONTAINER */}
</div >
);
}
// END: TEMPLATE
// START: STATES
state = {
todos: [
{
'id': 0,
'title': 'Title1',
'completed': true,
'editing': false,
},
{
'id': 1,
'title': 'Title2',
'completed': false,
'editing': false,
},
],
todoId: 3
}
todoInput = React.createRef();
// END: STATES
// START: METHODS
/**
* START:
* @name: addTodo()
* @description: adds new todo
* @param event
*/
addTodo = (event) => {
if (event.key === 'Enter') {
const todoInput = this.todoInput.current.value;
//Start: if input value is empty, do not add
if (todoInput.trim().length === 0) {
return;
}
//End--
// Start: Add new todo to todos for display
this.setState((prevState, props) => {
let newTodoId = prevState.todoId++;
let newTodos = [
...prevState.todos,
{
id: newTodoId,
title: todoInput,
completed: false,
}
]
return { todos: newTodos }
})
// End: Add new todo to todos for display
this.todoInput.current.value = ""
}
}
// END: addTodo()
/**
* START:
* @name: deleteTodo()
* @description: deletes todo
* @param index
*/
deleteTodo = index => {
// Start: Deletes todo
this.setState((prevState, props) => {
let todos = prevState.todos;
//have the index, and just remove that 1.
todos.splice(index, 1);
return {
todos: todos
}
})
// End: Deletes todo
}
// END: deleteTodo()
// /**
// * @name checkTodo()
// * @description checks unchecks a todo
// * @param todo, index
// */
checkTodo = (todo, index, event) => {
todo.completed = !todo.completed;
this.setState((prevState, props) => {
let todos = prevState.todos;
todos.splice(index, 1, todo);
return { todos };
});
}
// // END: checkTodo()
// END: METHODS
}
export default App;
make it more like vue's conditional classes? lets use this dependency
yarn add classnames
import * as classnames from 'classnames';
<div className={classnames({ 'todo-item-label': true, 'completed': todo.completed })} onDoubleClick={(event) => this.editTodo(todo, index, event)}>
{todo.title}
</div>
Method section:
/**
* @name editTodo()
* @description edits a todo
* @param todo, index
*/
editTodo = (todo, index, event) => {
todo.editing = true;
this.setState((prevState, props) => {
let todos = prevState.todos;
todos.splice(index, 1, todo);
return { todos };
});
}
// END: editTodo()
Template Section:
{todo.editing ? (
<input
type="text"
className="todo-item-edit"
onDoubleClick={(event) => this.doneEditTodo(todo, index, event)}
onKeyUp={(event) => {
if (event.key === "Enter") {
this.doneEditTodo(todo, index, event)
} else if (event.key === "Escape") {
this.cancelEditTodo(todo, index, event)
}
}}
defaultValue={todo.title}
autoFocus
/>
) : (
<div
className={classnames({ 'todo-item-label': true, 'completed': todo.completed })}
onDoubleClick={(event) => this.editTodo(todo, index, event)}
>
{todo.title}
</div>
)}
- onDoubleClick - when double clicked on
- autoFocus - auto focus the user to the input
- defaultValue - default value for the input
- onBlur - when loses focus
After pressing enter on input, it should save and update state, else if pressing escape, then cancel all edits.
Method Section:
/**
* @name doneEditTodo()
* @description updates todo after pressing "Enter"
* @param todo, index, event
*/
doneEditTodo = (todo, index, event) => {
event.persist();
todo.editing = false;
todo.title = event.target.value;
this.setState((prevState, props) => {
let todos = prevState.todos;
todos.splice(index, 1, todo);
return { todos };
});
}
// END: doneEditTodo()
/**
* @name cancelEditTodo()
* @description cancel updating todo after pressing "Escape"
* @param todo, index, event
*/
cancelEditTodo = (todo, index, event) => {
this.setState((prevState, props) => {
let todos = prevState.todos;
todo.editing = false;
todos.splice(index, 1, todo);
return { todos };
});
}
// END: cancelEditTodo()
In vue, remaining was a computed property. since in react theres no computed property, we just implement a method.
App.js create new Computed section and add this computed method
// START: COMPUTED
/**
* @name remainingTodosCount()
* @description counts remaining items
* @param
*/
remainingTodosCount = () => {
return this.state.todos.filter(todo => !todo.completed).length
}
// END: remaining()
// END: COMPUTED
then at template
{/* START: Select All and Item Remaining Counter */}
<div className="extra-container">
<div><label><input type="checkbox" /> Check All</label></div>
<div>{this.remainingTodosCount()} items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
then it works like a computed property
Computed:
/**
* @name completedTodosCount()
* @description counts completed items
* @param
*/
completedTodosCount = () => {
return this.state.todos.filter(todo => todo.completed).length
}
// END: completedTodosCount()
Methods:
/**
* @name clearCompletedTodos()
* @description clears completed todos
* @param
*/
clearCompletedTodos = () => {
this.setState((prevState, props) => {
return { todos: prevState.todos.filter((todo) => !todo.completed) }
}
);
}
// END: clearCompletedTodos()
Template:
{/* Start: Clear Completed Button */}
{this.completedTodosCount() > 0 &&
<div>
<button onClick={() => this.clearCompletedTodos()}>Clear Completed</button>
</div>
}
{/* End: Clear Completed Button */}
Computed:
/**
* @name updateFilter()
* @description counts completed items
* @param
*/
updateFilter = filter => {
this.setState({ filter: filter })
}
Template
{/* Start: Filter Buttons */}
<div>
<button className={classnames({ 'active': this.state.filter === 'all' })} onClick={() => this.updateFilter('all')}>All</button>
<button className={classnames({ 'active': this.state.filter === 'active' })} onClick={() => this.updateFilter('active')}>Active</button>
<button className={classnames({ 'active': this.state.filter === 'completed' })} onClick={() => this.updateFilter('completed')}>Completed</button>
</div>
{/* End: Filter Buttons */}
Computed:
/**
* @name filteredTodos()
* @description filtered todo data
* @param
*/
filteredTodos = () => {
if (this.state.filter === 'all') {
return this.state.todos;
} else if (this.state.filter === 'active') {
return this.state.todos.filter(todo => !todo.completed);
} else if (this.state.filter === 'completed') {
return this.state.todos.filter(todo => todo.completed);
}
return this.state.todos;
}
// END: filteredTodos()
Template:
replace this.state.todos into this.filteredTodos()
{/* Start: Display Todos */}
{this.filteredTodos().map((todo, index) => {
return (
<div className="todo-item" key={todo.id}>
<div className="todo-item-left">
<input type="checkbox" onChange={(event) => this.checkTodo(todo, index, event)} />
{todo.editing ? (
<input
type="text"
className="todo-item-edit"
onDoubleClick={(event) => this.doneEditTodo(todo, index, event)}
onKeyUp={(event) => {
if (event.key === "Enter") {
this.doneEditTodo(todo, index, event)
} else if (event.key === "Escape") {
this.cancelEditTodo(todo, index, event)
}
}}
defaultValue={todo.title}
autoFocus
/>
) : (
<div
className={classnames({ 'todo-item-label': true, 'completed': todo.completed })}
onDoubleClick={(event) => this.editTodo(todo, index, event)}
>
{todo.title}
</div>
)}
</div>
<div className="remove-item" onClick={() => this.deleteTodo(index)}>
×
</div>
</div>
)
})}
{/* End: Display Todos */}
Methods:
/**
* @name checkAllTodos()
* @description Select all Todos
* @param todo, index, event
*/
checkAllTodos = (event) => {
event.persist();
this.setState((prevState, props) => {
let todos = prevState.todos;
todos.forEach(todo => todo.completed = event.target.checked)
return { todos };
});
}
// END: checkAllTodos()
Template:
{/* START: Select All and Item Remaining Counter */}
<div className="extra-container">
<div><label><input type="checkbox" onChange={(event)=>this.checkAllTodos(event)} /> Check All</label></div>
<div>{this.remainingTodosCount()} items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
now it selects and unselects all todos. however if we try unchecking one todo, we want to untick the select all. with that lets reuse the remaining count and bind it to the checked attribute of the select all checkbox.
Template:
{/* START: Select All and Item Remaining Counter */}
<div className="extra-container">
<div><label><input type="checkbox" checked={this.remainingTodosCount() === 0} onChange={(event) => this.checkAllTodos(event)} /> Check All</label></div>
<div>{this.remainingTodosCount()} items left</div>
</div>
{/* END: Select All and Item Remaining Counter */}
terminal
yarn add react-addons-css-transition-group
App.js
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
well you already have the css. anyways just wrap the component with this around the todo, and the completed button. then done
<ReactCSSTransitionGroup
transitionName="fade"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
</ReactCSSTransitionGroup>
the frontend is finished, now we separate it into smaller components cause its all in one big ass App.js file.
the goi placed the App.js inside the components folder 😟 Dont do it. well do it our own way.
create new components/TodosRemaining.js then rcc
import React, { Component } from 'react'
export default class TodosRemaining extends Component {
render() {
return (
<div>
{this.props.remaining} items left
</div>
)
}
}
App.js - import then add the component. then pass the according prop
<TodosRemaining remaining={this.remainingTodosCount()} />
however it prolly be better to just use rsc (react stateless component) instead of just using class components. use rafce, theres no rsc anymore 🙁
TodosRemaining.js
import React from 'react'
const TodosRemaining = ({ remaining }) => {
return (
<div>
{remaining} items left
</div>
)
}
export default TodosRemaining
yarn add prop-types
import PropTypes from 'prop-types'
i mean instead of installing another lib, if you have typescript you can use those goodie types and interfaces but lets follow the goi.
TodosRemaining.js
import React from 'react'
import PropTypes from 'prop-types'
const TodosRemaining = ({ remaining }) => {
return (
<div>
{remaining} items left
</div>
)
}
TodosRemaining.propTypes = {
remaining: PropTypes.number.isRequired
}
export default TodosRemaining
same lng, just pass everything as props and destructure em.
TodoItem.js
import React from 'react'
import PropTypes from 'prop-types'
import * as classnames from 'classnames';
const TodoItem = ({
todo,
index,
checkTodo,
doneEditTodo,
cancelEditTodo,
deleteTodo,
editTodo
}) => {
return (
<div className="todo-item">
<div className="todo-item-left">
<input type="checkbox" onChange={(event) => checkTodo(todo, index, event)} checked={todo.completed} />
{todo.editing ? (
<input
type="text"
className="todo-item-edit"
onDoubleClick={(event) => doneEditTodo(todo, index, event)}
onKeyUp={(event) => {
if (event.key === "Enter") {
doneEditTodo(todo, index, event)
} else if (event.key === "Escape") {
cancelEditTodo(todo, index, event)
}
}}
defaultValue={todo.title}
autoFocus
/>
) : (
<div
className={classnames({ 'todo-item-label': true, 'completed': todo.completed })}
onDoubleClick={(event) => editTodo(todo, index, event)}
>
{todo.title}
</div>
)}
</div>
<div className="remove-item" onClick={() => deleteTodo(index)}>
×
</div>
</div>
)
}
export default TodoItem
App.js
{/* Start: Display Todos */}
<ReactCSSTransitionGroup
transitionName="fade"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
{this.filteredTodos().map((todo, index) => {
return (
<TodoItem
key={todo.id}
todo={todo}
index={index}
checkTodo={this.checkTodo}
doneEditTodo={this.doneEditTodo}
cancelEditTodo={this.cancelEditTodo}
deleteTodo={this.deleteTodo}
editTodo={this.editTodo}
/>
)
})}
</ReactCSSTransitionGroup>
{/* End: Display Todos */}
TodoItem.js
TodoItem.propTypes = {
todo: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
checkTodo: PropTypes.func.isRequired,
doneEditTodo: PropTypes.func.isRequired,
cancelEditTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired
}
create new components/TodosCheckAll.js
import React from 'react'
import PropTypes from 'prop-types'
const TodosCheckAll = ({ remainingTodosCount, checkAllTodos }) => {
return (
<div><label><input type="checkbox" checked={remainingTodosCount() === 0} onChange={(event) => checkAllTodos(event)} /> Check All</label></div>
)
}
TodosCheckAll.propTypes = {
remainingTodosCount: PropTypes.func.isRequired,
checkAllTodos: PropTypes.func.isRequired
}
export default TodosCheckAll
App.js
{/* START: Select All and Item Remaining Counter */}
<div className="extra-container">
<TodosCheckAll remainingTodosCount={this.remainingTodosCount} checkAllTodos={this.checkAllTodos} />
<TodosRemaining remaining={this.remainingTodosCount()} />
</div>
{/* END: Select All and Item Remaining Counter */}
create new components/TodosFiltered.js
import React from 'react'
import PropTypes from 'prop-types'
import * as classnames from 'classnames';
const TodosFiltered = ({ filter, updateFilter }) => {
return (
<div>
<button className={classnames({ 'active': filter === 'all' })} onClick={() => updateFilter('all')}>All</button>
<button className={classnames({ 'active': filter === 'active' })} onClick={() => updateFilter('active')}>Active</button>
<button className={classnames({ 'active': filter === 'completed' })} onClick={() => updateFilter('completed')}>Completed</button>
</div>
)
}
TodosFiltered.propTypes = {
filter: PropTypes.string.isRequired,
updateFilter: PropTypes.func.isRequired
}
export default TodosFiltered
App.js
<TodosFiltered filter={this.state.filter} updateFilter={this.updateFilter} />
create new components/TodosClearCompleted.js
import React from 'react'
import PropTypes from 'prop-types'
const TodosClearCompleted = ({ clearCompletedTodos }) => {
return (
<div>
<button onClick={() => clearCompletedTodos()}>Clear Completed</button>
</div>
)
}
TodosClearCompleted.propTypes = {
clearCompletedTodos: PropTypes.func.isRequired
}
export default TodosClearCompleted
App.js
{/* Start: Clear Completed Button */}
{this.completedTodosCount() > 0 &&
<ReactCSSTransitionGroup
transitionName="fade"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
<TodosClearCompleted clearCompletedTodos={this.clearCompletedTodos} />
</ReactCSSTransitionGroup>
}
{/* End: Clear Completed Button */}
-
shows need for state management as component tree becomes complex to pass down props and passing up actions to change ui/state.
-
state management to have an accessible store of states and functions anywhere.
yarn eject
yarn add mobx mobx-react babel-plugin-transform-decorators-legacy
package.json - add inside babel a plugins array
"plugins":[
"transform-decorators-legacy"
]
Having an issue? visit this:
basically in addition to the steps above for babel's decorators
terminal - also
yarn add @babel/plugin-proposal-decorators
package.json - instead
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
create new src/store/TodoStore.js
- import and create a class and its export statement. dont forget to initialise new store TodoStore object.
- copy paste the state we have in App.js, then change stuff so we would know its successful.
- then change all the colons and commas after every field to using "=" instead of ":" colons, and ";" instead of ',' commas,
- then add @observable before every field
import {observable, action, computed} from 'mobx'
class TodoStore{
// START: OBSERVABLES
@observable todos = [
{
'id': 0,
'title': 'Title3',
'completed': false,
'editing': false,
},
{
'id': 1,
'title': 'Title4',
'completed': false,
'editing': false,
},
];
@observable todoId = 3;
@observable filter = '';
@observable todoInput = React.createRef();
// END: OBSERVABLES
}
const store = new TodoStore();
export default store;
index.js - import the stuff then wrap app component with provider tag from mob-react
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
// Start: MobX
import {Provider} from 'mobx-react';
import TodoStore from './store/TodoStore'
// End: MobX
ReactDOM.render(
<React.StrictMode>
<Provider TodoStore={TodoStore}>
<App />
</Provider>
</React.StrictMode>
,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
App.js
import mobx to app.js
import {inject, observer} from 'mobx-react';
@inject('TodoStore')
@observer
class App extends Component {
Template: inside render method of app.js
class App extends Component {
// START: TEMPLATE
render() {
const TodoStore = this.props.TodoStore;
then try changing at display of todos from the filteredTodos method to the one coming from the store.
{/* Start: Display Todos */}
<ReactCSSTransitionGroup
transitionName="fade"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
{TodoStore.todos.map((todo, index) => {
return (
<TodoItem
key={todo.id}
todo={todo}
index={index}
checkTodo={this.checkTodo}
doneEditTodo={this.doneEditTodo}
cancelEditTodo={this.cancelEditTodo}
deleteTodo={this.deleteTodo}
editTodo={this.editTodo}
/>
)
})}
</ReactCSSTransitionGroup>
{/* End: Display Todos */}
and it works
react is very strict with making data immutable so u need to use setState to change the data and not directly changing them. in MobX, you can. Under the hood they make it immutable but in code you can set them directly !!
TodoStore.js at Actions section
// START: ACTIONS
/**
* START:
* @name: addTodo()
* @description: adds new todo
* @param event
*/
@action
addTodo = (event) => {
if (event.key === 'Enter') {
const todoInput = this.todoInput.current.value;
//if input value is empty, do not add
if (todoInput.trim().length === 0) {
return;
}
// Start: Add new todo to todos for display
const newTodo = {
id: this.todoId,
title: todoInput,
completed: false,
editing: false,
}
this.todos.push(newTodo)
this.todoId++;
this.todoInput.current.value = ""
// End: Add new todo to todos for display
}
}
// END: addTodo()
// END: ACTIONS
App.js Template: change this.addTodo to TodoStore.addTodo
<input type="text" className="todo-input" placeholder="What needs to be done" onKeyUp={TodoStore.addTodo} ref={TodoStore.todoInput} />
ok so aparrently this app no longer works because it was still 2018 (outdated mobx). so im dropping this :<< @e2655