※スマホ対応はしてません。

computedすごいぞ賢いぞ! (Vue.js始めるおれおれアドベントカレンダー2016 – 6日目)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 6日目

Vueでは data, methods の他に、両者の特性を併せ持った computed というものを指定することができます。これすげーんでお見知りおきください。

data と methods

この二つはまあ普通なんですが、 data がデータ、 methods がメソッドです。そのまんまだ。

イニシャルを表示する例

JS側で getInitial() というメソッドを実装して、HTML側で {{getInitial()}} と書いて呼び出しています。

<div id="app">
  <p>
    <label>
      First Name:
      <input v-model="firstName" type="text" />
    </label>
    <br />
    <label>
      Last Name:
      <input v-model="lastName" type="text" />
    </label>
  </p>
  <h1>Hello {{getInitial()}}!</h1>
</div>
var app = new Vue({
  el: '#app',
  data: {
    firstName: 'ginpei',
    lastName: 'takanashi',
  },
  methods: {
    getInitial: function() {
      var firstFirstNameLetter = this.getInitialOf(this.firstName);
      var firstLastNameLetter = this.getInitialOf(this.lastName);

      var initial = firstFirstNameLetter + '.' + firstLastNameLetter + '.';
      return initial;
    },

    /**
     * @param {string} name
     * @returns {string}
     * @example
     * this.getInitialOf("alice");  // => "A"
     */
    getInitialOf: function(name) {
      return name.charAt(0).toUpperCase();
    },
  },
});

initial
イニシャルを表示する例

まあ普通ですね。

computed

computed を使うと、先ほどの例がこう書けます。(だいたい一緒なので違う部分だけ。)

<h1>Hello {{initial}}!</h1>
computed: {
  initial: function() {
    var firstFirstNameLetter = this.getInitialOf(this.firstName);
    var firstLastNameLetter = this.getInitialOf(this.lastName);

    var initial = firstFirstNameLetter + '.' + firstLastNameLetter + '.';
    return initial;
  },
},

HTML側では data のように名前を書くだけ、JS側では methods のように関数を実装しています。

何かをあれこれ処理しているのではなくて、ここでは(処理した)結果をただ表示するぞ、というのが分かりやすいかと思います。え、そうでもない?

キャッシュ

メソッドとの最大の違いは、結果がキャッシュされ毎回実行されないという点です。

ドキュメントから引用します。( message が data に格納されており、それを順序反転する処理 reversedMessage が computed で定義されている文脈です。)

算出プロパティの代わりに、同じような関数をメソッドとして定義することも可能です。最終的には、2つのアプローチは完全に同じ結果になります。しかしながら、算出プロパティは依存関係にもとづきキャッシュされるという違いがあります。算出プロパティは、それが依存するものが更新されたときにだけ再評価されます。これはつまり、message が変わらない限りは、reversedMessage に何度アクセスしても、関数を再び実行することなく以前計算された結果を即時に返すということです。

参照している値を自動的に監視して、最低限の実行で処理してくれるそうです。すんげー。

注意点

逆に、 data の値を参照していない場合、例えば先のドキュメントでは Date.now() が使われていましたこういう場合には、キャッシュされた値は更新されなくなってしまうようです。

メソッドはあまり使わない?

というわけで画面に出力する計算は computed でやるのが良さそうです。

Date.now() みたいにVue外から情報を得る場合は、取得した結果を data に入れてそれを computed で出力するーみたいな感じでしょうか。コンポーネントなら porps 経由で。

するとメソッドを使う場面は、今回みたいにDRY的に単純に処理をまとめるのと、イベントハンドラを定義するのくらいでしょうか。

ユーザー一覧の各項目で情報を加工する場合はどうだろ。 doSomething(list[i]) みたいなのが必要? いやコンポーネントに分けて、子の方で porps で受けた値を computed でやれば良さそう。

というのを作ってみた:

component-computed

うーんこんな感じなんだろうか。日時情報の扱いはもっと上手にやれそうな気がする?

Vuexをさっくりインストールしたけどまだよくわからない。(Vue.js始めるおれおれアドベントカレンダー2016 – 5日目)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 5日目

いよいよVuexを使ってみ始めることにしました。なんかまだよくわかってないけどこれ使うと良い感じになるんでしょ?

困ってること

TODOリストのフォームから新規作成した項目を一覧へ渡す際に、情報のやり取り、コールバックの呼び合いがなんか面倒だった。これを楽ちんにしたい。

デモ

こちらにございます。

vue-todo

どうやって使うんだろ

