TodoApp.tsx
import React, { Component, MouseEvent, ChangeEvent } from 'react';
// Represents one item in the todo list.
type Item = {
name: string;
completed: boolean;
};
// State of the app is the list of items and the text that the user is typing
// into the new item field.
type TodoState = {
items: Item[]; // existing items
newName: string; // mirrors text in the field to add a new name
}
// Enable debug logging of events in this class.
const DEBUG: boolean = false;
// Top-level application that lets the user add/complete items on a todo list
export class TodoApp extends Component<{}, TodoState> {
constructor(props: {}) {
super(props);
this.state = {items: [], newName: ""};
}
render = (): JSX.Element => {
if (DEBUG) console.debug("rendering")
// Return a UI with all the items and elements that allow them to add a new
// item with a name of their choice.
return (
<div>
<h2>To-Do List</h2>
{this.renderItems()}
<p className="instructions">Check the item to mark it completed.</p>
<p className="more-instructions">New item:
<input type="text" className="new-item"
value={this.state.newName}
onChange={this.doNewNameChange} />
<button type="button" className="btn btn-link"
onClick={this.doAddClick}>Add</button>
</p>
</div>);
}
// Returns a div for each item in the todo list, with each div containing a
// label with the name of the item and a checkbox for marking it completed.
renderItems = (): JSX.Element[] => {
const items: JSX.Element[] = [];
// Inv: items contains a div for each item of this.state.items[0 .. i-1]
for (let i = 0; i < this.state.items.length; i++) {
if (this.state.items[i].completed) {
items.push(
<div className="form-check" key={i}>
<input className="form-check-input" type="checkbox"
id={"check" + i} checked={true} readOnly={true} />
<label className="form-check-label completed" htmlFor={"check" + i}>
{this.state.items[i].name}
</label>
</div>);
} else {
items.push(
<div className="form-check" key={i}>
<input className="form-check-input" type="checkbox"
id={"check" + i} checked={false}
onChange={evt => this.doItemClick(evt, i)} />
<label className="form-check-label" htmlFor={"check" + i}>
{this.state.items[i].name}
</label>
</div>);
}
}
return items;
};
// Called when the user checks the box next to an uncompleted item. The
// second parameter is the index of that item in the list.
doItemClick = (_: ChangeEvent<HTMLInputElement>, index: number): void => {
const item = this.state.items[index];
if (DEBUG) console.log(`marking item ${item.name} as completed`);
// Note: we cannot mutate the list. We have to create a new one.
const items = this.state.items.slice(0, index)
.concat([{name: item.name, completed: true}])
.concat(this.state.items.slice(index + 1));
this.setState({items: items});
setTimeout(() => this.doItemTimeout(index), 5000);
}
// Called after an item has been removed for 5 seconds.
doItemTimeout = (index: number): void => {
const item = this.state.items[index];
if (DEBUG) console.log(`removing item ${item.name}`);
// Note: we cannot mutate the list. We have to create a new one.
const items = this.state.items.slice(0, index)
.concat(this.state.items.slice(index + 1));
this.setState({items: items});
}
// Called when the user clicks on the button to add the new item.
doAddClick = (_: MouseEvent<HTMLButtonElement>): void => {
// Ignore the request if the user hasn't entered a name.
// (Would be better to disable the button in this case.)
const name = this.state.newName.trim();
if (name.length == 0)
return;
if (DEBUG) console.log(`adding new item ${name}`);
const items = this.state.items.concat([{name: name, completed: false}]);
this.setState({items: items, newName: ""});
}
// Called each time the text in the new item name field is changed.
doNewNameChange = (evt: ChangeEvent<HTMLInputElement>): void => {
if (DEBUG) console.log(`changeing new name to ${evt.target.value}`);
this.setState({newName: evt.target.value});
}
}
Full Code