cssのアニメーションで検索ボックス上部にレコメンド事例紹介機能を搭載してみた。[2019/07/13追記※3回]


やったこと

タイトル通りなのですが、画像を見た方がわかりやすいので下記のbefore/afterをご確認下さい。

before

after

やりたいこと

先日作成した、レファレンス協同データベースのランダム検索webアプリ「reftika」のバージョンアップ。
アプリの詳細はこちらの記事「<初心者>レファレンス協同データベースの記事をランダムに表示するWEBサービスをvue.jsで作りました。」を参照ください。

なぜ・なにをやるのか

現状の問題点

◆キーワード検索はできるものの、ランダムに表示されるためにユニークで面白い事例が見つかりにくい。
◆レファレンスサービス利用経験者が少ないため、どんな事例があるかわかりにくい。
(大学図書館の事例ですが、利用者が少ないことのソースは「日本の大学図書館における質問相談サービスの提供方法の現状と変化」調査結果報告書 私立大学図書館協会東地区部会研究部レファレンス研究分科会)

今回採用する改善方法

◆複数のレコメンドの用意。今回は10件。
◆cssのアニメーションで検索ボックス上部に切り替え表示を行うようにすることで、画面の文字量が増えすぎないようにする。

実装方法

大まかな流れは以下。
1.表示内容をhtmlで用意
2.cssでアニメーションを動かす
3.v-on:clickでmethodを呼び出して、クリックされた内容のキーワードを検索ボックスに入力する→検索の実行

tenplate部分

  <p class="exampletexts">
    <span v-on:click="setInputBox('モチモチ')">モチモチの木のアクセントは?</span>
    <span v-on:click="setInputBox('江戸時代 米 値段')">江戸時代の米の値段は?</span>
    <span v-on:click="setInputBox('おいしい カニ')">おいしいカニの見分け方</span>
    <span v-on:click="setInputBox('最後 仇討ち')">最後に仇討ちをした人は誰?</span>
    <span v-on:click="setInputBox('大仏 ぶつぶつ')">奈良の大仏さまの頭のぶつぶつはいくつ?</span>
    <span v-on:click="setInputBox('手話 誰が')">手話は誰が作ったの?</span>
    <span v-on:click="setInputBox('蛍 すぐ死ぬ')">蛍はなぜすぐ死んでしまうのか</span>
    <span v-on:click="setInputBox('トナカイ 名前')">サンタクロースのトナカイの名前は?</span>
    <span v-on:click="setInputBox('歳 おすすめ 絵本')">○歳の子どもにお勧めの絵本は?</span>
    <span v-on:click="setInputBox('市場動向')">○○の市場動向を調べたい!</span>
  </p>

表示切替のアニメーションを行うCSS部分。
line-heightを固定の45pxにして、keyframsによって45pxずつ上方向に移動させる。上下方向にはみ出した内容はhiddenにしている。

.exampletexts{
  line-height: 45px;
  display: inline-block;
  vertical-align: top;
  height: 45px;
  border: 0.1px solid #fff;
  text-align: center;
  overflow-y: hidden;
  cursor: pointer;
  font-size: 1rem;
  width: 100%;
}

.exampletexts span{
  position: relative;
  display: inline-block;
  width: 100%;
  height: 100%;
  animation: ShiftText 15s linear infinite;
}
@keyframes ShiftText{
  0%,10%{
    top: 0;
  }
  11%,20%{
    top: -45px;
  }
  21%,30%{
    top: -90px;
  }
  31%,40%{
    top: -135px;
  }
  41%,50%{
    top: -180px;
  }
  51%,60%{
    top: -225px;
  }
  61%,70%{
    top: -270px;
  }
  71%,80%{
    top: -315px;
  }
  81%,90%{
    top: -360px;
  }
  91%,100%{
    top: -405px;
  }
}

クリックしたときに、検索ボックスにキーワードを入力するメソッド

