Onsen UI + Vueでニフクラmobile backendを使ってみました

Monacaで作成したOnsen UI + Vueプロジェクトからニフクラmobile backendのデータストア機能を使ってみました。

NCMBライブラリのインストールのしかたや注意点については、以下の記事で解説されています。

MonacaのVueプロジェクトでNCMBを利用する際の注意点

この記事に従い以下の2点を実施します。

ちなみに、Vue向けのNCMBは v2.2.1 をベースにしているようですが、本家のNCMBは現時点で v3.0.2 になっています。Vue向けのNCMBの利用時は変更履歴を一読しておくといいと思います。

次に、ニフクラのドメイン変更に対応する必要があります。lib/ncmb.jsの2か所を書き換えます。

変更前

this.fqdn             = config.fqdn || "mb.api.cloud.nifty.com";
this.scriptFqdn       = config.scriptFqdn || "script.mb.api.cloud.nifty.com";

変更後

this.fqdn             = config.fqdn || "mbaas.api.nifcloud.com";
this.scriptFqdn       = config.scriptFqdn || "script.mbaas.api.nifcloud.com";

以上で、ニフクラmobile backendが利用可能になります。最後にデータストア機能を利用する場合の一例を示します。

const NCMB = require('ncmb')
const store = {
  param: {
    ncmb: null,
  },
  initBackend () {
    const applicationKey = "0123456789 ..."
    const clientKey = "0123456789 ..."
    this.param.ncmb = new NCMB(applicationKey, clientKey)
  },
  saveBackend () {
    if (!this.param.ncmb) {
      console.log("ニフクラ初期化エラー")
      return
    }
    var MdtRecords = this.param.ncmb.DataStore("MdtRecords")
    var mdtrecords = new MdtRecords()
    var dateObj = {
      y: this.param.recordData.date.y,
      m: this.param.recordData.date.m,
      d: this.param.recordData.date.d,
      w: this.param.recordData.date.w,
      t: this.param.recordData.total
    }
    mdtrecords.set("dateObj", dateObj)
      .save()
      .then(mdtrecords => {
        this.param.backendLoad = true
      })
      .catch(function(err) {
        console.log("保存に失敗しました。エラー: " + err)
      })
  },
  loadBackend (callback) {
    if (!this.param.ncmb) {
      console.log("ニフクラ初期化エラー")
      callback(null)
      return
    }
    var MdtRecords = this.param.ncmb.DataStore("MdtRecords")
    MdtRecords.order("createDate", true)
      .limit(10)
      .fetchAll()
      .then(results => {
        this.param.backendLoad = false
        this.param.backendData = results
        callback(results)
      })
      .catch(function(error) {
        callback(null)
      })
  },
}

[Onsen UI + Vue] v-ons-card トランジション

Onsen UIのカードコンポーネントv-ons-cardにVueのトランジションを適用したサンプルを作成しました。

See the Pen Onsen UI Vue by senmyou (@senmyou) on CodePen.

リスト移動トランジションについて

本サンプルのトランジションはVue公式マニュアルのリスト移動トランジションを参考にしました。要素を削除すると、対象要素の透過トランジションと周辺要素の移動トランジションが同時に発生します。

じつは私はこのマニュアルの解説をなんど読んでも理解できませんでした。この直前で説明していたv-moveクラスはなぜか使われていないですし、いきなりv-enter-active/v-leave-activeクラスからtransitionプロパティが消え、かわりに謎のposition: absoluteが追加されていたりします。

なにか違うコードを載せちゃったのかなと思ったりもしましたが、なぜかうまく動きます。解せません。そこでDOM 変更ブレークポイントを使って調べてみました。

DOM変更ブレークポイント

attributes modificationsを選択すると属性の変更を追跡できます。

FLIP

transition-groupコンポーネントのトランジション処理にはFLIPが使われています。

内部で Vue は transforms を使って、前の位置から新しい位置へ要素を滑らかにトランジションさせるために FLIP と呼ばれる単純なアニメーションテクニックを使っています。

