TodoApp.tsx
import React, { Component, MouseEvent, ChangeEvent } from 'react';
// Represents one item in the todo list.
type Item = {
name: string;
highPriority: boolean;
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
newHighPriority: boolean; // mirrors checkbox for high priority
}
// 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: "", newHighPriority: false};
}
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>Buggy To-Do List</h2>
{this.renderItems()}
<p className="instructions">Check the item to mark it completed.</p>
{this.renderAddUI()}
</div>);
}
renderAddUI = (): JSX.Element => {
if (this.state.newHighPriority) {
return (
<p className="more-instructions">
<span>New <b>high priority</b> item:
<input type="text" className="new-item"
onChange={this.doNewNameChange} />
<input className="form-check-input" type="checkbox"
id="high-priority" checked={true}
onChange={this.doHighPriorityChange}/>
<label className="form-check-label" htmlFor="high-priority">
High Priority
</label>
<button type="button" className="btn btn-link"
onClick={this.doAddClick}>Add</button>
</span></p>);
} else {
return (
<p className="more-instructions">New item:
<input type="text" className="new-item"
onChange={this.doNewNameChange} />
<input className="form-check-input" type="checkbox"
id="high-priority" checked={false}
onChange={this.doHighPriorityChange}/>
<label className="form-check-label" htmlFor="high-priority">
High Priority
</label>
<button type="button" className="btn btn-link"
onClick={this.doAddClick}>Add</button>
</p>);
}
};
// 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++) {
const text = this.state.items[i].highPriority ?
<b>{this.state.items[i].name}</b> :
<span>{this.state.items[i].name}</span>;
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}>
{text}
</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}>
{text}
</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, highPriority: item.highPriority, 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 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});
}
doHighPriorityChange = (evt: ChangeEvent<HTMLInputElement>): void => {
evt.preventDefault();
this.setState({newHighPriority: evt.target.checked});
};
// 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;
const priority = this.state.newHighPriority;
if (DEBUG) {
console.log(`adding new item ${name} (high priority: ${priority})`);
}
const items = this.state.items
.concat([{name: name, highPriority: priority, completed: false}]);
this.setState({items: items, newName: "", newHighPriority: false});
}
}
Full Code