Vue.js で絞込み&ソート機能を部分的に組み込む

Vue.js で絞込み&ソート機能を部分的に組み込む

Vue.jsJavaScript

最近 WordPress で部分的に Vue.js を組み込みました。最初は React を組み込もうとしましたが、大規模になりそうだったので Vue.js に切り替えました。やってみると驚くほど手軽でした。

かみーゆ
かみーゆ

Vue.js は Vanilla JS に比べると簡潔に書け、React よりも気軽に導入できます。
Vue.js はちょっとした動的機能をHTMLやPHPに組み込むのに最適

Vue.js のバージョン3(V3)からは、デフォルトで Compostion API が使えます。Compostion とは 構成 という意味 で、リアクティブな値やその値に関連した処理をコンポーネントから分割して扱えるコンポーネントの形式です。

当記事ではその導入方法の詳しい解説と、実践にも転用できるサンプルコードをご紹介します。

前提条件

  • JSON データを利用したい
  • JavaScript がある程度理解でき、実際に書いたこともある
  • 日にちなどのソート(昇順、降順)機能を実装したい
  • タグやカテゴリーなどで絞り込みたい
  • 数千件以上のデータを取り扱う可能性がある

完成イメージはこんなかんじ。

完成イメージはこんなかんじ
この記事を書いた人

かみーゆ/フロントエンドエンジニア

資金ゼロからフィリピンで起業した海外ノマドエンジニア。IT業界10年以上でテクニカルディレクター(技術責任者)・エンジニア講師・ブリッジSEを経てLenzTechnologies Inc.を設立し、代表を務める。CMS concreteCMSエバンジェリスト。テックブログ以外も「磨耗しない人生」や「海外ノマド」のライフスタイルについて発信。好きなものは肉とハイボール。

Vue.js Compostion API 基本の書き方(Options API と比較)

V3から使える Compostion API は Options API と比べると、記述を分ける必要がなく、変数・関数などをスッキリまとめることができる印象です。

new Vue() (Options API) と createApp() (Compostion API) といった感じでインスタンスの作り方が違います。

OptionsAPI
 const vueA = new Vue({
  data: () => ({
    // 変数など
  }),
  methods: {
    // 関数処理
  },
});
CompostionAPI
const app = createApp({
  setup() {
    // 変数や関数
    return {
      // 設定した変数や関数の戻り値
    }
  }
})
app.mount("#app");

JSON データを用意する

JSON データを用意します。

スプレッドシートから出力した JSON や WordPress REST API などを利用してもOK。

JSON
[
  {
    "slug":"https://ginneko-atelier.com/",
    "tags": [
      "セブ島",
      "海外移住"
    ],
    "title": "セブ島で日本語が通じる歯医者さんに行った話",
    "date": "2022-06-28"
  },
  {
    "slug":"https://ginneko-atelier.com/",
    "tags": [
      "JavaScript",
      "html"
    ],
    "title": "軽量スライダーSwiperでスマホ(SP)のみスライダー表示にする方法(複数対応)",
    "date": "2022-12-17"
  },
  {
    "slug":"https://ginneko-atelier.com/",
    "tags": [
      "セブ島ボランティア体験記",
      "ライフスタイル"
    ],
  //続く…
  }
]

スプシデータを使いたい場合は GAS で JSON を生成する事もできます。以下記事を参考にしてください。

スプシのデータをJSON化してPHPに取り込む(GAS使います)

以前Googleスプレッドシートにクライアント側でデータを入力してもらい、ウェブサイトに表示する処理を紹介しました。20...

JSON データから一覧を作る

今回は以下のような単純なディレクトリ構造を用意しました。

vue-sourt-sample/
  ├ index.js
  ├ articles.json
  └ index.html

まずは articles.json (JSON) から一覧を作ります。今回は配布されている Vue.js の CDN を使いました。使う機能をディレクティブで import します。

index.js
import {
  createApp,
  ref,
  onMounted,
} from "https://unpkg.com/Vue.js@3.2.4/dist/Vue.js.esm-browser.prod.js";

const jsonUrl = 'articles.json';

const app = createApp({
  setup() {
    const articles = ref(null);
    const sortArticle = async () => {
      let result;
      result = await fetch( jsonUrl );
      result = await result.json();
      articles.value = result;
    }
    onMounted( async () => {
      await sortArticle();
    })
    return {
      articles
    }
  }
})

app.mount("#app");