と思って見てたら “TodoMVC” といういかにもなのがREADMEのExamplesにあったのでそれを見てみます。

見た結果なんか色々他にも学ぶことがありそうだったのでそっと閉じて、ドキュメントの方を参照することにしました。

始めるを得る

んー自分の場合だと、こんなんだろうか。

var store = new Vuex.Store({
  state: {
    newTask: { finished: false, name: '' },
    tasks: [
      { finished: false, name: 'Buy milk 2L' },
      { finished: false, name: 'Call to Alice' },
      { finished: false, name: 'Return books' },
    ],
  },
  mutations: {
    add: function(state) {
      state.tasks.unshift(state.newTask);
      state.newTask = { finished: false, name: '' };
    },
  },
});
window.app = new Vue({
  data: store.state,
  methods: {
    newTask_submit: function(event) {
      store.commit('add');
    },
  },
  ...
});

やった動いた! これでコールバック類を削除できる。(今はひとつしかないけど。)

mutations?

mutation

主な意味: 変化、変更、転換、(世の)移り変わり、突然変異、突然変異体、母音変異

どうやらミュータント・ニンジャ・タートルズの「ミュータント」と同源らしい。はあ。あと最近のタートルズはごついな。

普通にオブジェクト

を直接参照するのと何が違うんだろう? 編集の方もコンポーネントに渡したオブジェクトを直接編集してもらってるし。

メソッド呼び出しは commit() を経由するようになるけど、でもそれくらい?

やってることが単純で簡単すぎるせいか、まだVuexの魅力に気付けていない……。もっと難しいアプリを作ったらわかるかしらん。

おしまい

なんかとりあえずVuexの導入はすげー簡単でした。いいのか?

TODOリストができたのでチュートリアル形式でお送りします。(Vue.js始めるおれおれアドベントカレンダー2016 – 4日目)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 4日目

CRUDを全て含むTODOリストを作るのは、新しいフレームワークを試すのにとても良いと言います。というわけで作ってみましたのでチュートリアルって感じでご紹介します。

バージョン

  • Vue 2.1.4

デモ

こちらにございます。

デザイン

こんな感じでガーッとHTMLを作りました。

vue-todo

Bootstrap最高!

何もしないVueアプリ

まずはここから始めることにしましょう。

<div id="app">
  <div class="container">
    <h1>Vue TODO</h1>
    <div>
      <form>
        <div class="input-group">
          <input class="form-control" type="text" placeholder="Buy milk 2L">
          <span class="input-group-btn">
            <button class="btn btn-primary">Add new task</button>
          </span>
        </div>
      </form>
    </div>
    <hr />
    <div class="taskListOperator">
      <button class="btn btn-default">Delete finished tasks</button>
    </div>
    <div class="list-group">
      <label class="list-group-item">
        <input type="checkbox">
        Item 1
      </label>
      <label class="list-group-item">
        <input type="checkbox">
        Item 2
      </label>
      <label class="list-group-item">
        <input type="checkbox">
        Item 3
      </label>
    </div>
  </div>
</div>
.taskListOperator {
    text-align: right;
    margin-bottom: 1em;
}
var app = new Vue({
  el: '#app',
  data: {
  },
  computed: {
  },
  methods: {
  },
});

フォームをコンポーネント化する

なんか機能的に独立してそうな感じするのでさくっとコンポーネント化します。

<div>
  <task-form></task-form>
</div>
<script id="template-task-form" type="text/x-template">
  <form>
    <div class="input-group">
      <input class="form-control" type="text" placeholder="Buy milk 2L">
      <span class="input-group-btn">
        <button class="btn btn-primary">Add new task</button>
      </span>
    </div>
  </form>
</script>
var taskForm = {
  template: '#template-task-form',
};
window.app = new Vue({
  el: '#app',
  components: {
    taskForm: taskForm,
  },
  ...
});

一覧をVue化する

一覧の方もコンポーネント化……と思ったけど、繰り返しになるので先にVue化?してアプリが持つ情報の分だけ出力するようにまとめます。

<div class="list-group">
  <label v-for="task in tasks" class="list-group-item">
    <input type="checkbox">
    {{task.name}}
  </label>
</div>
data: {
  tasks: [
    { name: 'Buy milk 2L' },
    { name: 'Call to Alice' },
    { name: 'Return books' },
  ],
},

これでCRUDのR = Readが完成。なのか?

一覧項目をコンポーネント化する

良い感じになったのでコンポーネント化します。

<div class="list-group">
  <task-item v-for="task in tasks" :task="task"></task-item>
