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:
Micah Godbolt
2022-01-13 09:22:50 -08:00
committed by GitHub
parent 4998c158d2
commit 7cea32428e
60 changed files with 923 additions and 929 deletions

View File

@@ -1,16 +1,14 @@
# Step 1.7 - Types and creating a UI-driven state (Demo)
Now that we have a UI that is purely driven by the state of our app, we need to add functionality to allow the UI to drive the state. This is often done by creating functions that call `setState` like we saw in the `TodoHeader`. Values from the state are then passed down child components as props.
Now that we have a UI that is purely driven by the state of our app, we need to add functionality to allow the UI to modify the state. This is our core "business logic" and handles our basic "CRUD" operations: Create, Read, Update, Delete.
> We'll be learning in part 2 of this workshop how we can expose these functions without explicitly passing them down via props.
This is our core "business logic" and handles our basic "CRUD" operations: Create, Read, Update, Delete. We don't have time to walk through writing all of those functions, but you can see that they are already provided in the demo's `TodoApp` and passed into our components.
This step in "Thinking in React" is called [Step 5: Add Inverse Data Flow](https://reactjs.org/docs/thinking-in-react.html#step-5-add-inverse-data-flow). Lets start by looking at the `TodoApp.tsx` and seeing how our components are going to be able to interact with app state.
## Intro to TypeScript
Taking a look at our components in `TodoApp`, you can see that our list of props is getting not just longer, but much more complex! We're passing through functions with various signatures, complex `todos` objects, and filter strings which are always one of three values.
As applications grow, it becomes difficult to remember what each function does or what each todo contains. Also, as JavaScript is a dynamically typed language, if I wanted to change the value of `todos` to an array inside my `TodoList`, JavaScript wouldn't care. But if `TodoListItems` was expecting an object, our application would break.
As applications grow, it becomes difficult to remember what each function does or what each todo contains. Also, as JavaScript is a dynamically typed language, if I wanted to change the value of `filter` to a boolean, JavaScript wouldn't care. But if `TodoHeader` was expecting a string, our application would break.
For these two reasons, the industry is shifting to writing applications that are strongly typed, and many are using TypeScript to accomplish that.
@@ -24,30 +22,28 @@ Let's dive in and see how TypeScript can help clarify our component props and gu
# Demo
Let's start off in the TodoList, as that has the most data flow up and down. There isn't any interactive UI in this component, as we're simply passing `completed` down to each `TodoListItem`, but we can write a props interface to make sure that everything gets passed down properly.
Let's start off in the TodoList, as that has the most data flow up and down. There isn't any interactive UI in this component, as we're simply passing our `todo` down to each `TodoListItem`, but we can write a props interface to make sure that everything gets passed down properly.
## Writing TodoListProps
Looking at our `TodoApp` we know that `TodoList` has three props: `filter`, `todos`, and `complete`. We'll start by creating an interface called `TodoListProps` that represents this component's props.
Looking at our `TodoApp` we know that `TodoList` has three props: `filter`, `todos`, and `toggleCompleted`. We'll start by creating an interface called `TodoListProps` that represents this component's props.
```ts
interface TodoListProps {
filter: any;
toggleCompleted: any;
todos: any;
complete: any;
}
```
> Note that we're using the `any` keyword for now. This won't give us any type safety, but it does let us specify valid prop names we can pass to this component.
With that interface written, we'll add it to our component class.
With that interface written, we'll add it to our component.
```ts
export class TodoList extends React.Component<TodoListProps, any>
export const TodoList = (props: TodoListProps) => {
```
> Note that the first value in `<>` is for a props interface, and the second is for state.
Now that we have a typed component, let's go back to our `TodoApp` and see what happens if we try to change the name of a prop.
## Adding type safety
@@ -61,8 +57,8 @@ We know that `filter` shouldn't be an object, array or function, so we can speci
```ts
interface TodoListProps {
filter: string;
toggleCompleted: any;
todos: any;
complete: any;
}
```
@@ -71,8 +67,8 @@ But since we know that the filter can be only one of three values, we can make t
```ts
interface TodoListProps {
filter: 'all' | 'active' | 'completed';
toggleCompleted: any;
todos: any;
complete: any;
}
```
@@ -80,39 +76,38 @@ Now try going back to `TodoApp` and changing the `filter` attribute in `TodoList
### Complete Type
The `complete` prop isn't data, but a function. Fortunately, TypeScript can handle function types just as well as data.
The `toggleComplete` prop isn't data, but a function.
```ts
interface TodoListProps {
filter: 'all' | 'active' | 'completed';
toggleCompleted: (id: string) => void;
todos: any;
complete: (id: string) => void;
}
```
For functions we are only concerned with the parameters passed in and the return value. You can see in the example above that the function takes in an `id` of type string and returns `void`, which means it has no returned value.
For functions we are concerned with the parameters passed in as well as returned. You can see in the example above that the function takes in an `id` of type string and returns `void`, which means it has no returned value.
> Technically, all functions in JavaScript return `undefined` if no other return value is specified, but declaring a return type of `void` causes TypeScript to error if you try to return a value from the function (or use its default returned value of `undefined`).
## Todos Type
The `todos` prop is interesting in that `todos` is an object with a bunch of unknown keys. So here's what that interface would look like.
The `todos` prop is an array of objects where each of those objects represent a `todo`. For now we'll write that `todo` interface right into the list props;
```ts
interface TodoListProps {
filter: 'all' | 'active' | 'completed';
todos: {
[id: string]: {
toggleCompleted: (id: string) => void;
todos: [
{
id: string;
label: string;
completed: boolean;
};
};
complete: (id: string) => void;
status: string;
}
];
}
```
> Note that `[id: string]` does not indicate an array; it is an object [index signature](https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types).
Now that our interface is complete, try changing the word "all" in `filter === all` and see that VS Code will tell you this condition will always be false. Compare this to plain JavaScript: if you had a typo in that line, you wouldn't understand why your filter wasn't working.
## Sharing types
@@ -120,44 +115,49 @@ Now that our interface is complete, try changing the word "all" in `filter === a
Most of our components will need to specify types for `todos` and `filter`, so it's a good thing that TypeScript allows us to share types between files. I've already written up and exported those shared types in the file `TodoApp.types.ts`, so we just need to import them and use them in our interface.
```ts
import { FilterTypes, Todos, CompleteTodo } from '../TodoApp.types';
import { FilterTypes, Todos, ToggleCompleted } from '../TodoApp.types';
interface TodoListProps {
complete: CompleteTodo;
todos: Todos;
filter: FilterTypes;
toggleCompleted: ToggleCompleted;
todos: Todos;
}
```
## Writing TodoListItemProps
Jumping down to the TodoListItem, as we start to write the `TodoListItemProps` we realize that two of the props, `label` and `completed`, have already been defined in the `TodoItem` interface. So we can make `TodoListItemProps` reuse the `TodoItem` interface by extending it.
Jumping down to the TodoListItem, as we start to write the `TodoListItemProps` we realize that three of the props, `label`, `status`, `id`, have already been defined in the `TodoItem` interface. So we can make `TodoListItemProps` reuse the `TodoItem` interface by extending it.
```ts
import { CompleteTodo } from '../TodoApp.types';
import { ToggleCompleted } from '../TodoApp.types';
interface TodoListItemProps extends TodoItem {
id: string;
complete: CompleteTodo;
toggleCompleted: ToggleCompleted;
}
```
The end result of this is an interface with all four properties: `id`, `complete`, `completed` and `label`.
The end result of this is an interface with all four properties: `id`, `toggleCompleted`, `status` and `label`.
Next we can pull in the remaining props in the render function:
```jsx
const { label, completed, complete, id } = this.props;
const { label, status, id, toggleCompleted } = props;
```
And then use the input's `onChange` event to fire our `complete` callback. We can see in the signature that `complete` expects an `id` of type string, so we'll pass our `id` prop in.
And then use the input's `onChange` event to call a function that toggles the todo's completed state. We can see in the signature that `toggleCompleted` expects an `id` of type string, so we'll pass our `id` prop in.
> A [callback function](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function) is a function passed into a component as a prop.
```jsx
<input type="checkbox" checked={completed} onChange={() => complete(id)} />
const handleCheck = () => toggleCompleted(id);
...
<input type="checkbox" checked={status === 'completed'} onChange={handleCheck} />
```
> Note that the function param and prop name just happen to be the same. This isn't required.
## Passing props down
Now that our todos are firing the `onChange` callback, give them a click and take look at how the app responds. Since our footer text is based on the number of unchecked todos, the footer will automatically update to reflect the new state.
Now that we have added `toggleCompleted` to our `TodoListItemProps` we'll see that the `TodoListItem` in our `TodoList` is complaining about a missing prop. We successfully passed the function into our `TodoList`, but we aren't passing it down into `TodoListItem`. This process is often called `prop drilling` and can be a signal for refactoring (which you'll see in the final example).
```jsx
<TodoListItem key={todo.id} {...todo} toggleCompleted={toggleCompleted} />
```

View File

@@ -5,3 +5,5 @@
<div id="app"></div>
</body>
</html>

View File

@@ -2,89 +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',
},
];
interface TodoAppState {
todos: Todos;
filter: FilterTypes;
}
export const TodoApp = () => {
const [filter, setFilter] = React.useState<FilterTypes>('all');
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
export class TodoApp extends React.Component<any, TodoAppState> {
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'
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>
);
}
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={filter} toggleCompleted={toggleCompleted} />
<TodoFooter todos={todos} clearCompleted={clearCompleted} />
</div>
);
}

