end0tknr's kipple - 新web写経開発

http://d.hatena.ne.jp/end0tknr/ から移転しました

React js の練習 (javascript?)

https://reactjs.org/

とりあえず react に触れてみたかった。

ググってみると、nodejs のinstallから紹介しているサイトを多く見かけますが、 ブラウザ + エディタだけで気軽に試すことができる dotinstall.com にて写経。

以下は、その成果物のTODO管理。読めば、分かると思います

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>reactjs.org の練習 at dotinstall.com </title>
  <link rel="stylesheet" href="css/styles.css">
  <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>

  <!--  #root 以下を Reactが制御/描画します -->
  <div id="root"></div>
  
  <script type="text/babel">
   (() => {
     function TodoHeader(props) {
       // filter()で未完了のTODOを抽出
       const remaining = props.todos.filter(todo => {
         return !todo.isDone;
       });
       
       return (
         <h1>
         My Todos
         <span>({remaining.length}/{props.todos.length})</span>
    
         <button onClick={props.purge}>完了TODOの一括消去</button>
    </h1>
       );
     }
     
     function TodoList(props) {
       // map() で、TODOを <TodoItem / > Component に変換
       const todos = props.todos.map(todo => {
         return (
           <TodoItem
           key={todo.id}
           todo={todo}
           checkTodo={props.checkTodo}
           deleteTodo={props.deleteTodo}
           />
         );
       });
       return (
         <ul>
         {props.todos.length ? todos : <li>対応すべきTODOはありません!</li>}
         </ul>
       );
     }


     function TodoItem(props) {
       return (
         <li key={props.todo.id}>
         <label>
         <input type="checkbox"
         checked={props.todo.isDone}
         onChange={() => props.checkTodo(props.todo)}
         />
         <span className={props.todo.isDone ? 'done' : ''}>
         {props.todo.title}
         </span>
         </label>
         <span className="cmd" onClick={() => props.deleteTodo(props.todo)}>[×]</span>
         </li>
       );
     }
     

     function TodoForm(props) {
       return (
         <form onSubmit={props.addTodo}>
         <input type="text" value={props.item} onChange={props.updateItem}/>
         <input type="submit" value="追加"/>
         </form>
       );
     }

     function getUniqueId() {
       return new Date().getTime().toString(36) + '-' + Math.random().toString(36);
     }
     
     class App extends React.Component {
       constructor() {
         super();
    // Reactの作法的に、stateは1箇所のcomponentで管理するらしい
         this.state = {
           todos: [],
           item: ''   // 新たに追加するTODOの内容
         };
    
         this.checkTodo =  this.checkTodo.bind(this);
         this.deleteTodo = this.deleteTodo.bind(this);
         this.updateItem = this.updateItem.bind(this);
         this.addTodo =    this.addTodo.bind(this);
         this.purge =      this.purge.bind(this);
       }
       
       purge() {
         if ( ! confirm('完了TODOを一括消去してよろしいですか ?')) {
           return;
         }
    // filter() で未完了TODOを集め、 setState() にて更新
         const todos = this.state.todos.filter(todo => {
           return ! todo.isDone;
         });
         this.setState({
           todos: todos
         });
       }
       
       addTodo(e) {
    // onSubmit()で呼ばれる為、画面遷移されることを防ぎます
         e.preventDefault();
    
         if (this.state.item.trim() === '') {
           return;
         }
    
         const item = {
           id: getUniqueId(),
           title: this.state.item,
           isDone: false
         };

    // slice()にて、元の配列をshallow copy
         const todos = this.state.todos.slice();
         todos.push(item);
         this.setState({
           todos: todos,
           item: ''
         });
       }

       deleteTodo(todo) {
         if (!confirm('削除してよろしいですか ?')) {
           return;
         }
    
    // slice()にて、元の配列をshallow copyし
    // indexOf()にて削除対象の位置を探索
         const todos = this.state.todos.slice();
         const pos = this.state.todos.indexOf(todo);

    // splice()にて、該当のTODOを削除
          todos.splice(pos, 1);
          this.setState({
           todos: todos
         });
       }

       checkTodo(todo) {
         const todos = this.state.todos.map(todo => {
           return {id: todo.id, title: todo.title, isDone: todo.isDone};
         });
    
         const pos = this.state.todos.map(todo => {
           return todo.id;
         }).indexOf(todo.id);

         todos[pos].isDone = !todos[pos].isDone;
         this.setState({
           todos: todos
         });
       }
       
       // TodoForm componentの onChange()から呼ばれます
       updateItem(e) {
         this.setState({
           item: e.target.value
         });
       }

       // componentDidUpdate()は、Component の props や state が
       // 更新された際に呼ばれます.
       // ここでは、TODOの内容をjsonにして、localStorage へ保存しています
       componentDidUpdate() {
         localStorage.setItem('todos', JSON.stringify(this.state.todos));
       }
       // componentDidMount()により、上記で json保存した内容を画面にloadしています
       componentDidMount() {
         this.setState({
           todos: JSON.parse(localStorage.getItem('todos')) || []
         });
       }
       
        render() {
          return (
            <div className="container">
              <TodoHeader
                todos={this.state.todos}
                purge={this.purge}
              />
              <TodoList
                todos={this.state.todos}
                checkTodo={this.checkTodo}
                deleteTodo={this.deleteTodo}
              />
              <TodoForm
                item={this.state.item}
                updateItem={this.updateItem}
                addTodo={this.addTodo}
              />
            </div>
          );
        }
      }

      ReactDOM.render(
        <App/>,
        document.getElementById('root')
      );
    })();
  </script>
</body>
</html>
body {
}

.container {
  width: 400px;
  margin: auto;
}

.container h1 {
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}

.container ul {
  padding: 0;
  list-style: none;
}

.container li {
  line-height: 2;
}

.container input[type="checkbox"] {
  margin-right: 5px;
}

.cmd {
  cursor: pointer;
  color: #08c;
  margin-left: 5px;
}

.container input[type="text"] {
  padding: 2px;
  margin-right: 5px;
}

h1 > span {
  color: #ccc;
  font-weight: normal;
  margin-left: 5px;
}


h1 > button {
  float: right;
}

.done {
  text-decoration: line-through;
  color: #ccc;
}