mmyoji's diary

プログラミングとか日々のどうでもいいこととか

今更Vue.js始めた(Todoアプリちょっとだけ)

先日仕事でjsを半年ぶりくらいに触ったんですが、その際にjQueryオンリーで渡されたものをVue.jsで置き換えたりして、めちゃくちゃ楽しかったので週末はずっとVue.js触ってました。

つい最近 日本版ページ もできて、より入門の敷居が下がったとは思うんですが、もっと色んな方に知っていただきたいなーと思ったのでTodoアプリ作成のごく一部をササッと共有しようかなと思って書いていきます。

前提

  • MacOSX 10.10.3
  • npm 2.5.1
  • browserify とか gulp 使える ( Google )
  • Angularほぼ触ったことない(directive初心者)

作成手順

  1. 骨組みとなるHTML用意
  2. formに新しく追加したいtodoを入力 -> リストに追加
  3. 繰り返し表現(iterate)の確認

全部やる場合は、 TodoMVC - vue.js からソースを辿ってやるようにしてみてください。

では早速。

1. 骨組みとなるHTML用意

必要なnpmパッケージのインストール

*1: GulpやGruntを使わずワンライナーでES6をコンパイル - mmyoji's diary にES6のコンパイルについて書いたので参考にしてくださいmm

# 適当に自分がいつも作業しているDirectoryとかに移動してください
$ cd /path/to/workspace

$ mkdir -p ./Vue/Todos

$ cd ./Vue/Todos

# js/build, js/src フォルダを置いて、js/src/main.jsを主に編集、buildしたやつを js/build/bundle.js としてhtmlでは読み込むようにしてます
$ mkdir -p ./js/{build,src}

# browserifyとかbabelとか入れてgulpfileも作ってますがここでは省略 *1
$ npm init

# Vue.js, Todoアプリ用のCSSインストール
$ npm i -S vue todomvc-app-css todomvc-common

$ touch index.html

元となるhtml

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>Todos</title>
    <link href="./node_modules/todomvc-common/base.css" rel="stylesheet"/>
    <link href="./node_modules/todomvc-app-css/index.css" rel="stylesheet"/>
  </head>
  <body>
    <section class="todoapp" id="todoapp">
      <header class="header">
        <h1>Todos</h1>
        <input class="new-todo" type="text"
               placeholder="What needs to be done?" />
      </header>

      <section class="main">
        <input class="toggle-all" type="checkbox" />
        <label for="toggle-all">Mark all as completed</label>
        <ul class="todo-list">
          <li>
            <div class="view">
              <input class="toggle" type="checkbox" />
              <label>TODO1</label>
              <button class="destroy"></button>
            </div>
            <input class="edit" type="text" />
          </li>
        </ul>
      </section>
    </section>
    <script src="./js/build/bundle.js"></script>
  </body>
</html>

footerとかは省略してます。

2. formに新しく追加したいtodoを入力 -> リストに追加

基本的にVue.jsは、適用したい範囲を el に宣言して、 data にpropertyを定義、 methodsメソッドを定義していく、ってことだけわかっておけばある程度は書けます。

ディレクティブも v-on, v-class, v-text, v-model, v-repeat ぐらいわかっておけば割と書けます(コードが簡潔になるかどうかは一旦置いておいて。個人的にはこれぐらいでもわかりやすいコードが書けるとおもってます。)

以下雛形

/* js/src/main.js */

// もし browserify とか使わずにhtml側でvue.jsのパスを読み込んだりしていれば `require` は不要
import Vue from "vue"

let app = new Vue({
  el: "#todoapp", // 適用したい範囲のid

  // プロパティ。この範囲で使いまわしたいデータって理解でおk
  data: {
    todos: [],
    newTodo: ""
  },

  methods: {
    addTodo(event) {
      // add new todo to todos
    }
  }
});

すごいシンプルで且つわかりやすいと思います。

では本題のformに新しいtodoを入力して、リスト( todos ) に追加するところまで。

<!-- body とか余計な部分は省略していきます -->
    <section class="todoapp" id="todoapp">
      <header class="header">
        <h1>Todos</h1>
        <input class="new-todo" type="text" v-model="newTodo"
               v-on="keyup: addTodo | key 'enter'"
               placeholder="What needs to be done?" />
      </header>

いきなり v-modelv-on ってのが出てきましたが、v-model は基本的にform系のタグと一緒に使うと覚えておいてください。例えば input タグの場合、valueとこの newTodo が連動することになります。詳しくは フォームのハンドリング - vue.js を参照してください。

