<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	
	xmlns:georss="http://www.georss.org/georss"
	xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
	>

<channel>
	<title>Vue.js | みんたく</title>
	<atom:link href="https://mintaku-blog.net/category/develop/vue/feed/" rel="self" type="application/rss+xml" />
	<link>https://mintaku-blog.net</link>
	<description>みんたくの技術ブログ</description>
	<lastBuildDate>Mon, 06 Jan 2025 12:47:27 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.0.11</generator>

<image>
	<url>https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2018/06/cropped-ipad-820272_640.jpg?fit=32%2C32&#038;ssl=1</url>
	<title>Vue.js | みんたく</title>
	<link>https://mintaku-blog.net</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">144480658</site>	<item>
		<title>Intersection Observer APIを使用したGTM/GA4イベントトラッキングの実装</title>
		<link>https://mintaku-blog.net/io-api-ga4/</link>
					<comments>https://mintaku-blog.net/io-api-ga4/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 05 Jan 2025 01:50:27 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[サイト運営]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2565</guid>

					<description><![CDATA[<p>Webサイトでユーザーの行動を分析する際、要素が画面に表示されたタイミングを検知したいと思い、Vue.jsのコンポーネントで Intersection Ob …</p>
The post <a href="https://mintaku-blog.net/io-api-ga4/">Intersection Observer APIを使用したGTM/GA4イベントトラッキングの実装</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Webサイトでユーザーの行動を分析する際、要素が画面に表示されたタイミングを検知したいと思い、Vue.jsのコンポーネントで Intersection Observer API を使用して要素の表示を検知し、Google Analytics 4（GA4）でトラッキングする実装方法を紹介します。</p>
<h2>目的</h2>
<ul>
<li>カードコンポーネントの画面表示を検知</li>
<li>GTM経由でGA4にイベントを送信</li>
<li>表示データの分析</li>
</ul>
<h2>実装フロー</h2>
<ol>
<li>Vueコンポーネントで要素の表示を検知</li>
<li>dataLayerにイベントをプッシュ</li>
<li>GTMでイベントを捕捉</li>
<li>GA4にデータを送信</li>
</ol>
<h2>Vueコンポーネントの実装</h2>
<p></p><pre class="crayon-plain-tag">import { ref, onMounted } from 'vue'

interface Props {
  items: Array&lt;{
    id: string
    title: string
  }&gt;
}

const props = defineProps&lt;Props&gt;()
const itemRefs = ref&lt;any[]&gt;([])

onMounted(() =&gt; {
  itemRefs.value.forEach((elementRef, index) =&gt; {
    if (!elementRef) return

    const observer = new IntersectionObserver((entries) =&gt; {
      if (entries[0].isIntersecting) {
        const item = props.items[index]
        // dataLayerにイベントをプッシュ
        window.dataLayer?.push({
          event: 'item_view',
          item_id: item.id,
          item_title: item.title,
          eventModel: {
            send_to: 'G-XXXXXXXX'
          }
        })
        observer.disconnect()
      }
    })

    observer.observe(elementRef.$el || elementRef)
  })
})
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;div
      v-for="(item, index) in items"
      :key="item.id"
      :ref="el =&gt; itemRefs[index] = el"
    &gt;
      &lt;!-- カードの内容 --&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;</pre><p></p>
<h3>ref配列の使用</h3>
<p>itemRefs配列を使って、複数のカードコンポーネントへの参照を管理しています。Vue3のref属性の動的な割り当てを活用しています。</p>
<h3>Intersection Observer APIの設定</h3>
<p>各カードごとに個別のObserverを作成しています。一度表示を検知したらdisconnect()で監視を停止し、重複計測を防止します。</p>
<h3>dataLayerへのデータ送信</h3>
<p>カードのIDとタイトルを送信し、eventModelを使って送信先のGA4を指定しています。</p>
<h2>GTMの設定</h2>
<p>続いて、Google Tag Managerの設定手順です。</p>
<h3>1. 変数の設定</h3>
<p>まずは、dataLayerから送信されたデータを受け取るための変数を設定します。</p>
<p>GTMの変数設定で以下の2つを作成します。</p><pre class="crayon-plain-tag">【Item ID変数の設定】
- 変数名: Item ID
- 種類: データレイヤー変数
- データレイヤー変数名: item_id

【Item Title変数の設定】
- 変数名: Item Title
- 種類: データレイヤー変数
- データレイヤー変数名: item_title</pre><p>これらの変数は、後ほどGA4にデータを送信する際のパラメータとして使用します。</p>
<h3>2. トリガーの設定</h3>
<p>次に、イベントを検知するためのトリガーを設定します。</p><pre class="crayon-plain-tag">【トリガーの設定】
- トリガー名: Item View
- トリガータイプ: カスタムイベント
- イベント名: item_view</pre><p>このトリガーは、dataLayerにプッシュされたitem_viewイベントを検知します。</p>
<h3>3. タグの設定</h3>
<p>最後に、GA4にデータを送信するためのタグを設定します。</p><pre class="crayon-plain-tag">【タグの設定】
- タグ名: GA4 - Item View
- タグタイプ: Google タグ
- 測定ID: G-XXXXXXXX

イベントパラメータ:
- item_id: {{Item ID}}
- item_title: {{Item Title}}</pre><p></p>
<h2>GA4での設定と分析</h2>
<h3>カスタムディメンションの設定</h3>
<p>GA4でデータを分析するために、まずカスタムディメンションを設定する必要があります。</p>
<p>管理画面から以下のディメンションを作成します。</p>
<ol>
<li>Item ID（イベントスコープ）</li>
<li>Item Title（イベントスコープ）</li>
</ol>
<h3>レポートの作成</h3>
<p>探索レポートを使って、以下のように分析が可能です。</p>
<ul>
<li>ディメンション：Item Title</li>
<li>指標：イベントカウント</li>
<li>フィルター：イベント名 = item_view</li>
</ul>
<p>これにより、各カードの表示回数を確認できます。</p>
<h2>デバッグのポイント</h2>
<h3>1. GTMのプレビューモード</h3>
<p>GTMのプレビューモードでは以下を確認します。</p>
<ul>
<li>dataLayerにイベントが正しく送信されているか</li>
<li>変数が期待通りの値を取得できているか</li>
<li>タグが正しいタイミングで発火しているか</li>
</ul>
<h3>2. GA4のデバッグビュー</h3>
<p>GA4のデバッグビューでは</p>
<ul>
<li>イベントが正しく送信されているか</li>
<li>パラメータが正しく設定されているか</li>
</ul>
<p>を確認できます。</p>
<h2>よくあるトラブルと解決方法</h2>
<h3>イベントが発火しない</h3>
<ul>
<li>dataLayerの実装を確認</li>
<li>コンソールでエラーがないか確認</li>
</ul>
<h3>パラメータが取得できない</h3>
<ul>
<li>GTMの変数名が正しいか確認</li>
<li>dataLayerの構造を確認</li>
</ul>
<h3>GA4にデータが反映されない</h3>
<ul>
<li>測定IDが正しいか確認</li>
<li>最大48時間の遅延を考慮</li>
</ul>
<p>→ 実際に24時間くらいデータが反映されず、実装が間違っているのかと見直していましたが時間が経てば反映されてました</p>
<h2>参考リンク</h2>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API">交差オブザーバー API - Web API | MDN</a>
			</p>
							<div class="ys-blog-card__dscr">
					交差オブザーバー API (Intersection Observer API)&hellip;				</div>
										<div class="ys-blog-card__domain">developer.mozilla.org</div>
					</div>
	</div>
</div>


<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/www.gstatic.com/devrel-devsite/prod/v80d9a52eefe4eccd9a262cfbc94473a846b10e2b3115e0674427719fa6f74364/developers/images/opengraph/white.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://developers.google.com/analytics/devguides/collection/ga4">Google Analytics for developers &nbsp;|&nbsp; Google for Developers</a>
			</p>
										<div class="ys-blog-card__domain">developers.google.com</div>
					</div>
	</div>
</div>


<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/www.gstatic.com/devrel-devsite/prod/v80d9a52eefe4eccd9a262cfbc94473a846b10e2b3115e0674427719fa6f74364/developers/images/opengraph/white.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://developers.google.com/analytics/devguides/collection/ga4?hl=ja">デベロッパー向け Google アナリティクス &nbsp;|&nbsp; Google Analytics &nbsp;|&nbsp; Google for Developers</a>
			</p>
										<div class="ys-blog-card__domain">developers.google.com</div>
					</div>
	</div>
</div>The post <a href="https://mintaku-blog.net/io-api-ga4/">Intersection Observer APIを使用したGTM/GA4イベントトラッキングの実装</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/io-api-ga4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2565</post-id>	</item>
		<item>
		<title>Nuxt3への移行作業時に気になったところをまとめてみた</title>
		<link>https://mintaku-blog.net/nuxt3-rc/</link>
					<comments>https://mintaku-blog.net/nuxt3-rc/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 12 Oct 2022 02:55:21 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2272</guid>

					<description><![CDATA[<p>Nuxt2で開発したWebサービスをNuxt3 RCに移行作業を行なっているので、その際に気になった機能などをまとめておきます。 公式ページ：https:/ …</p>
The post <a href="https://mintaku-blog.net/nuxt3-rc/">Nuxt3への移行作業時に気になったところをまとめてみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Nuxt2で開発したWebサービスをNuxt3 RCに移行作業を行なっているので、その際に気になった機能などをまとめておきます。</p>
<p>公式ページ：https://v3.nuxtjs.org/</p>
<h2>Nuxt2との違い</h2>
<p>Nuxt3は、Vue3をベースになっています。Nuxt2との違いとしてSSRの高速化やバンドルサイズの縮小などのパフォーマンスの向上、Composition APIの導入、TypeScript サポートなどが挙げられます。</p>
<p>&nbsp;</p>
<h2>v3.0.0-rc.10でFull SSGモードに</h2>
<p>rc.3で開発していたのですが、SSGしてもクライアント側でデータフェッチしている挙動があり、どうにかならないかと悩んでいました。</p>
<p>Nuxt3のRC情報を見ていたら、SSG後にクライアント側で再フェッチしないFull SSGモードなるものがリリースされており、バージョンを上げたところ無事解決しました。</p>
<div class="ys-blog-card__text-link"><a href="https://github.com/nuxt/framework/discussions/7513" >https://github.com/nuxt/framework/discussions/7513</a></div>
<p>&nbsp;</p>
<h2>Nuxt3のコアパッケージ</h2>
<p>Nuxt3を構成するコアパッケージは以下のようになっています。</p>
<ul>
<li>コアエンジン: nuxt</li>
<li>バンドラー: @nuxt/vite-builder and @nuxt/webpack-builder</li>
<li>コマンドライン: nuxi</li>
<li>サーバーエンジン: nitro</li>
<li>開発キットt: @nuxt/kit</li>
<li>Nuxt 2ブリッジ: @nuxt/bridge</li>
</ul>
<h3>nuxt/bridge</h3>
<p>既存のNuxt 2プロジェクトからNuxt3の機能を使いたい場合はNuxt Bridge を使用するのが良さそうです。変更を最小限に抑えながら、ほとんどの新機能を試すことができます。</p>
<h2>nitro</h2>
<p>nitro(読み方はニトロじゃなくてナイトロ)の基盤は、rollupとh3で、高いパフォーマンスと移植性のために構築された最小限のhttpフレームワークです。特徴は以下の通りです。</p>
<h3>クロスプラットフォームサポート</h3>
<p>様々なプラットフォームにも対応しているエンジンで、JavaScript をサポートする様々なシステムでデプロイできます。</p>
<h3>サーバーレスサポート</h3>
<p>サーバーレス環境でも動作します。</p>
<h3>API routes</h3>
<p>unjs/h3プロジェクトを使用し、h3という軽量のhttp サーバーが使われています。</p>
<h3>自動コード分割</h3>
<p>設定しなくても自動的にコード分割してくれます。</p>
<h3>ホットリロード</h3>
<p>プログラム実行中にコードを変更できます。</p>
<h3>ハイブリッド・モード</h3>
<p>ページのレンダリングを細かくコントロールし、静的サイトとサーバーレスサイトでハイブリッドモードが使用可能です。</p>
<p>ローカル開発時のホットリロードの速度、CICD 上でのビルド実行速度が従来の WebPack とは段違いに速いと感じます。</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/nitro.build/_og/_index.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://nitro.unjs.io/">Nitro - Next Generation Server Toolkit</a>
			</p>
							<div class="ys-blog-card__dscr">
					Create web servers with everything you n&hellip;				</div>
										<div class="ys-blog-card__domain">nitro.unjs.io</div>
					</div>
	</div>
</div>

<p>&nbsp;</p>
<h2>自動インポート</h2>
<p>Nuxt3では、CompositionAPIのsetupで使用する関数のインポートを追加する必要はなく、エンジンによって自動的にインポートされます。また、ディレクトリとファイル名で自動的にComponentもインポートされます。</p>
<h3>components</h3>
<p>インポートしたい Vue コンポーネントを配置するディレクトリ</p>
<h3>composables</h3>
<p>各コンポーネントを記入しなくても、Composables ディレクトリを使って自動的にインポートできる</p>
<p>&nbsp;</p>
<h2>ネスト化されたコンポーネント</h2>
<p>ネスト化されたディレクトリにコンポーネントがある場合は、パス・ディレクトリのコンポーネントの名前を指定するだけで使用できます。</p>
<p>例えば、components/bar/foo.vue の場合、以下のようになります。</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;BarFoo /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</pre><p>アトミックデザインでディレクトリを分けてファイルを作成する際は&lt;AtomButton&gt;みたいな感じになりそうですね。ちょっと冗長になりそうな気もしています。</p>The post <a href="https://mintaku-blog.net/nuxt3-rc/">Nuxt3への移行作業時に気になったところをまとめてみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt3-rc/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2272</post-id>	</item>
		<item>
		<title>Vue.jsの特徴やライフサイクル、Vuexなどについて整理する</title>
		<link>https://mintaku-blog.net/about-vuejs/</link>
					<comments>https://mintaku-blog.net/about-vuejs/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 11 Jul 2021 01:25:39 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[まとめ系]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2022</guid>

					<description><![CDATA[<p>Vue.jsの特徴やライフサイクル、Vuexなどについて改めて理解を深めるためにも自分なりに整理しました。 Vue.jsの特徴 Progressive Fr …</p>
The post <a href="https://mintaku-blog.net/about-vuejs/">Vue.jsの特徴やライフサイクル、Vuexなどについて整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Vue.jsの特徴やライフサイクル、Vuexなどについて改めて理解を深めるためにも自分なりに整理しました。</p>
<h2>Vue.jsの特徴 Progressive Framework</h2>
<p><img data-attachment-id="2023" data-permalink="https://mintaku-blog.net/about-vuejs/the-progressive-framework/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?fit=960%2C720&amp;ssl=1" data-orig-size="960,720" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="The Progressive Framework" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?fit=300%2C225&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?fit=800%2C600&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?resize=800%2C600&#038;ssl=1" alt="" width="800" height="600" class="aligncenter size-full wp-image-2023" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?w=960&amp;ssl=1 960w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?resize=300%2C225&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/The-Progressive-Framework.png?resize=768%2C576&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>Vue.jsの特徴として、Progressive Frameworkという概念に基づいて開発されています。</p>
<p>ビルドシステムを利用せずにVue.js単体で使うことやVuexやvue-routerを使って動的なWebアプリケーションの構築もできます。</p>
<h3>Declative Rendering 宣言的なDOMレンダリングに関する領域</h3>
<p>テンプレートにレンダリングする対象を宣言的に記載することでDOMレンダリングやユーザによる入力データの同期が可能になります。Vue.jsでは本体自体でこの領域をサポートしているため、LPのようなシンプルなWebサイトはじめ、小規模的なものはこの領域で解決できます。</p>
<h3>Component System UIをモジュール化して再利用する必要がある領域</h3>
<p>Vue.js本体自体にコンポーネント化する機能があるため、それによって開発効率を向上させることが可能になります。</p>
<h3>Client-side Routing WebサイトがSPAとして必要とされるWebアプリケーションの領域</h3>
<p>Vue.jsが提供しているルーティングライブラリのvue-routerを利用することで、これまでに作成したコンポーネントでSPAに対応することが可能になります。</p>
<h3>Large-scale State Management コンポーネント間で状態の共有方法が必要となる領域</h3>
<p>Vue.jsが提供している状態管理ライブラリのVuexを利用することで、既存のコンポーネントを拡張する形で状態を集中管理することが可能になります。</p>
<h3>Build System コンポーネント管理やプロジェクト構成などの領域</h3>
<p>Vue.jsが提供しているvue-cliやvue-loaderなどを利用することで、プロジェクトの環境構築や構成管理の手間が省略かされ、継続的な開発を持続できるようになります。</p>
<p>&nbsp;</p>
<p>参考：<br />

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://vuex.vuejs.org/ja/">Vuex とは何か？ | Vuex</a>
			</p>
							<div class="ys-blog-card__dscr">
					Vue.js のための集中状態管理				</div>
										<div class="ys-blog-card__domain">vuex.vuejs.org</div>
					</div>
	</div>
</div>
<br />

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/gihyo.jp/assets/images/ICON/2016/1522_vuejs.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://gihyo.jp/dev/serial/01/vuejs/0001?page=2">第1回　プログレッシブフレームワーク Vue.js | gihyo.jp</a>
			</p>
							<div class="ys-blog-card__dscr">
					Vue.js（ビュージェイエス）は、インタラクティブなUIを構築するためのJav&hellip;				</div>
										<div class="ys-blog-card__domain">gihyo.jp</div>
					</div>
	</div>
</div>
<br />

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-user-contents.imgix.net%2Fhttps%253A%252F%252Fcdn.qiita.com%252Fassets%252Fpublic%252Farticle-ogp-background-afbab5eb44e0b055cce1258705637a91.png%3Fixlib%3Drb-4.0.0%26w%3D1200%26blend64%3DaHR0cHM6Ly9xaWl0YS11c2VyLXByb2ZpbGUtaW1hZ2VzLmltZ2l4Lm5ldC9odHRwcyUzQSUyRiUyRnFpaXRhLWltYWdlLXN0b3JlLnMzLmFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20lMkYwJTJGMjc5NzMlMkZwcm9maWxlLWltYWdlcyUyRjE1ODUxOTM3Mjk_aXhsaWI9cmItNC4wLjAmYXI9MSUzQTEmZml0PWNyb3AmbWFzaz1lbGxpcHNlJmJnPUZGRkZGRiZmbT1wbmczMiZzPTYzNmU3NDRlNzg4MDMxM2NjZDVmMTJlODJhZThkNDM1%26blend-x%3D120%26blend-y%3D467%26blend-w%3D82%26blend-h%3D82%26blend-mode%3Dnormal%26s%3D83413c28100a79c361c9eb154280a278?ixlib=rb-4.0.0&amp;w=1200&amp;fm=jpg&amp;mark64=aHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjAuMCZ3PTk2MCZoPTMyNCZ0eHQ9VnVlLmpzJTIwUHJvZ3Jlc3NpdmUlMjBGcmFtZXdvcmslMjAmdHh0LWFsaWduPWxlZnQlMkN0b3AmdHh0LWNvbG9yPSUyMzFFMjEyMSZ0eHQtZm9udD1IaXJhZ2lubyUyMFNhbnMlMjBXNiZ0eHQtc2l6ZT01NiZ0eHQtcGFkPTAmcz04ZWY3OTEzNWJjMGE3NjMzNGNmODdhMmFiMDAxMzBjNQ&amp;mark-x=120&amp;mark-y=112&amp;blend64=aHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjAuMCZ3PTgzOCZoPTU4JnR4dD0lNDBtaWtha2FuZSZ0eHQtY29sb3I9JTIzMUUyMTIxJnR4dC1mb250PUhpcmFnaW5vJTIwU2FucyUyMFc2JnR4dC1zaXplPTM2JnR4dC1wYWQ9MCZzPWVhYTUxZDEzZjFhZmM5ZjU2OTk4YTM0MTE2MjBmMWEz&amp;blend-x=242&amp;blend-y=480&amp;blend-w=838&amp;blend-h=46&amp;blend-fit=crop&amp;blend-crop=left%2Cbottom&amp;blend-mode=normal&amp;s=3023216ec89ed77c2258ebed1b790de4" alt="">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://qiita.com/mikakane/items/3bd6af69259f5af6fecb">Vue.js Progressive Framework  #Vue.js - Qiita</a>
			</p>
							<div class="ys-blog-card__dscr">
					Vue.js の作者Evan氏によるVue.jsの紹介スライドで触れられていた「&hellip;				</div>
										<div class="ys-blog-card__domain">qiita.com</div>
					</div>
	</div>
</div>
<br />
<a href="https://amzn.to/3k4ooBe" target="_blank" rel="noopener">Hello!! Vue.js　最新プログレッシブフレームワーク入門</a></p>
<p>&nbsp;</p>
<h2>ライフサイクル</h2>
<p><img data-attachment-id="2024" data-permalink="https://mintaku-blog.net/about-vuejs/lifecycle/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?fit=1200%2C3039&amp;ssl=1" data-orig-size="1200,3039" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="lifecycle" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?fit=118%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?fit=404%2C1024&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?resize=404%2C1024&#038;ssl=1" alt="" width="404" height="1024" class="aligncenter size-large wp-image-2024" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?resize=404%2C1024&amp;ssl=1 404w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?resize=118%2C300&amp;ssl=1 118w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?resize=768%2C1945&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?resize=809%2C2048&amp;ssl=1 809w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/lifecycle.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 404px) 100vw, 404px" data-recalc-dims="1" /></p>
<h3>ライフサイクルフック</h3>
<p>Vueのインスタンスは、インスタンスを生成時やDOM要素へのマウント時、データ更新時など、ある特定のタイミングでユーザが処理を行えるようにいくつかのライフサイクルフックが実行されます。</p>
<h4>beforeCreate</h4>
<p>インスタンスの初期化時に実行されます。dataなどのプロパティは生成前のためアクセスできません。</p>
<h4>created</h4>
<p>インスタンスの初期化後に実行されます。この時点でプロパティへのアクセスが可能になります。</p>
<h4>beforeMount</h4>
<p>マウント前に実行されます。この時点ではDOM要素にはアクセスできません。</p>
<h4>mounted</h4>
<p>マウント後に実行されます。この時点でDOM要素へのアクセスが可能になります。</p>
<h4>beforeUpdate</h4>
<p>DOM更新前に実行されます。</p>
<h4>update</h4>
<p>DOM更新後に実行されます。</p>
<h4>beforeDestroy</h4>
<p>インスタンスの破棄の前に実行されます。この時点ではプロパティへのアクセスは可能です。</p>
<h4>destroyed</h4>
<p>インスタンスが破棄された後に実行されます。この時点からプロパティへのアクセスはできません。</p>
<p>&nbsp;</p>
<h3>ライフサイクルフックの動きを確認する</h3>
<p>実際にいくつかのライフサイクルフックを使って動きを確認してみました。</p>
<h4>beforeCreate</h4>
<p>インスタンス初期化時に実行されます。このタイミングでは、インスタンスは初期化前ですので、msgは呼ばれません。</p><pre class="crayon-plain-tag">&lt;script&gt;
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  beforeCreate () {
    console.log('beforeCreate：' + this.msg)
  }
}
&lt;/script&gt;</pre><p></p><pre class="crayon-plain-tag">// beforeCreate：undefined</pre><p></p>
<h4>created</h4>
<p>インスタンス生成後に実行されるため、msgが呼び出されます。</p><pre class="crayon-plain-tag">&lt;script&gt;
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  created () {
    console.log('created：' + this.msg)
  }
}
&lt;/script&gt;</pre><p></p><pre class="crayon-plain-tag">// created：Welcome to Your Vue.js App</pre><p>&nbsp;</p>
<h4>beforeMount</h4>
<p>インスタンスが作成された後、elementへのマウントされる前で実行されます。</p>
<p>そのためmsgは更新されず、「Welcome to Your Vue.js App」と表示されます。</p><pre class="crayon-plain-tag">...

  &lt;h1&gt;{{ msg }}&lt;/h1&gt;

...

&lt;script&gt;
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  beforeMount () {
    this.msg = 'Update Your Vue.js App'
  }
}
&lt;/script&gt;</pre><p></p><pre class="crayon-plain-tag">// Welcome to Your Vue.js App</pre><p>&nbsp;</p>
<h4>mounted</h4>
<p>elementへのマウントがされた後に実行されるため、msgが更新され「Update Your Vue.js App」と表示されます。</p>
<p>createdとの違いとしてmountedは、elementへのマウントが行われた後処理されます。また、mountedはSSRの場合は使えないので注意が必要です</p><pre class="crayon-plain-tag">...


  &lt;h1&gt;{{ msg }}&lt;/h1&gt;

...

&lt;script&gt;
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  mounted () {
    this.msg = 'Update Your Vue.js App'
  }
}
&lt;/script&gt;</pre><p></p><pre class="crayon-plain-tag">// Update Your Vue.js App</pre><p>&nbsp;</p>
<p>参考：<br />

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/jp.vuejs.org/images/logo.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://jp.vuejs.org/v2/guide/instance.html">Vue インスタンス — Vue.js</a>
			</p>
							<div class="ys-blog-card__dscr">
					Vue.js - The Progressive JavaScript Fram&hellip;				</div>
										<div class="ys-blog-card__domain">jp.vuejs.org</div>
					</div>
	</div>
</div>
<br />

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i.vimeocdn.com/video/699168105-5c72ea3b44769bd02e93b27405737287cc7693eeda72cc92346b2c7d02382d9e-d_1280x720?r=pad" alt="">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://vueschool.io/lessons/understanding-the-vuejs-lifecycle-hooks?friend=vuejs">Understanding the Vue.js Lifecycle Hooks</a>
			</p>
							<div class="ys-blog-card__dscr">
					Each Vue instance has many lifecycle hoo&hellip;				</div>
										<div class="ys-blog-card__domain">vueschool.io</div>
					</div>
	</div>
</div>
</p>
<p>&nbsp;</p>
<h2>Vuex</h2>
<p><img data-attachment-id="2025" data-permalink="https://mintaku-blog.net/about-vuejs/vuex/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/vuex.png?fit=701%2C551&amp;ssl=1" data-orig-size="701,551" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="vuex" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/vuex.png?fit=300%2C236&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/vuex.png?fit=701%2C551&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/vuex.png?resize=701%2C551&#038;ssl=1" alt="" width="701" height="551" class="aligncenter size-full wp-image-2025" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/vuex.png?w=701&amp;ssl=1 701w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/07/vuex.png?resize=300%2C236&amp;ssl=1 300w" sizes="(max-width: 701px) 100vw, 701px" data-recalc-dims="1" /></p>
<p>VuexとはVue.js アプリケーションのための 状態管理パターン + ライブラリです。</p>
<p>Vuexを使うことで、複雑に構成されたコンポーネントでのデータ管理の難しさを解決することができます。</p>
<p>Vuexという一つの入れ物にデータを入れることで、どのコンポーネントからでもVuex内に保持するデータへのアクセスが可能になります。</p>
<h3>Actions</h3>
<p>API等と非同期通信を行い、データを取得します。取得後はデータをコミットし、Mutationsを呼び出します。</p>
<h3>Mutations</h3>
<p>Actionsやコンポーネントからデータがコミットされた時に呼び出されます。唯一State内のデータを変更することが出来ます。</p>
<h3>State</h3>
<p>各コンポーネントで使用するデータはここに集約します。</p>
<h3>Vuexの使いどころ</h3>
<p>Vuexの使いどころについては以下の記事がとても参考になりました。</p>
<p>そもそもVuexを使うべきか、使うとしてもOrganismレベル以上のコンポーネントからのみStoreの参照、変更を許可するなど、どういったルールで運用すべきかを考慮することが大切です。</p>
<p>参考：<br />

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://python.ms/vuex/#vuex-%E4%BB%A5%E5%A4%96%E3%81%AE%E3%82%A2%E3%83%95%E3%82%9A%E3%83%AD%E3%83%BC%E3%83%81">なんで Vuex はなるべく避けるの？ | 民主主義に乾杯</a>
			</p>
							<div class="ys-blog-card__dscr">
					副作用を持ち、意識する範囲が広がるから。				</div>
										<div class="ys-blog-card__domain">python.ms</div>
					</div>
	</div>
</div>
</p>The post <a href="https://mintaku-blog.net/about-vuejs/">Vue.jsの特徴やライフサイクル、Vuexなどについて整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/about-vuejs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2022</post-id>	</item>
		<item>
		<title>【Nuxt.js】JestでTDDを実践してみる</title>
		<link>https://mintaku-blog.net/jest-tdd/</link>
					<comments>https://mintaku-blog.net/jest-tdd/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 27 Jun 2021 13:42:53 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[TDD]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2013</guid>

					<description><![CDATA[<p>JestでTDDを実践しながらNuxt.jsで簡単なToDoリストをつくってみました。 ToDoリストのストーリーを考える Nuxt.jsで簡単なToDoリ …</p>
The post <a href="https://mintaku-blog.net/jest-tdd/">【Nuxt.js】JestでTDDを実践してみる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>JestでTDDを実践しながらNuxt.jsで簡単なToDoリストをつくってみました。</p>
<h2>ToDoリストのストーリーを考える</h2>
<p>Nuxt.jsで簡単なToDoリストをつくりにあたり、ユーザーストーリーを考えます。考えた結果、以下のストーリーができました。</p>
<ul>
<li>ユーザーはフォームと作成ボタンを見ることができる</li>
<li>ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる</li>
<li>ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる</li>
<li>ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない</li>
<li>ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる</li>
<li>ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる</li>
</ul>
<p>ToDoリストの画面は、TaskFormコンポーネントとTaskListコンポーネントに分けて実装します。</p>
<p>またタスクのデータはFirestoreに格納するようにし、REST APIでデータを操作します。</p>
<p>Nuxt.jsやJestのインストールやFirestoreのセットアップなどは省略しています。</p>
<p>&nbsp;</p>
<h2>「ユーザーはフォームと作成ボタンを見ることができる」のテストを書く</h2>
<p>まず「ユーザーはフォームと作成ボタンを見ることができる」のテストを書いていきます。</p>
<p>TaskFormコンポーネントを作成し、タスク入力フォームと作成ボタンが存在することをチェックします。</p>
<p>・components/TaskForm.spec.js</p><pre class="crayon-plain-tag">import { mount } from '@vue/test-utils'
import TaskForm from '@/components/TaskForm.vue'

describe('components/TaskForm.vue', () =&gt; {
  let wrapper
  beforeEach(() =&gt; {
    wrapper = mount(TaskForm)
  })

  describe('template', () =&gt; {
    test('タスク入力フォームが存在すること', () =&gt; {
      const form = wrapper.find('input');
      expect(form.exists()).toBe(true);
    })

    test('タスク作成ボタンが存在すること', () =&gt; {
      const button = wrapper.find('input');
      expect(button.exists()).toBe(true);
   })
  })
})</pre><p>テストを作成したら実際にテストを実行してみます。TDDでいうレッドのところです。</p>
<p>まだ実装はしていないので全部のテストが失敗しますが、期待通りの落ち方をしているかを確認します。</p><pre class="crayon-plain-tag">FAIL  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✕ タスク入力フォームが存在すること (10 ms)
      ✕ タスク作成ボタンが存在すること (1 ms)

  ● components/TaskForm.vue &gt; template &gt; タスク入力フォームが存在すること

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      16 |     test('タスク入力フォームが存在すること', () =&gt; {
      17 |       const form = wrapper.find('input');
    &gt; 18 |       expect(form.exists()).toBe(true);
         |                             ^
      19 |     })
      20 |
      21 |     test('タスク作成ボタンが存在すること', () =&gt; {

      at Object.&lt;anonymous&gt; (components/TaskForm.spec.js:18:29)

  ● components/TaskForm.vue &gt; template &gt; タスク作成ボタンが存在すること

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      21 |     test('タスク作成ボタンが存在すること', () =&gt; {
      22 |       const button = wrapper.find('input');
    &gt; 23 |       expect(button.exists()).toBe(true);
         |                               ^
      24 |     })
      25 |
      26 |   })

      at Object.&lt;anonymous&gt; (components/TaskForm.spec.js:23:31)</pre><p>存在していない、実装されていないなどの落ち方をしていればひとまず完了です。</p>
<p>&nbsp;</p>
<h2>「ユーザーはフォームと作成ボタンを見ることができる」を実装する</h2>
<p>テストで書いたシナリオを元にTaskFormコンポーネントを実装してきます。今回は簡易的なHTMLでフォームとボタンを作成しました。</p>
<p>・components/TaskForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;input&gt;
    &lt;button&gt;
      タスク作成
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームと作成ボタンを見ることができる」をテストする</h2>
<p>実装が終わったら、再度TaskFormのテストを流してみます。TDDでいうグリーンのところです。</p>
<p>テストが全て通ったらこのストーリーの実装は完了です。必要に応じてリファクタリングをします。</p><pre class="crayon-plain-tag">PASS  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✓ タスク入力フォームが存在すること (9 ms)
      ✓ タスク作成ボタンが存在すること (1 ms)</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書く</h2>
<p>次に「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書いていきます。</p>
<p>TaskListコンポーネントでToDoリストと完了リストを表示するようにします。</p>
<p>そのため、ToDoリストと完了リストにFirestoreに格納されているデータをfetchしてくるようにします。</p>
<p>・components/TaskList.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue} from '@vue/test-utils'
import * as indexStore from '@/store'
import TaskList from '@/components/TaskList.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskList.vue', () =&gt; {
  let wrapper
  let store
  let todoTask
  let doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(indexStore)
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
    wrapper = mount(TaskList, {
      store: store,
      localVue
    })
    store.replaceState({ tasks: [todoTask, doneTask] })
  })

  describe('template', () =&gt; {
    test('todoリストが表示されること', () =&gt; {
      const li = wrapper.find('li.todo')
      expect(li.find('span').text()).toBe(todoTask.content)
      expect(li.find('button).text()).toBe('完了')
    })

    test('完了リストが表示されること', () =&gt; {
      const li = wrapper.find('li.done')
      expect(li.find('span').text()).toBe(doneTask.content)
      expect(li.find('button).text()).toBe('ToDoに戻す')
    })
  })

  describe('script', () =&gt; {
    describe('computed', () =&gt; {
      describe('todos', () =&gt; {
        test('storeからtodoTasksが取得できること', () =&gt; {
          expect(wrapper.vm.todoTasks).toEqual(expect.arrayContaining([todoTask]))
        })
        test('storeからdoneTasksが取得できること', () =&gt; {
          expect(wrapper.vm.doneTasks).toEqual(expect.arrayContaining([doneTask]))
        })
      })
    })
  })
})</pre><p>タスクリストの状態管理をするストアのテストを書きます。</p>
<p>ステータスの状態によってToDoタスクと完了タスクを出し分けるようにします。</p>
<p>Storeではaxiosを使って外部サービスにHTTPリクエストしているためaxiosをモック化してテストしています。</p>
<p>async/awaitでaxiosの非同期処理を実現し、結果をgettersから取り出し比較します。</p>
<p>参考：https://github.com/nuxt-community/axios-module/issues/105</p>
<p>・store/index.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import * as index from '@/store'
import { createLocalVue } from '@vue/test-utils'
import _ from 'lodash'
import axios from 'axios'

const localVue = createLocalVue()
localVue.use(Vuex)

let mockAxiosGetResult
jest.mock('axios', () =&gt; ({
  get: jest.fn(() =&gt; Promise.resolve(mockAxiosGetResult))
}))

let action
const testedAction = (context = {}, payload = {}) =&gt; {
  return index.actions[action].bind({ $axios: axios })(context, payload)
}

describe('store/index.js', () =&gt; {
  let store
  let todoTask, doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(_.cloneDeep(index))
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
  })

  describe('getters', () =&gt; {
    let tasks
    let todoTasks
    let doneTasks
    beforeEach(() =&gt; {
      tasks = [todoTask, doneTask]
      todoTasks = [todoTask]
      doneTasks = [doneTask]
      store.replaceState({
        tasks,
        todoTasks,
        doneTasks
      })
    })

    describe('todoTasks', () =&gt; {
      test('statusがtodoのtaskが取得できること', () =&gt; {
        expect(store.getters['todoTasks']).toContainEqual(todoTask)
        expect(store.getters['todoTasks']).not.toContainEqual(doneTask)
      })
    })
    describe('doneTasks', () =&gt; {
      test('statusがdoneのtaskが取得できること', () =&gt; {
        expect(store.getters['doneTasks']).not.toContainEqual(todoTask)
        expect(store.getters['doneTasks']).toContainEqual(doneTask)
      })
    })
    describe('tasks', () =&gt; {
      test('すべてのtasksが取得できること', () =&gt; {
        expect(store.getters['tasks']).toEqual(
          expect.arrayContaining(tasks)
        )
      })
    })
  })

  describe('actions', () =&gt; {
    let commit
    beforeEach(() =&gt; {
      commit = store.commit
    })

    describe('fetchTasks', () =&gt; {
      test('tasksが取得できること', async () =&gt; {
        action = 'fetchTasks'
        mockAxiosGetResult = {
          data: {
            documents: [
              {
                name: `tasks/${todoTask.id}`,
                fields: {
                  content: { stringValue: todoTask.content },
                  status: { stringValue: todoTask.status }
                }
              },
              {
                name: `tasks/${doneTask.id}`,
                fields: {
                  content: { stringValue: doneTask.content },
                  status: { stringValue: doneTask.status }
                }
              }
            ]
          }
        }

        await testedAction({ commit })
        expect(store.getters['tasks']).toEqual([todoTask, doneTask])
      })
    })
  })
})</pre><p>テスト結果を省略していますが、ここでまたテストを実行し期待通りに落ちているかを確認します。</p>
<p>&nbsp;</p>
<h2>「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」を実装する</h2>
<p>テストが書いたシナリオを元にTaskListコンポーネントとindexページ、indexストアを実装してきます。</p>
<p>TaskListコンポーネントではToDoリストと完了リストがあり、indexページではTaskListコンポーネントとTaskFormコンポーネントを呼び出してタスクをストアから呼び出すようにします。</p>
<p>indexストアではFirestoreからデータを取ってきてToDoタスクと完了タスクに状態管理するようにします。</p>
<p>・components/TaskList.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;ToDoリスト&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="todoTask in todoTasks"
        :key="todoTask.id"
        class="todo"
      &gt;
        &lt;span&gt;{{ todoTask.content }}&lt;/span&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;hr&gt;
    &lt;p&gt;完了リスト&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="doneTask in doneTasks"
        :key="doneTask.id"
        class="done"
      &gt;
        &lt;span&gt;{{ doneTask.content }}&lt;/span&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters } from 'vuex'
import _ from 'lodash'

export default {
  computed: {
    ...mapGetters(['todoTasks']),
    ...mapGetters(['doneTasks'])
  }
}
&lt;/script&gt;</pre><p>・pages/index.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;section&gt;
    &lt;TaskForm/&gt;
    &lt;TaskList/&gt;
  &lt;/section&gt;
&lt;/template&gt;

&lt;script&gt;
import TaskForm from '@/components/TaskForm.vue'
import TaskList from '@/components/TaskList.vue'

export default {
  async asyncData({ store }) {
    await store.dispatch('fetchTasks')
  },
  components: {
    TaskForm,
    TaskList
  }
}
&lt;/script&gt;</pre><p><span style="font-weight: 400;">・store/index.spec.js</span></p><pre class="crayon-plain-tag">import _ from 'lodash'

export const state = () =&gt; ({
  tasks: []
})

export const getters = {
  tasks: state =&gt; state.tasks,
  todoTasks: state =&gt; state.tasks.filter(task =&gt; task.status === 'todo'),
  doneTasks: state =&gt; state.tasks.filter(task =&gt; task.status === 'done')
}

export const mutations = {
  updateTasks(state, tasks) {
    state.tasks = tasks
  }
}

export const actions = {
  async fetchTasks({ commit }) {
    await this.$axios.get(`/tasks`, {
      baseURL: process.env._AXIOS_BASE_URL_
    })
      .then(res =&gt; {
      let tasks = []
      if (_.has(res.data, 'documents')) {
        tasks = res.data.documents.map(doc =&gt; {
          return {
            id: _.last(doc.name.split('/')),
            content: doc.fields.content.stringValue,
            status: doc.fields.status.stringValue
          }
        })
      }
      commit('updateTasks', tasks)
    })
  }
}</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」をテストする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskList.spec.js
  components/TaskList.vue
    template
      ✓ todoリストが表示されること (13 ms)
      ✓ 完了リストが表示されること (2 ms)
    script
      computed
        todos
          ✓ storeからtodoTasksが取得できること (2 ms)
          ✓ storeからdoneTasksが取得できること (1 ms)</pre><p></p><pre class="crayon-plain-tag">PASS  store/index.spec.js
  store/index.js
    getters
      todoTasks
        ✓ statusがtodoのtaskが取得できること (3 ms)
      doneTasks
        ✓ statusがdoneのtaskが取得できること (1 ms)
      tasks
        ✓ すべてのtasksが取得できること (1 ms)
    actions
      fetchTasks
        ✓ tasksが取得できること</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」のテストを書く</h2>
<p>先ほどTaskFormコンポーネントに作成したフォームに入力して作成ボタンを押すとToDoタスク一覧に追加されることが確認できるテストを書きます。</p>
<p>・components/TaskForm.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as store from '@/store'
import TaskForm from '@/components/TaskForm.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskForm.vue', () =&gt; {
  let wrapper
  beforeEach(() =&gt; {
    wrapper = mount(TaskForm, {
      store: store,
      localVue
    })
  })

  describe('template', () =&gt; {
    
...

    describe('タスク入力がある場合', () =&gt; {
      beforeEach(() =&gt; {
        const mock = jest.fn()
        wrapper.vm.addTask = mock
        wrapper.find('input').setValue('content')
      })

      test('dataに入力が反映されること', () =&gt; {
        expect(wrapper.vm.taskForm.content).toBe('content')
      })

      test('タスク作成ボタンを押下するとaddTaskが呼ばれること', () =&gt; {
        wrapper.find('button').trigger('click')
        
        expect(wrapper.vm.addTask).toHaveBeenCalled()
      })
    })
  })

  describe('script', () =&gt; {
    describe('data', () =&gt; {
      test('dataの構造が正しいこと', () =&gt; {
        expect(wrapper.vm.$data).toHaveProperty('taskForm.content')
      })
    })
  })
})</pre><p>ストアのテストでは作成ボタンが押下された際にFirestoreに入力されたタスクのデータが保存されることを確認します。</p>
<p>・store/index.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import * as index from '@/store'
import { createLocalVue } from '@vue/test-utils'
import _ from 'lodash'
import axios from 'axios'

