20200515のvue.jsに関する記事は10件です。

Vuetfiy v-dialogのモーダル化が機能しない

Vuetify v-dialogのモーダル化が機能しない

新規で作成もしくはソースコードの内容を大体把握してある個人の方・一人でプロジェクトを回している方には参考にならないかと思います。

Vuetifyのv-dialogにはオプションで色々指定できる
モーダル化させるためにpersistentを指定することでダイアログ外を押しても閉じないようにできる

それが機能しなかったので備忘録的にまとめる

Vuetify
https://vuetifyjs.com/ja/components/dialogs/

結論

outsideというオプションが悪さしていました。
このオプションはダイアログ外を押した時に発火するイベントです。
そいつが諸悪の権化なので削除するなりよしなに修正することで回避しましょう。

こんなことで時間取られるの辛い…辛い…

ちなみに調べるとVuetify2.1以下だとバグで機能しないらしいですが、orverlayと合わせるとバグるみたいなので多分今回の場合は関係ないです。
https://github.com/vuetifyjs/vuetify/issues/8697

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リモートカードゲームを遊べるランダムマッチアプリを開発した話

はじめに

私の趣味の一つに、トレーディングカードゲーム(TCG)があります。
昨今の外出自粛要請による影響は、TCG 界隈においても例外に漏れず、対面で TCG を遊ぶ機会のほとんどがなくなってしまいました。
この記事では、少しでも TCG 界隈の停滞感をなくすために開発した Web アプリの紹介と開発手順のメモ書きをまとめています。鋭意作成中です。
スクリーンショット 2020-05-15 12.57.24.png

対象読者

  • IT 分野以外の趣味で IT を活用したい人
  • Twitter アカウントによる認証を用いた Web アプリをつくりたい人
  • WebRTC(ビデオ通話など)を活用した Web アプリをつくりたい人
  • Nuxt.js/TypeScript の開発に興味がある人

モチベーション

TCG は大前提として対面で遊ぶことを想定したアナログゲームですが、学生以下をメインターゲット層としたものも多く、歳を重ねるにつれて身の周りから TCG を一緒に遊ぶ友達がだんだんと減っていくものです(悲しい)。
他方、TCG を専門に取り扱うカードショップなどでは、店舗大会や公認自主イベントと呼ばれるような、誰でも気軽に参加できるイベントが定期的に開催され、友達や知り合いがいなくても TCG を楽しめる環境が整っていました。

しかしながら、昨今の外出自粛要請により、カードショップでのイベントは全面的に中止に。
このような状況を受けてか、TCG 界隈では、知り合い同士で Skype や Discord などのビデオ通話アプリを用いたリモートカードゲームの環境を積極的に取り入れつつあります。
私自身、今回の騒動以前から何度かリモートカードゲームを遊んだことがありますが、以下のような課題があると感じています。
スクリーンショット 2020-05-15 12.55.53.png

  • 知り合いの中に、都合のつく人がなかなか見つからない
  • とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
  • かといって、知り合い以外の募集に自分から声をかける勇気もない
  • 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒

ただの面倒くさがりやんけ。
まあ、とにかくそのような課題の解決を目指す気持ち半分、「転活のポートフォリオがわりに使えねえかなぁ」の気持ち半分で開発した Web アプリがこのPTCGrandです。

基本アイディアとして、対戦型オンラインゲームでは一般的なランダムマッチング機能を実装しました。
そのため、たとえ知り合いがいなくても、リアルタイムに対戦相手を募集している人同士でリモートカードゲームを遊ぶことができます。

  • 知り合いの中に、都合のつく人がなかなか見つからない
  • とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
  • かといって、知り合い以外の募集に自分から声をかける勇気もない

さらに、ビデオ通話機能を Web 上で完結させることで、Skype や Discord の連絡先交換は言わずもがな、アプリのインストールすら不要にしました。
また、この Web アプリを利用するのに複雑な手順があるとかえって面倒になってしまうため、Twitter アカウントとの連携による認証を導入し、設定手順も最小化しています。

  • 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒

特定 TCG プレイヤー向けの紹介記事 は(別名義で)note に投稿済みなので、興味ある方はそちらも併せてご覧ください。

フレームワーク と アーキテクチャ

開発した Web アプリの大まかな構成と動作は、以下の通りです。
スクリーンショット 2020-05-15 12.56.00.png

Nuxt.js/Typescript

  • SPA (Single Page Application) の生成
  • Bootstrap4 (BootstrapVue) によるデザインレイアウト

Firebase

SkyWay

  • ユーザー間のビデオ通話・データ通信の確立

開発手順

重要そうなところやハマったところを中心に、メモ書きを残しています。
用意が面倒なのでスクリーンショットなどを掲載していないため、詳しく知りたい方は、適宜、記事末尾にある参考記事をご覧いただければと思います。

バージョン

$ node -v
v12.16.2
$ npm -v
6.14.4

Nuxt.js プロジェクトの作成

Nuxt.js 公式サイトに書かれているとおり、create-nuxt-app コマンドでプロジェクトを作成します。
開発言語に TypeScript、UI フレームワークに BootstrapVue を選択していますが、このあたりは好みかと思います。

Nuxt.js プロジェクトにおける TypeScript 記法は、公式の Nuxt TypeScript ガイドが参考になります。

$ npx create-nuxt-app <project-name>

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in <project-name>
? Project name <project-name>
? Project description <project-description>
? Author name @blachocolat
? Choose programming language TypeScript
? Choose the package manager Npm
? Choose UI framework Bootstrap Vue
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript Default
? Choose Nuxt.js modules Progressive Web App (PWA) Support
? Choose linting tools ESLint, Prettier
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)

Bootstrap テーマ と Web フォントの導入

Nuxt.js プロジェクトの UI フレームワークに Bootstrap を採用する強みの一つは、過去のプロジェクトのテーマを使い回せることかもしれません1
今回は、以前「所有しているボードゲームの管理サイト」を開発した際に使用した Bootswatch の Lumen テーマを導入することにしました。

/assets 以下に scss/bootswatch.scss を作成し、Global に読み込む設定を nuxt.config.js に追加します。
アプリのテーマ色を設定するため、元の設定値が書かれた _variables.scss@import するよりも前に変数 $primary を定義することで、実質的に変数を書き換えています2
CSS セレクタによる Style の適用は、より後に書かれたものが優先されるため、@import の後に記述します。

また、同様に SCSS の変数を書き換える形で、Google Fonts で公開されている Web フォントを適用しています。

/assets/scss/bootswatch.scss
$primary: #d44050;
$font-family-sans-serif: 'M PLUS 1p', sans-serif;
$font-family-monospace: 'M PLUS 1p', monospace;

@import '~/node_modules/bootswatch/dist/lumen/_variables';
// $primary: $blue !default;
@import '~/node_modules/bootstrap/scss/bootstrap';
@import '~/node_modules/bootswatch/dist/lumen/_bootswatch';

