LoginSignup
16
28

More than 5 years have passed since last update.

jQuery しか使ったことのないデザイナーに Vue を教えた 二回目(報告書)

Last updated at Posted at 2017-04-11

jQuery しか使ったことのないデザイナーに Vue を教えた(報告書)

上記記事の続きになります。

今回は「もっと見る」ボタンを実装します。
固定データ、API 方式の2種類とオマケでページングも作ります。
動作環境などは上記記事をご参考ください。

固定データ編

要件

  • 使用するのは固定データ
  • 初期表示は3行だけ
  • 「もっと見る」を押したら4行追加される

HTML と CSS を用意する

今回もデザイナーさんが作ってくださったものをべた張りさせていただきます。

HTML
<div class="moreBlock" id="news">
  <ul class="moreList">
    <li><a href="http://www.google.co.jp/" target="_blank"><span>2016/12/01</span>いろはにほへと</a></li>
  </ul>
  <p class="btnArea"><a class="btnMore">もっと見る</a></p>
</div>
SCSS
.moreBlock{
  width: 500px;
  ul{
    list-style-type: none;
    li{
      margin-bottom: 5px;
      a{
        display: block;
        color: #333;
        position: relative;
        padding-left: 1em;
        text-decoration: none;
        &::before{
          content: '>';
          position: absolute;
          top: 0.01em;
          left: 0;
          color: #4eb6cc;
        }
        span{
          font-size: 13px;
          margin-right: 10px;
          color: #666;
        }
      }
    }
  }
}
.btnArea{
  text-align: center;
}
.btnMore{
  display: inline-block;
  min-width: 300px;
  height: 40px;
  line-height: 40px;
  background: #4eb6cc;
  color: #fff;
  text-align: center;
  text-decoration: none;
  -webkit-border-radius: 3px;
  -moz-border-radius: 3px;
  border-radius: 3px;
  cursor: pointer;
  &:hover{
    opacity: 0.6;
  }
}

Vue を書く

Vue
/**
 * 基本となるデータです。
 * ほとんどの場合においてバックエンドで書き出されることが多いので別にしておきました。
 */
var data = [{
  date:'2016/12/21',
  content:'ゑひもせす',
  href: 'http://www.xxx.co.jp/news/detail/13'
},{
  date:'2016/12/20',
  content:'あさきゆめみし',
  href: 'http://www.xxx.co.jp/news/detail/12'
},{
  date:'2016/12/19',
  content:'けふこえて',
  href: 'http://www.xxx.co.jp/news/detail/11'
},{
  date:'2016/12/16',
  content:'うゐのおくやま',
  href: 'http://www.xxx.co.jp/news/detail/10'
},{
  date:'2016/12/15',
  content:'つれならむ',
  href: 'http://www.xxx.co.jp/news/detail/9'
},{
  date:'2016/12/14',
  content:'わかよたれそ',
  href: 'http://www.xxx.co.jp/news/detail/8'
},{
  date:'2016/12/13',
  content:'ちりぬるを',
  href: 'http://www.xxx.co.jp/news/detail/7'
},{
  date:'2016/12/12',
  content:'いろはにほへと',
  href: 'http://www.xxx.co.jp/news/detail/6'
},{
  date:'2016/12/09',
  content:'ながめせしまに',
  href: 'http://www.xxx.co.jp/news/detail/5'
},{
  date:'2016/12/08',
  content:'わがみよにふる',
  href: 'http://www.xxx.co.jp/news/detail/4'
},{
  date:'2016/12/07',
  content:'いたずらに',
  href: 'http://www.xxx.co.jp/news/detail/3'
},{
  date:'2016/12/06',
  content:'うちりにけりな',
  href: 'http://www.xxx.co.jp/news/detail/2'
},{
  date:'2016/12/05',
  content:'はなのいろは',
  href: 'http://www.xxx.co.jp/news/detail/1'
}
];