const localVue = createLocalVue()
localVue.use(Vuex)

let mockAxiosGetResult
jest.mock('axios', () =&gt; ({
  
...

  post: jest.fn(() =&gt; Promise.resolve(mockAxiosGetResult))
}))

let action
const testedAction = (context = {}, payload = {}) =&gt; {
  return index.actions[action].bind({ $axios: axios })(context, payload)
}

describe('store/index.js', () =&gt; {

...

  describe('actions', () =&gt; {
    let commit
    beforeEach(() =&gt; {
      commit = store.commit
    })

...

    describe('createTask', () =&gt; {
      test('taskが追加されること', async () =&gt; {
        mockAxiosGetResult = {
          data: {
            name: `tasks/${todoTask.id}`,
            fields: {
              content: { stringValue: todoTask.content },
              status: { stringValue: todoTask.status }
            }
          }
        }
        action = 'createTask'
        await testedAction({ commit })
        expect(store.getters['todoTasks']).toContainEqual(todoTask)
      })
    })
  })
})</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」を実装する</h2>
<p>タスク作成ボタンが押されるとaddTaskが呼び出されるように実装していきます。</p>
<p>addTaskメソッドはストアのcreateTaskを呼び出し、タスクが状態管理されるようにします。</p>
<p>・components/TaskForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;input
      v-model="taskForm.content"
    &gt;
    &lt;button
      @click="addTask"&gt;
      タスク作成
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import _ from 'lodash'