https://jp.vuejs.org/v2/guide/transitions.html#リスト移動トランジション

FLIPはFirst、Last、Invert、Playの頭文字で、アニメーションの実行前にJavaScriptで事前計算(First、Last、Invert)を行い、CSSにアニメーション処理をさせる(Play)技術です。

  • First
    • 要素の初期状態を取得
  • Last
    • 要素をアニメーション実行後の状態に設定
    • 要素の状態を取得
  • Invert
    • FirstとLastの変化量(差分)を算出
    • 要素に変化量を適用(Firstの状態に戻る)
  • Play
    • 要素にアニメーション用のクラスを追加
    • Invertで適用した変化量を削除(FirstからLastへのアニメーションが起動)

事前計算のコストは発生しますが、その分アニメーションの実行中にかかるコストがなくなるため、スムーズなアニメーションを実現できます。

デバッグ

本サンプルの3番目(真ん中)のカードを削除するケースを調べます。3番目と5番目のカードにDOM変更ブレークポイントを付け、3番目のカードの削除ボタンを押します。

3番目のカードが消えると4番目と5番目のカードがひとつ上に移動します。このとき3番目のカードにはv-leave、v-leave-to、v-leave-activeクラス、4番目と5番目のカードにはv-moveクラスが追加・削除されました。以下にブレークポイントにより停止したポイントの内容を示します。

丸数字:カード番号

  1. ③ にv-leaveを追加
  2. ③ にv-leave-activeを追加(追加した瞬間 ④ と ⑤ がひとつ上に移動。③ は ④ に隠れる)
  3. ⑤ のtransformプロパティに”translate(0px, 115.2px)”、transitionDurationプロパティに”0s”を代入
  4. ⑤ にv-moveを追加
  5. ⑤ のtransformプロパティに空文字を代入(代入した瞬間 ⑤ がひとつ上に移動)
  6. ⑤ からv-moveを削除
  7. ③ にv-leave-toを追加
  8. ③ からv-leaveを削除
  9. ③ からv-leave-toを削除
  10. ③ からv-leave-activeを削除

実際は3番目のカードと4番目&5番目のカードで別々のトランジションが同時に発生しているので、ブレークで止めることでお互いのタイミングがずれてみえることがあるかもしれませんが、それぞれのトランジションの流れは把握できそうです。

この内容をFLIPにあてはめると、No.2はアニメーションの最終的な状態になっているのでLast、No.3は5番目のカードを下に移動して元の位置に戻しているのでInvert、No.4とNo.5はアニメーション用のクラスを追加しtransformプロパティの値を空文字で消してアニメーションを発生させているのでPlayの処理に関連していると言えそうです。

次に、v-leave-activeクラスのposition: absoluteをマスクして、同じように3番目のカードを削除してみます。

  1. ③ にv-leaveを追加
  2. ③ にv-leave-activeを追加
  3. ③ にv-leave-toを追加
  4. ③ からv-leaveを削除
  5. ③ からv-leave-toを削除
  6. ③ からv-leave-activeを削除

4番目と5番目のカードはアニメーションせずに移動しました。FLIPが適用されなかったようです。v-moveクラスの追加も行われていません。

考察

以下はデバッグの結果から私が個人的に考察した内容になります。間違っている可能性がありますのでご注意ください。

要素を追加する場合はアニメーション開始前に実際に要素が追加されます。その後opacityを0から1に変化させていきます。それに対し、要素を削除する場合はアニメーション開始前に要素を削除しません。opacityを1から0に変化させていき最後に削除します。削除のケースでは、FLIPの事前計算のときに要素が実際には削除されていないため、このままでは変化量が0となりFLIPが適用されません。そこで削除する要素のpositionプロパティをabsoluteに設定してレイアウトから外すことで、実際のアニメーション実行後の状態を取得し、算出した変化量をFLIPでアニメーションさせることができるようになります。

