프로그래밍/ReactJS 🤞
6편 - Reactant 로 TODO 만들어 보기
재우니
2020. 8. 13. 01:57
https://stackblitz.com/edit/reactant-todomvc
먼저 해더, 푸터, 리스트 및 내역을 컴포넌트로 만들어 봅니다.
서비스 : todo.service.ts
상단 메뉴를 제외하곤 전부 사용하는 서비스이다. 아마 여기가 데이터 창고일 것이다.
import { injectable, action, state, autobind } from 'reactant'; const filters = ['All', 'Active', 'Completed'] as const; export type Filters = typeof filters; export type VisibilityFilter = Filters[number]; export interface Todo { id: string; text: string; completed: boolean; } @injectable() class TodoService { filters = filters; @state list: Todo[] = [ { id: `${Math.random()}`, text: 'Use Reactant', completed: false, } ]; @state visibilityFilter: VisibilityFilter = 'All'; @autobind @action add(text: string) { this.list.push({ id: `${Math.random()}`, text, completed: false, }) } getItem(id: string) { return this.list.find(item => item.id === id); } @autobind @action edit(id: string, text: string) { const item = this.getItem(id); item.text = text; } @autobind @action toggle(id: string) { const item = this.getItem(id); item.completed = !item.completed; } @action toggleAll(allCompleted: boolean) { this.list.forEach(item => { if (item.completed !== !allCompleted) { item.completed = !allCompleted; } }); } @autobind @action delete(id: string) { const index = this.list.findIndex(item => item.id === id); this.list.splice(index, 1); } @autobind @action clearCompleted() { this.list = this.list.filter(item => item.completed === false); } @autobind @action setVisibilityFilter(filter: VisibilityFilter) { this.visibilityFilter = filter; } } export { TodoService };
푸터 : Footer.tsx
import React, { FC } from "react"; import { VisibilityFilter, Filters } from "../todo.service"; interface FooterProps { activeCount: number; completedCount: number; visibilityFilter: string; filters: Filters; onClearCompleted(): void; onSetVisibilityFilter(filter: VisibilityFilter): void; } export const Footer: FC<FooterProps> = ({ activeCount, completedCount, filters, visibilityFilter, onClearCompleted, onSetVisibilityFilter, }) => { const itemWord = activeCount === 1 ? "item" : "items"; return ( <footer className="footer"> <span className="todo-count"> <strong>{activeCount}</strong> {itemWord} left </span> <ul className="filters"> {filters.map((filter) => ( <li key={filter}> <a className={visibilityFilter === filter ? "selected" : ""} style={{ cursor: "pointer" }} onClick={() => { onSetVisibilityFilter(filter); }} > {filter} </a> </li> ))} </ul> {!!completedCount && ( <button className="clear-completed" onClick={onClearCompleted}> Clear completed </button> )} </footer> ); };
해더 : Header.tsx
제일 상단에 위치한 부분입니다.
import React, { useState, FC } from "react"; interface HeaderProps { onAdd(text: string): void; } export const Header: FC<HeaderProps> = ({ onAdd }) => { const [text, setText] = useState(""); return ( <header className="header"> <h1>todos</h1> <input className="new-todo" placeholder="What needs to be done?" onKeyPress={(e) => { if (e.which === 13) { onAdd(text.trim()); setText(""); } }} value={text} onChange={(e) => setText(e.target.value)} /> </header> ); };
리스트 : List.tsx
노란색으로 칠한 부분이 리스트 영역이다.
import React, { FC } from "react"; import { Item } from "./Item"; import { Todo } from "../todo.service"; interface ListProps { filteredList: Todo[]; allSelected: boolean; onToggleAll(): void; onEdit(id: string, text: string): void; onToggle(id: string): void; onDelete(id: string): void; } export const List: FC<ListProps> = ({ filteredList, allSelected, onToggleAll, onEdit, onToggle, onDelete, }) => ( <section className="main"> <input id="toggle-all" type="checkbox" className="toggle-all" checked={allSelected} onChange={onToggleAll} /> <label htmlFor="toggle-all" /> <ul className="todo-list"> {filteredList.map((todo) => ( <Item key={todo.id} todo={todo} onEdit={onEdit} onToggle={onToggle} onDelete={onDelete} /> ))} </ul> </section> );
항목 : Item.tsx
각각의 row 을 생성해 주는 부분이다.
import React, { useState, FC } from "react"; import { Todo } from "../todo.service"; interface ItemProps { todo: Todo; onEdit(id: string, text: string): void; onToggle(id: string): void; onDelete(id: string): void; } export const Item: FC<ItemProps> = ({ todo, onEdit, onToggle, onDelete }) => { const [editing, setEditing] = useState(false); const [text, setText] = useState(todo.text); const onUpdate = () => { onEdit(todo.id, text.trim()); setText(text); setEditing(false); }; return ( <li onDoubleClick={() => !editing && setEditing(true)} className={`${editing ? "editing" : ""} ${ todo.completed ? "completed" : "" }`} > <div className="view"> <input type="checkbox" className="toggle" checked={todo.completed} onChange={() => onToggle(todo.id)} autoFocus /> <label>{todo.text}</label> <button className="destroy" onClick={() => onDelete(todo.id)} /> </div> {editing && ( <input className="edit" value={text} autoFocus onChange={(e) => setText(e.target.value)} onKeyPress={(e) => { if (e.which === 13) { onUpdate(); } }} onBlur={onUpdate} /> )} </li> ); };
메인 화면 : app.view.tsx
메인화면이며, 위에 구현한 상단, 하단, 목록들의 컴포넌트를 호출하여 위치에 맞게 넣는 작업을 한다.
import React from "react"; import { ViewModule, injectable, useConnector, computed } from "reactant"; import { TodoService } from "./todo.service"; import { Header } from "./components/Header"; import { List } from "./components/List"; import { Footer } from "./components/Footer"; @injectable() class AppView extends ViewModule { constructor(public todo: TodoService) { super(); } @computed(({ todo }: AppView) => [todo.visibilityFilter, todo.list]) get filteredList() { return this.todo.list.filter( (item) => (this.todo.visibilityFilter === "Active" && !item.completed) || (this.todo.visibilityFilter === "Completed" && item.completed) || this.todo.visibilityFilter === "All" ); } @computed(({ todo }: AppView) => [todo.list]) get completedTodoList() { return this.todo.list.filter((item) => item.completed); } @computed(({ todo, completedTodoList }: AppView) => [todo, completedTodoList]) get allSelected() { return ( this.todo.list.length > 0 && this.completedTodoList.length === this.todo.list.length ); } @computed(({ todo }: AppView) => [todo.list]) get activeCount() { return this.todo.list.filter(({ completed }) => !completed).length; } get completedCount() { return this.todo.list.length - this.activeCount; } onToggleAll = () => this.todo.toggleAll(this.allSelected); component() { const data = useConnector(() => ({ filteredList: this.filteredList, allSelected: this.allSelected, visibilityFilter: this.todo.visibilityFilter, })); return ( <div> <Header onAdd={this.todo.add} /> <List filteredList={data.filteredList} allSelected={data.allSelected} onToggleAll={this.onToggleAll} onEdit={this.todo.edit} onToggle={this.todo.toggle} onDelete={this.todo.delete} /> <Footer activeCount={this.activeCount} completedCount={this.completedCount} filters={this.todo.filters} visibilityFilter={data.visibilityFilter} onClearCompleted={this.todo.clearCompleted} onSetVisibilityFilter={this.todo.setVisibilityFilter} /> </div> ); } } export { AppView };
최상위 루트 화면 : index.tsx
import { render } from 'reactant-web'; import { createApp } from 'reactant'; import { AppView } from './app.view'; import 'todomvc-app-css/index.css'; const app = createApp({ modules: [], main: AppView, render, }); app.bootstrap(document.getElementById('app'));