#はじめに
初めてのJavaScriptを学習後、ES6を理解するために、素のjavascriptとrails APIを使ってtodoアプリを作成してみました。
#RailsでAPI作成
RailsでWEB API入門
こちらが良記事でしたので、まんま写経しました。
その後、rack-coreのgemをインストールします。
gem 'rack-cors'
写経したRails APIアプリを、外部サイトからアクセス出来るように、生成された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がメインなので見た目は適当です。
<!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は生成されていませんが、いつでも生成できるよう準備を整えます。
class Todo {
constructor() {
}
}
具体的にtodoを生成するためにはnewを使います。
todo = new Todo();
console.log(todo instanceof Todo); //true
それではクラスTodoの記述をもう少し細かくしていきましょう。todoのテキストを入力する欄、入力した値をsendするボタン、それから作成されたtodoをアペンドする箇所のidを追加していきます。
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を予め読み込ませます。
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に変更が生じてしまうためで、「_ 」を前に付けることでデータの保護をします。
setEventListener() {
var _this = this
this.sendButton.addEventListener("click", function(e) {
var text = _this.textField.value
});
}
さて、ここでtodoを投稿する(HTTPメソッドでAPI側にPOSTメソッドを送信する)、つまりバックエンド間のやり取りのためaxiosを導入します。axiosはHTTP通信を簡単に行うことができるJavascriptライブラリです。
<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側へ送信する処理の記述をします。
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クラスに予め記述しておきます。
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を追加しておきましょう。
appendTodo(todo) {
var list = document.createElement("li")
list.innerText = todo.title
list.classList.add('item')
this.todoList.appendChild(list)
}
また、次のtodoを入力しやすいように、入力欄の入力された値(value)はPOSTされたと同時に空にしたいので、そのためのメソッドも定義しておきます。
removeTextFromInput() {
this.textField.value = '';
}
このappendTodoとremoveTextFromInputを、postTodoで呼び出すことによって返ってきたデータを元にtodoをappendし同時に入力欄を空にします。
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したと同時に呼び出します。
setEventListener() {
var _this = this
this.sendButton.addEventListener("click", function(e) {
var text = _this.textField.value
_this.postTodo(text); // 追加
});
}
setEventListenerはデフォルトで呼ばれるようにしましょう。
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を動的に表示させるために$を使用します。
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クラスに削除ボタンを追加し、インスタンス生成の際にも呼び出します。
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メソッドで削除します。
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を呼び出しておきます。
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メソッドの記述も忘れずに。
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クラスに記述しておきましょう。
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メッセージ機能とか、実装してみたいです。
完成コード
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も現状適当です。
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