Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

初めてのJavaScriptを学習後、ES6を理解するために、素のjavascriptとrails APIを使ってtodoアプリを作成してみました。

RailsでAPI作成

RailsでWEB API入門
こちらが良記事でしたので、まんま写経しました。
その後、rack-coreのgemをインストールします。

Gemfile.
gem 'rack-core'

写経した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
RailsでWEB API入門
ワンライナーWebサーバを集めてみた
Rails 5 の API モードのイニシャライザーで CORS に対応する
生JSとjQueryの基本操作比較
ライブラリを使わない素のJavaScriptでDOM操作
CORSまとめ
axios

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away