- 投稿日:2020-05-15T21:07:06+09:00
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
- 投稿日:2020-05-15T19:52:01+09:00
リモートカードゲームを遊べるランダムマッチアプリを開発した話
はじめに
私の趣味の一つに、トレーディングカードゲーム(TCG)があります。
昨今の外出自粛要請による影響は、TCG 界隈においても例外に漏れず、対面で TCG を遊ぶ機会のほとんどがなくなってしまいました。
この記事では、少しでも TCG 界隈の停滞感をなくすために開発した Web アプリの紹介と開発手順のメモ書きをまとめています。鋭意作成中です。
対象読者
- IT 分野以外の趣味で IT を活用したい人
- Twitter アカウントによる認証を用いた Web アプリをつくりたい人
- WebRTC(ビデオ通話など)を活用した Web アプリをつくりたい人
- Nuxt.js/TypeScript の開発に興味がある人
モチベーション
TCG は大前提として対面で遊ぶことを想定したアナログゲームですが、学生以下をメインターゲット層としたものも多く、歳を重ねるにつれて身の周りから TCG を一緒に遊ぶ友達がだんだんと減っていくものです(悲しい)。
他方、TCG を専門に取り扱うカードショップなどでは、店舗大会や公認自主イベントと呼ばれるような、誰でも気軽に参加できるイベントが定期的に開催され、友達や知り合いがいなくても TCG を楽しめる環境が整っていました。しかしながら、昨今の外出自粛要請により、カードショップでのイベントは全面的に中止に。
このような状況を受けてか、TCG 界隈では、知り合い同士で Skype や Discord などのビデオ通話アプリを用いたリモートカードゲームの環境を積極的に取り入れつつあります。
私自身、今回の騒動以前から何度かリモートカードゲームを遊んだことがありますが、以下のような課題があると感じています。
- 知り合いの中に、都合のつく人がなかなか見つからない
- とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
- かといって、知り合い以外の募集に自分から声をかける勇気もない
- 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒
ただの面倒くさがりやんけ。
まあ、とにかくそのような課題の解決を目指す気持ち半分、「転活のポートフォリオがわりに使えねえかなぁ」の気持ち半分で開発した Web アプリがこの『PTCGrand』です。【ゆる宣伝】#リモートポケカ を気軽に遊べるランダムマッチアプリ「PTCGrand」を開発中です?
— てぃら (@tilanosaur) May 3, 2020
SkypeやDiscordの連絡先交換いらずで、TwitterアカウントがあればOK?♀️
まだ開発途上なのでちょいちょい不具合もありますが、ひとまず動かせるようになったので公開しちゃいます↓https://t.co/tUvD7lSmf9 pic.twitter.com/YWhnuVIwWm基本アイディアとして、対戦型オンラインゲームでは一般的なランダムマッチング機能を実装しました。
そのため、たとえ知り合いがいなくても、リアルタイムに対戦相手を募集している人同士でリモートカードゲームを遊ぶことができます。
- 知り合いの中に、都合のつく人がなかなか見つからない
- とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
- かといって、知り合い以外の募集に自分から声をかける勇気もない
さらに、ビデオ通話機能を Web 上で完結させることで、Skype や Discord の連絡先交換は言わずもがな、アプリのインストールすら不要にしました。
また、この Web アプリを利用するのに複雑な手順があるとかえって面倒になってしまうため、Twitter アカウントとの連携による認証を導入し、設定手順も最小化しています。
- 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒
特定 TCG プレイヤー向けの紹介記事 は(別名義で)note に投稿済みなので、興味ある方はそちらも併せてご覧ください。
フレームワーク と アーキテクチャ
開発した Web アプリの大まかな構成と動作は、以下の通りです。
Nuxt.js/Typescript
- SPA (Single Page Application) の生成
- Bootstrap4 (BootstrapVue) によるデザインレイアウト
Firebase
- Nuxt.js で生成した SPA のホスティング (Firebase Hosting)
- Twitter 連携によるユーザー認証 (Firebase Authentication)
- データベース機能の提供(Peer 情報と対戦希望の管理) (Firebase Cloud Firestore)
SkyWay
- ユーザー間のビデオ通話・データ通信の確立
開発手順
重要そうなところやハマったところを中心に、メモ書きを残しています。
用意が面倒なのでスクリーンショットなどを掲載していないため、詳しく知りたい方は、適宜、記事末尾にある参考記事をご覧いただければと思います。バージョン
$ node -v v12.16.2 $ npm -v 6.14.4Nuxt.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.jsexport 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.jsexport default { ... + generate: { + dir: 'public' + }, ...
以上で、準備が整いました。
下記コマンドで SPA を生成し、Firebase Hosting にデプロイします。$ npm run generate $ firebase deployなお、一度公開した SPA を非公開にしたい場合、下記コマンドを実行します。
$ firebase hosting:disable参考記事
- Nuxt.js + Firebase Authentication + FireStoreでwebアプリケーションハンズオン (@ririli)
- Nuxt.js + Vuex + Firebase(Authentication, Cloud Firestore)でTwitterユーザー向けの伝言板を作る (@ysd_marrrr)
- はじめてのvue-property-decorator (nuxtにも対応) (@simochee)
- vuex + typescriptをvuex-module-decoratorsで無敵になる (@tsrnk)
- Nuxt.js + Typescript + Vuexする現時点のベストと思う方法 (@suzukenz)
- 0から始める Firestore + Firebase Authentication (@karayok)
- Nuxt(SPA)+Firebaseでログイン処理を実装する (@pochopocho13)
- Nuxt.jsのプロジェクトでnuxt-i18nを使ってi18n(国際化)対応する方法 (@munieru_jp)
- nuxt-i18nでNuxt.jsの国際化して、英語版と日本語版を用意する (くらげになりたい。)
おわりに
せっかくそれなりの形になった Web アプリを開発したので、開発手順のメモ書きをまとめ
ました。たかったのですが途中で力尽きてしまいました。アプリについては、公開から数日経ったものの肝心のユーザー数はまだまだ少なく、なかなかランダムマッチングが成立していない状況です。
その結果、半ば “待ち合わせ時間を決めておく” のが必要になっており、上であげた課題そのものにぶち当たっています(悲しい)。
開発モチベーションの半分が半分なのであれですが、個人開発だからこそ、機能をゴリゴリ実装するだけでなく、必要な人のもとに届くための施策を考えていきたいですね。
- 投稿日:2020-05-15T19:52:01+09:00
リモートカードゲームを気軽に遊べるランダムマッチアプリを開発した話
はじめに
私の趣味の一つに、トレーディングカードゲーム(TCG)があります。
昨今の外出自粛要請による影響は、TCG 界隈においても例外に漏れず、対面で TCG を遊ぶ機会のほとんどがなくなってしまいました。
この記事では、少しでも TCG 界隈の停滞感をなくすために開発した Web アプリの紹介と開発手順のメモ書きをまとめています。鋭意作成中です。
対象読者
- IT 分野以外の趣味で IT を活用したい人
- Twitter アカウントによる認証を用いた Web アプリをつくりたい人
- WebRTC(ビデオ通話など)を活用した Web アプリをつくりたい人
- Nuxt.js/TypeScript の開発に興味がある人
モチベーション
TCG は大前提として対面で遊ぶことを想定したアナログゲームですが、学生以下をメインターゲット層としたものも多く、歳を重ねるにつれて身の周りから TCG を一緒に遊ぶ友達がだんだんと減っていくものです(悲しい)。
他方、TCG を専門に取り扱うカードショップなどでは、店舗大会や公認自主イベントと呼ばれるような、誰でも気軽に参加できるイベントが定期的に開催され、友達や知り合いがいなくても TCG を楽しめる環境が整っていました。しかしながら、昨今の外出自粛要請により、カードショップでのイベントは全面的に中止に。
このような状況を受けてか、TCG 界隈では、知り合い同士で Skype や Discord などのビデオ通話アプリを用いたリモートカードゲームの環境を積極的に取り入れつつあります。
私自身、今回の騒動以前から何度かリモートカードゲームを遊んだことがありますが、以下のような課題があると感じています。
- 知り合いの中に、都合のつく人がなかなか見つからない
- とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
- かといって、知り合い以外の募集に自分から声をかける勇気もない
- 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒
ただの面倒くさがりやんけ。
まあ、とにかくそのような課題の解決を目指す気持ち半分、「転活のポートフォリオがわりに使えねえかなぁ」の気持ち半分で開発した Web アプリがこの『PTCGrand』です。【ゆる宣伝】#リモートポケカ を気軽に遊べるランダムマッチアプリ「PTCGrand」を開発中です?
— てぃら (@tilanosaur) May 3, 2020
SkypeやDiscordの連絡先交換いらずで、TwitterアカウントがあればOK?♀️
まだ開発途上なのでちょいちょい不具合もありますが、ひとまず動かせるようになったので公開しちゃいます↓https://t.co/tUvD7lSmf9 pic.twitter.com/YWhnuVIwWm基本アイディアとして、対戦型オンラインゲームでは一般的なランダムマッチング機能を実装しました。
そのため、たとえ知り合いがいなくても、リアルタイムに対戦相手を募集している人同士でリモートカードゲームを遊ぶことができます。
- 知り合いの中に、都合のつく人がなかなか見つからない
- とはいえ、事前に日程調整しておく(待ち合わせ時間を決めておく)のは面倒
- かといって、知り合い以外の募集に自分から声をかける勇気もない
さらに、ビデオ通話機能を Web 上で完結させることで、Skype や Discord の連絡先交換は言わずもがな、アプリのインストールすら不要にしました。
また、この Web アプリを利用するのに複雑な手順があるとかえって面倒になってしまうため、Twitter アカウントとの連携による認証を導入し、設定手順も最小化しています。
- 相手が見つかったとて、Skype やら Discord やらを使い分け、都度連絡先を登録するのも面倒
特定 TCG プレイヤー向けの紹介記事 は(別名義で)note に投稿済みなので、興味ある方はそちらも併せてご覧ください。
フレームワーク と アーキテクチャ
開発した Web アプリの大まかな構成と動作は、以下の通りです。
Nuxt.js/Typescript
- SPA (Single Page Application) の生成
- Bootstrap4 (BootstrapVue) によるデザインレイアウト
Firebase
- Nuxt.js で生成した SPA のホスティング (Firebase Hosting)
- Twitter 連携によるユーザー認証 (Firebase Authentication)
- データベース機能の提供(Peer 情報と対戦希望の管理) (Firebase Cloud Firestore)
SkyWay
- ユーザー間のビデオ通話・データ通信の確立
開発手順
重要そうなところやハマったところを中心に、メモ書きを残しています。
用意が面倒なのでスクリーンショットなどを掲載していないため、詳しく知りたい方は、適宜、記事末尾にある参考記事をご覧いただければと思います。バージョン
$ node -v v12.16.2 $ npm -v 6.14.4Nuxt.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.jsexport 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.jsexport default { ... + generate: { + dir: 'public' + }, ...
以上で、準備が整いました。
下記コマンドで SPA を生成し、Firebase Hosting にデプロイします。$ npm run generate $ firebase deployなお、一度公開した SPA を非公開にしたい場合、下記コマンドを実行します。
$ firebase hosting:disable参考記事
- Nuxt.js + Firebase Authentication + FireStoreでwebアプリケーションハンズオン (@ririli)
- Nuxt.js + Vuex + Firebase(Authentication, Cloud Firestore)でTwitterユーザー向けの伝言板を作る (@ysd_marrrr)
- はじめてのvue-property-decorator (nuxtにも対応) (@simochee)
- vuex + typescriptをvuex-module-decoratorsで無敵になる (@tsrnk)
- Nuxt.js + Typescript + Vuexする現時点のベストと思う方法 (@suzukenz)
- 0から始める Firestore + Firebase Authentication (@karayok)
- Nuxt(SPA)+Firebaseでログイン処理を実装する (@pochopocho13)
- Nuxt.jsのプロジェクトでnuxt-i18nを使ってi18n(国際化)対応する方法 (@munieru_jp)
- nuxt-i18nでNuxt.jsの国際化して、英語版と日本語版を用意する (くらげになりたい。)
おわりに
せっかくそれなりの形になった Web アプリを開発したので、開発手順のメモ書きをまとめ
ました。たかったのですが途中で力尽きてしまいました。アプリについては、公開から数日経ったものの肝心のユーザー数はまだまだ少なく、なかなかランダムマッチングが成立していない状況です。
その結果、半ば “待ち合わせ時間を決めておく” のが必要になっており、上であげた課題そのものにぶち当たっています(悲しい)。
開発モチベーションの半分が半分なのであれですが、個人開発だからこそ、機能をゴリゴリ実装するだけでなく、必要な人のもとに届くための施策を考えていきたいですね。
- 投稿日:2020-05-15T19:12:35+09:00
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.jsver 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.jsvar 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.jsvar 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ディレクティブでよく使われる。sample.jsvar 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
・ 引数が必要な処理の場合
・ 値を常に更新したい場合
- 投稿日:2020-05-15T17:21:10+09:00
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/サンプルアプリ:Swagger定義ファイルエディタ
さきほど作った双方向データバインディングの定型を使って、Swagger定義ファイルのエディタを作ってみました。
Web上から、エントリポイントを作成したり、メソッドを追加して、複数のパラメータを作ったりしできるようにしました。
(かなり端折っていますし、バグもたくさんあるとは思いますが、自己満足です。。。)画面はこんな感じです。
お決まりの、Petstoreをサンプルとして読み込むボタンを用意したので、なんとなくイメージはできるかもしれません。poruruba/vuecomp_laboratory
https://github.com/poruruba/vuecomp_laboratory/以下からアクセスできます。
https://poruruba.github.io/vuecomp_laboratory/swagger_editor/以上
- 投稿日:2020-05-15T17:20:30+09:00
【Nuxt.js】Modal実践編:QueryでModalを管理する②
前置き
前回の続きです?
https://note.com/aliz/n/n47b0d98be5241つのcomponentsで
複数のModalを表示させましょう✨Step5: 中身をcomponents分けする
Modalの外側と内側で
componentsを切り分けていきましょう。
queryによって切り替えているpタグを
organismsに移動させます??
propsを使わなくて済むので
本当にただ移動させるだけです、楽ちん♪filecomponents/ --| organisms/ ----| modals/ -----| ModalContainer.vue --| templates/ ----| modals/ -----| ModalRoute.vue layouts/ --| default.vueModalContainer.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にする
ついでに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/2d0cfea4fffbfdbff87aStep7: Formを分ける
今はformが2種類ですが
もし増えたら管理が面倒なので
分けてしまいましょう?え、1つのコンポーネントを
queryで切り替えるのが
メリットなんじゃないの??
分けたら意味なくない??
と思ったそこのアナタ❗️安心してください?
componentタグを使えば良いのです✨【ディレクトリ 】
modalsというファイルを作り
ModalContainerのform2つを
それぞれに分けましょう。?before?
filecomponents/ --| organisms/ ----| modals/ -----| ModalContainer.vue --| templates/ ----| modals/ -----| ModalRoute.vue layouts/ --| default.vue?after?
filecomponents/ --| 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(火)です?
- 投稿日:2020-05-15T15:38:55+09:00
CompositionAPIのwatchとwatchEffectの違い
みなさん、Vue v3(vue-next)使ってますか?私は使ってません。
Vue v2にComposition Apiパッケージを導入して雰囲気を楽しんでいます。Composition API v0.5.0で追加されたwatchEffect関数をご存知でしょうか。
実はもう2ヶ月ほど前に追加されてますが私はつい先日触ったのでここに書いておこうと思います。Vue v2のwatch
vue.v2watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } }皆さんに馴染みがあるのはこのwatchではないでしょうか。
firstName
とlastName
の変数を監視して、変更があった場合に別の変数を更新する。
しかし、この場合は公式ページでも書かれているようにcomputed
を使ったほうが良いでしょう。vue.v2computed: { fullName: function () { return this.firstName + ' ' + this.lastName } }なので、
watch
を使う場合は何らかの変数の変更を検知して他の処理を行いたい場合に使うことが多く、このように別の変数に入れ直す場合はcomputed
、算出プロパティを使うのが良いと思います。
この辺は公式ページで丁寧に説明されてるのでおさらい程度で。Vue v3のwatch
Vue v3もとい、CompoisitionAPI(v0.5.0以降)は
watch
とwatchEffect
の2つのウォッチャーを提供しています。vue.v3setup() { 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関数の第三引数、
WatchOptions
でdeep: true
と指定することで実現できます。watchEffect
Composition API v0.5.0で追加された
watchEffect
はwatch
とは異なり、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
が実行されるのでおそらく無限ループされます。
こういう場合はfuga
をcomputed
にするか、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楽しみですね。
- 投稿日:2020-05-15T14:25:15+09:00
vue.jsにgoogle analyticsを導入する
やりたいこと
vue.jsで構築したサービスにgoogle analyticsを簡単に入れたい
使用するライブラリ
実装
ライブラリのインストール
npm install vue-gtagmain.jsimport 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-15T10:05:48+09:00
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');以上。
- 投稿日:2020-05-15T01:22:39+09:00
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現在)ので、それを利用します。また、フォームの中身の変更の監視はanswer
のprop
を受け取り、それを監視することで行います。基本的には、公式docにもあるように
elForm.validate()
を行うことでvalid
かどうかをcallbackで受け取れるので、それを利用します。ただし、これを行うとエラーメッセージが出てとても煩わしいので、それをclearするようなメソッドも作っています。そのために
ramda
を使用していますが、clone
を使っているのはanswer
はold valueをwatch
の引数として受け取れない(refなので第2引数と第1引数が同じになる)という理由です。また、equal
は、deep equalのようなものの実現のために入れています。