export default {
  data() {
    return {
      taskForm: { content: '' }
    }
  },
  methods: {
    async addTask() {
        await this.$store.dispatch('createTask', _.cloneDeep(this.taskForm))
        this.taskForm.content = ''
    }
  }
}
&lt;/script&gt;</pre><p>addTaskから呼びだされるcreateTaskの実装をストアに書いていきます。</p>
<p>・store/index.js</p><pre class="crayon-plain-tag">import _ from 'lodash'

export const state = () =&gt; ({
  tasks: []
})

export const getters = {
  tasks: state =&gt; state.tasks,

...

}

export const mutations = {

  ...

  addTask(state, newTask) {
    state.tasks.push(newTask)
  }

...

}

export const actions = {
  async createTask({ commit }, payload) {
    const req = {
      fields: {
        content: {
          stringValue: payload.content
        },
        status: {
          stringValue: 'todo'
        }
      }
    }
    await this.$axios.post('/tasks', req, {
      baseURL: process.env._AXIOS_BASE_URL_
    }).then(res =&gt; {
      const newTask = {
        id: _.last(res.data.name.split('/')),
        content: res.data.fields.content.stringValue,
        status: res.data.fields.status.stringValue
      }
      commit('addTask', newTask)
    })
  }
 }</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」をテストする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✓ タスク入力フォームが存在すること (11 ms)
      ✓ タスク作成ボタンが存在すること (6 ms)
      タスク入力がある場合
        ✓ dataに入力が反映されること (3 ms)
        ✓ タスク作成ボタンを押下するとaddTaskが呼ばれること (2 ms)
    script
      data
        ✓ dataの構造が正しいこと (1 ms)</pre><p></p><pre class="crayon-plain-tag">PASS  store/index.spec.js
  store/index.js
    getters
      todoTasks
        ✓ statusがtodoのtaskが取得できること (3 ms)
      doneTasks
        ✓ statusがdoneのtaskが取得できること (1 ms)
      tasks
        ✓ すべてのtasksが取得できること (1 ms)
    actions
      fetchTasks
        ✓ tasksが取得できること (1 ms)
      createTask
        ✓ taskが追加されること (1 ms)</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」のテストを書く</h2>
