DjangoのテンプレートにVue.jsを導入してaxiosでAjaxしてみた話（前編）

# はじめに

Vue.jsが便利そうなのでDjangoと組み合わせて使ってみようと試してみた．使ってみたいといっても，大それたことしたいわけではなく，まずはDjangoのテンプレートの中にVue.jsのスクリプトを埋め込んでクライアント側でちょこっとした機能を実現できるようになるあたりまでが目標だ．

架空のTodoアプリを具体例として話を進めていくが，これはあくまで例示のためで，アプリ自体は実用には程遠いので悪しからず．．．

全体のコードはまとめて[GitHub](https://github.com/j54854/ajaxTest)に置いた．

# Django側での雛形の作成

最初に，Todoアプリに登録したいタスクのモデルを設計しておく．具体的には，次のように，タスクの名称（task），納期（due），完了済みかどうか（done）の3つのフィールドをもつデータモデル（Todo）を定義した．

```python
class Todo(models.Model):
    task = models.CharField(max_length=255, unique=True)
    due = models.DateTimeField()
    done = models.BooleanField()

    def __str__(self):
        return self.task
```

後で利用するために，このデータモデルをadminサイトに登録し，adminサイトから架空のタスクをいくつか追加しておこう．

続いて，データベース内のタスクをすべて取得して納期順に並べて表示するページの雛形をつくる．具体的には，次のようなview関数

```python
def show_todo(request):
    todos = models.Todo.objects.all().order_by("due")
    my_context = {
        "todos":todos,
        }
    return render(request, "name_of_your_template", my_context)
```

とテンプレート

```html
<html>
  <head><title>Todo</title></head>
  <body>
    <h1>My Todo List</h1>
    <div>
      <ul>
        {% for item in todos %}
          <li>
            {% if item.done %}
              <span style="text-decoration: line-through; color: red">
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% else %}
              <span>
                {{ item.task }}
                ({{ item.due|date:"n" }}/{{ item.due|date:"j" }})
              </span>
            {% endif %}
          </li>
        {% endfor %}
      </ul>
    </div>
  </body>
</html>
```

を用意した．urlルーティングを適切に設定してページが表示されれば準備OKだ．

# テンプレートへのVue.jsの導入

土台となる雛形ができたので，このテンプレートにVue.jsを導入していこう．簡単に試してみるには，次のように，CDNからロードするのが手っ取り早い．

```html
<script src="https://unpkg.com/vue"></script>
```

今回はお試しなので，Vue.jsの具体的なスクリプトもこのテンプレートの中に直接書き込んでいくことにする．Vueのインスタンスを作成し，タスクの情報をリアクティブにするためにdata内のプロパティに取り込み，簡単な機能をまずひとつ実装してみた．

```html
<html>
  <head><title>Todo</title></head>
  <body>
    <h1>My Todo List</h1>
    <div id="app">
      <ul v-cloak v-for="item in todos">
        <li v-on:click="changeStatus(item)">
          <span style="text-decoration: line-through; color: red" v-if="item.done">
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
          <span v-else>
            [[ item.task ]]
            ([[ item.due.getMonth()+1 ]]/[[ item.due.getDate() ]])
          </span>
        </li>
      </ul>
    </div>

    <script src="https://unpkg.com/vue"></script>
    <script>

var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: []
  },
  mounted: function() {
    {% for item in todos %}
    this.todos.push({
      "task": "{{ item.task }}",
      "due": new Date({{ item.due|date:'U' }} * 1000),
      "done": {{ item.done|lower }},
    })
    {% endfor %}
  },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
    }
  }
})

    </script>
  </body>
</html>
```

DjangoのテンプレートにVue.jsを導入する際には`{{}}`の扱いに注意する必要がある．デフォルトのままだと，双方でこの表記を使うことになるのでぶつかってしまう．これを回避するために個人的に気にいっている方法は，Vue.js側で`{{}}`を，例えば，`[[]]`に変更してしまうことだ．

これは，上の例にもあるように，Vueインスタンスを生成する際に次のように指定すればよい．

```javascript
delimiters: ['[[', ']]']
```

次に，`mounted:`の項目を見てほしい．まだAjax通信を実装していないので，苦肉の策として，Djangoから`context`として渡された`todos`からタスクの情報をforループで取り出し，JavaScriptのオブジェクトに変換してからVue.js側の`todos`に順に追加していっている．

ひとまずこれで動いた．この部分は，後でAjax通信に変更する．なお，考えてみれば当然だが，Vue.jsのスクリプトの中でDjangoのテンプレートタグが使えていることが確認できる．

html側のリスト表示（`<ul></ul>`）が，Djangoの`context`ではなく，Vue.jsの`data`からタスクの情報を引き出す形に変更されていることにも注意する．また，リスト要素（`<li></li>`）にクリックイベントを処理するディレクティブも追加してある．

これによって，リスト要素をクリックすると，`methods:`の項目で定義されている`changeStatus`が呼び出され，対応するタスクの`done`のブール値が裏返る．

# Ajax通信（その1，GET編）

続いて，Ajax通信を実装していこう．Vue.js自体にはAjax通信の機能は用意されていないらしいので，まずどういう策を取るかを決めなければならない．Vue.jsを導入していけはjQueryを使う機会は減っていくだろうから，Ajax通信のためだけにjQueryをロードするのは気が引ける．そういう場合にはaxiosがおすすめらしい（ので．それに従うことにする）．

まず，Vue.jsの場合と同じように，axiosもCDNからロードしておく．

```html
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
```

その上で，テンプレート内のVue.jsのスクリプトを下記のように更新する．

```javascript
var vm = new Vue({
  delimiters: ['[[', ']]'],
  el: '#app',
  data: {
    todos: []
  },
  mounted: function() {
    axios.get('{% url "for_view_function_get_todo()" %}')
      .then(function (response) {
        for(var d in response.data) {
          var item = response.data[d];
          item.due = new Date(item.due);
          vm.todos.push(item);
        }
      })
      .catch(function (error) {
        console.log(error);
      })
      .then(function () {
      });
  },
  methods: {
    changeStatus: function(item) {
      item.done = !item.done;
    }
  }
})
```

`mounted:`の項目の中身がaxiosを利用したAjaxのGET通信に置き換わっている．中身を見ると，JSON形式で取得したタスクのリストを`response.data`からforループで取り出し，順に`todos`に追加していっていることがわかる．また，この際に，納期（`due`）は，JavaScriptのDateオブジェクトに変換している．

テンプレートの側はこれでOKだ．ただし，このAjaxリクエストに対して，Django側がレスポンスを返してくれないと話にならない．続いて，Django側の設定を行おう．

まずこのリクエストを処理するために，下記のようなview関数を用意する，

```python
import json
from django.http import JsonResponse
from . import models

def get_todo(request):
    todos = models.Todo.objects.all().order_by("due").values()
    todolist = list(todos)
    return JsonResponse(todolist, safe=False)
```

そして，上の`axios.get()`で，このview関数が呼ばれるようにurlルーティングを設定する．

実は，ここで詰まってかなりの時間を浪費した．ポイントは，クエリセットの末尾に`.values()`を追加すること，それを`list()`でリストに変換してから返すこと，そして`JsonResponse()`に`safe=False`を指定することである（[このページ](https://simpleisbetterthancomplex.com/tutorial/2016/07/27/how-to-return-json-encoded-response.html)に助けられた，感謝）．

なお，テンプレートの中ではもうDjango側の`todos`は使わなくなったので，ページ全体の方のview関数でそれを作成して`context`に含める処理は不要になる．したがって，こちらのview関数は，例えば次のように簡略化してしまえる．

```python
from django.views import generic

class ShowTodoView(generic.TemplateView):
    template_name = "todo/show_todo.html"
```

# まとめ

前編では，Vue.jsをDjangoのテンプレートに導入し，Django側からAjaxでとってきたJSON形式の情報をVue.js側で取得して処理するところまでできた．話は[後編](https://qiita.com/j54854/items/266ab0a3fc76742e6295)に続く．後編では，逆に，Vue.js側の情報をDjango側に送ってデータベースを更新することを試す．