new Vue({
  el:'#news',
  data:{
    // 表示するデータ
    list:data,
    // 初期表示で表示する行数
    first:3,
    // 「もっと見る」で追加される行数
    count:4,
    // 「もっと見る」をクリックした数
    step:0
  },
  /**
   * computed は計算値を返します。
   * 関連するプロパティが変更される度に再計算をしてくれます。
   * これを上手く使うことでプロパティに値を代入する必要がなくなります。
   * 
   * インスタンスが初期化のされる時に1回目の計算をして使用したプロパティを監視する対象とします。
   * ですので、代入でもいいので、全てのプロパティを一度呼び出す必要があります。
   * 悪い例:
   *   // 1回目で this.count が呼び出されていないので this.count を監視できません。
   *   if (this.step === 0) {
   *     return this.first;
   *   } else {
   *     return this.first + this.count * this.step;
   *   }
   */
  computed:{
    // this.list の上から何行を表示するかを表します。
    // 初期表示行数 + 「もっと見る」で追加する行数 ×「もっと見る」をクリックした回数
    end:function(){
      return this.first + this.count * this.step;
    }
  },
  methods:{
    /**
     * データを全て表示しきったかどうか
     */
    isEnd: function(){
      return this.end > this.list.length;
    },
    /**
     * 「もっと見る」をクリックした時の動作
     */
    seemore:function(){
      // computed で指定しているので
      // this.step (クリック回数)を変更するだけで
      // this.end が再計算されます。
      this.step++;
    }
  }
});

HTML をテンプレートに直す

HTML.vue
<div class="moreBlock" id="news">
  <ul class="moreList">
    <!--
      template タグは、テンプレートを作ったり、タグのブロックを作成できます。
      表示されるのは template タグの中身だけになります。

      後に続く、v-if と v-for は共存できないので template タグで囲みました。
   -->
   <!--
      v-for は for です。
      使い方は複数あります。
      詳しい使い方はドキュメントを読んでください。

      (li,index) in list は
      list の中身を1つずつ取り出し、データを li に、添字を index に保存します。
    -->
    <template v-for="(li,index) in list">
      <!-- v-if で表示するデータを絞り込みます。 -->
      <!--
        アトリビュートにデータを入れたい時は : を使います。
        :href は href に li.href のデータ入れています。
      -->
      <li v-if="index < end"><a :href="li.href" target="_blank"><span>{{li.date}}</span>{{li.content}}</a></li>
    </template>
  </ul>
  <!-- データを最後まで表示したら非表示にします。 -->
  <p class="btnArea" v-if="!isEnd()"><a class="btnMore" @click="seemore()">もっと見る</a></p>
</div>

説明が長くなりましたが、コレで完成です。

API 編

API のダミーを用意する

下記の記述内で定義されていない store は上記の data を使用することとします。
Promise と setTimeout を使って $.ajax(options).then() のような動きにしております。
デザイナーさんにはダミーコードについて説明しませんでした。
ページングのアルゴリズムです。(これ以外の方法もあります。)

js
/**
 * @typedef Page
 * @property {Array} data - ページのデータ。
 * @property {bool} next - 次のページがあるかどうか。
 * @property {number} start - 開始行のオフセット。
 * @property {number} end - 終了行のオフセット。
 * @property {number} page - 現在のページ番号。
 * @property {number} pageMax - ページ数。
 * @property {number} count - 1ページ枚の行数。
 * @property {number} length - データ総数。
 * @property {number} first - 1ページ目の行数。
 */

/**
 * API のダミーです。
 * @param {Object} options - データを呼び出す時のパラメータです。
 * @param {number} options.first - 1ページ目の行数を指定します。
 *                                 2ページ目以降は count を使用します。
 * @param {number} options.count - 1ページ毎の行数を指定します。
 *                                 デフォルト値は 3 です。
 * @param {number} options.page  - 取得するページ数を指定します。
 * @return Promise<Page|Error>  - 取得に成功した場合は ページングに必要な Object を返します。
 */