ちなみに、v-leave-activeクラスのかわりにv-leaveクラスにposition: absoluteを記述して動作確認してみました。結果、FLIPが適用され、変化量(移動量)も正しいように見えました。ですが、v-leaveクラスはトランジション開始前に削除される仕様なので、そのタイミングでレイアウトから外していた要素が元に戻り、カードひとつ分下の位置からアニメーションが開始されているようにみえました。その点、v-leave-activeクラスはトランジション終了時に削除される仕様なので問題ないといえます。

リスト移動トランジションについての当初の疑問に回答を付けてみます。

  • 追加・削除対象要素に追加されるv-enter-active/v-leave-activeクラスと、周辺要素に追加されるv-moveクラスのtransitionプロパティの値が同じということで、共通で使用するクラスにtransitionプロパティを記述した。よって、v-enter-active/v-leave-activeクラスとv-moveクラスにtransitionプロパティを記述する必要がなくなった。(それぞれのクラスにtransitionプロパティを記述しても問題なし)
  • v-leave-activeクラスでposition: absoluteとしているのは周辺要素にFLIPを適用させるため。 FLIPの事前計算を行うときに削除する要素をabsoluteによりレイアウトから外すことでアニメーション実行後の状態を取得できるようになる。

サンプルのソースコード

本サンプルのソースコードについて2点説明します。

1.createdフックでkey属性に設定する文字列を作成しています。要素の追加や削除を行う場合、key属性に配列のインデックスを設定するとトランジションが正しくかからなくなります。配列の追加や削除を行うとインデックスが振り直され、要素とkey値の組み合わせが変更されるためです。配列内の値や文字列が使えればいいのですが、本サンプルでは一意の値がない(同じ値が使われる可能性がある)場合に対応するためcounterを使用して一意のkey値を作成しています。

created () {
  this.$ons.platform.select('android')
  this.items.forEach(item => {
    item.key = "res_" + this.counter++
  })
}

2.FLIPを適用させるためpositionプロパティをabsoluteに設定しました。このため対象となる要素やコンポーネントによっては副作用が発生することが考えられます。カードコンポーネントはmargin: 8pxに設定されているのですが、absoluteを設定すると8px下に移動します。おそらく相殺されていた上下のmarginが相殺されなくなったためと考え、最初から相殺が起きないようにmargin-topを0に設定しました。

.mycard {
  position: relative;
  transition: all 500ms ease-in;
  margin-top: 0;
}

また、absoluteにするとカードコンポーネントの幅がコンテンツの幅に縮小されるため、leftとrightプロパティを0にして対策しました。

.cards-leave-active {
  position: absolute;
  left: 0;
  right: 0;
}

[Onsen UI + Vue] ボタンコンポーネント

Onsen UIのボタンコンポーネントv-ons-buttonをラップしたカスタムボタンコンポーネントを作ってみました。

See the Pen Onsen UI Vue by senmyou (@senmyou) on CodePen.

ボタンのスタイリングはOnsen UIにおまかせしています。Onsen UIはモバイル端末に合わせて自動的にAndroid/iOSのスタイルを適用します。上記CodePenのサンプルではパソコンでもAndroid用のスタイルに見えるようにスタイルを固定しています。そのため、マテリアルデザインとリップル(さざ波)エフェクト付きのボタンになっています。

汎用的なボタンコンポーネントを目指して作り始めたのですが、最終的にアイコンとテキストを組み合わせたトグルボタン用コンポーネントになってしまったという気もします。以下にソースを記載します。

  • CodePenではHTMLファイル/JSファイル/CSSファイルに記述していますが、以下のソースはVueファイル(単一ファイルコンポーネント)になります。
  • アイコンはFont Awesome専用になっています。ちなみにFont Awesomeの読み込みはOnsen UIのCSSファイルで行われます。
CustomButton.vue
<template>
  <v-ons-button
    :class="buttonClass"
    :disabled="disabled"
    @click="clickHandler"
  >
    <i
      v-if="icon"
      class="fa"
      :class="iconClass"
    >
    </i>
    <slot>{{ buttonText }}</slot>
  </v-ons-button>
