「ES6を理解する」→「素のjavascriptとrails APIを使ってtodoアプリを作成する。」

#はじめに

[初めてのJavaScript](https://www.amazon.co.jp/%E5%88%9D%E3%82%81%E3%81%A6%E3%81%AEJavaScript-%E7%AC%AC3%E7%89%88-%E2%80%95ES2015%E4%BB%A5%E9%99%8D%E3%81%AE%E6%9C%80%E6%96%B0%E3%82%A6%E3%82%A7%E3%83%96%E9%96%8B%E7%99%BA-Ethan-Brown/dp/4873117836)を学習後、ES6を理解するために、素のjavascriptとrails APIを使ってtodoアプリを作成してみました。

#RailsでAPI作成
[RailsでWEB API入門](https://qiita.com/drafts/bcec5cc278ad198c7eca/edit)
こちらが良記事でしたので、まんま写経しました。
その後、rack-coreのgemをインストールします。

```Gemfile.
gem 'rack-cors'
```

写経したRails APIアプリを、外部サイトからアクセス出来るように、生成されたcors.rbを編集します。

```config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```
これでAPI側の実装は完了です。これで外部サイトからhttpリクエストを送るとjson形式でデータを返してくれます。


#外部サイト作成
今回はapiがメインなので見た目は適当です。

```todo/index.html
<!DOCTYPE html>
<html>
<head>
  <title>todoApp</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h2>Todo</h2>
    <input type="text" id="field" />
    <button id="btn">Send</button>
    <ul id="todo">
    </ul>
    <script src="./main.js"></script>
  </div>
</body>
</html>
```

さてここからが本題です。早速素のjsを書いていきましょう。まずはTodoのclassを作成しておきます。以前は面倒だったクラス定義もES2015では簡単に作成できます。現時点で具体的なtodoは生成されていませんが、いつでも生成できるよう準備を整えます。

```todo/main.js
class Todo {
  constructor() {
  }
}
```

具体的にtodoを生成するためにはnewを使います。

```todo/main.js
todo = new Todo();

console.log(todo instanceof Todo); //true
```

それではクラスTodoの記述をもう少し細かくしていきましょう。todoのテキストを入力する欄、入力した値をsendするボタン、それから作成されたtodoをアペンドする箇所のidを追加していきます。

```todo/main.js
class Todo {
  constructor(textField, sendButton, todoList) {
    this.textField = textField; // text入力欄
    this.sendButton = sendButton; // sendボタン
    this.todoList = todoList; // appendする箇所
  }
}
```
ここで使われているキーワードthisはメソッドが結びつけられているインスタンスを指します。

続いてonloadメソッドによって、windowが読み込まれた段階でTodoクラスのインスタンスを作成し、そのインスタンスに上記text入力欄、sendボタン、append先のidを予め読み込ませます。

```todo/main.js
window.onload = function() {
  todo = new Todo(
    document.getElementById("field"),
    document.getElementById("btn"),
    document.getElementById("todo")
  )
}
```
ボタンがクリックされた時の処理の記述をします。clickイベントによって入力欄に記述されたvalue(ここでいうtodo)を取得し、textという変数に代入します。ここでvar _this = thisとしているのは、最初のthisとaddEventListenerが呼ばれた時のthisに変更が生じてしまうためで、「\_ 」を前に付けることでデータの保護をします。

```todo/main.js
setEventListener() {
  var _this = this
  this.sendButton.addEventListener("click", function(e) {
    var text = _this.textField.value
  });
}
```

さて、ここでtodoを投稿する（HTTPメソッドでAPI側にPOSTメソッドを送信する）、つまりバックエンド間のやり取りのためaxiosを導入します。axiosはHTTP通信を簡単に行うことができるJavascriptライブラリです。

```todo/index.html
<head>
  <title>todoApp</title>
  <link rel="stylesheet" href="style.css">
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
```

todoを投稿するためのメソッドpostTodoを具体的にsetEventListenerの外に記述していきます。まずは入力されたtextをPOSTメソッドでapi側へ送信する処理の記述をします。

```todo/main.js
postTodo(text) {
  var _this = this
  axios.post(this.host + '/todos', {title: text })
    .then(function(res) {
      var todo = res.data
  });
}
```
'/todos'をお忘れなく。title: textとしているのは、api側のカラムがtitleだからです。返ってきたresponseのデータを取得し変数todoに代入しておきます。

hostにはapi側のアドレスが入ります。この場合はローカル環境なので http://localhost:3000 を指定します。その記述をTodoクラスに予め記述しておきます。

```todo/main.js
class Todo {
  constructor(textField, sendButton, todoList) {
    this.textField = textField
    this.sendButton = sendButton
    this.todoList = todoList
    this.host     = "http://localhost:3000" // 追加
  }
```
さて、返ってきたデータを元に、todoのlistを生成しidにappendするためのメソッドを定義します。ulにlistを追加するためにcreateElementメソッドを使用します。titleをinnerTextメソッドでlistに挿入し、後々のメンテナンスのためにもclassを追加しておきましょう。

```todo/main.js
appendTodo(todo) {
  var list = document.createElement("li")
  list.innerText = todo.title
  list.classList.add('item')
  this.todoList.appendChild(list)
}
```
また、次のtodoを入力しやすいように、入力欄の入力された値(value)はPOSTされたと同時に空にしたいので、そのためのメソッドも定義しておきます。

```todo/main.js
removeTextFromInput() {
  this.textField.value = '';
}
```

このappendTodoとremoveTextFromInputを、postTodoで呼び出すことによって返ってきたデータを元にtodoをappendし同時に入力欄を空にします。

```todo/main.js
  postTodo(text) {
    var _this = this
    axios.post(this.host + '/todos', { title: text })
      .then(function(res) {
        var todo = res.data
        _this.appendTodo(todo); // 追加
        _this.removeTextFromInput(); // 追加
      });
  }
```

さて、今度はこのpostTodoをclickしたと同時に呼び出します。

```todo/main.js
  setEventListener() {
    var _this = this
    this.sendButton.addEventListener("click", function(e) {
      var text = _this.textField.value
      _this.postTodo(text); // 追加
    });
  }
```
setEventListenerはデフォルトで呼ばれるようにしましょう。

```todo/main.js
class Todo {
  constructor(textField, sendButton, todoList, deleteButtons) {
    this.textField = textField
    this.sendButton = sendButton
    this.todoList = todoList
    this.host     = "http://localhost:3000"
    this.setEventListener(); // 追加
  }
```

特定のtodoを削除する機能を実装しましょう。まずは作成されたtodoに削除ボタンを付与します。また削除したいtodoを特定するためdata属性でidを付与します。ES6ではテンプレートリテラル(`)が使えるので各タグを+で繋げる必要がなく非常に便利です。またtodoを動的に表示させるために$を使用します。

```todo/main.js
  appendTodo(todo) {
    var list = document.createElement("li")
    list.innerText = todo.title
    list.classList.add('item')
    const button = `<div>${todo.title}</div>
                      <button class="delete" data-id=${todo.id}></button>` // 追加
    list.innerHTML = button // 追加
    this.todoList.appendChild(list)
  }
}
```
Todoクラスに削除ボタンを追加し、インスタンス生成の際にも呼び出します。

```todo/main.js
class Todo {
  constructor(textField, sendButton, todoList, deleteButtons) { // 追加
    this.textField = textField
    this.sendButton = sendButton
    this.todoList = todoList
    this.host     = "http://localhost:3000"
    this.deleteButtons = deleteButtons // 追加
    this.setEventListener();
  }
(中略)
}

window.onload = function() {
  todo = new Todo(
    document.getElementById("field"),
    document.getElementById("btn"),
    document.getElementById("todo"),
    document.getElementsByClassName("delete") // 追加
  )
}
```


削除ボタンをクリックした時のイベントを記述しましょう。targetとdatasetメソッドを使用して、clickイベントが発火したtodoのidを取得して、DELETEメソッドでAPI側へ飛ばします。その後返ってきたtargetの親要素をremoveメソッドで削除します。

```todo/main.js
clickDeleteButton() {
  var _this = this
  for (let i = 0; i < this.deleteButtons.length; i++) {
    var btn = this.deleteButtons[i]
    btn.addEventListener("click", function(e) {
      axios.delete(_this.host + '/todos/' + e.target.dataset.id).then(function() {
        e.target.paretNode.remove(e.target);
      })
    })
  }
}
```
追加されたtodoが削除できるようにpostTodoメソッド内でclickDeleteButtonを呼び出しておきます。

```todo/main.js
  postTodo(text) {
    var _this = this
    axios.post(this.host + '/todos', { title: text })
      .then(function(res) {
        var todo = res.data
        _this.appendTodo(todo);
        _this.removeTextFromInput();
        _this.clickDeleteButton(); // 追加
      });
  }
```
todoリスト一覧を表示させるために、現在保存してあるtodoの一覧をGETメソッドで取得するメソッドの記述をしましょう。取ってきた全てのデータを便利メソッドforEachで出力します。その際、削除機能もつけたいのでclickDeleteButtonメソッドの記述も忘れずに。

```todo/main.js
  fetchTodos() {
    var _this = this
    axios.get(this.host + '/todos')
      .then(function(res) {
        var todos = res.data
        todos.forEach(function(todo) {
          _this.appendTodo(todo)
        });
        _this.clickDeleteButton();
      });
  }
```

デフォルトの画面でtodoが全て表示されるように、fetchTodoメソッドをTodoクラスに記述しておきましょう。

```todo/main.js
class Todo {
  constructor(textField, sendButton, todoList, deleteButtons) {
    this.textField = textField
    this.sendButton = sendButton
    this.todoList = todoList
    this.host     = "http://localhost:3000"
    this.deleteButtons = deleteButtons
    this.fetchTodos(); // 追加
    this.setEventListener();
  }
```

こうして書くと、todoリストと言えども侮れないですね...
時間見つけて追加でFlashメッセージ機能とか、実装してみたいです。

完成コード

```todo/main.js
class Todo {
  constructor(textField, sendButton, todoList, deleteButtons) {
    this.textField = textField
    this.sendButton = sendButton
    this.todoList = todoList
    this.host     = "http://localhost:3000"
    this.deleteButtons = deleteButtons
    this.fetchTodos();
    this.setEventListener();
  }

  setEventListener() {
    var _this = this
    this.sendButton.addEventListener("click", function(e) {
      var text = _this.textField.value
      _this.postTodo(text);
    });
  }

  clickDeleteButton() {
    var _this = this
    for (let i = 0; i < this.deleteButtons.length; i++) {
      var btn = this.deleteButtons[i]
      btn.addEventListener("click", function(e) {
        axios.delete(_this.host + '/todos/' + e.target.dataset.id).then(function() {
          e.target.parentNode.remove(e.target);
        })
      })
    }
  }

  postTodo(text) {
    var _this = this
    axios.post(this.host + '/todos', { title: text })
      .then(function(res) {
        var todo = res.data
        _this.appendTodo(todo);
        _this.removeTextFromInput();
        _this.clickDeleteButton();
      });
  }

  removeTextFromInput() {
    this.textField.value = '';
  }

  fetchTodos() {
    var _this = this
    axios.get(this.host + '/todos')
      .then(function(res) {
        var todos = res.data
        todos.forEach(function(todo) {
          _this.appendTodo(todo)
        });
        _this.clickDeleteButton();
      });
  }

  appendTodo(todo) {
    var list = document.createElement("li")
    list.innerText = todo.title
    list.classList.add('item')
    const button = `<div>${todo.title}</div>
                      <button class="delete" data-id=${todo.id}></button>`
    list.innerHTML = button
    this.todoList.appendChild(list)
  }
}


window.onload = function() {
  todo = new Todo(
    document.getElementById("field"),
    document.getElementById("btn"),
    document.getElementById("todo"),
    document.getElementsByClassName("delete")
  )
}

```
cssも現状適当です。

```todo/style.css
body {
  margin: 0;
  background: #e0e0e0;
  text-align: center;
  font-family: : Verdana, sans-serif;
}

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

h2 {
  padding-right: 50px;
}

#todo {
  width: 200px;
}

.item {
  list-style: none;
  text-align: left;
}

input {
  width: 200px;
  box-sizing: border-box;
  height: 30px;
  padding: 14px;
  font-size: 16px;
  border-radius: 5px;
  margin-bottom: 14px;
  line-height: 1.5;
  outline: none;
}

#btn {
  display: inline-block;
  width: 60px;
  background: #00aaff;
  padding: 5px;
  color: #fff;
  border-radius: 5px;
  text-align: center;
  cursor: pointer;
  outline: none;
}
#btn:hover {
  opacity: 0.8;
}
```

#参考
[初めてのJavaScript](https://www.amazon.co.jp/%E5%88%9D%E3%82%81%E3%81%A6%E3%81%AEJavaScript-%E7%AC%AC3%E7%89%88-%E2%80%95ES2015%E4%BB%A5%E9%99%8D%E3%81%AE%E6%9C%80%E6%96%B0%E3%82%A6%E3%82%A7%E3%83%96%E9%96%8B%E7%99%BA-Ethan-Brown/dp/4873117836)
[RailsでWEB API入門](https://qiita.com/drafts/bcec5cc278ad198c7eca/edit)
[ワンライナーWebサーバを集めてみた](https://qiita.com/sudahiroshi/items/e74d61d939f18779970d)
[Rails 5 の API モードのイニシャライザーで CORS に対応する](https://qiita.com/takuya0301/items/facf2f81b8d3e0929cf6)
[生JSとjQueryの基本操作比較](https://qiita.com/_sim_a/items/ba3a57a81d9780030969)
[ライブラリを使わない素のJavaScriptでDOM操作](https://qiita.com/kouh/items/dfc14d25ccb4e50afe89#%E5%B1%9E%E6%80%A7%E3%81%AE%E6%93%8D%E4%BD%9C)
[CORSまとめ](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34)
[axios](https://github.com/axios/axios)