<p>フォームにタスクが入力していない場合は、作成ボタンを押すことができないことを確認できるテストを書いていきます。</p>
<p>・components/TaskForm.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as store from '@/store'
import TaskForm from '@/components/TaskForm.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskForm.vue', () =&gt; {
  let wrapper
  beforeEach(() =&gt; {
    wrapper = mount(TaskForm, {
      store: store,
      localVue
    })
  })


  describe('template', () =&gt; {

    ...

    describe('タスク入力がない場合', () =&gt; {
      beforeEach(() =&gt; {
        const mock = jest.fn()
        wrapper.vm.addTask = mock
        wrapper.find('input').setValue('')
      })

      test('dataに入力が反映されること', () =&gt; {
        expect(wrapper.vm.taskForm.content).toBe('')
      })

      test('タスク作成ボタンを押下するとaddTaskが呼ばれないこと', () =&gt; {
        wrapper.find('button').trigger('click')
        
        expect(wrapper.vm.addTask).not.toHaveBeenCalled()
      })
    })
  
  })

 ...

})</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」を実装する</h2>
<p>ボタンにdisabled属性を付与し、computedでフォームの入力を監視してボタンの活性非活性を制御するように実装しました。</p>
<p>・components/TaskForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;input
      v-model="taskForm.content"
    &gt;
    &lt;button
      :disabled="activateSubmit"
      @click="addTask"&gt;
      タスク作成
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import _ from 'lodash'