</template>

<script>
export default {
  data () {
    return {
      index: 0,
    }
  },

  props: {
    text: {
      type: [String, Array],
      default: ''
    },
    icon: {
      type: [String, Array],
      default: ''
    },
    disabled: {
      type: Boolean,
      default: false
    },
    buttonClass: {
      type: String,
      default: ''
    }
  },

  computed: {
    toggle () {
      console.log('toggle')
      var num = 0
      var icon = (this.icon instanceof Array) ? this.icon.length : 0
      var text = (this.text instanceof Array) ? this.text.length : 0
      if (icon) {
        if (text) {
          num = icon < text ? icon : text
        } else {
          num = icon
        }
      } else if (text) {
        num = text
      }
      console.log(icon, text, num)
      return { icon, text, num }
    },
    iconClass () {    // memo: iconが空の配列の場合、暗黙的にjoin()により""になる
      console.log('iconClass')
      return this.toggle.icon ? 'fa-' + this.icon[this.index] : 'fa-' + this.icon
    },
    buttonText () {
      console.log('buttonText')
      return this.toggle.text ? this.text[this.index] : this.text
    }
  },

  methods: {
    clickHandler (event) {
      var index = this.index;
      if (this.toggle.num) {
        this.index = (index >= this.toggle.num - 1) ? 0 : index + 1
      }
      var arg = {
        event,
        oldIdx: index,
        newIdx: this.index
      }
      this.$emit('click', arg)
    }
  }
}
</script>

<style scoped>
/*
・親コンポーネントのクラスで上書きされる
・scopedを付けないとOnsen UIのbuttonクラスで上書きされる
  (onsen-css-components.css .button)
*/
.greenButton,
.greenButton:active {
  background-color: green;
}
.redButton,
.redButton:active {
  background-color: red;
}
</style>

このコンポーネントの使い方について説明します。

ボタンのテキストはコンポーネントの開始タグと終了タグの間に記述する方法と、textプロパティに記述する方法の2通りに対応しています。アイコンはiconプロパティに記述します。テキストとアイコンは一緒に使用することができます。それぞれの位置関係は固定です。コンポーネントにプロパティを追加して上下に配置できるようにするのもいいかもしれません。

<custom-button>slot</custom-button>
<custom-button text="props"></custom-button>

<custom-button icon="cog"></custom-button>

<custom-button icon="cog">slot</custom-button>
<custom-button icon="cog" text="text"></custom-button>

disabled属性でボタンの有効/無効を設定できます。

<custom-button disabled>hello</custom-button>
<custom-button :disabled="false">hello</custom-button>
<custom-button :disabled="true">hello</custom-button>

ボタンのクリックにより “click” というイベント名でイベントを発火します。また、button-classプロパティでボタンへ適用するクラスを渡すことができます。

<custom-button
  icon="smile-o"
  @click="clickHandler"
  button-class="bigButton"
>
</custom-button>

textプロパティおよびiconプロパティに配列を渡すことでトグルボタンやグルグルボタンになります。

<custom-button
  :text="['グー', 'チョキ', 'パー']"
  @click="clickHandler"
  button-class="sample01"
>
</custom-button>

<custom-button
  :icon="['hand-rock-o', 'hand-peace-o', 'hand-paper-o']"
  @click="clickHandler"
  button-class="sample02"
>
</custom-button>

<custom-button
  :text="['グー', 'チョキ', 'パー']"
  icon="smile-o"
  @click="clickHandler"
  button-class="sample03 align-left"
>
</custom-button>

<custom-button
  text="じゃんけん"
  :icon="['hand-rock-o', 'hand-peace-o', 'hand-paper-o']"
  @click="clickHandler"
  button-class="sample04 align-right"
>
</custom-button>

<custom-button
  :text="['グー', 'チョキ', 'パー']"
  :icon="['hand-rock-o', 'hand-peace-o', 'hand-paper-o']"
  @click="clickHandler"
  button-class="sample03 align-left"
>
</custom-button>