</div>
<script id="template-task-item" type="text/x-template">
  <label class="list-group-item">
    <input type="checkbox">
    {{task.name}}
  </label>
</script>
var taskItem = {
  template: '#template-task-item',
  props: [
    'task',
  ],
};
window.app = new Vue({
  el: '#app',
  components: {
    taskItem: taskItem,
  },
  ...
});

利用者の入力を受け付ける

ちょっと流れが複雑。

  1. アプリ本体側で、入力用のオブジェクト { name: '' } を用意する
  2. :task="newTask" で、フォームへ渡す
  3. :on-submit="newTask_submit" で、変更時のコールバックを指定する
  4. フォーム送信されたら(入力値が空でなければ)先のコールバックを実行
  5. コールバックから入力値を利用して何かする(まだ何もしてない)
  6. 新しい入力用のオブジェクト { name: '' } を設定することでフォームを初期化する

フォームはあくまで利用者の入力を受け付けて、親(アプリ本体)へ結果を伝えるだけ。

フォームの初期化なんかはフォームのコンポーネント内部で完結することもできるけど、なんかアプリ本体側でやった方が中央集権ぽくて良いかなと思った。思ったんだけど、ううん、自信ない。実際はVuexだか何だかでもっとうまくやるからあんまり考えない方が良いか。

<div>
  <task-form :task="newTask" :on-submit="newTask_submit"></task-form>
</div>
<form v-on:submit.prevent="form_submit">
  <div class="input-group">
    <input v-model="task.name" class="form-control" type="text" placeholder="Buy milk 2L">
    <span class="input-group-btn">
      <button class="btn btn-primary">Add new task</button>
    </span>
  </div>
</form>
window.app = new Vue({
  data: {
    newTask: { name: '' },
    ...
  },
  methods: {
    newTask_submit: function(event) {
      console.log(this.newTask.name);  // TODO: implement

      this.newTask = { name: '' };
    },
    ...
  },
  ...
});
var taskForm = {
  template: '#template-task-form',
  props: [
    'task',
    'on-submit',
  ],
  methods: {
    form_submit: function(event) {
      if (!this.task.name) {
        return;
      }

      this.onSubmit(event, this.task);
    },
  },
};

一覧へ追加する

これは簡単。

newTask_submit: function(event) {
  this.tasks.unshift(this.newTask);
  this.newTask = { name: '' };
},

配列に追加するだけで画面側の一覧も増える。はーVue便利。

これでCRUDのCができあがり。

ちなみに array.unshift(item) は配列の先頭に項目を追加するもの。 array.push(item) だと最後に追加する。本来なら期限情報なんかも持ってそれで並び替える感じだろうか。

チェックしたものを削除

今まで作業の情報は名前 name しかなかったけど、これに追加して完了したかどうか finished も持つようにしよう。そうしよう。

で、その finished フラグを見て終わったものを削除する。フィルターで取り除いた結果を代入しなおせば良いだけ。

<div class="taskListOperator">
  <button v-on:click="delete_click" class="btn btn-default">Delete finished tasks</button>
</div>
<label class="list-group-item">
  <input v-model="task.finished" type="checkbox">
  {{task.name}}
</label>
window.app = new Vue({
  methods: {
    delete_click: function(event) {
      this.tasks = this.tasks.filter(v=>!v.finished);
    },
    ...
  },
  ...
});

DRUDのD = Deleteもできたー!

あとUはどうしよう。更新。更新ボタン追加するか。

更新ボタンを用意

<label class="list-group-item">
  <span v-on:click.prevent="edit_click" class="pull-right btn btn-link">Edit</span>
  <input v-model="task.finished" type="checkbox">
  {{task.name}}
</label>
var taskItem = {
  methods: {
    edit_click: function(event) {
      // update if not canceled
      var newName = window.prompt('Task Name', this.task.name);
      if (typeof newName === 'string') {
        this.task.name = newName;
      }
    },
  },
  ...
};

ラベル内をクリックするので、意図せずチェックボックスが押されないよう v-on:click に .prevent をお忘れなく。

入力はなんかもう面倒なので prompt() を使ってしまった。いやーでも楽ちんだ。

そしてCRUDのU = Updateもおしまい。

一覧が空ならメッセージ

あ、忘れてた。全部終わったら空っぽになるので、わかるように何かメッセージを出せると親切。

<div class="list-group">
  <task-item v-for="task in tasks" :task="task"></task-item>
  <p v-show="tasks.length < 1" class="text-muted">No tasks. Yay!</p>
</div>