View File

@@ -1,12 +1,16 @@
export type FilterTypes = 'all' | 'active' | 'completed';
export type TodoType = 'active' | 'completed' | 'cleared';
export type CompleteTodo = (id) => void;
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;

View File

@@ -1,14 +1,19 @@
import React from 'react';
import { Todos } from '../TodoApp.types';
export const TodoFooter = (props: any) => {
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>
);
};

View File

@@ -1,31 +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 demo)</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>
);
}
_onChange = evt => {
this.setState({ labelInput: evt.target.value });
const onInput = (e) => {
setInputText(e.target.value);
};
}
const onSubmit = () => {
if (inputText.length > 0) addTodo(inputText);
setInputText('');
};
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>
);
}

View File

@@ -1,22 +1,21 @@
import React from 'react';
import { TodoListItem } from './TodoListItem';
import { FilterTypes, Todos } from '../TodoApp.types';
export class TodoList extends React.Component<any, any> {
render() {
const { filter, todos, complete } = this.props;
export const TodoList = (props) => {
const { filter, todos } = 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} />
))}
</ul>
);
}

View File

@@ -1,16 +1,13 @@
import React from 'react';
import { TodoItem } from '../TodoApp.types';
export class TodoListItem extends React.Component<any, any> {
render() {
const { label, completed } = this.props;
export const TodoListItem = (props) => {
const { label, status, id } = props;
return (
<li className="todo">
<label>
<input type="checkbox" checked={completed} onChange={() => undefined} /> {label}
</label>
</li>
);
}
}
return (
<li className="todo">
<label>
<input type="checkbox" checked={status === 'completed'} /> {label}
</label>
</li>
);
};