.b-avatar {
  &.badge-secondary {
    color: $gray-500;
  }
}

...
nuxt.config.js
  export default {
    head: {
      ...
      link: [
        ...
+       { rel: 'stylesheet', type: 'text/css', href: 'https://fonts.googleapis.com/css?family=M+PLUS+1p' }
      ]
    },
    ...
    css: [
      ...
+     '~/assets/scss/bootswatch.scss'
    ],
    ...
  }

Firebase Hosting へのデプロイ

Firebase CLI (firebase-tools) をインストールしていなければ、npm 経由でインストールします。
インストール完了後、firebase login コマンドを実行し、Web ブラウザ経由で Google アカウントを紐づけます。

$ npm install -g firebase-tools
$ firebase login

作成した Nuxt.js プロジェクトに移動し、Firebase プロジェクトとしての初期設定を行います。
Firebase 公式ドキュメントを参考に、あらかじめ Web ブラウザから Firebase プロジェクトを作成しておくとよいでしょう。

ここでは、? Which Firebase CLI features... の設問に対して Firebase Hosting のみを選択しています。

$ cd /path-to/<project-name>
$ firebase init

...

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Ente
r to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites

...

=== Project Setup

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: <project-name> (<project-name>)
i  Using project <project-name> (<project-name>)

...

=== Hosting Setup

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No

...

Firebase Hosting の公開ディレクトリに /public を指定したため、それに合わせるように nuxt.config.js の出力ディレクトリを変更します。

nuxt.config.js
  export default {
    ...
+   generate: {
+     dir: 'public'
+   },
    ...

以上で、準備が整いました。
下記コマンドで SPA を生成し、Firebase Hosting にデプロイします。

$ npm run generate
$ firebase deploy

なお、一度公開した SPA を非公開にしたい場合、下記コマンドを実行します。

$ firebase hosting:disable

参考記事

おわりに

せっかくそれなりの形になった Web アプリを開発したので、開発手順のメモ書きをまとめました。たかったのですが途中で力尽きてしまいました。

アプリについては、公開から数日経ったものの肝心のユーザー数はまだまだ少なく、なかなかランダムマッチングが成立していない状況です。
その結果、半ば “待ち合わせ時間を決めておく” のが必要になっており、上であげた課題そのものにぶち当たっています(悲しい)。

開発モチベーションの半分が半分なのであれですが、個人開発だからこそ、機能をゴリゴリ実装するだけでなく、必要な人のもとに届くための施策を考えていきたいですね。


  1. 開発を一段落を終えた時点での感想としては、「見た目に大きなこだわりがなければ BootstrapVue ではなく Material Design フレームワークである Vuetify を採用するのが無難そう」という感じです。 

  2. _variables.scss 内の変数宣言の末尾に付された !default は、すでに同名の変数が宣言されていなければ適用する、という意味らしいです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リモートカードゲームを気軽に遊べるランダムマッチアプリを開発した話

はじめに

私の趣味の一つに、トレーディングカードゲーム(TCG)があります。
昨今の外出自粛要請による影響は、TCG 界隈においても例外に漏れず、対面で TCG を遊ぶ機会のほとんどがなくなってしまいました。
この記事では、少しでも TCG 界隈の停滞感をなくすために開発した Web アプリの紹介と開発手順のメモ書きをまとめています。鋭意作成中です。
スクリーンショット 2020-05-15 12.57.24.png

対象読者

  • IT 分野以外の趣味で IT を活用したい人
  • Twitter アカウントによる認証を用いた Web アプリをつくりたい人
  • WebRTC(ビデオ通話など)を活用した Web アプリをつくりたい人
  • Nuxt.js/TypeScript の開発に興味がある人

モチベーション

TCG は大前提として対面で遊ぶことを想定したアナログゲームですが、学生以下をメインターゲット層としたものも多く、歳を重ねるにつれて身の周りから TCG を一緒に遊ぶ友達がだんだんと減っていくものです(悲しい)。
他方、TCG を専門に取り扱うカードショップなどでは、店舗大会や公認自主イベントと呼ばれるような、誰でも気軽に参加できるイベントが定期的に開催され、友達や知り合いがいなくても TCG を楽しめる環境が整っていました。

しかしながら、昨今の外出自粛要請により、カードショップでのイベントは全面的に中止に。
このような状況を受けてか、TCG 界隈では、知り合い同士で Skype や Discord などのビデオ通話アプリを用いたリモートカードゲームの環境を積極的に取り入れつつあります。
私自身、今回の騒動以前から何度かリモートカードゲームを遊んだことがありますが、以下のような課題があると感じています。
スクリーンショット 2020-05-15 12.55.53.png

  • 知り合いの中に、都合のつく人がなかなか見つからない
  • とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
  • かといって、知り合い以外の募集に自分から声をかける勇気もない
  • 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒

ただの面倒くさがりやんけ。
まあ、とにかくそのような課題の解決を目指す気持ち半分、「転活のポートフォリオがわりに使えねえかなぁ」の気持ち半分で開発した Web アプリがこのPTCGrandです。

基本アイディアとして、対戦型オンラインゲームでは一般的なランダムマッチング機能を実装しました。
そのため、たとえ知り合いがいなくても、リアルタイムに対戦相手を募集している人同士でリモートカードゲームを遊ぶことができます。

  • 知り合いの中に、都合のつく人がなかなか見つからない
  • とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
  • かといって、知り合い以外の募集に自分から声をかける勇気もない

さらに、ビデオ通話機能を Web 上で完結させることで、Skype や Discord の連絡先交換は言わずもがな、アプリのインストールすら不要にしました。
また、この Web アプリを利用するのに複雑な手順があるとかえって面倒になってしまうため、Twitter アカウントとの連携による認証を導入し、設定手順も最小化しています。

  • 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒

特定 TCG プレイヤー向けの紹介記事 は(別名義で)note に投稿済みなので、興味ある方はそちらも併せてご覧ください。

フレームワーク と アーキテクチャ

開発した Web アプリの大まかな構成と動作は、以下の通りです。
スクリーンショット 2020-05-15 12.56.00.png

Nuxt.js/Typescript

  • SPA (Single Page Application) の生成
  • Bootstrap4 (BootstrapVue) によるデザインレイアウト

Firebase

SkyWay

  • ユーザー間のビデオ通話・データ通信の確立

開発手順

重要そうなところやハマったところを中心に、メモ書きを残しています。
用意が面倒なのでスクリーンショットなどを掲載していないため、詳しく知りたい方は、適宜、記事末尾にある参考記事をご覧いただければと思います。

バージョン

$ node -v
v12.16.2
$ npm -v
6.14.4

Nuxt.js プロジェクトの作成

Nuxt.js 公式サイトに書かれているとおり、create-nuxt-app コマンドでプロジェクトを作成します。
開発言語に TypeScript、UI フレームワークに BootstrapVue を選択していますが、このあたりは好みかと思います。

Nuxt.js プロジェクトにおける TypeScript 記法は、公式の Nuxt TypeScript ガイドが参考になります。

$ npx create-nuxt-app <project-name>

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in <project-name>
? Project name <project-name>
? Project description <project-description>
? Author name @blachocolat
? Choose programming language TypeScript
? Choose the package manager Npm
? Choose UI framework Bootstrap Vue
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript Default
? Choose Nuxt.js modules Progressive Web App (PWA) Support
? Choose linting tools ESLint, Prettier
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)