setInputBox(searchtext){
      document.getElementById( "searchbox" ).value = searchtext;
      this.keyword=searchtext;   ←この後、this.keywordを監視しているメソッドが検索を実行する
    },

参考:
CSS テキスト切り替えアニメーション
イベントハンドリング-公式

初めはvue.jsのtransitionを使ってみようと考えていた。しかし、こちらの記事「Atomic DesignでAtomが走るゲームを作った話」で紹介されていた VueでのCSSアニメーションの話によるとVue transitionが向いてないものに状態のトランジション( 常に表示され続けるもの、 class名の変化、位置やサイズが変化するもの、数値計算によるデータ変化)があげられるらしい。よって、全く手の込んだアニメーションではないが、今回はcssのみで作成した。
[2019/07/13追記:コメントいただきました。下記に追記として違う手法を記載しています。]
今後使ってみたいので、わかりやすかったページをメモしておく。

Vueのアニメーションが凄い
Vue.jsのトランジションアニメーション[基礎編]
Vue.jsのトランジションとCSSで作るアニメーションの基本をサンプルでわかりやすく解説
Vue.jsで使われているFLIPアニメーションの仕組み
VueとFirebaseの基本機能全部使ってぬるぬる動くポートフォリオサイトを作ったのでソースと解説

おまけ

大仏の頭のぶつぶつは「今も頭についているものが483個、落下する等してはずれてしまったと思われるものが9個ある」そうです。

トナカイの名前の方は「Dasher(ダッシャー)・Dancer(ダンサー)・Prancer(プランサー)・Vixen(ビクスン/ヴィクセン)・Comet(コメット)・Cupid(キューピッド)・Donder(ダンダー/ドンダー)・Blitzen(ブリッツェン/ブリツェン)の8頭」
ちゃんと名前が決まっていたんですね。。

他にもユニークな事例が盛りだくさんのレファレンス協同データベース。ランダムに検索できるwebサービス「reftika」もぜひ使ってみてください。

追記[2019/07/13追記] ※下記に更に[追記あり]

引用した記事『Atomic DesignでAtomが走るゲームを作った話』を書かれたdaitasu様より「描画時から同じアニメーションを繰り返す場合なら、appearというものが使えるかと」とコメントいただきました。
ありがとうございます!!

反映しました!
が、恥を重ねるようですが、私の勉強不足のためappearでループを形成する方法の見当がつかず...。模索するうちに、現在は以下のように実装することにいたしました。申し訳ないです。

大まかな流れ

①アニメーションのループのために別途timerを設ける。3000ミリ秒ごとのインターバルで関数を呼び出す。

createdで呼び出すtime計測の関数

startRotation: function() {
  this.timer = setInterval(this.next, 3000);
},

②インターバルで呼び出される関数。

next: function() {
  this.currentNumber += 1
},

③文言が格納された配列のindexとしてcurrentNumberを使用。

computed内の関数。this.wordsは20個程度の表示したい文言を格納した配列。

currentWord: function() {
  return this.words[Math.abs(this.currentNumber) % this.words.length];
},

④文言の切り替わりアニメーションはvue transitionを利用

.slide-enter{
transform: translateY(30px);
}
.slide-enter-active{
  transition: all 1.3s ease;
}
.slide-leave-active {
  transform: translate(0px, 0px);
  transition: all 0.7s ease;
}
.slide-leave-to{
transform: translateY(-30px);
}

また、前回の方法では、表示したい文言(現在20個)が今後多くなっていった場合、追加する際に非常に手間がかかるという問題がありました。(0%,10%{top: 0;} 11%,20%{top: -45px;}...をドンドン分割して延々と書き直し・書き加える羽目になるところでした...。ひぇ~)
その点も今回の改良で前回よりかなり改善いたしました。さらに、別途jsonファイルとして保存することで追加のしやすさもアップしました!

そのほか、前回の方法では100%から0%の切り替え(最後の文言から最初の文言への切り替え)だけスパッと点滅するようになっていました。それも、今回の方法に変えたことでスムーズに動くようになりました!