index.js ファイルを外部ファイルとして読み込む際、import を利用したいので属性 type="module" を付与します。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sort JSON</title>
</head>
<body>
  <div id="app" v-cloak class="contaienr">
     <ul class="articles">
      <li v-for="article in articles">
        <time :datetime="article.date">{{article.date}}</time>
        <a :href="article.slug">{{article.title}}</a>
      </li>
    </ul>
  </div>
  <script src="./index.js" type="module" defer></script>
</body>
</html>

すべての記事が表示されます。

記事一覧

属性に変数や処理を入れる方法

Vue.js では :属性名 とすると、値に変数や処理を入れることができます。

<!-- :属性名="変数等" -->

<a :href="article.slug">{{article.title}}</a>
<time :datetime="article.date">{{article.date.replace(/-/g, "/")}}</time>

replace で日付フォーマットを変えることもできます。

値に変数や処理を入れることができます

チラツキ防止

Vue.js ではページ読み込み時処理が間に合わず、一瞬コードがむき出しになるチラツキがよく起こります。チラツキを抑えるために以下 CSS を追加しておきましょう。

[v-cloak] {
  display: none;
}

ref とは?

久々に Vue.js を使うと ref なるものが登場してきました汗

ref を使うと、内部の値を受け取ることができ、リアクティブで変更可能な変数の型を返すことができます。値を取り出すときは value プロパティを使います。

const array = ref([1, 20, 30])

console.log(array.value)
// 結果[1,20,30]

ref 関連 | Vue.js

ページ送り(ページネーション)をつける

このままだとページが長くなり、ひたすらスクロールしなければならないので、ページネーションをつけます。

index.js
const paged = 10 //何ページごとに表示するか
const total = ref(null) //ページ総数
const currentPage = ref(1) //現在のページ
const maxPage = ref(null) //最大ページ数
// 省略
const sortArticle = async () => {
  // 省略
  total.value = result.length;
  maxPage.value = Math.ceil(total.value / paged)
  articles.value  = result.slice( paged *  (currentPage.value - 1), paged * currentPage.value)
}

ページネーションボタンのための関数。

index.js
const pagination = async (dir) => {
  if(dir === 'prev') {
    if (perPage.value === 1) {
      return;
    }
    perPage.value--;
  }
  if(dir === 'next') {
    if (perPage.value === maxPage.value) {
      return;
    }
    perPage.value++;
  }
  await sortArticle();
};

それぞれの値や関数を HTML 側で受け取れるよう戻り値に追加します。

index.js
return {
  pagination,
  currentPage,
  total,
  maxPage,
}

ページネーションを HTML 側に組み込みます。:disabledcurrentPage (現在ページ)が1以下になったり、maxPage (最大ページ数)以上にならないよう制御します。

index.html
<div class="patination">
  <button @click="pagination(`prev`)" :disabled="currentPage === 1">PREV</button>
  {{currentPage}}/{{maxPage}}
  <button @click="pagination(`next`)" :disabled="currentPage == maxPage">NEXT</button>
</div>
ページネーション

記事を新しい・古い順でソート(並べ替え)する

記事を新しい・古い順でソートするための機能を作成します。

ソートできる機能
index.js
// 省略
const sortDate = ref(null)
// 省略
const sortArticle = async () => {
  // 省略
  if(sortDate.value !== null) {
    // 省略
    result = await result.json();

    result = result.sort((a, b) => {
      if(sortDate.value === 'desc') {
        return (a.date < b.date ? 1 : -1);
      } else if(sortDate.value === 'asc') {
        return (a.date > b.date ? 1 : -1);
      }
    });
    total.value = result.length;
    // 省略
  }
}
// 省略

日付を選んた時にソートできる関数を追加します。今回はソート解除もできるようにしました。

index.js
const sort = async () => {
  currentPage.value = 1;
  await sortArticle();
}
const clear = async () => {
  currentPage.value = 1;
  sortDate.value = null
  await sortArticle();
}

HTML 側で使えるように戻り値を追加します。

index.js
return {
  // 省略
  sortDate,
  clear,
  sort,
}

昇順降順の UI を追加します。V3も v-model (データの双方向のやり取り)の記述方法は変わりません。

選択した項目は sortDate に格納されます。

index.html
<div class="sort">
  <h3>日時でソートする</h3>
  <label for="desc" class="sortBtn">
    <input type="radio" value="desc" v-model="sortDate" name="date" id="desc">新しい順
  </label>
  <label for="asc" class="sortBtn">
    <input type="radio" value="asc" v-model="sortDate" name="date" id="asc">古い順
  </label>
  <div class="sortBtns">
    <button @click="sort">実行</button>
    <button @click="clear">クリア</button>
  </div>