テンプレートの編集だけで済んじゃった。すげー。

できた

できました。

コンポーネントを橋渡しして新規項目を作成するところがややこしく感じた。んー噂のVuex導入したらサクッとできるのかな。やってみよう。

フォームを繰り返して出力するときに属性値を変えようとしてちょっと詰まった。(Vue.js始めるおれおれアドベントカレンダー2016 – 3日目)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 3日目

なんだかこんな感じのことを言われてしまいまして。

[Vue warn]: id="item-name-{{item.id}}": Interpolation inside attributes has been removed. Use v-bind or the colon shorthand instead. For example, instead of <div id="{{ val }}">, use <div :id="val">.

読んで字のごとくなんですが、属性で title="{{foo}}" みたいな書き方は(昔は大丈夫だったけど)今は駄目で、代わりに :title="foo" みたいにしてね、とのことでした。(ちなみに昔の文法を知ってたんじゃなくてAngularJSと混同して間違えました。)

警告が出る版

<label for="item-name-{{item.id}}">Name</label>
<input id="item-name-{{item.id}}" v-model="item.name" class="form-control" type="text" />

警告は出るけど動く。つまり本当に form の値が文字通りの item-name-{{item.id}} になる。

警告が出ない版(修正版)

<label :for="`item-name-${item.id}`">Name</label>
<input :id="`item-name-${item.id}`" v-model="item.name" class="form-control" type="text" />

これで想定通りの動作。ラベルクリックで入力欄にフォーカス。

あ、でもテンプレート文字列を使ってるので、IE以外の環境でご覧ください。あー今回はただの例だからってんで簡単に書いちゃったけど、実務ではどうするんだろな。JSはBabeれば良いんだろうけど、HTMLだしなあ。IE 11を無視するわけにもいかんだろうし。

警告が出ない昔の版(おまけ)

コードは警告出る版と同じだけど、読み込むVueのバージョンだけv1.0.28にしたもの。

(直近この件については <input> を <label> の中に持てれば for とか指定する必要もなくなってそもそも問題が起きないんだけど、 Bootstrap なんか使うとほら、分けて書けって感じだったから。)

その他

なんか言われたり言われなかったりもしたんだけど、縮小版 vue.min.js の方だと無言で、開発者版 vue.js の方で言われました。おおなるほど。

item-name-${item.id} みたいなのって

どう扱ったら良いんだろう? そのまま書く今回ので動いてるけど、関数化して例えば :for="getControlId('name')" みたいにした方が良いんだろうか。

methods: {
  getControlId: function(name) {
    return `item-${name}-${this.id}`;
  },
},

みたいな。

ガイドを読んで基本文法まとめ。(Vue.js始めるおれおれアドベントカレンダー2016 – 2日目)

カテゴリー: JavaScript

Vue.js始めるおれおれアドベントカレンダー2016 – 2日目

すぐ触れるやつ

試したい場合はこちらでどうぞ。

コンストラクター

new Vue({ el: selector }) で生成。

<div id="app">
  <h1>My First App</h1>
</div>
var vm = new Vue({
  el: '#app',
});

el: $('body').children().first().get(0) みたいに、セレクターの代わりにDOMオブジェクトでもいける。(jQueryオブジェクトではない。)

data でデータ

<div id="app">
  <h1>{{message1 + message2}}</h1>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    message1: 'Hello ',
    message2: 'World!',
  },
});

computed で動的情報

「算出プロパティ」(Computed Properties) と呼ぶみたい。

<div id="app">
  <h1>Welcome {{fullName}}!</h1>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    firstName: 'Ginpei',
    lastName: 'Takanashi',
  },
  computed: {
    fullName: function() {
      return this.firstName + ' ' + this.lastName;
    },
  },
});

めっちゃ便利っぽい。

しかも計算結果はキャッシュされ、使用情報(今回なら firstName と lastName )が更新された場合だけ再実行されるとか。すごい。

methods でメソッド

単数形 method ではない。(百万回くらい間違えた。)

<div id="app">
  <button v-on:click="hello_click">Hello</button>
</div>
var vm = new Vue({
  el: '#app',
  methods: {
    hello_click: function(event) {
      alert('Hello!');
    },
  },
});

あー言ってるそばから method と書いてしまった……。(修正した。)

computed があるので、単純な画面出力に使う場面は少なさそう?

components でコンポーネント

v-bind で情報を与えて、 props で名前を指定して受け取る。

<div id="app">
  <app-title v-bind:title="'Sugoi Yo!'"></app-title>
