ワイのメモ帳なんやで...

主にWebプログラミングのお話をします。

Vue.js エクセルのセルみたいなラベル

エクセルのセルみたいなラベル

ちょっと何言ってるかわかんないっす。
ってなると思いますが、アレですよアレアレ。Excel2013?くらいから導入された、セルの変更時に変更したセルの値を参照しているセルが同時に変更されて、古い値が下に消えて新しい値が上から降ってくるあのアニメーション。
実案件でExcelみたいな表を実装することになりまして、

「あーあんな感じのアニメーションできたらいいなぁ」

と思って調べたけどなかったのでComponentを作ってみました。
結果だけ知りたいって人は下にSandboxの実行結果貼ってるから飛ばしちゃってください。

全体のソースこんな感じ

<template>
  <div class="parent">
    <label v-if="change" :class="updateing && 'rotation'">
      {{ oldValue }}
    </label>
    <label v-else :class="updateing && 'rotation'"> {{ value }} </label>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data: () => ({
    init: false,
    change: false,
    updateing: false,
    oldValue: null
  }),
  watch: {
    value: {
      handler(newValue) {
        // immediate fire
        if (!this.init) {
          this.init = true;
          this.oldValue = newValue;
          return;
        }
        this.updateing = true;
        this.change = true;
        setTimeout(() => {
          this.change = false;
          this.oldValue = newValue;
        }, 250);
setTimeout(() =>
{
this.updateing = false;
}, 500); }, immediate: true } } }; </script> <style scoped lang="scss"> div.parent { position: relative; height: inherit; width: inherit; label { position: absolute; top: 0; left: 0; width: 100%; height: 100%; &.rotation { animation-name: rotation; animation-duration: 0.5s; } } } @keyframes rotation { 0% { opacity: 1; } 33% { opacity: 0; transform: translateY(0.5rem); } 66% { opacity: 0; transform: translateY(-0.5rem); } 100% { opacity: 1; } } </style>

アニメーション初心者なこともあって、これ作るのに1日くらい費やしてしまった。
もうちょっとシンプルにできないかと思っていたのだが、なかなかに複雑に。。。
機能説明を付け加えておく。

まずテンプレート部分

  <div class="parent">
    <label v-if="change" :class="updateing && 'rotation'">
      {{ oldValue }}
    </label>
    <label v-else :class="updateing && 'rotation'"> {{ value }} </label>
  </div>

なんで if-else してんのって思われるかもしれませんが、僕の知識不足かもしれません。
アニメーションを実行する際に、下に落ちるアニメーションの時にすでに親側で変えられた値が表示されちゃうんですよね。
下に落ちるときは古い値、その後降ってくるときは新しい値になってほしかったの。

苦肉の策なの。

次にScript部分

export default {
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data: () => ({
    init: false,
    change: false,
    updateing: false,
    oldValue: null
  }),
  watch: {
    value: {
      handler(newValue) {
        // immediate fire
        if (!this.init) {
          this.init = true;
          this.oldValue = newValue;
          return;
        }
        this.updateing = true;
        this.change = true;
        setTimeout(() => {
          this.change = false;
          this.oldValue = newValue;
        }, 250);
setTimeout(() => {
this.updateing = false;
}, 500); }, immediate: true } } };

主にWatchの所ですね。

mounted時にはアニメーションが走らないようにするためinit変数で管理
② アニメーションのclassを付与するためにupdateingフラグを立てて
③ 変更中はchangeフラグを立てて古い値を表示
アニメーションの後半になったら変更値を表示

Q. アニメーション表示のためにここまでいろいろ必要なんですか?
A. 僕の脳ミソだと必要です。

最後にcss

div.parent {
  position: relative;
  height: inherit;
  width: inherit;
  label {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    &.rotation {
      animation-name: rotation;
      animation-duration: 0.5s;
    }
  }
}
@keyframes rotation {
  0% {
    opacity: 1;
  }
  33% {
    opacity: 0;
    transform: translateY(0.5rem);
  }
  66% {
    opacity: 0;
    transform: translateY(-0.5rem);
  }
  100% {
    opacity: 1;
  }
}

すまん、scssだったは。

キモはrotationのkeyframesのとこっすね。
アニメーション0/3~1/3の間消しながら下に
アニメーション1/3~2/3の消えてる間に要素を上に持ってくる
アニメーション2/3~3/3で上から降らす

といった感じになってます。
ちなみにこのComponent、Componentを呼んでる親要素にheightがpxやremで設定がされてないと高さが取得できなくてうまくアニメーションしてくれないので注意な!

ベストなソリューションが知りたいです。
こっちのほうがいいよ!ってのがあれば教えていただけると幸いです。

ということでこのブログの恒例にしたい Sandboxの結果を貼っていくスタイルで締めたいと思います。