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'));