View File

@@ -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!

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -2,67 +2,94 @@ 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, AppContextProps } from './TodoApp.types';
let index = 0;
export const AppContext = React.createContext<AppContextProps>(undefined);
export class TodoApp extends React.Component<{}, { todos: Todos; filter: FilterTypes }> {
constructor(props) {
super(props);
this.state = {
todos: {},
filter: 'all'
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 const TodoApp = () => {
const [filter, setFilter] = React.useState<FilterTypes>('all');
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
// TODO Convert to useReducer
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>
);
}
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);
};
const getFilter = () => {
return filter;
}
const getTodos = () => {
return todos;
}
return (
<AppContext.Provider value={{
addTodo,
toggleCompleted,
clearCompleted,
changeFilter,
getFilter,
getTodos
}}>
<TodoHeader />
<TodoList />
<TodoFooter />
</AppContext.Provider>
);
};

View File

@@ -1,10 +1,20 @@
export type FilterTypes = 'all' | 'active' | 'completed';
export type TodoType = 'active' | 'completed' | 'cleared';
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 interface AppContextProps {
addTodo: (label: string) => void;
toggleCompleted: (id: string) => void;
clearCompleted: () => void;
changeFilter: (filter: FilterTypes) => void;
getFilter: () => FilterTypes;
getTodos: () => Todos;
}

View File