Bootstrap テーマ と Web フォントの導入

Nuxt.js プロジェクトの UI フレームワークに Bootstrap を採用する強みの一つは、過去のプロジェクトのテーマを使い回せることかもしれません1
今回は、以前「所有しているボードゲームの管理サイト」を開発した際に使用した Bootswatch の Lumen テーマを導入することにしました。

/assets 以下に scss/bootswatch.scss を作成し、Global に読み込む設定を nuxt.config.js に追加します。
アプリのテーマ色を設定するため、元の設定値が書かれた _variables.scss@import するよりも前に変数 $primary を定義することで、実質的に変数を書き換えています2
CSS セレクタによる Style の適用は、より後に書かれたものが優先されるため、@import の後に記述します。

また、同様に SCSS の変数を書き換える形で、Google Fonts で公開されている Web フォントを適用しています。

/assets/scss/bootswatch.scss
$primary: #d44050;
$font-family-sans-serif: 'M PLUS 1p', sans-serif;
$font-family-monospace: 'M PLUS 1p', monospace;

@import '~/node_modules/bootswatch/dist/lumen/_variables';
// $primary: $blue !default;
@import '~/node_modules/bootstrap/scss/bootstrap';
@import '~/node_modules/bootswatch/dist/lumen/_bootswatch';

.b-avatar {
  &.badge-secondary {
    color: $gray-500;
  }
}

...
nuxt.config.js
  export default {
    head: {
      ...
      link: [
        ...
+       { rel: 'stylesheet', type: 'text/css', href: 'https://fonts.googleapis.com/css?family=M+PLUS+1p' }
      ]
    },
    ...
    css: [
      ...
+     '~/assets/scss/bootswatch.scss'
    ],
    ...
  }

Firebase Hosting へのデプロイ

Firebase CLI (firebase-tools) をインストールしていなければ、npm 経由でインストールします。
インストール完了後、firebase login コマンドを実行し、Web ブラウザ経由で Google アカウントを紐づけます。

$ npm install -g firebase-tools
$ firebase login

作成した Nuxt.js プロジェクトに移動し、Firebase プロジェクトとしての初期設定を行います。
Firebase 公式ドキュメントを参考に、あらかじめ Web ブラウザから Firebase プロジェクトを作成しておくとよいでしょう。

ここでは、? Which Firebase CLI features... の設問に対して Firebase Hosting のみを選択しています。

$ cd /path-to/<project-name>
$ firebase init

...

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Ente
r to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites

...

=== Project Setup

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: <project-name> (<project-name>)
i  Using project <project-name> (<project-name>)

...

=== Hosting Setup

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No

...

Firebase Hosting の公開ディレクトリに /public を指定したため、それに合わせるように nuxt.config.js の出力ディレクトリを変更します。