var getData = function (options) {
  return new Promise(function (resolve, reject) {
    setTimeout(function() {
      if (!options) {
        options = {};
      }
      var count = options.count || 3;
      var first = options.first || count;
      var page = options.page || 1;
      var start = 0;
      var end = 0;

      var pageMax = Math.ceil((store.length - first) / count) + 1

      if (page < 1 || pageMax < page) {
        reject({
          result: 'OptionError'
        });
        return
      }

      if (page === 1) {
        start = 0;
        end = first;
      } else {
        start = first + (count * (page - 2));
        end = start + count;
      }

      if (store.length < end) {
        end = store.length
      }

      var next = end < store.length;

      var data = null

      if (store.length) {
        data = store.slice(start, end);
      } else {
        page = 0
        pageMax = 0
        start = -1
        end = 0
      }

      resolve({
        data: data,
        next: next,
        start: start+1,
        end: end,
        page: page,
        pageMax: pageMax,
        count: count,
        length: store.length,
        first: first
      });
    }, 1000);
  });
}

HTML を書き直す

HTML
<div class="moreBlock" id="news">
  <ul class="moreList">
    <!--
      <template></template> に書いていた v-for を <li> に移動し、 <li> の v-if を消しました。
      ついでに v-for は `(li, index) in list` ではなく `li in list` にします。
    -->
    <li v-for="li in list"><a :href="li.href" target="_blank"><span>{{li.date}}</span>{{li.content}}</a></li>
  </ul>
  <p class="btnArea" v-if="isEnd()"><a class="btnMore" @click="seemore()">もっと見る</a></p>
</div>

Vue を書き直す

Vue
new Vue({
  el:'#news',
  /**
   * Vue インスタンスが作成された後に同期的に呼ばれます。
   * 
   * インスタンス作成時は 0 ページ目となっているので、
   * 1ページ目を呼び出すために seemore をコールします。
   */
  created:function(){
    this.seemore()
  },
  data:{
    list:[],
    page:0,
    next:false,
    sending:false
  },
  methods:{
    isEnd: function () {
      return !this.next;
    },
    seemore: function () {
      // 連続クリックを防止するために既に押していた場合は実行を抑止します。
      if (this.sending) return;

      // 連続実行を抑止するためにフラグを立てます。
      this.sending = true;

      // 現在の this を self に保存して置きます。
      // ES6 以降では `function () {}` を `() => {}` と書くことで this の保存ができますが
      // プリコンパイラが必要なのでココでは伝統的な方法を採用します。
      var self = this;

      getData({
        // 取得したいページ番号です。
        // 前置インクリメントでページ数を加算した後に代入しております。
        page: ++this.page,
        // ページ毎の行数
        // 本来は定数定義をして置くべきですが省きました。
        count: 3,
        // 1ページ目の行数
        // 本来は定数定義をして置くべきですが省きました。
        first: 4
      }).then(function (result) {
        // 今回取得したデータと前回取得したデータを結合します。
        self.list = self.list.concat(result.data);
        // 次のページがあるかどうかを保存します。
        self.next = result.next;
        // 処理が完了したので連続実行を抑止するフラグを下げます。
        self.sending = false;
      });
    }
  }
});

以上で完成です。

ページング編(おまけ)

せっかく API のダミーがあるので「もっとみる」ボタンではなく汎用性のあるページングも実装してみました。
こちらはおまけですので説明は入れておりません。

