mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
Rewrite of Day 1 to use modern React (#294)
* update to hooks * more class to function * cleanup * finish ts final * update html lesson * add lessons page * clean up * move getters into context * adding type * fix bug * step 5 cleanup * init final pass * text tweak * fix ternaries * readme cleanup * fixed root readme
This commit is contained in:
@@ -4,28 +4,22 @@ If you don't already have the app running, start it by running `npm start` from
|
||||
|
||||
## TodoFooter
|
||||
|
||||
1. Open TodoFooter and write a `TodoFooterProps` interface. It should include two values, a `clear` and `todos`. Use this interface in the function props like this: `(props: TodoFooterProps)`
|
||||
1. Open TodoFooter and write a `TodoFooterProps` interface. It should include two values, a `clearCompleted` and `todos`. Use this interface in the function props like this: `(props: TodoFooterProps)`
|
||||
|
||||
2. Write an `_onClick` function that calls `props.clear`.
|
||||
2. Write an `handleClick` function that calls `props.clear`.
|
||||
|
||||
- Since TodoFooter is not a class, the `_onClick` function needs to be stored in a const placed before the `return`.
|
||||
- Remember to use an arrow function to define this click handler.
|
||||
|
||||
3. Assign `_onClick` to the button's `onClick` prop. You won't need to use `this` since the component isn't a class.
|
||||
3. Assign `handleClick` to the button's `onClick` prop.
|
||||
|
||||
4. Test out this functionality. Check a few todos complete and click the `Clear Completed` button.
|
||||
|
||||
## TodoHeader
|
||||
|
||||
1. Open TodoHeader and write `TodoHeaderProps` which will include `addTodo`, `setFilter` and `filter`. Replace the first `any` in the class declaration with this interface.
|
||||
1. Open TodoHeader then write and use the `TodoHeaderProps` which will include `addTodo`, `changeFilter` and `filter`.
|
||||
|
||||
2. This component also has state. Write `TodoHeaderState` (there's just one value), and add this where the second `any` was.
|
||||
2. Add `onFilter` to each of the filter buttons
|
||||
|
||||
3. Add `_onFilter` to each of the filter buttons
|
||||
- Note that we can't add new parameters to onClick, but we can pull information from the event target!
|
||||
|
||||
- Note that we can't add new parameters to onClick, but we can pull information from the event target!
|
||||
- Remember to use an arrow function for this one too
|
||||
|
||||
4. Call `_onAdd` from the submit button
|
||||
4. Call `onSubmit` from the submit button
|
||||
|
||||
5. Check out this new functionality! We can now add and filter todos!
|
||||
|
||||
@@ -2,86 +2,75 @@ import React from 'react';
|
||||
import { TodoFooter } from './components/TodoFooter';
|
||||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
import { Todos, FilterTypes } from './TodoApp.types';
|
||||
import { Todo, Todos, FilterTypes } from './TodoApp.types';
|
||||
|
||||
let index = 0;
|
||||
const defaultTodos: Todos = [
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
export class TodoApp extends React.Component<any, { todos: Todos; filter: FilterTypes }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {
|
||||
'04': {
|
||||
label: 'Todo 4',
|
||||
completed: true
|
||||
},
|
||||
'03': {
|
||||
label: 'Todo 3',
|
||||
completed: false
|
||||
},
|
||||
'02': {
|
||||
label: 'Todo 2',
|
||||
completed: false
|
||||
},
|
||||
'01': {
|
||||
label: 'Todo 1',
|
||||
completed: false
|
||||
}
|
||||
},
|
||||
filter: 'all'
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState('all');
|
||||
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
|
||||
|
||||
const addTodo = (label: string): void => {
|
||||
const getId = () => Date.now().toString();
|
||||
const newTodo: Todo = {
|
||||
id: getId(),
|
||||
label: label,
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader addTodo={this._addTodo} setFilter={this._setFilter} filter={filter} />
|
||||
<TodoList complete={this._complete} todos={todos} filter={filter} />
|
||||
<TodoFooter clear={this._clear} todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// business logic
|
||||
|
||||
private _addTodo = label => {
|
||||
const { todos } = this.state;
|
||||
const id = index++;
|
||||
|
||||
this.setState({
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
});
|
||||
setTodos([...todos, newTodo]);
|
||||
};
|
||||
|
||||
private _complete = id => {
|
||||
const { todos } = this.state;
|
||||
const todo = todos[id];
|
||||
const newTodos = { ...todos, [id]: { ...todo, completed: !todo.completed } };
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
};
|
||||
|
||||
private _clear = () => {
|
||||
const { todos } = this.state;
|
||||
const newTodos = {};
|
||||
|
||||
Object.keys(this.state.todos).forEach(id => {
|
||||
if (!todos[id].completed) {
|
||||
newTodos[id] = todos[id];
|
||||
const toggleCompleted = (id: string) => {
|
||||
const newTodos = todos.map((todo): Todo => {
|
||||
if (todo.id === id) {
|
||||
return { ...todo, status: todo.status === 'active' ? 'completed' : 'active' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
setTodos(newTodos);
|
||||
};
|
||||
|
||||
private _setFilter = filter => {
|
||||
this.setState({
|
||||
filter: filter
|
||||
const clearCompleted = () => {
|
||||
const updatedTodos = todos.map((todo): Todo => {
|
||||
if (todo.status === 'completed') {
|
||||
return { ...todo, status: 'cleared' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
setTodos(updatedTodos);
|
||||
};
|
||||
|
||||
const changeFilter = (filter: FilterTypes) => {
|
||||
setFilter(filter);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} changeFilter={changeFilter} addTodo={addTodo} />
|
||||
<TodoList todos={todos} filter={'all'} toggleCompleted={toggleCompleted} />
|
||||
<TodoFooter todos={todos} clearCompleted={clearCompleted} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
export type TodoType = 'active' | 'completed' | 'cleared';
|
||||
|
||||
export type CompleteTodo = (id) => null;
|
||||
|
||||
export interface TodoItem {
|
||||
export interface Todo {
|
||||
id: string;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
status: TodoType;
|
||||
}
|
||||
|
||||
export interface Todos {
|
||||
[id: string]: TodoItem;
|
||||
}
|
||||
export type Todos = Todo[];
|
||||
|
||||
export type AddTodo = (label: string) => void;
|
||||
export type ToggleCompleted = (id: string) => void;
|
||||
export type ClearCompleted = () => void;
|
||||
export type ChangeFilter = (filter: FilterTypes) => void;
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Todos } from '../TodoApp.types';
|
||||
|
||||
export const TodoFooter = props => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
export const TodoFooter = (props) => {
|
||||
const { clearCompleted, todos } = props;
|
||||
|
||||
const itemCount = todos.filter((todo) => todo.status === 'active').length;
|
||||
|
||||
|
||||
return (
|
||||
<footer>
|
||||
<span>
|
||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||
</span>
|
||||
<button className="submit">Clear Completed</button>
|
||||
<button className="submit">
|
||||
Clear Completed
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
import React from 'react';
|
||||
import { FilterTypes } from '../TodoApp.types';
|
||||
|
||||
export class TodoHeader extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
export const TodoHeader = (props) => {
|
||||
const [inputText, setInputText] = React.useState<string>('');
|
||||
const { filter, addTodo, changeFilter } = props;
|
||||
|
||||
render() {
|
||||
const { filter } = this.props;
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.7 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}>all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
_onFilter = evt => {
|
||||
this.props.setFilter(evt.target.innerText);
|
||||
const onInput = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onSubmit = () => {
|
||||
if (inputText.length > 0) addTodo(inputText);
|
||||
setInputText('');
|
||||
};
|
||||
|
||||
_onAdd = () => {
|
||||
this.props.addTodo(this.state.labelInput);
|
||||
this.setState({ labelInput: '' });
|
||||
const onFilter = (e) => {
|
||||
changeFilter(e.currentTarget.textContent)
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.6 exercise)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
<button className="submit">Add</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}> all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -3,26 +3,26 @@ import { TodoListItem } from './TodoListItem';
|
||||
import { FilterTypes, Todos } from '../TodoApp.types';
|
||||
|
||||
interface TodoListProps {
|
||||
complete: (id: string) => void;
|
||||
todos: Todos;
|
||||
filter: FilterTypes;
|
||||
toggleCompleted: (id: string) => void;
|
||||
todos: Todos;
|
||||
}
|
||||
|
||||
export class TodoList extends React.Component<TodoListProps, any> {
|
||||
render() {
|
||||
const { filter, todos, complete } = this.props;
|
||||
export const TodoList = (props: TodoListProps) => {
|
||||
const { filter, todos, toggleCompleted } = props;
|
||||
|
||||
// filteredTodos returns an array of filtered todo keys [01,02,03]
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
const filteredTodos = todos.filter((todo) => {
|
||||
if (todo.status === 'cleared') return false;
|
||||
return filter === 'all' ||
|
||||
(filter === 'completed' && todo.status === 'completed') ||
|
||||
(filter === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} complete={complete} {...todos[id]} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} toggleCompleted={toggleCompleted} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import React from 'react';
|
||||
import { TodoItem } from '../TodoApp.types';
|
||||
import { Todo, ToggleCompleted } from '../TodoApp.types';
|
||||
|
||||
interface TodoListItemProps extends TodoItem {
|
||||
id: string;
|
||||
complete: (id: string) => void;
|
||||
interface TodoListItemProps extends Todo {
|
||||
toggleCompleted: ToggleCompleted;
|
||||
}
|
||||
|
||||
export class TodoListItem extends React.Component<TodoListItemProps, any> {
|
||||
render() {
|
||||
const { label, completed, complete, id } = this.props;
|
||||
export const TodoListItem = (props: TodoListItemProps) => {
|
||||
const { label, status, id, toggleCompleted } = props;
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={completed} onChange={() => complete(id)} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
const handleCheck = () => toggleCompleted(id);
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={status === 'completed'} onChange={handleCheck} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user