nuxt.config.js
  export default {
    ...
+   generate: {
+     dir: 'public'
+   },
    ...

以上で、準備が整いました。
下記コマンドで SPA を生成し、Firebase Hosting にデプロイします。

$ npm run generate
$ firebase deploy

なお、一度公開した SPA を非公開にしたい場合、下記コマンドを実行します。

$ firebase hosting:disable

参考記事

おわりに

せっかくそれなりの形になった Web アプリを開発したので、開発手順のメモ書きをまとめました。たかったのですが途中で力尽きてしまいました。

アプリについては、公開から数日経ったものの肝心のユーザー数はまだまだ少なく、なかなかランダムマッチングが成立していない状況です。
その結果、半ば “待ち合わせ時間を決めておく” のが必要になっており、上であげた課題そのものにぶち当たっています(悲しい)。

開発モチベーションの半分が半分なのであれですが、個人開発だからこそ、機能をゴリゴリ実装するだけでなく、必要な人のもとに届くための施策を考えていきたいですね。


  1. 開発を一段落を終えた時点での感想としては、「見た目に大きなこだわりがなければ BootstrapVue ではなく Material Design フレームワークである Vuetify を採用するのが無難そう」という感じです。 

  2. _variables.scss 内の変数宣言の末尾に付された !default は、すでに同名の変数が宣言されていなければ適用する、という意味らしいです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsの基礎

はじめに

Vue.jsとはjavascriptのフレームワークの1つ。
フレームワークを利用することでより効率的な開発をすることができる。

VueはHTMLとjavascriptの間でデータ連携(紐づける)を行う役割を果たし、紐づけにはディレクティブというVue特有の機能を用いる。

プロジェクトにVue.jsをインストールすることでVueでの開発が可能になる。
・ scriptタグを埋め込む
・ NPMを利用してインストール
・ CLIを利用してインストール
などいくつかのインストール方法がある。(公式サイトを参照

フレームワークとは

フレームワークはアプリケーションの設計を手助けするため、アプリケーションの骨組みとなり基本的な機能やルールを提供するもの。

Vueのディレクティブ

Vueには名前がv-から始まるディレクティブというデータバインディング(HTMLとjavascriptを紐づける)を行うための機能がある。

Vueアプリケーションの作成

Vueアプリケーションを起動させるにはVue関数(コンストラクタ関数)を使ってVueインスタンスを生成する。
Vueインスタンスを生成するにはオプションオブジェクトを渡す。

コンストラクタ・インスタンス・クラスなどについてはこちら

sample.js
// Vueインスタンスを作成し、変数vmに代入
var vm = new Vue({
  // オプションオブジェクト
})

※上記コードで指定されているVueクラスはフレームワーク側で定義されているクラス。
※Vueインスタンスは変数に代入しなくても使用可能。複数のVueインスタンスを使用する場合は変数に代入する。

Vueアプリケーションはnew Vueで生成されたルートVueインスタンスで構成され、必要に応じてネストされたツリーや再利用可能なコンポーネントで形成される。

例えば、Todoアプリのコンポーネントツリーは次のようになる。

ルートインスタンス
└─ Todoリスト
   ├─ Todoアイテム
   │  ├─ Todo削除ボタン
   │  └─ Todo編集ボタン
   └─ Todoリストフッター
      ├─ Todoクリアボタン
      └─ Todoリスト統計

ここでいうルート(root)はツリー構造の最上位に位置する要素のこと。

コンポーネントとは

機能ごとに切り分けられたアプリケーションを構成する部品のようなもの。

基本的なオプションオブジェクトの構成

オプションには使用するデータやメソッドなどを定義していく。

sample.js
ver vm = new Vue({
  // ① マウント(繋げる)する要素
  el: 'app',
 // ② アプリケーションで使用するデータ
  data: {
    name: 'book',
    price: 1000
  },
 // ③ 算出プロパティ
  computed: {
    halfPrice: function () {
      return this.price / 2;
    }
 },
  // ④ アプリケーションで使用するメソッド
  methods: {
    halfPrice: function() {
      return this.price / 2;
    }
  }
})

※インスタンス内で定義したオブジェクトにアクセスする際はthisをつける必要がある。

① el (マウント(繋げる)する要素)

Vueインスタンスを生成した際にどのHTMLにマウントする(繋げる)かをここで指定する。
elオプションの中にはHTMLの要素であるidを指定する。
ここでマウントされたHTMLの要素がアプリケーションのルートになる。

sample.html
<div id="app">
  <p>Hello</p>
</div>
sample.js
// id appにVueインスタンスを紐付ける
var vm = new Vue ({
  el: '#app'
})

② data (アプリケーションで使用するデータ)

アプリケーションで使用するデータをここで定義する。
データには数値・文字列・ブーリアン・配列・オブジェクトなどjavascriptで使用できるデータを定義することができる。

sample.js
var vm = new Vue ({
  el: '#app',
  // data: { プロパティ名: 値 }
  data: {
    name: 'book',
    price: 1000
}
})
// console.log(vm.name)でnameの内容が出力

HTMLで表示するにはマスタッシュタグ{{ }}を用いる。

sample.html
<div id="app">
  <p>{{ name }}</p>
</div>

③ computed (算出プロパティ)

computedオプションでは何らかの処理(関数)に名前をつけてプロパティとして定義することができる。

データに処理を与えたい場合、下記コードのようにHTML内に直接式を記述することも可能だが、仕組みが複雑になればコードも肥大化しコードの中身も理解しにくくなる。

sample.html
<div id="app">
// dataオプションで定義したpriceを半分の値にして返し表示
  <p>{{ price(function() { return price / 2 }) }}</p>
</div>

computedオプションを利用することで与えたい処理に名前をつけてプロパティに置き換え定義することで理解しやすいコードを記述することができる。

sample.js
var vm = new Vue ({
  el: '#app',
  data: {
    name: 'book',
    price: 1000
},
  // computed: {
  //     プロパティ名: function () {
  //       ここに処理をかく
  //     }
  //  }
  computed: {
    // プロパティhalfPriceを定義
    halfPrice: function () {
    // priceの値を半分にして返す
      return this.price / 2;
    }
 }
})

computedオプションで定義したプロパティはdataオプションのプロパティと同じように扱うことができ、マスタッシュタグやディレクティブの値として利用することができる。

sample.html
<div id="app">
  <!-- 500が表示される -->
  <p>{{ halfPrice }}円</p>
</div>

computedはメソッドではなくプロパティなので呼び出す際に( )はいらない。

メソッドについてはこちら

④ methods (アプリケーションで使用するメソッド)

methodsオプションではメソッドを定義することができ、定義したメソッドはマスタッシュタグやディレクティブで呼び出すことができる。ここにメソッドをまとめることで見通しの良いコードを記述することができる。
メソッドはv-onディレクティブでよく使われる。

v-onディレクティブについてはこちら

sample.js
var vm = new Vue ({
  el: '#app',
  data: {
    name: 'book',
    price: 1000
},
  // methods: {
  //  メソッド名: function() {
  //    ここに処理をかく
  //  }
  // }
  methods: {
    // メソッドhalfPriceを定義
    halfPrice: function() {
      // priceの値を半分にして返す
      return this.price / 2;
    }
  }
})
sample.html
<div>
  <!-- ボタンをクリックするとhalfPrice関数が実行される -->
  <button @click="halfPrice()">半額</button>
  <!-- 500円と表示される -->
  <p>{{ price }}円</p>
</div>

computedとmethodsの違い

書き方の違い

computed
プロパティなので呼び出す際に( )は不要。引数を使った処理ができない。

methods
メソッドなので呼び出す際に( )が必要。引数を使った処理ができる。

動作の違い

computed
紐づいているデータ(dataオプションで定義した)に変更があった場合にのみ処理が実行。
データが更新されない限り一度処理した結果はキャッシュ(保存)される。毎回処理が実行されないのでその分処理が速くなる。
データと紐づいていない場合は処理が一度しか実行されない。(データが更新されないので)

methods
メソッドを呼び出すたびに処理が実行される。

↓実際に動作を確認してみる

sample.js
// Date.now()は時間を取得するメソッド
computed: {
  computedDate: function() {
    return Date.now();
  }
},
methods: {
  methodsDate: function() {
    return Date.now();
  }
}
sample.html
<ul>
  <li>{{ computedDate }}</li>
  <li>{{ computedDate }}</li>
  <!-- ↓ 処理結果 -->
  <li>1589364083951</li>
  <li>1589364083951</li>

</ul>
<ul>
  <li>{{ methodsDate }}</li>
  <li>{{ methodsDate }}</li>
  <!-- ↓ 処理結果 -->
  <li>1589364083951</li>
  <li>1589364292550</li>
</ul>

computedは何度呼び出しても一度しか処理が実行されないので出力される値も変わらない。
methodsは呼び出された回数分処理が実行されるのでバラバラの値になる。

使い分け方

computed
・ 引数が必要ない処理の場合
・ 処理を高速化させたい場合

methods
・ 引数が必要な処理の場合
・ 値を常に更新したい場合

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueのカスタムコンポーネントで双方向データバインディングを入れてみた

Vueのカスタムコンポーネントはすごく便利ですね。
HTMLのテンプレートとして使えて、しかも使う側がさらにHTMLを差し込むことができるのは重宝しています。

今回は、まずはカスタムコンポーネントを単純なテンプレートとして使う例を示した後、さらに汎用的にするために、カスタムコンポーネントを双方向データバインディングに対応させます。

ちなみに、ここらへんVueの方々が頑張っていただいているようで、仕様が変わる(使いやすくなる)ことがありますので、その際にはまた追従したいと思います。

最後に、自作のswagger定義ファイルエディタを紹介しています。

単純なコンポーネントの例:HTMLテンプレートとして使う

HTMLのテンプレートとして使い、使う側がさらにHTMLを差し込む例です。
期待はこんな感じです。

使う側はこんな風に書きます。

<custom-template>
    使う側が差し込みたいHTML
</custom-template>

そして、テンプレート側ではこんな感じで記載しておきます。

<div class="panel panel-default">
    <slot></slot>
</div>

最終的に、こんな感じに合成したいです。

<div class="panel panel-default">
    使う側が差し込みたいHTML
</div>

実際のテンプレート側は、以下のようなコードとなります。

Vue.component('custom-template-01', {
    template: `
        <div class="panel panel-default">
            <slot></slot>
        </div>`,
});

そうすると、使う側には以下のように合成されて見えるようになります。<slot></slot>が挿し代わっています。

    <custom-template-01>
      <div class="panel-body">
        Hello World
      </div>
    </custom-template-01>

ちなみに、使う側が差し込んだHTMLは使う側の制御範囲なので、v-modelなどそのまま使えます。
例えばこんな感じ。(使う側です)

    <label>data</label> {{data}}
    <br>
    <label>custom_template_01_a</label>
    <custom-template-01>
      <div class="panel-body">
        <input type="text" v-model="data" class="form-control">
      </div>
    </custom-template-01>

以降では、使う側を親、使われる側(HTMLテンプレート側)を子と呼ぶようにします。

HTMLテンプレート例でのデータバインディング

さきほどの、HTMLテンプレート例では、子の<slot></slot>の内容をすべて親に任せています。一方で、親からのパラメータを使って子が内容を作成したいことが多々あります。

そこで、もう一つ例を挙げます。まず子の方です。

Vue.component('custom-template-02', {
    props: ['header'],
    template: `
        <div class="panel">
            <div class="panel-heading">
                <h4 class="panel-title">{{header}}</h4>
            </div>
            <div class="panel-body">
                <slot></slot>
            </div>
            <div class="panel-footer">
                <slot name="footer"></slot>
            </div>
        </div>`,
});

親の方はこちらです。

    <custom-template-02 class="panel-default" header="This is Header">
      <template>
        Hello World
      </template>
      <template v-slot:footer>
        since 2020
      </template>
    </custom-template-02>

この例では、最初の例に比較して、追加の仕組みを3つ使っています。

・1つめ:差し込み先のslotに名前を付けました。

テンプレート内のname=footerの属性 が付いたslotエレメントが、v-slot:footer の属性を付けたtemplateエレメントの中身に置き換わります。footerという名前は自由に決められます。名前を付けたことで、差し込む場所を複数作ることができるようになります。

・2つめ:親から子にパラメータを渡しています。

子に、props: [ ‘header’] というプロパティが増えています。
これは、子側は親側からheaderというプロパティを受け取ることを宣言しています。

親は、header="This is Header" という感じで、カスタムコンポーネントの要素にheaderを追加していますので、それを子が受け取ることができています。
受け取った値は、

            <div class="modal-header">
                <h4 class="modal-title">{{header}}</h4>
            </div>

のような感じで、HTMLの中に含めているのがわかります。
これにより、すべてを親側に任せるだけでなく、親からパラメータを取得して子側でHTMLに反映することができます。

・3つ目:親で指定した属性が子のHTMLに渡されます。

class="panel-default" の部分ですが、headerと異なり、子側で受け取る準備(props指定)をしていません。
その場合、HTMLのテンプレートのルートのエレメントの属性として追加されます。

テンプレート上は、

        <div class="panel">

となっていますが、実際のHTMLに描画されたときには、

        <div class="panel panel-default">

となります。子側にclassがすでに指定済みですので、子側のclassに親からのclassの属性値が追加された形になっています。当然ながら、propsに指定済みの属性は除きます。

親と子の双方向データバインディング

propsを使って親のデータを子に渡しました。
また、slotを使って子がHTMLを生成する一部を親に任せることができました。
しかしながら、今まで説明してきた方法では子で処理した結果を親が受け取ることはできません。

何をいっているわからないかもしれませんが、Vueで必ず使う双方向データバインディングであるv-modelが使えていないということです。

inout_textのエディットボックスで入力した値を初期値とし、カスタムコンポーネント側でダイアログを表示して初期値を表示し、別の値を入力してもらって、その値をinout_textのエディットボックスに表示したい例です。
以下が期待する親のHTMLです。

    <input type="text" v-model="inout_text" class="form-control">

    <custom-template-03 class="panel-default" header="This is Header" v-model="inout_text">
      <template>
        Input Dialog Test
      </template>
    </custom-template-03>

少し分解すると、Vueとしてv-model=”inout_text”は以下に置き換えられます。

  v-bind:value=”input_text” v-on:input=”inout_text”

valueという名前で子に渡し、inputというイベントで親に返してもらえればよいわけです。

まず、valueの子への渡し方はすでに説明しました。
propsにvalueを追加すればよいだけです。
残るは、inputイベントです。

それには、子で以下を呼べばよいのです。

this.$emit(input, 返したい値)

それを反映したのがこちら。

Vue.component('custom-template-03', {
    props: ['value'],
    template: `
        <div class="panel">
            <div class="panel-body">
                <slot></slot>
            </div>
            <div class="panel-footer">
                <button class="btn btn-default" v-on:click="do_input">do_input</button>
            </div>
        </div>`,
    methods:{
        do_input: function(){
            var ret = window.prompt('入力してください。', this.value);
            if( ret )
                this.$emit('input', ret);
        }
    }
});

親から受け取ったthis.valueを子で書き換えたら、そのまま親にイベントが伝わってほしいかもしれません。ですが、this.valueやemitで、親からの子のデータバインディングと子から親へのイベントを組み合わせて実現しているだけで、それぞれの要素は、片方向でしかないのです。

子と孫の双方向データバインディング

HTMLテンプレート例では、親が子のカスタムコンポーネントを使っていました。
実際には、親が子のカスタムコンポーネントを使って、子がさらに孫のカスタムコンポーネントを使って、というように、階層的につながっていく場合が多々あります。
そうすると、子の中でもv-modelを使いたくなります。

まず、孫から。

Vue.component('custom-template-04-a', {
    props: ['value'],
    template: `
        <div>
            <button class="btn btn-default" v-on:click="do_input">do_input</button>
        </div>
    `,
    methods:{
        do_input: function(){
            var ret = window.prompt('入力してください。', this.value);
            if( ret )
                this.$emit('input', ret);
        }
    }
});

さきほど子側で、入力ダイアログを出していた部分を抜き出したものです。
次が子側です。孫に対してv-modelを使っています。

Vue.component('custom-template-04', {
    props: ['value'],
    template: `
        <div class="panel">
            <div class="panel-body">
                <slot></slot>
            </div>
            <div class="panel-footer">
                <custom-template-04-a v-model="value_"></custom-template-04-a>
            </div>
        </div>`,
    data: function(){
      return {
          value_: this.value,
      }
    },
    watch: {
        value: function(newValue){
            this.value_ = newValue;
        },
        value_: function(newValue){
            this.$emit('input', newValue);
        }
    }
});

親から受け取ったthis.valueは、親から子への片方向専用であり、子の中での孫との双方向データバインディングには使うことができません。
そこで、親から受け取ったthis.valueをthis.value_にコピーして、それを孫との双方向データバインディングに使っています。

    data: function(){
      return {
          value_: this.value,
      }
    },

の部分で、孫とのv-modelに使う変数を宣言し、(コンポーネントの場合、dataは関数で返さないといけないです)

    watch: {
        value: function(newValue){
            this.value_ = newValue;
        },

によって、親からvalueの変更通知を受け取ったら、this.value_に再コピーしています。
一方で、以下の孫のカスタムコンポーネント側でthis.value_値の更新通知が来ます。

    <custom-template-04-a v-model="value_"></custom-template-04-a>

そのイベントも、watchでthis.value_を監視することでフックしています。
トリガーされると、親に変更された新しい値を伝えています。

    watch: {
・・・
        value_: function(newValue){
            this.$emit('input', newValue);
        }
    }

まとめると、以下の形を覚えておけば、親-子-孫の間の双方向データバインディングを実現できそうです。

Vue.component('カスタムコンポーネント名', {
    props: ['value'],
    template: `
   表示したいHTML
    `,
    data: function(){
      return {
          value_: this.value,
      }
    },
    watch: {
        value: function(newValue){
            this.value_ = newValue;
        },
        value_: function(newValue){
            this.$emit('input', newValue);
        }
    }
});

以上でVueコンポーネントの実験は終わりです。

以下に、上記を確認できるページを用意しました。

Vueコンポーネント実験室
 https://poruruba.github.io/vuecomp_laboratory/labo_01/

image.png

サンプルアプリ:Swagger定義ファイルエディタ

さきほど作った双方向データバインディングの定型を使って、Swagger定義ファイルのエディタを作ってみました。

Web上から、エントリポイントを作成したり、メソッドを追加して、複数のパラメータを作ったりしできるようにしました。
(かなり端折っていますし、バグもたくさんあるとは思いますが、自己満足です。。。)

画面はこんな感じです。
お決まりの、Petstoreをサンプルとして読み込むボタンを用意したので、なんとなくイメージはできるかもしれません。

image.png

image.png

poruruba/vuecomp_laboratory
 https://github.com/poruruba/vuecomp_laboratory/

以下からアクセスできます。
 https://poruruba.github.io/vuecomp_laboratory/swagger_editor/

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】Modal実践編:QueryでModalを管理する②

前置き

picture_pc_8be74d31bdb9a712682ef9abdc34e5a9.gif

前回の続きです?
https://note.com/aliz/n/n47b0d98be524

1つのcomponentsで
複数のModalを表示させましょう✨

Step5: 中身をcomponents分けする

Modalの外側と内側で
componentsを切り分けていきましょう。
queryによって切り替えているpタグを
organismsに移動させます??
propsを使わなくて済むので
本当にただ移動させるだけです、楽ちん♪

file
components/
--| organisms/
----| modals/
-----| ModalContainer.vue
--| templates/
----| modals/
-----| ModalRoute.vue

layouts/
--| default.vue
ModalContainer.vue
<template>
 <div class="modal-container">
   <!-- 切り替える中身 -->
   <p
     v-if="$route.query.modal === 'login'"
     class="text"
   >
     {{ $route.query.modal }}
   </p>
   <p
     v-if="$route.query.modal == 'register'"
     class="text"
   >
     {{ $route.query.modal }}
   </p>
 </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
 name: 'Modal',
})
</script>

<style lang="scss" scoped>
 .modal-container {
   .text {
     font-size: 36px;
   }
 }
</style>

Step6: 中身をFormにする

picture_pc_8be74d31bdb9a712682ef9abdc34e5a9.gif

ついでにformを作りましょう?
password入力のinputも作ると
inputをmoleculesで作る必要が出てくるので
一旦イメージだけできればOKです??

ModalContainer.vue
<template>
 <div class="modal-container">
   <!-- 切り替える中身 -->
   <form
     class="form"
     v-if="$route.query.modal === 'login'"
     @submit.prevent
   >
     <label class="label">
       <span class="label">
         {{ $route.query.modal }}
       </span>
       <input
         v-model="form.email"
         :type="type"
         placeholder="email"
       >
     </label>
     <button
       class="button"
       type="submit"
       @click="$emit('submit', form)"
     >
       {{ $route.query.modal }}
     </button>
   </form>
   <form
     class="form"
     v-if="$route.query.modal === 'register'"
     @submit.prevent
   >
     <label class="label">
       <span class="label">
         {{ $route.query.modal }}
       </span>
       <input
         v-model="form.email"
         :type="type"
         placeholder="email"
       >
     </label>
     <button
       class="button"
       type="submit"
       @click="$emit('submit', form)"
     >
       {{ $route.query.modal }}
     </button>
   </form>
 </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
 name: 'Modal',
 data () {
   return {
     form: {
       email: '',
     },
   }
 },
 props: {
   type: {
     type: String,
     default: 'text',
   },
 },
})
</script>