SCSS
@mixin btn($minW:40px) {
  display: inline-block;
  min-width: $minW;
  height: 40px;
  background: #2795ee;
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  border-radius: 5px;
  line-height: 41px;
  text-decoration: none;
  text-align: center;
  color: #fff;
}
@mixin act($minW:40px) {
  display: inline-block;
  min-width: $minW;
  height: 40px;
  background: #eee;
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  border-radius: 5px;
  line-height: 41px;
  text-decoration: none;
  text-align: center;
  color: #666;
}
.moreBlock{
  width: 500px;
  ul{
    list-style-type: none;
    li{
      margin-bottom: 5px;
      a{
        display: block;
        color: #333;
        position: relative;
        text-decoration: none;
        &::before{
          position: absolute;
          top: 0.01em;
          left: 0;
          color: #4eb6cc;
        }
        span{
          font-size: 13px;
          margin-right: 10px;
          color: #666;
        }
      }
    }
  }
}
.paging{
  margin: 30px 0 0;
  text-align: center;
  letter-spacing: -0.4em;
  > *{
    letter-spacing: normal;
  }
  > a{
    @include btn(60px);
    margin: 0 2px;
    &:first-child{  }  
    &:last-child{  }
    &.act{
      @include act(60px);
    }
  }
  ul{
    display: inline-block;
    padding: 0 10px;
    letter-spacing: -0.4em;
    li{
      display: inline-block;
      margin: 0 2px;
      letter-spacing: normal;
      a{
        @include btn();
      }
      &.act{
        a{
          @include act();
        }
      }
    }
  }
}
HTML.vue
<div class="moreBlock" id="news">
  <ul class="moreList" v-if="!sending">
    <li v-for="li in list"><a :href="li.href" target="_blank"><span>{{li.date}}</span>{{li.content}}</a></li>
  </ul>
  <div class="loading" v-else>Loading...</div>
  <div class="paging">
    <a :class="{act:isFirstPage()}" @click="goFirst()"><<</a>
    <a :class="{act:isFirstPage()}" @click="goPrev()"></a>
    <ul>
      <li v-for="i in pages" :class="{act:i==page}"><a @click.stop="paging(i)">{{i}}</a></li>
    </ul>
    <a :class="{act:isLastPage()}" @click="goNext()"></a>
    <a :class="{act:isLastPage()}" @click="goLast()">>></a>
  </div>
</div>
Vue
new Vue({
  el:'#news',
  created:function(){
    this.paging()
  },
  data:{
    list:[],
    page:0,
    pageMax:0,
    next:false,
    sending:false
  },
  computed: {
    pages: function () {
      var pages = []
      var max = 5
      var half = Math.floor(max / 2)
      var start = this.page - half
      if (start <= 0) {
        start = 1
      }
      var end = start + max - 1
      if (this.pageMax < end) {
        end = this.pageMax
      }
      if (end <= max) {
        start = 1
      }
      for (var i = start; i <= end; i++) {
        pages.push(i)
      }
      return pages
    }
  },
  methods:{
    isFirstPage: function () {
      return this.page === 1
    },
    isLastPage: function () {
      return this.page === this.pageMax
    },
    goFirst: function () {
      if (this.isFirstPage()) {
        return
      }
      this.paging(1)
    },
    goPrev: function () {
      if (this.isFirstPage()) {
        return
      }
      this.paging(this.page-1)
    },
    goNext: function () {
      if (this.isLastPage()) {
        return
      }
      this.paging(this.page+1)
    },
    goLast: function () {
      if (this.isLastPage()) {
        return
      }
      this.paging(this.pageMax)
    },
    paging:function(page){
      if(this.sending) return;
      this.sending = true;
      this.page = page || 1;
      var self = this;
      getData({
        page:this.page,
        count:5
      }).then(function(result){
        self.list = result.data;
        self.next = result.next;
        self.pageMax = result.pageMax;
        self.sending = false;
      });
    }
  }
});

振り返り

やはり、デザイナーさんには javascript の this の概念が難しいようでした。
var self = this は「そう言われたから、そう書いた。」になってしまいました。
インスタンスを知らないからだと思います。
インスタンスはクラス型オブジェクトで説明されることが多く、javascript のようなプロトタイプ型オブジェクトでの説明が少ないので、参考になる引用がなく、私としても説明しづらかったです。
(息をするように this を使っているので。)

そうなると「javascript をクラス型オブジェクトのように使えばいい」という流れができますが、私はオススメしません。
(ES6 で class が実験的に追加されましたが、私は悪い仕様だと思います。javascript をクラス型オブジェクトにしたいなら別ですが。)
日本語の直訳では文脈の持つ雰囲気が壊れてしまい、訳すことができない英文があるように、プログラム言語にも移植できない色味があると私は思います。

中々、憎々しい javascript の仕様ですが、プログラマーの開発に対する愛は言語の特性を大切にすることでも生まれるものだと思いますので。

以上

16
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
28