v-onjQueryで言う $(elem).on(eventName, func) ってやつですね。ここでは keyup イベントが起きたら addTodo メソッドを実行する。ただし enter key が押された時、って感じです。

keyupだとkeyを指定することができたりして一見小難しいように思えますが、説明さえ聞くと直感的に書けていてわかりやすいかなと思います。例えば click イベントをlisteningすることとか多いと思いますがその場合はもっとシンプルで v-on="click: onClickHandler" のように書くだけで引数に event を持つ onClickHandler 関数を実行する、という風に書けます。

ざっくりと説明しましたがもっとちゃんと知りたい場合は公式に戻りましょう!

/* js/src/main.js */
import Vue from "vue"

let app = new Vue({
  el: "#todoapp",

  data: {
    todos: [],
    newTodo: "" // 初期値は空文字列
  },

  methods: {
    addTodo(event) {
      event.preventDefault()
      this.todos.push({ title: this.newTodo, completed: false })
      this.newTodo = ""
    }
  }
});

これが最小限の実装になりますが、もうちょっと丁寧にやっておきましょう。

/* js/src/main.js*/

    addTodo(event) {
      event.preventDefault()
      var newMsg = this.newTodo.trim()
      if ( !newMsg ) return

      this.todos.push({ title: newMsg, completed: false })
      this.newTodo = ""
    }

これでinput部分に何かメッセージを入力して、エンターを押すとtodosにオブジェクトが挿入されます。(今の時点では見た目には反映されませんが...)

3. 繰り返し表現(iterate)の確認

ここで繰り返し表現についてみておきます。とは言ってもめちゃ簡単です。

v-repeat を使います。

<ul>
  <li v-repeat="todos">{{ title }}</li>
</ul>

ここの todos は配列 or オブジェクトです。話をシンプルにするため配列の場合のみ説明します。

new Vue({

  data: {
    todos: [
      { title: "todo1", completed: false },
      { title: "todo2", completed: false },
      { title: "todo3", completed: false },
      { title: "todo4", completed: false },
      { title: "todo5", completed: false }
    ]
  }
})

上記のようなデータがあるとすれば li 要素は5回繰り返して、それぞれの title が出力されます。

titleってどのタイトル??ってのがわかりづらくなる場合もあるため、iterateされている各要素の名前を指定することもできます。

<ul>
  <li v-repeat="todo: todos">{{ todo.title }}</li>
</ul>

こちらの方がわかりやすいので僕はこっちを使う方向でいきます。(公式は前者)

ということでもう大分察している方もいらっしゃると思いますがリスト部分は以下のような形になります。

        <ul class="todo-list">
          <li v-repeat="todo: todos"
              v-class="completed: todo.completed">
            <div class="view">
              <input class="toggle" type="checkbox" v-model="todo.completed" />
              <label v-text="todo.title"></label>
              <button class="destroy"></button>

ちゃっかり v-class が出てきてますが、これも割と見たままで v-class="class-name: value"value が真(true) なら class-name が適用される、という感じです。ここでは todo.completed が真なら class="completed" が適用されるという意味です。 , 区切りで複数のクラスを書くこともできます。

v-text は要素のテキスト部分に対象モデルが適用されるという意味なので todo.title"TODO1" なら <label>TODO1</label> となります。

これで要素を追加したらどんどん追加されていくと思います。試してみてください。

終わりに

  • 各todoの操作
    • completed/uncompletedの切り替え
    • 削除
    • 編集

とかもやりたかったんですが以外と長くなりそうなのでここら辺で止めておきますmm

(completed/uncompleted の切り替え は実装できてました...w)

今日早く帰ってこれたら書くかも(フラグがたっちゃいました...)

Angularは他のライブラリとかを組み合わせて使う、ってのが難しい(ちょっとしか試してませんが)のであまり使いたくないんですが、Vue.jsは既存のjsをちょっとずつ置き換えていくことなどもできるし、ドキュメントもシンプルでわかりやすいため、かなりお勧めです。ディレクティブも楽しいですし!

去年はもくもく会とかあったみたいですが、さすがに今はReact!React!ってなっててほとんど誰も使ってないのかなーという印象です。なので一人でもくもくすることにします。

大規模なサービスとかを作ってみたいんですがなかなか試す機会がないので、自社の管理ページとかを一人で作ってみようかなと思います。

js内にhtmlを書きたくない人にはおすすめです!(componentとかはちょっと書くことになりそうですが...)