<style lang="scss" scoped>
 .modal-container {
   .form {
     .label {
       font-size: 24px;

       .label {
         display: block;
       }
     }

     .button {
       display: block;
     }
   }
 }
</style>

【解説】
@click="$emit('submit', form)"
 inputに入力する値formを$emitで渡します。

ModalRoute.vue
<template>
 <div
   v-if="$route.query.modal"
   class="modal-route"
 >
   <div
     class="bg"
     @click="$router.push('/')"
   />
   <div class="modal-wrap">
     <button
       class="button"
       @click="$router.push('/')"
     >
       <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z" /></svg>
     </button>
     <ModalContainer
       @submit="submit"
     />
   </div>
 </div>
</template>

<script>
import Vue from 'vue'

export default Vue.extend({
 name: 'Modal',
 components: {
   ModalContainer: () => import('@/components/organisms/modals/ModalContainer.vue'),
 },
 methods: {
   submit (form) {
     console.log(form) // eslint-disable-line
   },
 },
})
</script>

<style lang="scss" scoped>
 .modal-route {
   position: fixed;
   top: 0;
   width: 100%;
   height: 100%;

   .bg {
     width: 100%;
     height: 100%;
     background-color: rgba(0,0,0,0.5);
   }

   .modal-wrap {
     border-radius: 8px;
     background-color: #ffffff;
     width: 50%;
     height: 50%;
     position: absolute;
     top: 50%;
     left: 50%;
     transform: translate(-50%, -50%);
     padding: 30px;

     .button {
       border: none;
       position: absolute;
       top: 5%;
       right: 2%;
     }
   }
 }