</div>
// <app-title>
var appTitle = {
  template: '<h1>{{title}}</h1>',
  props: [ 'title' ],
};

// Main
var vm = new Vue({
  el: '#app',
  components: {
    appTitle: appTitle,
  },
});

情報は v-bind:foo で与え、 props で受け取る。一度受け取った情報はコンポーネント側に所属?するようになり、変更は親へ通知されないみたい。今回やってないけどイベントハンドラーも v-bind で渡す。

奥が深そうなので別にします。また今度詳しく見る。

ちなみにES2015 (ES6) で追加された文法だと、 { appTitle: appTitle } の代わりに短く { appTitle } と書ける。

watach で値を監視

あんまり使わなさそ。いやでも外部ライブラリと連携したりするときにもりもり使うか。

<div id="app">
  <label>
    Search:
    <input v-model="keyword" type="search" />
  </label>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    keyword: '',
  },
  watch: {
    keyword: function(keyword) {
      var url = '/search?keyword=' + this.keyword;
      axios.get(url)
        .then(function(response) {
          // ...
        })
    },
  },
});

とはいえ変更の瞬間に通信開始してもアレだな。 throttling が必要?

テンプレートで出力

文字列を表示する

コンストラクターの data に除法を格納、テンプレートの {{key}} で出力。HTMLエスケープされる。

<div id="app">
  <p>{{message}}</p>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    message: '<b>BOLD</b>',
  },
});

data 以外の方法でも同様。JavaScriptコードを書けるので、 {{message.trim()}} とかしても良い。でもたぶん computed に入れちゃった方が良いよね。

HTMLを出力する

v-html で指定すると、HTMLをそのまま出力する。

<div id="app">
  <p v-html="message"></p>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    message: '<b>BOLD</b>',
  },
});

Vue使ってるとあんまりそういうことなさそう。外部サービスのやつを埋め込むときとか?

テンプレートの制御

v-show で表示切替

<div id="app">
  <label><input v-model="visible" type="checkbox" /> Visible</label>
  <p v-show="visible">Hello!</p>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    visible: false,
  },
});

v-hide はないみたい。

v-if で表示切替

あと v-else-if (v2.1+)と v-else も。

<div id="app">
  <select v-model="language">
    <option value="en">English</option>
    <option value="ja">Japanese</option>
    <option value="ja_KS">Kansaian</option>
  </select>
  <div v-if="language==='en'">
    <h1>Hello!</h1>
  </div>
  <div v-else-if="language==='ja'">
    <h1>こんにちは!</h1>
  </div>
  <div v-else>
    <h1>☺</h1>
  </div>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    language: 'en',
  },
});

v-else-if はあんまり使わない方が良いんじゃいかなあ。

v-for で繰り返し出力

<div id="app">
  <ul>
    <li v-for="name in names">{{name}}</li>
    <li v-for="i in 3">{{i}}</li>
  </ul>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    names: [
      'Alice',
      'Bob',
      'Charlie',
    ],
  },
});

v-for="value in object" とか v-for="(value, key) in object" とか も使えるらしい。デバッグ時に便利かも。

あと v-for="i in 999" と数字を指定して繰り返せるのも、デザイン考えるときにダミー出力するのに使えそう。

インタラクション

v-on でイベント監視

<div id="app">
  <form v-on:submit.prevent="form_submit">
    <input type="search" />
    <button>Search</button>
  </form>
</div>
var vm = new Vue({
  el: '#app',
  methods: {
    form_submit: function(event) {
      // preventDefault不要!
    },
  },
});

.prevent を付けると自動で event.preventDefault() を呼び出しておいてくれる。おお、これべんり。

v-model でフォーム入力値

<div id="app">
  <p>{{message}}, {{available}}, {{number}}</p>
  <p><input v-model="message" type="text" /></p>
  <p><input v-model="available" type="checkbox" /></p>
  <p><input v-model.number="number" type="number" /></p>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    available: true,
    message: 'abc',
    number: 100,
  },
});

いわゆる双方向バインディングってやつかしら。フォーム側で変更すると即座に反映される。

.number を付けると数値に変換して操作してくれる。これ便利! これでもう '1' + '2' が '12' になってしまうようなアレに悩まされることもなくなるのでは。

あとフォームの種類は豊富だけど、チェックボックスなら真偽値に、みたいな感じでなんかうまいことやってくれるっぽい。

そんな感じ。

そんなにたくさん覚えなくちゃいけない感じではないけど、 v-on.submit.prevent みたいな便利なやつがちょくちょく 追加されて良い感じの感じでした。

参考