export default {
  
...

  computed: {
    activateSubmit () {
      return this.taskForm.content == ''
    }
  },
  
...

}
&lt;/script&gt;</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」をテストする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✓ タスク入力フォームが存在すること (9 ms)
      ✓ タスク作成ボタンが存在すること (1 ms)
      タスク入力がある場合
        ✓ dataに入力が反映されること (2 ms)
        ✓ タスク作成ボタンを押下するとaddTaskが呼ばれること (2 ms)
      タスク入力がない場合
        ✓ dataに入力が反映されること (1 ms)
        ✓ タスク作成ボタンを押下するとaddTaskが呼ばれないこと (1 ms)
    script
      data
        ✓ dataの構造が正しいこと (1 ms)</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストを書く</h2>
<p>ToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できるテストを書いていきます。</p>
<p>・components/TaskList.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as indexStore from '@/store'
import TaskList from '@/components/TaskList.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskList.vue', () =&gt; {
  let wrapper
  let store
  let todoTask
  let doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(indexStore)
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
    wrapper = mount(TaskList, {
      store: store,
      localVue
    })
    store.replaceState({ tasks: [todoTask, doneTask] })
  })

  describe('template', () =&gt; {
   
...

    describe('todoリストの完了をクリックする場合', () =&gt; {
      test('doneTaskが指定の引数で呼び出されること', () =&gt; {
        const mock = jest.fn(todoTask =&gt; todoTask)
        wrapper.vm.doneTask = mock
     
        wrapper.find('li.todo button').trigger('click')
        expect(wrapper.vm.doneTask).toHaveBeenCalled()
        expect(wrapper.vm.doneTask.mock.results[0].value).toBe(todoTask)
      })
    })
  })

  ...

})</pre><p>ストアではタスクのステータスが更新されることが確認できるテストを書いていきます。</p>
<p>・store/index.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import * as index from '@/store'
import { createLocalVue } from '@vue/test-utils'
import _ from 'lodash'
import axios from 'axios'

const localVue = createLocalVue()
localVue.use(Vuex)

let mockAxiosGetResult
jest.mock('axios', () =&gt; ({
  
...

  patch: jest.fn(() =&gt; Promise.resolve(mockAxiosGetResult))
}))

let action
const testedAction = (context = {}, payload = {}) =&gt; {
  return index.actions[action].bind({ $axios: axios })(context, payload)
}

describe('store/index.js', () =&gt; {
  let store
  let todoTask, doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(_.cloneDeep(index))
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
  })


...

  describe('actions', () =&gt; {
    let commit
    beforeEach(() =&gt; {
      commit = store.commit
    })

   ...

    describe('updateTask', () =&gt; {
      beforeEach(() =&gt; {
        store.replaceState({
          tasks: [todoTask]
        })
      })

      test('taskが更新されること', async () =&gt; {
        mockAxiosGetResult = {
          data: {
            name: `tasks/${todoTask.id}`,
            fields: {
              content: { stringValue: 'updatedContent' },
              status: { stringValue: 'done' }
            }
          }
        }
        action = 'updateTask'
        await testedAction({ commit })
        expect(store.getters['doneTasks']).toContainEqual({
          id: todoTask.id,
          content: 'updatedContent',
          status: 'done'
        })
      })
    })
  })
})</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」を実装する</h2>
<p>ToDoタスクの完了ボタンを押すと、ストアのupdateTaskを呼び出しステータスを完了にすることで完了タスク一覧に移動されるように実装します。</p>
<p>・components/TaskList.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;ToDoタスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="todoTask in todoTasks"
        :key="todoTask.id"
        class="todo"
      &gt;
        &lt;span&gt;{{ todoTask.content }}&lt;/span&gt;
        &lt;button 
          @click="doneTask(todoTask)"&gt;
          完了
        &lt;/button&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;hr&gt;
    &lt;p&gt;完了タスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="doneTask in doneTasks"
        :key="doneTask.id"
        class="done"
      &gt;
        &lt;span&gt;{{ doneTask.content }}&lt;/span&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters } from 'vuex'
import _ from 'lodash'

export default {
  
...

  methods: {
    async doneTask(task) {
      const payload = _.cloneDeep(task)
      payload.status = 'done'
      await this.$store.dispatch('updateTask', payload)
    }
  }
}
&lt;/script&gt;</pre><p>・store/index.js</p><pre class="crayon-plain-tag">import _ from 'lodash'

export const state = () =&gt; ({
  tasks: []
})

export const getters = {
  tasks: state =&gt; state.tasks,

  ...

}

export const mutations = {
  
...

  updateTask(state, newTask) {
    const task = state.tasks.find(task =&gt; task.id === newTask.id)
    if (task) {
      task.content = newTask.content
      task.status = newTask.status
    }
  }
}

export const actions = {
 
...

  async updateTask({ commit }, payload) {
    const req = {
      fields: {
        content: {
          stringValue: payload.content
        },
        status: {
          stringValue: payload.status
        }
      }
    }
    await this.$axios.patch(`/tasks/${payload.id}`, req, {
      baseURL: process.env._AXIOS_BASE_URL_
    }).then(res =&gt; {
      const updateTask = {
        id: _.last(res.data.name.split('/')),
        content: res.data.fields.content.stringValue,
        status: res.data.fields.status.stringValue
      }
      commit('updateTask', updateTask)
    })
  }
}</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストをする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskList.spec.js
  components/TaskList.vue
    template
      ✓ todoリストが表示されること (17 ms)
      ✓ 完了リストが表示されること (2 ms)
      todoリストの完了をクリックする場合
        ✓ doneTaskが指定の引数で呼び出されること (3 ms)
    script
      computed
        todos
          ✓ storeからtodoTasksが取得できること (1 ms)
          ✓ storeからdoneTasksが取得できること (2 ms)</pre><p></p><pre class="crayon-plain-tag">PASS  store/index.spec.js
  store/index.js
    getters
      todoTasks
        ✓ statusがtodoのtaskが取得できること (3 ms)
      doneTasks
        ✓ statusがdoneのtaskが取得できること (1 ms)
      tasks
        ✓ すべてのtasksが取得できること
    actions
      fetchTasks
        ✓ tasksが取得できること (1 ms)
      createTask
        ✓ taskが追加されること
      updateTask
        ✓ taskが更新されること</pre><p>&nbsp;</p>
<h2>「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストを書く</h2>
<p>完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できることが確認できるテストを書いていきます。</p>
<p>ストアのタスクステータス更新のテストは先程のストーリーで確認済みなため、新しくストアのテストは書いていません。</p>
<p>・components/TaskList.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as indexStore from '@/store'
import TaskList from '@/components/TaskList.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskList.vue', () =&gt; {
  let wrapper
  let store
  let todoTask
  let doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(indexStore)
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
    wrapper = mount(TaskList, {
      store: store,
      localVue
    })
    store.replaceState({ tasks: [todoTask, doneTask] })
  })

  describe('template', () =&gt; {
   
...

   describe('完了リストのToDoに戻すをクリックする場合', () =&gt; {
      test('returnTaskが指定の引数で呼び出されること', () =&gt; {
        const mock = jest.fn(doneTask =&gt; doneTask)
        wrapper.vm.returnTask = mock
     
        wrapper.find('li.done button').trigger('click')
        expect(wrapper.vm.returnTask).toHaveBeenCalled()
        expect(wrapper.vm.returnTask.mock.results[0].value).toBe(doneTask)
      })
    })
 })

  ...

})</pre><p>&nbsp;</p>
<h2>「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」を実装する</h2>
<p>完了タスクのToDoに戻すボタンを押すと、ストアのupdateTaskを呼び出しステータスをToDoにすることでToDoタスク一覧に移動されるように実装します。</p>
<p>・components/TaskList.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;ToDoタスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="todoTask in todoTasks"
        :key="todoTask.id"
        class="todo"
      &gt;
        &lt;span&gt;{{ todoTask.content }}&lt;/span&gt;
        &lt;button 
          @click="doneTask(todoTask)"&gt;
          完了
        &lt;/button&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;hr&gt;
    &lt;p&gt;完了タスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="doneTask in doneTasks"
        :key="doneTask.id"
        class="done"
      &gt;
        &lt;span&gt;{{ doneTask.content }}&lt;/span&gt;
        &lt;button 
          @click="returnTask(doneTask)"&gt;
          ToDoに戻す
        &lt;/button&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters } from 'vuex'