</style>

【解説】
・@submit
 $emitでつけたイベント名
・// eslint-disable-line
 consoleなど特定の行に書かないと
 ESLintでエラーになります。
 https://qiita.com/nju33/items/2d0cfea4fffbfdbff87a

Step7: Formを分ける

今はformが2種類ですが
もし増えたら管理が面倒なので
分けてしまいましょう?

え、1つのコンポーネントを
queryで切り替えるのが
メリットなんじゃないの??
分けたら意味なくない??
と思ったそこのアナタ❗️

安心してください?
componentタグを使えば良いのです✨

コンポーネント の基本

【ディレクトリ 】
modalsというファイルを作り
ModalContainerのform2つを
それぞれに分けましょう。

?before?

file
components/
--| organisms/
----| modals/
-----| ModalContainer.vue
--| templates/
----| modals/
-----| ModalRoute.vue

layouts/
--| default.vue

?after?

file
components/
--| templates/
----| modals/
-----| ModalRoute.vue

modals/
--| login.vue
--| register.vue

layouts/
--| default.vue

【modals/login.vue】
・formのregister部分を除きましょう?
・$emitのイベント名を分かりやすく
 submitLoginに変更しましょう!

login.vue
<template>
 <div class="modal-container">
   <!-- 切り替える中身 -->
   <form
     v-if="$route.query.modal === 'login'"
     class="form"
     @submit.prevent="$emit('submitLogin', form)"
   >
     <label class="label">
       <span class="label">
         {{ $route.query.modal }}
       </span>
       <input
         v-model="form.email"
         :type="type"
         placeholder="email"
       >
     </label>
     <button
       class="button"
       type="submit"
     >
       {{ $route.query.modal }}
     </button>
   </form>
 </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
 props: {
   type: {
     type: String,
     default: 'text',
   },
 },
 data () {
   return {
     form: {
       email: '',
     },
   }
 },
})
</script>