それぞれの全体像はこちらになります。
template部分

<p class="exampletexts">
    <transition-group name='slide' tag='div' mode="out-in">
        <div v-for="number in [currentNumber]" :key='number'>
            <span v-text="currentWord" v-on:click="setInputBox(currentInputWord)" />
        </div>
    </transition-group>
</p>

data

import examplesearch from "../assets/example.json"

export default {
  data() {
    return {
        省略
        currentNumber: 0,    ←文言が格納されている配列で、現在表示している文言のindexとなる
        timer: null,    ←タイム計測
        words: examplesearch.words,    ←表示したい文言が格納された配列
        inputwords: examplesearch.inputwords,    ←文言クリック時に検索ボックスに入力されるキーワード
    }

computed

computed: {
currentWord: function() {
  return this.words[Math.abs(this.currentNumber) % this.words.length];
},
currentInputWord: function() {
  return this.inputwords[Math.abs(this.currentNumber) % this.inputwords.length];
},

結果。前より動きもヌルッとしました。

以上となります。

まだまだ、至らないところばかりです。ここがいまいち、これは必要ない、など、もしお気づきの点ありましたらコメントいただけますと大変ありがたいです!

参考:
Vue.js で json を読み込む
初期描画時のトランジション-公式
VueでイケてるTransition! fade, slide編

追記[2019/07/13追記★2回目]

appearの使い方が(たぶん)わかりました!
実装もかなりシンプルになります。

大まかな流れ

①初期描画時にappearでアニメーションを実行
②after-enterにjavascriptフックを設定→次の文言をセットする

<transition-group
appear            ←追加:<1>初期描画時にappearでアニメーションを実行
name='fade'
tag='div'
v-on:after-enter="nextappear"    ←追加:<2>javascriptのフック
>
  <div v-for="number in [currentNumberappear]" :key='number'>
    <span v-text="currentWordappear"/>
  </div>
</transition-group>

フックの中身。

methods: {
    nextappear:function() {
      setTimeout(()=>{
        this.currentNumberappear += 1
      },3000)
    },
computed: {
    currentWordappear: function() {
        return this.words[Math.abs(this.currentNumberappear) % this.words.length];
    }
}

スタイルは上記(7/13、1回目更新分)と同様

<style>
.fade-enter {
  transform: translateY(30px);
}
.fade-enter-active {
  transition: all 1.3s ease;
}
.fade-leave-active {
  transform: translate(0px, 0px);
  transition: all 0.7s ease;
}
.fade-leave-to {
  transform: translateY(-30px);
}
</style>

appearとフックを使えば、わざわざtimerを設けたりする必要はなかったんですね。以上、お目汚し失礼いたしました。
参考:
VueでのCSSアニメーションの話

追記[2019/07/13追記★3回目]

表示するのは1項目ずつなので、わざわざtransition-groupを使う必要はありませんでした...。

    <transition
    appear
    name='slide'
    tag='div'
    v-on:after-enter="nextappear"
    mode="out-in"
    >
        <span v-html="currentWordappear" :key='currentNumberappear' />
    </transition>

nextappearは変わらず。

    nextappear:function() {
      setTimeout(()=>{
        this.currentNumberappear += 1
        console.log(this.currentNumberappear);
      },3000)
    }

computedも変わらず。

  computed: {
  currentWordappear: function() {
    return this.words[this.currentNumberappear % this.words.length];
  },
}

cssは書き直しました。

<style>
.slide-enter-active {
 transition: all 0.6s ease 0s;
 position: absolute;
}
.slide-leave-active {
 transition: all 0.3s ease 0s;
 position: absolute;
}
.slide-enter,
.slide-leave-to {
 opacity: 0;
}
.slide-enter {
 transform: translateY(30px);
}
.slide-enter-to,
.slide-leave {
 transform: translateY(0);
}
.slide-leave-to {
 transform: translateY(-30px);
}
</style>

以上となります。