</div>

Vue.js では @アクション名="処理" でクリックなどのイベントに対して様々な処理をすることができます。

<!-- @アクション名="処理" -->

<!-- クリックで処理 -->
<button @click="sort">絞り込む</button>

<!-- 値が変わったことを感知して処理 -->
<label><input type="checkbox" @change="checked">プライバシーポリシーに同意する。</label>

複数のタグで絞り込む

今回は複数のタグを選んで広域に絞り込める(OR検索)ようにします。

ソートできる機能

タグだけ抜き取る

まずは記事から、タグだけ抜き取ります。重複は JS の reduce を使って取り除きます。

index.js
const tags = ref(null)

const getTags = async () => {
  let tagList;
  tagList = await fetch( jsonUrl );
  tagList = await tagList.json();
  tagList = tagList.reduce((tags, article) => {
    if (article.tags) {
      article.tags.map(item => {
        if (tags.find(i => i === item)) {
          return false;
        }
        tags = [...tags, item]
      })
    }
    return tags
  },[])

  tags.value = tagList
}
onMounted( async () => {
  await getTags();
})

return {
  // 省略
  tags
}

その他の配列操作についてさらに詳しく知りたい場合はこちらをご覧ください。

混乱しがちなJSの配列ループ・操作まとめ

通常配列はもちろん連想配列など、配列のループでどの方法を使うべきか用途ごとに詳しく解説。命令文for...of、for....

v-for を使ってループでHTML側に出力します。

タグの絞り込み機能を作る

複数のタグの記事を絞り込めるようにします。

index.js
// 省略
const selectedTags = ref([])
const sortArticle = async () => {
  if(selectedTags.value.length !== 0) {
    // 省略
    result = result.filter((i) => {
      const article = i.tags.map(k => {
        if(selectedTags.value.includes(k)) return true;
      })
      if(article.includes(true)) return i;
    })
  }
  total.value = result.length;
}
return {
  // 省略
  selectedTags
}

タグの絞り込み解除できるようにしておきます。

また絞り込んだ時に、何件になったか、どのタグで絞り込んだかなどを、表示できるようにします。

index.js
// 省略
const message = ref(null)
// 省略
const sort = async () => {
  currentPage.value = 1;
  await sortArticle();

  // 選択したタグが0じゃない場合、メッセージを表示
  message.value = selectedTags.value.length !== 0 ? `${selectedTags.value.join(", ")} の記事が ${total.value} 件あります。` : null;
}
const clear = async () => {
  currentPage.value = 1;
  selectedTags.value = []
  sortDate.value = null
  message.value = null

  await sortArticle();
}

絞り込み部分の HTML。選択した項目は v-modelselectedTags に格納されます。

index.html
<div class="sort">
  <h3>タグで絞り込む</h3>
  <!-- 省略 -->
  <ul class="select-tags">
    <li v-for="tag, n in tags">
      <label :for="`tag-${n}`">
        <input
          type="checkbox"
          :value="tag"
          v-model="selectedTags"
          name="tag" :id="`tag-${n}`"
        >{{tag}}
      </label>
    </li>
  </ul>
  <!-- 省略 -->
</div>

v-if で、絞り込み件数と、どのタグで絞り込んだか表示させます。
message が null の場合は非表示にします。

index.html
<p v-if="message">{{message}}</p>
ソートできる機能

すべてのコード

すべてのコードは Github にあげています。

まとめ・Vue.jsはちょっと機能を足すのに便利

実は4年ぶりに Vue.js を使いました。V3になって書き方も諸々バリエーションが増えていてビビりました。

かみーゆ
かみーゆ

書き方も簡潔になっており、初学者にも React よりはとっつきやすいかなぁという印象です。

この記事が、みなさんのコーディングライフの一助となれば幸いです。

最後までお読みいただき、ありがとうございました。

コンポーネントを使ったデータバインドについてはこちらをお読みくだだい。
コンポーネント(部品)間で、複雑なデータの受け渡しをする【Vue3】

Vue.js でコンポーネント(部品)を作成する方法をご紹介します。開発環境準備、親から子、子から親へのデータ受け渡し方...

参考 : HTMLにちょい足しでできる!Vue.js.jsでサクッと動きをつける方法