<style lang="scss" scoped>
 .modal-container {
   .form {
     .label {
       font-size: 24px;

       .label {
         display: block;
       }
     }

     .button {
       display: block;
     }
   }
 }
</style>

【modals/register.vue】
変更部分はlogin.vueと同じです

register.vue
<template>
 <div class="modal-container">
   <!-- 切り替える中身 -->
   <form
     v-if="$route.query.modal === 'register'"
     class="form"
     @submit.prevent="$emit('submitRegister', form)"
   >
     <label class="label">
       <span class="label">
         {{ $route.query.modal }}
       </span>
       <input
         v-model="form.email"
         :type="type"
         placeholder="email"
       >
     </label>
     <button
       class="button"
       type="submit"
     >
       {{ $route.query.modal }}
     </button>
   </form>
 </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
 props: {
   type: {
     type: String,
     default: 'text',
   },
 },
 data () {
   return {
     form: {
       email: '',
     },
   }
 },
})
</script>

<style lang="scss" scoped>
 .modal-container {
   .form {
     .label {
       font-size: 24px;

       .label {
         display: block;
       }
     }

     .button {
       display: block;
     }
   }
 }
</style>

【components/organisms/modals/ModalRoute.vue】

NidakRoute.vue
<template>
 <div
   v-if="$route.query.modal"
   class="modal-route"
 >
   <div
     class="bg"
     @click="$router.push('/')"
   />
   <div class="modal-wrap">
     <button
       class="button"
       @click="$router.push('/')"
     >
       <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z" /></svg>
     </button>
     <!-- <ModalContainer
       @submit="submit"
     /> -->
     <component
       :is="$route.query.modal"
       @submitLogin="submit($event)"
       @submitRegister="submit($event)"
     />
   </div>
 </div>
</template>

<script>
import Vue from 'vue'

export default Vue.extend({
 name: 'Modal',
 components: {
   login: () => import('@/modals/login.vue'),
   register: () => import('@/modals/register.vue'),
 },
 methods: {
   submit (form) {
     console.log(form) // eslint-disable-line
   },
 },
})
</script>

<style lang="scss" scoped>
 .modal-route {
   position: fixed;
   top: 0;
   width: 100%;
   height: 100%;

   .bg {
     width: 100%;
     height: 100%;
     background-color: rgba(0,0,0,0.5);
   }

   .modal-wrap {
     border-radius: 8px;
     background-color: #ffffff;
     width: 50%;
     height: 50%;
     position: absolute;
     top: 50%;
     left: 50%;
     transform: translate(-50%, -50%);
     padding: 30px;

     .button {
       border: none;
       position: absolute;
       top: 5%;
       right: 2%;
     }
   }
 }
</style>

【解説】
・component :is="$route.query.modal"
 └:isでcomponentを呼び出します!
  コンポーネントの登録が必要なので
  中身をそれぞれimportしましょう?
 └それぞれを1つのコンポーネントとみなし
  queryで切り替えましょう?
@submitLogin="submit($event)"
 └$emitのイベント名を変えたので
  どちらも書きましょう✍️
 └($event)は省略可能

お疲れ様でした?
完成です??

次回予告

【Nuxt.js】アプリ開発実践編:
Nuxt + Vuex + firebaseでログイン付きToDoリスト

こちらのTODOリストに
ログイン機能をつけていきます!✨
https://note.com/aliz/n/n8411db2c9a20

公開予定日は5/19(火)です?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CompositionAPIのwatchとwatchEffectの違い

みなさん、Vue v3(vue-next)使ってますか?私は使ってません。
Vue v2にComposition Apiパッケージを導入して雰囲気を楽しんでいます。

Composition API v0.5.0で追加されたwatchEffect関数をご存知でしょうか。
実はもう2ヶ月ほど前に追加されてますが私はつい先日触ったのでここに書いておこうと思います。

Vue v2のwatch

vue.v2
watch: {
  firstName: function (val) {
    this.fullName = val + ' ' + this.lastName
  },
  lastName: function (val) {
    this.fullName = this.firstName + ' ' + val
  }
}

皆さんに馴染みがあるのはこのwatchではないでしょうか。
firstNamelastNameの変数を監視して、変更があった場合に別の変数を更新する。
しかし、この場合は公式ページでも書かれているようにcomputedを使ったほうが良いでしょう。

vue.v2
computed: {
  fullName: function () {
    return this.firstName + ' ' + this.lastName
  }
}

なので、watchを使う場合は何らかの変数の変更を検知して他の処理を行いたい場合に使うことが多く、このように別の変数に入れ直す場合はcomputed、算出プロパティを使うのが良いと思います。
この辺は公式ページで丁寧に説明されてるのでおさらい程度で。

Vue v3のwatch

Vue v3もとい、CompoisitionAPI(v0.5.0以降)はwatchwatchEffectの2つのウォッチャーを提供しています。

vue.v3
setup() {
  const hoge = ref('')
  watch(
    () => hoge,
    (hoge) => {
      somethingMethod(hoge)
    }
  )
}

第一引数に監視対象、第二引数に変更を検出した際に実行したい関数を定義できます。
ほとんどv2と変わらない感じですね。

複数の監視対象にも対応していて次のように配列で渡すこともできます。

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

もうちょっと詳細に型定義を見てみると