import _ from 'lodash'

export default {

...

  methods: {

    ...

    async returnTask(task) {
      const payload = _.cloneDeep(task)
      payload.status = 'todo'
      await this.$store.dispatch('updateTask', payload)
    }
  }
}
&lt;/script&gt;</pre><p>&nbsp;</p>
<h2>「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストをする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskList.spec.js
  components/TaskList.vue
    template
      ✓ todoリストが表示されること (12 ms)
      ✓ 完了リストが表示されること (3 ms)
      todoリストの完了をクリックする場合
        ✓ doneTaskが指定の引数で呼び出されること (3 ms)
      完了リストのToDoに戻すをクリックする場合
        ✓ returnTaskが指定の引数で呼び出されること (2 ms)
    script
      computed
        todos
          ✓ storeからtodoTasksが取得できること (1 ms)
          ✓ storeからdoneTasksが取得できること (5 ms)</pre><p>&nbsp;</p>
<h2>全部のテストを流す</h2>
<p>全部の実装が終わったら、全てのテストを流します。package.jsonのscriptに&#8221;test&#8221;: &#8220;jest &#8211;config jest.config.js&#8221;を追加し、以下のコマンドで実行します。</p><pre class="crayon-plain-tag">$ npm run test

PASS components/TaskForm.spec.js
PASS components/TaskList.spec.js
PASS store/index.spec.js</pre><p>&nbsp;</p>
<p>全てのテストが通ったら完了です。</p>The post <a href="https://mintaku-blog.net/jest-tdd/">【Nuxt.js】JestでTDDを実践してみる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/jest-tdd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2013</post-id>	</item>
		<item>
		<title>【Nuxt.js】ルートを拡張してページネーションを静的化する</title>
		<link>https://mintaku-blog.net/ssg-pagination/</link>
					<comments>https://mintaku-blog.net/ssg-pagination/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 28 Apr 2021 02:50:38 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[Vue.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1912</guid>

					<description><![CDATA[<p>本一覧ページを例にルートを拡張してページネーションを静的化する処理を実装していきます。実際のページネーションの処理は省略しています。 ルートを拡張する nu …</p>
The post <a href="https://mintaku-blog.net/ssg-pagination/">【Nuxt.js】ルートを拡張してページネーションを静的化する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>本一覧ページを例にルートを拡張してページネーションを静的化する処理を実装していきます。実際のページネーションの処理は省略しています。</p>
<h2>ルートを拡張する</h2>
<p>nuxt.config.jsのrouterプロパティにextendRoutesオプションを追加することで、Nuxt.js によって作成されたルートを拡張することができます。今回は本一覧ページにページネーションを拡張するので以下のような実装になります。</p>
<p>&nbsp;</p>
<p>・nuxt.config.js</p><pre class="crayon-plain-tag">...

  router: {
    extendRoutes (routes, resolve) {
      routes.push({
        path: '/books/page/:p',
        component: resolve(__dirname, 'pages/books/index.vue'),
        name: 'books-pager'
      })
    }
  },

...</pre><p>&nbsp;</p>
<p>ちなみに/libraries/1(ライブラリID)/books/page/1のように間にID(例ではライブラリID)を挟んでルートを拡張したい場合は以下のような感じになります。page構成はpages/libraries/_libraries/reviews.vueでファイルを作成します。</p><pre class="crayon-plain-tag">...

  router: {
    extendRoutes (routes, resolve) {
      routes.push({
        path: '/libraries/:libraries/books/page/:p',
        component: resolve(__dirname, 'pages/libraries/_libraries/books.vue'),
        name: 'libraries-books-pager'
      })
    }
  },

...</pre><p>&nbsp;</p>
<p>・extendRoutesオプション</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/v2.nuxt.com/preview.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://ja.nuxtjs.org/docs/2.x/configuration-glossary/configuration-router/#extendroutes">Nuxt 2 - router プロパティ</a>
			</p>
							<div class="ys-blog-card__dscr">
					router プロパティを使って Nuxt ルーターをカスタマイズできます。				</div>
										<div class="ys-blog-card__domain">ja.nuxtjs.org</div>
					</div>
	</div>
</div>

<p>&nbsp;</p>
<h2>generateのタイミングでページネーションに必要なデータを渡す</h2>
<p>ページネーション用にルートを拡張したら、必要なページの生成とページごとのデータをpayloadで渡していきます。例として本のデータと総件数をもとにページの生成をしています。</p>
<p>10のところは1ページあたりに表示するデータ数で、とりあえずそのまま書いてますがconfigファイルを用意して設定しておきましょう。総件数を10で割った余りが0とそうではない場合で条件分岐し、その回数分ループするようにしています。これは例えば総件数が20件の場合は10で割った余りが0であり、20 / 10で2回分ループします。総件数が21件の場合は10で割った余りが0でないため(20 / 10) + 1となり3回分ループします。1ページに表示できる件数は10件までであるため、このようなロジックになります。</p><pre class="crayon-plain-tag">...

  const books = … // 取得処理省略
  const booksTotalCount = ... // 取得処理省略

  const loops = booksTotalCount % 10 === 0
    ? booksTotalCount / 10
    : parseInt(booksTotalCount / 10) + 1

  return [...Array(loops)].map((_, index) =&gt; ({
    route: `/books/page/${index + 1}`,
    payload: {
      books,
      totalCount: booksTotalCount
    }
  }))

...</pre><p>&nbsp;</p>
<h2>generateで渡されたデータをそれぞれのページで受け取る</h2>
<p>ページネーションのページごとにpayloadで渡されたデータから表示分のデータと総件数をstoreにセットします。mapGettersでstoreからデータを取り出してPagerコンポーネントにデータを渡しています(Pagerコンポーネントの実装部分は省略)。payloadで渡されない場合は別途APIから取得するようにしています。</p>
<p>&nbsp;</p>
<p>・pages/books/index.vue</p><pre class="crayon-plain-tag">…

  &lt;Pager
    v-if="totalCount"
    :page-number="param"
    :per-page="perPage"
    :total-count="totalCount"
    :base-path="'/books'"
  /&gt;

…

&lt;script&gt;
import { mapGetters } from 'vuex'
export default {
  async asyncData ({ store, payload, params }) {
    const perPage = 10
    const param = Number(params.p) || 1
    const offset = perPage * (param - 1)
    if (payload) {
      store.commit('books/setBooks', { books: payload.books.slice(offset, offset + perPage) })
      store.commit('books/setTotalCount', { totalCount: payload.totalCount })
    } else {
      await Promise.all([
      store.dispatch('books/fetchBooks', { limit: perPage, offset }),
    ])
  }
  return {
    param,
    perPage
    }
  },
  computed: {
    ...mapGetters('books', ['books']),
    ...mapGetters('books', ['totalCount'])
  }
};
&lt;/script&gt;</pre><p></p>The post <a href="https://mintaku-blog.net/ssg-pagination/">【Nuxt.js】ルートを拡張してページネーションを静的化する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ssg-pagination/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1912</post-id>	</item>
		<item>
		<title>【Vue.js】vue-json-to-csvを使ってCSVダウンロードする方法</title>
		<link>https://mintaku-blog.net/vue-csv/</link>
					<comments>https://mintaku-blog.net/vue-csv/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Fri, 02 Apr 2021 09:45:48 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1708</guid>

					<description><![CDATA[<p>Nuxt.js + Vue.jsでvue-json-to-csvを使ってCSVダウンロードする方法をメモしておきます。 公式：https://www.npm …</p>
The post <a href="https://mintaku-blog.net/vue-csv/">【Vue.js】vue-json-to-csvを使ってCSVダウンロードする方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Nuxt.js + Vue.jsでvue-json-to-csvを使ってCSVダウンロードする方法をメモしておきます。</p>
<p>公式：<a href="https://www.npmjs.com/package/vue-json-to-csv" target="_blank" rel="noopener noreferrer">https://www.npmjs.com/package/vue-json-to-csv</a></p>
<h2>vue-json-to-csvのインストールと初期設定</h2>
<p>まず、vue-json-to-csvをインストールします。</p><pre class="crayon-plain-tag">$ npm i vue-json-to-csv</pre><p>&nbsp;</p>
<p>vue-json-to-csvを使用するためにplugins配下にvue-json-to-csv.jsファイルを生成して、componentで使えるようにしておきます。</p>
<p>・plugins/vue-json-to-csv.js</p><pre class="crayon-plain-tag">import Vue from 'vue'
import VueJsonToCsv from 'vue-json-to-csv'

Vue.component('VueJsonToCsv', VueJsonToCsv)</pre><p>&nbsp;</p>
<p>nuxt.config.jsのpluginsに作成したファイルのパスを追記します。</p>
<p>・nuxt.config.js</p><pre class="crayon-plain-tag">...

  plugins: [
    { src: '@/plugins/vue-json-to-csv.js' }
  ],

...</pre><p>&nbsp;</p>
<h2>vue-json-to-csvでCSVダウンロードを実装</h2>
<p>実際にCSVダウンロードしてみます。コンポーネントとして使えるようにしたVueJsonToCsvを使ってbuttonタグを囲います</p>
<p>VueJsonToCsvのjson-dataはCSVのデータとして出力され、labelsはCSVの列名として出力され、csv-titleはCSVファイル名になります。</p>
<p>他にもオプションがあり、詳しくは公式サイトを参照してください。</p>
<p>公式：<a href="https://www.npmjs.com/package/vue-json-to-csv" target="_blank" rel="noopener noreferrer">https://www.npmjs.com/package/vue-json-to-csv</a></p>
<p>・pages/books.vue</p><pre class="crayon-plain-tag">&lt;VueJsonToCsv
  :json-data="books"
  :labels="labels"
  :csv-title="title"
&gt;
  &lt;button @click="download"&gt;
    CSVダウンロード
  &lt;/button&gt;
&lt;/VueJsonToCsv&gt;</pre><p>&nbsp;</p>
<p>今回は例として、CSVダウンロードをクリックするとdownloadメソッドによって、computedで取得していたbookDatasからbooksにCSVで必要なデータを渡しています。</p>
<p>labelsは列名としてnameを本のタイトル、authorを著者として表示されるようにしています。</p>
<p>CSVファイル名は、moment.jsを使って「本のCSVデータ_現在日時」となるようにしています。</p>
<p>・pages/books.vue</p><pre class="crayon-plain-tag">...

data () {
  return {
    title: '本のCSVデータ_' + moment().format('YYYY年MM月DD日HH:mm'),
    labels: {
      name: { title: '本のタイトル' },
      author: { title: '著者' }
    },
    books: []
  }
},
computed: {
  ...mapGetters(books, [bookDatas])
},
methods: {
  download () {
    this.bookDatas.forEach((book) =&gt; {
      this.books.push({
        name: book.name,
        author: book.author
      })
    })
  }
}

...</pre><p></p>The post <a href="https://mintaku-blog.net/vue-csv/">【Vue.js】vue-json-to-csvを使ってCSVダウンロードする方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/vue-csv/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1708</post-id>	</item>
		<item>
		<title>【Nuxt.js】ログインせずにWebStorageを使ってお気に入り機能を実装する</title>
		<link>https://mintaku-blog.net/nuxt-webstorage/</link>
					<comments>https://mintaku-blog.net/nuxt-webstorage/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Thu, 01 Apr 2021 15:31:59 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1881</guid>

					<description><![CDATA[<p>ログイン認証の永続化やカート追加などブラウザの一時的なデータ保存に役立つWebStorageを使ってお気に入り機能の実装の流れをメモしておきます。 vuex …</p>
The post <a href="https://mintaku-blog.net/nuxt-webstorage/">【Nuxt.js】ログインせずにWebStorageを使ってお気に入り機能を実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>ログイン認証の永続化やカート追加などブラウザの一時的なデータ保存に役立つWebStorageを使ってお気に入り機能の実装の流れをメモしておきます。</p>
<h2>vuex-persistedstateのインストール・設定</h2>
<p>vuex-persistedstateを使ってWebStorageにVuexのステート情報を保存する方法でやっていきます。package.jsonにvuex-persistedstateを追記し、mpm installコマンドでインストールします。</p>
<p>・package.json</p><pre class="crayon-plain-tag">...

  "dependencies": {
    "vuex-persistedstate": "^2.7.0"
  },

...</pre><p>&nbsp;</p>
<p>インストールしたらpluginsディレクトリ配下にpersistedstate.jsファイルを作成し、以下のように永続化させる情報を設定していきます。</p>
<p>keyはWebStorageにキーとなる文字列を入力し、pathsはVuexのステート情報を保存しているファイル名を入力します。</p>
<p>WebStorageにはlocalStorageとsessionStorageがあります。localStorageはブラウザを閉じてもデータを保持されますが、sessionStorageはブラウザを閉じるとデータは破棄されます。今回はsessionStorageを使用して実装していきます。</p>
<p>・persistedstate.js</p><pre class="crayon-plain-tag">import createPersistedState from 'vuex-persistedstate'

export default ({ store, isHMR }) =&gt; {
  if (isHMR) { return }

  if (process.client) {
    window.onNuxtReady((nuxt) =&gt; {
      createPersistedState(
        {
          key: 'sample',
          paths: ['favorite'],
          storage: window.sessionStorage
        }
      )(store)
    })
  }
}</pre><p>&nbsp;</p>
<p>nuxt.config.jsのpluginsに先ほど作成したファイルパスを追記し、設定完了です。</p>
<p>・nuxt.config.js</p><pre class="crayon-plain-tag">...

  plugins: [
    // Doc: https://github.com/robinvdvleuten/vuex-persistedstate
    { src: '@/plugins/persistedstate', mode: 'client' }
  ],

...</pre><p>&nbsp;</p>
<h2>お気に入りストアを作成する</h2>
<p>実際にWebStorageと連携し保存するストアを書いていきます。</p>
<p>今回はお気に入りのIDを保存することで、そのIDに紐づくデータを後から取得できるようにします。</p>
<p>後で呼び出されるようにお気に入り登録した際にIDを格納する処理と削除する処理も書いておきます。</p>
<p>・store/favorite.js</p><pre class="crayon-plain-tag">export const state = () =&gt; ({
  /**
  * お気に入り情報
  * @typedef {object} favorite
  * @property {array} id サンプルID
  */
  favorite: {
    id: []
  }
})

export const getters = {
  /**
  * お気に入り情報を取得する
  * @return {object}
  */
  favorite: state =&gt; state.favorite,
  /**
  * お気に入り情報のサンプルIDを取得する
  * @return {array}
  */
  id: state =&gt; state.favorite.id
}

export const mutations = {
  /**
  * お気に入り登録するサンプルIDを格納する
  * すでに格納されていた場合はエラー
  * 25件以上の場合はエラー
  * @param state
  * @param {array} id
  */
  addFavoriteId (state, { id }) {
    if (state.favorite.id.includes(id)) {
      alert('既に登録済みです')
      return
    }
    if (state.favorite.id.length &gt;= 25) {
      alert('お気に入りに登録できるのは25件までです')
      return
    }

    state.favorite.id.push(id)
  },
  /**
  * お気に入り登録するサンプルIDを解除する
  * すでに解除されていた場合はエラー
  * @param state
  * @param {array} id
  */
  releaseFavoriteId (state, { id }) {
    if (!state.favorite.id.includes(id)) {
      alert('既に解除済みです')
      return
    }

    state.favorite.id.splice(state.favorite.id.indexOf(id), 1)
  },
  /**
  * お気に入りを情報をクリアする（=初期状態のデータを格納する）
  * @param state
  */
  clearFavorite (state) {
    state.favorite = {
      id: []
    }
  }
}</pre><p>&nbsp;</p>
<h2>お気に入り登録する</h2>
<p>お気に入り登録したいページに登録・解除の処理を実装していきます。</p>
<p>まずmapGettersからWebStorageに格納されているお気に入り情報を取得します。既にお気に入り登録されているかどうかの条件判定をし、登録・解除のボタンをそれぞれ出し分けます。</p>
<p>お気に入り登録した情報を表示したい場合は、mapGettersからfavoriteを呼び出しidに紐づいたデータをAPIから取得する感じになります。情報量が少ない場合は、APIからデータを呼び出さずにfavoriteにプロパティを追加してWebStorage(容量5MB)に持っておいても良いと思います。</p>
<p>・pages/sample/index.vue</p><pre class="crayon-plain-tag">...

  &lt;button
    v-if="favorite.id.includes(sample.id)"
    @click="releaseFavoriteId(sample.id)"
  &gt;
    お気に入り解除
  &lt;/button&gt;
  &lt;button
    v-else
    @click="addFavoriteId(sample.id)"
  &gt;
    お気に入り登録
  &lt;/button&gt;

...

&lt;script&gt;
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(samples, [samples]),
    ...mapGetters('favorite', ['favorite'])
  },
  methods: {
    addFavoriteId (id) {
      this.$store.commit('favorite/addFavoriteId', { id })
    },
    releaseFavoriteId (id) {
      this.$store.commit('favorite/releaseFavoriteId', { id })
    }
  }
}
&lt;/script&gt;</pre><p></p>The post <a href="https://mintaku-blog.net/nuxt-webstorage/">【Nuxt.js】ログインせずにWebStorageを使ってお気に入り機能を実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt-webstorage/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1881</post-id>	</item>
		<item>
		<title>vue-simple-suggestでサジェスト機能を実装する方法</title>
		<link>https://mintaku-blog.net/vue-suggest/</link>
					<comments>https://mintaku-blog.net/vue-suggest/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 08 Nov 2020 06:22:37 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1701</guid>

					<description><![CDATA[<p>vue-simple-suggestでテキスト入力候補を表示するサジェスト機能を実装する方法を紹介します。 vue-simple-suggestのインストー …</p>
The post <a href="https://mintaku-blog.net/vue-suggest/">vue-simple-suggestでサジェスト機能を実装する方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>vue-simple-suggestでテキスト入力候補を表示するサジェスト機能を実装する方法を紹介します。</p>
<h2>vue-simple-suggestのインストール・設定</h2>
<p>サジェスト機能を実装するためにvue-simple-suggestをインストールします。</p><pre class="crayon-plain-tag">$ npm install --save vue-simple-suggest</pre><p>&nbsp;</p>
<p>vue-simple-suggestを使用するためにplugins配下にvue-simple-suggest.jsファイルを生成して、componentで使えるようにしておきます。</p>
<p>・plugins/vue-simple-suggest.js</p><pre class="crayon-plain-tag">import Vue from 'vue'
import VueSimpleSuggest from 'vue-simple-suggest'

Vue.component('VueSimpleSuggest', VueSimpleSuggest)</pre><p>&nbsp;</p>
<p>nuxt.config.jsのpluginsに作成したファイルのパスを追記します。</p>
<p>・nuxt.config.js</p><pre class="crayon-plain-tag">...

  plugins: [
    // Doc: https://github.com/KazanExpress/vue-simple-suggest
    { src: '@/plugins/vue-simple-suggest.js' }
  ],

...</pre><p>&nbsp;</p>
<h2>vue-simple-suggestでサジェスト機能を実装</h2>
<p>実際にvue-simple-suggestでサジェスト機能を実装していきます。まずはサジェストを表示するための検索機能のHTMLを書いていきます。</p>
<p>今回はinputに入力するとサジェスト候補に一致したキーワードがinputの下に表示されるように実装します。</p>
<p>入力するクエリによって表示するサジェストをフィルタリングするためにfilter-by-queryオプションをtrueにしています。表示させるサジェストの最大候補数なども変更できるので、詳しくはこちらの公式を参考にしてください。</p>
<p>参考：<a href="https://github.com/KazanExpress/vue-simple-suggest#emitted-events" target="_blank" rel="noopener noreferrer">https://github.com/KazanExpress/vue-simple-suggest#emitted-events</a></p>
<p>入力候補はdivタグ内のspanタグのsuggestionに表示されます。クリックするとinputに表示されます。</p>
<p>・components/search.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;form @submit.prevent="submitHandler"&gt;
    &lt;client-only&gt;
      &lt;VueSimpleSuggest
        v-model="searchQuery"
        :list="getSuggestionLists"
        :filter-by-query="true"
      &gt;
        &lt;input
          v-model="searchQuery"
          :name="'searchQuery'"
        &gt;
        &lt;div slot="suggestion-item" slot-scope="{ suggestion }"&gt;
          &lt;span&gt;{{ suggestion }}&lt;/span&gt;
        &lt;/div&gt;
      &lt;/VueSimpleSuggest&gt;
    &lt;/client-only&gt;
  &lt;/form&gt;
&lt;/template&gt;</pre><p>&nbsp;</p>
<p>getSuggestionListsでサジェスト候補のリストを取得しています。</p>
<p>あらかじめstoreにセットされたデータを使います。今回は都道府県ごとの観光カテゴリをサジェストで表示させます。</p>
<p>・components/search.vue</p><pre class="crayon-plain-tag">...

  data () {
    return {
      searchQuery: ''
    }
  },
  computed: {
    ...mapGetters('prefectures', ['prefectures']),
    ...mapGetters('categories', ['categories'])
  },
  methods: {
    async getSuggestionLists () {
      const suggestionLists = []
      await this.prefectures.forEach((prefecure) =&gt; {
        suggestionLists.push(prefecure.name)

        this.categories.forEach((category) =&gt; {
          suggestionLists.push(prefecure.name + ' ' + category.name)
        })
      }

      return suggestionLists
    }
  }

...</pre><p>&nbsp;</p>
<p>実際に動かしてみると、以下のようにサジェストが表示されていることが確認できます。</p>
<p><img data-attachment-id="1702" data-permalink="https://mintaku-blog.net/vue-suggest/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2020-10-28-17-06-29/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?fit=946%2C626&amp;ssl=1" data-orig-size="946,626" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="スクリーンショット 2020-10-28 17.06.29" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?fit=300%2C199&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?fit=800%2C529&amp;ssl=1" loading="lazy" class="aligncenter size-full wp-image-1702" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?resize=800%2C529&#038;ssl=1" alt="" width="800" height="529" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?w=946&amp;ssl=1 946w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?resize=300%2C199&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/10/3835ce4b62d05acf968d4cfbc7e21385.png?resize=768%2C508&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>The post <a href="https://mintaku-blog.net/vue-suggest/">vue-simple-suggestでサジェスト機能を実装する方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/vue-suggest/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1701</post-id>	</item>
		<item>
		<title>【Nuxt.js】vee-validateでバリデーションを実装する方法</title>
		<link>https://mintaku-blog.net/vee-validate/</link>
					<comments>https://mintaku-blog.net/vee-validate/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Thu, 12 Mar 2020 13:36:51 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1375</guid>

					<description><![CDATA[<p>vee-validateというライブラリでNuxt.jsにバリデーションを実装する方法をメモしておきます。 バージョン2系からバージョン3系への仕様変更 2 …</p>
The post <a href="https://mintaku-blog.net/vee-validate/">【Nuxt.js】vee-validateでバリデーションを実装する方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>vee-validateというライブラリでNuxt.jsにバリデーションを実装する方法をメモしておきます。</p>
<h2>バージョン2系からバージョン3系への仕様変更</h2>
<p>2019/8のバージョン3系への更新によって大規模な仕様変更が生じたため、それまでv-validate ディレクティブを用いていましたが、バージョン3系ではValidationProviderを使うようになっています。</p>
<p>ex) バージョン2系</p><pre class="crayon-plain-tag">&lt;input name="name" type="text" v-validate="'required'"&gt;</pre><p>&nbsp;</p>
<p>ex) バージョン3系</p><pre class="crayon-plain-tag">&lt;ValidationProvider rules="required" v-slot="{ errors }"&gt;
    &lt;input type="text" v-model="name"&gt;
&lt;/ValidationProvider&gt;</pre><p>&nbsp;</p>
<h2>vee-validateのインストール</h2>
<p>今回はバージョン2系をインストールします。</p><pre class="crayon-plain-tag">$ npm i vee-validate@2.2.15 -save</pre><p>&nbsp;</p>
<h2>vee-validateの設定</h2>
<p>pluginsディレクトリ配下にvee-validate.jsファイルを作成します。vee-validate/dist/locale/jaを読み込むことで、エラーメッセージの日本語化をします。</p>
<p>・plugins/vee-validate.js</p><pre class="crayon-plain-tag">import Vue from 'vue'
import VeeValidate, { Validator } from 'vee-validate'
import ja from 'vee-validate/dist/locale/ja'

Vue.use(VeeValidate)
Validator.localize('ja', ja)</pre><p>&nbsp;</p>
<p>nuxt.config.jsのpluginsにvee-validateを追加します。</p>
<p>・nuxt.config.js</p><pre class="crayon-plain-tag">plugins: [
...
    { src: '~/plugins/vee-validate', ssr: true }
...
],</pre><p>ちなみにssrをfalseにすると、以下のエラーが発生します。</p>
<p>バリデート自体はSSR時に必要ないのだが、tmplateがレンダリングしようとするため、errorsのhasが読み込めないということになります。</p><pre class="crayon-plain-tag">ERROR [Vue warn]: Error in render: "TypeError: Cannot read property 'has' of undefined"</pre><p>&nbsp;</p>
<h2>vee-validateによるバリデート</h2>
<p>実際にバリデート処理を実装してみます。バリデートしたいinputタグにv-validateを追加し、バリデーションルールを指定します。</p>
<p>バリデーションルールに反していた場合、v-show=&#8221;errors.has(&#8216;name&#8217;)&#8221;がtrueとなり、エラーメッセージが表示されます。</p><pre class="crayon-plain-tag">&lt;form @submit.prevent="register"&gt;
    &lt;input
        :name="'name'"
        :placeholder="'名前'"
        v-validate="'required'"
        data-vv-as="名前"
    /&gt;
    &lt;div class="form-control-feedback" v-show="errors.has('name')"&gt;
        &lt;p class="alert alert-danger"&gt;{{ errors.first('name') }}&lt;/p&gt;
    &lt;/div&gt;
    &lt;button type="submit"&gt;登録&lt;/button&gt;
&lt;/form&gt;</pre><p>&nbsp;</p>
<p>登録ボタンを押した際にバリデートをかけるには以下のように実装します。バリデーションルールに則っていた場合のみ、登録処理を実行すことができます。</p><pre class="crayon-plain-tag">methods: {
    register () {
        this.$validator.validateAll().then((result) =&gt; {
            if (result) {
                this.$store.dispatch(test/register, { name: this.name })
            }
        })
    }
}</pre><p>・ex) バリデート使用例</p>
<p><img data-attachment-id="1377" data-permalink="https://mintaku-blog.net/vee-validate/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2020-03-06-15-19-07/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?fit=1304%2C196&amp;ssl=1" data-orig-size="1304,196" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="スクリーンショット 2020-03-06 15.19.07" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?fit=300%2C45&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?fit=800%2C120&amp;ssl=1" loading="lazy" class="aligncenter size-large wp-image-1377" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?resize=800%2C120&#038;ssl=1" alt="" width="800" height="120" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?resize=1024%2C154&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?resize=300%2C45&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?resize=768%2C115&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/049bc31afa47ef5ec81e936221f589f4.png?w=1304&amp;ssl=1 1304w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p><img data-attachment-id="1376" data-permalink="https://mintaku-blog.net/vee-validate/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2020-03-06-15-18-52/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?fit=1320%2C236&amp;ssl=1" data-orig-size="1320,236" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="スクリーンショット 2020-03-06 15.18.52" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?fit=300%2C54&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?fit=800%2C143&amp;ssl=1" loading="lazy" class="aligncenter size-large wp-image-1376" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?resize=800%2C143&#038;ssl=1" alt="" width="800" height="143" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?resize=1024%2C183&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?resize=300%2C54&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?resize=768%2C137&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2020/03/734ca2c489b69a3dbd47221f43bde266.png?w=1320&amp;ssl=1 1320w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>&nbsp;</p>
<h2>バリデーションルール</h2>
<p>主なバリデーションのルールとして以下のルールがあります。</p>
<ul>
<li>required : 入力必須</li>
<li>email : メールアドレス形式</li>
<li>numeric : 半角数値のみ</li>
<li>min : 最小入力文字数</li>
<li>max : 最大入力文字数</li>
</ul>
<p>詳しくはこちらの公式ページに全てのバリデーションルールが記載されています。</p>
<p>参考：<a href="http://vee-validate.logaretm.com/v2/guide/rules.html" target="_blank" rel="noopener noreferrer">Validation Rules</a></p>
<p>&nbsp;</p>
<p>以上、vee-validateというライブラリでNuxt.jsにバリデーションを実装する方法でした。</p>The post <a href="https://mintaku-blog.net/vee-validate/">【Nuxt.js】vee-validateでバリデーションを実装する方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/vee-validate/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1375</post-id>	</item>
		<item>
		<title>【Nuxt.js】vue-infinite-loadingで検索結果を無限スクロールする方法</title>
		<link>https://mintaku-blog.net/nuxt-scroll/</link>
					<comments>https://mintaku-blog.net/nuxt-scroll/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Fri, 21 Feb 2020 11:45:04 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1351</guid>

					<description><![CDATA[<p>Nuxt.jsで「vue-infinite-loading」というプラグインを使って、検索結果を無限スクロールする方法を紹介します。 事前準備 まずは「vu …</p>
The post <a href="https://mintaku-blog.net/nuxt-scroll/">【Nuxt.js】vue-infinite-loadingで検索結果を無限スクロールする方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Nuxt.jsで「vue-infinite-loading」というプラグインを使って、検索結果を無限スクロールする方法を紹介します。</p>
<h2>事前準備</h2>
<p>まずは「vue-infinite-loading」プラグインをインストールします。</p><pre class="crayon-plain-tag">$ npm install vue-infinite-loading --save</pre><p>pluginsディレクトリ配下にinfiniteloading.jsファイルを作成し、以下を追加します。</p>
<p>・infiniteloading.js</p><pre class="crayon-plain-tag">import Vue from 'vue'
import InfiniteLoading from 'vue-infinite-loading'

Vue.use(InfiniteLoading)</pre><p>nuxt.config.jsのpluginsに「InfiniteLoading」を追加します。これで準備完了です。<br />
・nuxt.config.js</p><pre class="crayon-plain-tag">...

plugins: [
    { src: '~/plugins/infiniteloading', ssr: false }
]

...</pre><p>&nbsp;</p>
<h2>無限スクロール実装</h2>
<p>無限スクロールしたい検索結果の下に以下のコードを追加します。スクロールされるとinfiniteメソッドが呼び出されます。</p>
<p>検索結果が取得できなくなった場合はデフォルトだと「No more data :)」になっています。</p>
<p>変更したい場合は&lt;span slot=&#8221;no-more&#8221;&gt;&lt;/span&gt;と&lt;span slot=&#8221;no-results&#8221;&gt;&lt;/span&gt;の中に文言を追加することで変更できます。</p>
<p>spinnerは変更することで読み込み中のローディングをカスタマイズできます。</p>
<p>■カスタマイズ</p>
<ol>
<li>default</li>
<li>circle</li>
<li>bubbles</li>
<li>spiral</li>
<li>waveDots</li>
</ol>
<p></p><pre class="crayon-plain-tag">...

&lt;form @submit.prevent="search" method="post" action="?"&gt;
    &lt;label&gt;
        &lt;input v-model="name" type="text" placeholder="ここで検索"&gt;
    &lt;/label&gt;
&lt;/form&gt;

...

&lt;infinite-loading
    ref="infiniteLoading"
    spinner="waveDots"
    @infinite="infinite"&gt;
        &lt;span slot="no-more"&gt;検索結果は以上です&lt;/span&gt;
        &lt;span slot="no-results"&gt;検索結果はありません&lt;/span&gt;
&lt;/infinite-loading&gt;

...</pre><p>infiniteメソッドが呼び出されると、APIを通してデータを取得してきます(API側の取得処理は割愛)。setTimeoutで3秒間指定した後、実行しています。</p>
<p>offsetを++していることで取得する次に取得すべきデータを判定しています。</p>
<p>取得データがある場合は、this.$refs.infiniteLoading.stateChanger.loaded()を呼び出し、取得データがなくなると、this.$refs.infiniteLoading.stateChanger.complete()メソッドを呼び出します。</p>
<p>また今回は検索機能もあるため、検索を実行した場合はthis.$refs.infiniteLoading.stateChanger.reset()でローディング結果をリセットして再度データを1から取得してきます。</p><pre class="crayon-plain-tag">import InfiniteLoading from 'vue-infinite-loading'

export default {

    ...

    components { InfiniteLoading },
        data () {
            return {
                name: '',
                searchParam: { name: '', offset: 0, per: 5 }
            }
        },
        methods: {
            search () {
                this.$refs.infiniteLoading.stateChanger.reset()
                this.searchParam.name = this.name
                this.searchParam.offset = 1
                this.$store.dispatch('test/searchTests', { searchParam: this.searchParam })
            },
            infinite () {
                setTimeout(() =&gt; {
                    this.searchParam.name = this.name
                    this.searchParam.offset += 1
                    this.$store.dispatch('test/searchInfiniteTests', { searchParam: this.searchParam }).then((result) =&gt; {
                    if (result.length &gt;= this.searchParam.per) {
                        this.$refs.infiniteLoading.stateChanger.loaded()
                    } else {
                        this.$refs.infiniteLoading.stateChanger.complete()
                    }
                })
            }, 3000)
        }
    }
...

}</pre><p>以上、Nuxt.jsで「vue-infinite-loading」を使って、検索結果を無限スクロールする方法でした。</p>The post <a href="https://mintaku-blog.net/nuxt-scroll/">【Nuxt.js】vue-infinite-loadingで検索結果を無限スクロールする方法</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt-scroll/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1351</post-id>	</item>
	</channel>
</rss>