@@ -1,14 +1,12 @@
import React from 'react';
import { Todos } from '../TodoApp.types';
interface TodoFooterProps {
clear: () => void;
todos: Todos;
}
import { AppContext } from '../TodoApp';
export const TodoFooter = (props: TodoFooterProps) => {
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
const _onClick = () => {
props.clear();
export const TodoFooter = () => {
const { clearCompleted, getTodos } = React.useContext(AppContext);
const itemCount = getTodos().filter((todo) => todo.status === 'active').length;
const handleClick = () => {
clearCompleted();
};
return (
@@ -16,7 +14,7 @@ export const TodoFooter = (props: TodoFooterProps) => {
<span>
{itemCount} item{itemCount === 1 ? '' : 's'} left
</span>
<button onClick={_onClick} className="submit">
<button onClick={handleClick} className="submit">
Clear Completed
</button>
</footer>

View File

@@ -1,58 +1,39 @@
import React from 'react';
import React, { ChangeEventHandler, MouseEventHandler, useState, useContext } from 'react';
import { FilterTypes } from '../TodoApp.types';
import { AppContext } from '../TodoApp';
interface TodoHeaderProps {
addTodo: (label: string) => void;
setFilter: (filter: FilterTypes) => void;
filter: FilterTypes;
}
export const TodoHeader = () => {
const { changeFilter, addTodo, getFilter } = useContext(AppContext);
const [inputText, setInputText] = useState<string>('');
interface TodoHeaderState {
labelInput: string;
}
export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
constructor(props) {
super(props);
this.state = { labelInput: '' };
}
render() {
const { filter, setFilter } = this.props;
return (
<header>
<h1>todos <small>(1.7 final)</small></h1>
<div className="addTodo">
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
<button onClick={this._onAdd} className="submit">
Add
</button>
</div>
<nav className="filter">
<button onClick={this._onFilter} className={filter === 'all' ? 'selected' : ''}>
all
</button>
<button onClick={this._onFilter} className={filter === 'active' ? 'selected' : ''}>
active
</button>
<button onClick={this._onFilter} className={filter === 'completed' ? 'selected' : ''}>
completed
</button>
</nav>
</header>
);
}
_onFilter = evt => {
this.props.setFilter(evt.target.innerText);
const onInput: ChangeEventHandler<HTMLInputElement> = (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: MouseEventHandler<HTMLButtonElement> = (e) => {
changeFilter(e.currentTarget.textContent as FilterTypes)
};
}
return (
<header>
<h1>
todos <small>(1.7 final)</small>
</h1>
<div className="addTodo">
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
<button onClick={onSubmit} className="submit">
Add
</button>
</div>
<nav className="filter">
<button onClick={onFilter} className={getFilter() === 'all' ? 'selected' : ''}> all</button>
<button onClick={onFilter} className={getFilter() === 'active' ? 'selected' : ''}>active</button>
<button onClick={onFilter} className={getFilter() === 'completed' ? 'selected' : ''}>completed</button>
</nav>
</header>
);
};

View File

@@ -1,27 +1,22 @@
import React from 'react';
import { TodoListItem } from './TodoListItem';
import { FilterTypes, Todos } from '../TodoApp.types';
import { AppContext } from '../TodoApp';
interface TodoListProps {
complete: (id: string) => void;
todos: Todos;
filter: FilterTypes;
}
export const TodoList = () => {
const { getFilter, getTodos } = React.useContext(AppContext);
export class TodoList extends React.Component<TodoListProps, any> {
render() {
const { filter, todos, complete } = this.props;
const filteredTodos = getTodos().filter((todo) => {
if (todo.status === 'cleared') return false;
return getFilter() === 'all' ||
(getFilter() === 'completed' && todo.status === 'completed') ||
(getFilter() === 'active' && todo.status === 'active');
});
const filteredTodos = Object.keys(todos).filter(id => {
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
});
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} />
))}
</ul>
);
};

View File

@@ -1,21 +1,16 @@
import React from 'react';
import { TodoItem } from '../TodoApp.types';
import { Todo } from '../TodoApp.types';
import { AppContext } from '../TodoApp';
interface TodoListItemProps extends TodoItem {
id: string;
complete: (id: string) => void;
}
export const TodoListItem = (props: Todo) => {
const { label, status, id } = props;
const { toggleCompleted } = React.useContext(AppContext);
export class TodoListItem extends React.Component<TodoListItemProps, any> {
render() {
const { label, completed, complete, id } = this.props;
return (
<li className="todo">
<label>
<input type="checkbox" checked={completed} onChange={() => complete(id)} /> {label}
</label>
</li>
);
}
}
return (
<li className="todo">
<label>
<input type="checkbox" checked={status === 'completed'} onChange={() => toggleCompleted(id)} /> {label}
</label>
</li>
);
};