// wacthing single source
function watch<T>(
  source: WatcherSource<T>,
  callback: (
    value: T,
    oldValue: T,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options?: WatchOptions
): StopHandle

// watching multiple sources
function watch<T extends WatcherSource<unknown>[]>(
  sources: T
  callback: (
    values: MapSources<T>,
    oldValues: MapSources<T>,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options? : WatchOptions
): StopHandle

type WatcherSource<T> = Ref<T> | (() => T)

type MapSources<T> = {
  [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}

// see `watchEffect` typing for shared options
interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // default: false
  deep?: boolean
}

StopHandleを返り値としているので以下のように監視を停止することができます。

setup() {
  const hoge = ref('')
  const stop = watch(
    () => hoge,
    (hoge) => {
      somethingMethod(hoge)
    }
  )
  stop()
}

監視対象のオブジェクトが階層化していて、そのすべての変更検出したい場合もあるかと思います。

setup() {
  const hoge = reactive({
    fuga: {
      piyo: true,
      hogera: false,
    },
    hogehoge: false
  })
  watch(
    () => hoge,
    () => {},
    { deep: true }
  )
}

この場合、watch関数の第三引数、WatchOptionsdeep: trueと指定することで実現できます。

watchEffect

Composition API v0.5.0で追加されたwatchEffectwatchとは異なり、callback内部にあるオブザーバブル値を検出して実行されます。

setup() {
  const hoge = ref('')
  watchEffect(() => {
    somethingMethod(hoge.value)
  })
}

watchに比べると監視対象を明示的に宣言していないので簡略して書けますが、何がトリガーで実行されてるかがわかりにくいという反面もあります。
また、複数のオブザーバブルな値が入る場合には注意が必要で

setup() {
  const hoge = ref('')
  const fuga = ref('')
  watchEffect(() => {
    fuga.value = somethingMethod(hoge.value)
  })
}

このように別のリアクティブ変数に入れ直す処理を書いた場合に、fuga変数の変更も監視されてwatchEffectが実行されるのでおそらく無限ループされます。
こういう場合はfugacomputedにするか、watchを使うと良いと思います。

型定義はこちら

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle

interface WatchEffectOptions {
  flush?: 'pre' | 'post' | 'sync'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

type InvalidateCbRegistrator = (invalidate: () => void) => void

type StopHandle = () => void

より詳細な情報は公式を見てください。
Vue v3楽しみですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.jsにgoogle analyticsを導入する

やりたいこと

vue.jsで構築したサービスにgoogle analyticsを簡単に入れたい

使用するライブラリ

vue-gtag

実装

ライブラリのインストール

npm install vue-gtag
main.js
import Vue from 'vue'
import App from './App.vue'
import router from "./router";
+ import VueGtag from "vue-gtag";


+ Vue.use(VueGtag, {
+   config: { id: "UA-xxxxx-xxx" }
+ });

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

以上です!

アナリティクスのページで確認
スクリーンショット 2020-05-15 14.16.55.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.js laravel-mix npm run watch 時々 TypeError: Cannot read property 'call' of undefined at __webpack_require__

このエラーがきつい。
極稀にビルドしするとなるんだよ。

で、ググっても同じエラーでて来ない。
参考として、

https://github.com/webpack/webpack/issues/95

なんだけど。

結論として、
「ブラウザのキャッシュ残ってまっせー!」
ってこと。

なので、毎回ファイル名を変更して吐き出すように設定する。

const mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|i

キャッシュ対策に version(); をつけているので、本番は削除する。

*/

/* めっちゃ大事 */
mix.webpackConfig({

    output: {
        chunkFilename: 'js/chunks/[name]-[hash].js',
    }
});



/* この version ってのも大事 */
mix.js('resources/js/app.js','public/js') .version()
.sass('resources/sass/app.scss','public/css')
.sass('resources/sass/lightbox.scss','public/css');


以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ElementUIのFormの自動バリデーション

コード

import { clone, equals } from 'ramda'
import Vue, { PropType } from 'vue'

type Answer = Record<any, any>

const isObject = (item: any): item is Record<any, any> =>
  typeof item === 'object' && item !== null && !Array.isArray(item)

export const AutoValidate = Vue.extend({
  name: 'FormAutoValidator',

  inject: ['elForm'],

  props: {
    valid: {
      type: Boolean,
      required: true,
    },
    answer: {
      type: Object as PropType<Answer>,
      required: true,
    },
    form: {
      type: Object as PropType<any>,
      default(): any {
        return this.elForm
      },
    },
  },

  data: () => ({
    prevAns: {} as Answer,
  }),

  computed: {
    isAnsInitial(): boolean {
      const isInitial = (a: Record<any, any>): boolean =>
        Object.values(a)
          .map(v => (isObject(v) ? isInitial(v) : v === undefined))
          .every(v => v)

      return isInitial(this.answer)
    },
  },

  watch: {
    answer: {
      handler(ans: Answer): void {
        this.validate(() => {
          if (this.form && this.form.clearValidate && typeof this.form.clearValidate === 'function') {
            const differentKeys = this.findDifference(ans, this.prevAns)
            this.form.clearValidate(Object.keys(this.answer).filter(k => !differentKeys.includes(k)))
          }
        })
        this.prevAns = clone(ans)
      },
      deep: true,
      immediate: true,
    },
  },

  methods: {
    validate(cb?: (valid: boolean) => void): void {
      const form = this.form
      if (form) {
        if (form.validate && typeof form.validate === 'function') {
          form.validate((valid: boolean) => {
            const v = !this.isAnsInitial && valid
            if (cb) cb(v)
            this.$emit('update:valid', v)
          })
        }
      } else if (process.env.NODE_ENV === 'development') {
        console.error('This component should be used in el-form or MUST pass form prop')
      }
    },

    findDifference(a1: Answer, a2: Answer): string[] {
      try {
        return Object.keys(a1)
          .map(key => (equals(a1[key], a2[key]) ? '' : key))
          .filter(v => Boolean(v))
      } catch {
        return []
      }
    },
  },

  render() {
    return <div />
  },
})

使用例

<template>
  <el-form :model="answer" label-position="top" :rules="rules">
    <auto-validate :valid.sync="valid" :answer="answer" />

    <el-form-item label="Q1. 質問!!" prop="q1">
      <el-select v-model="answer.q1">
        <el-option v-for="({ lavel, value }) in data.q1" :key="value" :label="label" :value="value" />
      </el-select>
    </el-form-item>
  </el-form>
</template>

簡単な解説

ElementUIのFormは、Vuetifyのそれと違ってv-modelでvalidかどうかを取得できません。それの対処としてのこのコンポーネントです。

el-formの子孫ではinjectを使用してelFormが受け取れる(少なくともv2.13.2現在)ので、それを利用します。また、フォームの中身の変更の監視はanswerpropを受け取り、それを監視することで行います。

基本的には、公式docにもあるようにelForm.validate()を行うことでvalidかどうかをcallbackで受け取れるので、それを利用します。ただし、これを行うとエラーメッセージが出てとても煩わしいので、それをclearするようなメソッドも作っています。

そのためにramdaを使用していますが、cloneを使っているのはanswerはold valueをwatchの引数として受け取れない(refなので第2引数と第1引数が同じになる)という理由です。また、equalは、deep equalのようなものの実現のために入れています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む