재우니의 블로그

https://stackblitz.com/edit/reactant-todomvc

reactant-todomvc - StackBlitz

Reactant TodoMVC Example

stackblitz.com

 

먼저 해더, 푸터, 리스트 및 내역을 컴포넌트로 만들어 봅니다.

 

 

 

서비스 : 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'));