- 投稿日:2020-07-13T19:09:14+09:00
Vueitfyのv-list-itemでネストしたpathの子のページに行くと親もactiveになってしまう問題
あらすじ
/items
と/items/edit
のようなリンクのあるサイドバーを実現したい- デフォルトでは
/items/edit
を開くと/items
もアクティブになるので困るvue-router
の:exact
のpropをBooleanで渡して完全一致か否かを指定するexact
では/items?category=プロテイン
の様にQuery Paramを指定するとアクティブにならない- 特定ページにいる場合、
:to
propに渡すpathにもquery paramを渡してあげる- 解決!!
解決コード
<template> <v-app> <v-navigation-drawer v-model="drawer" app> <v-list dense nav> <template v-for="(page, index) in pageList"> <!-- ネストしていない構造 --> <v-list-item v-if="typeof page.items !== 'object'" :key="index" :to="page.to" :exact="page.exact" > <v-list-item-action> <v-icon>{{ page.icon }}</v-icon> </v-list-item-action> <v-list-item-content> <v-list-item-title v-text="page.title" /> </v-list-item-content> </v-list-item> <!-- ネストしている構造 --> <v-list-group v-else :key="index" :prepend-icon="page.icon" > <template v-slot:activator> <v-list-item-content> <v-list-item-title v-text="page.title" /> </v-list-item-content> </template> <v-list-item v-for="(childPage, cIndex) in page.items" :key="cIndex" :to="childPage.to" :exact="childPage.exact" > <v-list-item-icon> <v-icon>{{ childPage.icon }}</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title v-text="childPage.title" /> </v-list-item-content> </v-list-item> </v-list-group> </template> </v-list> </v-navigation-drawer> </v-app> </template> <script> export default { computed: { pageList () { const route = this.$route // NOTE: どうしてもクエリーパラメーターを使いたい場合は当該pathにいる場合にrouteからfullPathを取得して当てる const itemsPath = route.path === '/items' ? route.fullPath : '/items' return [ // ホームはexactをtrueにしなくても問題無い { icon: 'home', title: 'ホーム', to: '/', exact: true, }, { icon: 'shopping_cart', title: '商品', // `/items/`以下に反応されたら困るのでexactをfalse // NOTE: しかし、このままだとクエリーパラメーターを使えない items: [ { title: '商品一覧', // to: '/items' to: itemsPath, exact: true, }, // 新規と編集の両方を許可したいのでexacrtはfalse { title: '商品登録', to: '/items/edit', exact: false, }, ] }, ] } } } </script>解説
デフォルトでは/items/editを開くと/itemsもアクティブになるので困る
vue-touterはデフォルトでは部分一致でアクティブの判定をしてしまいます。
/items
と/items/edit
のような構造のpathがある場合はexact
で完全一致でアクティブ判定をするように指定してあげます。https://router.vuejs.org/ja/api/#exact
この時、exactはBooleanで指定できるので
完全一致にしたいものだけexactをtrueにしていしてあげる事もできます。<v-list-item v-if="typeof page.items !== 'object'" :key="index" :to="page.to" :exact="page.exact" >exact(完全一致)ではQuery Paramを指定するとアクティブにならない
exactでは/items?category=プロテインのようにQuery Paramを指定するとアクティブにならない
GitHubのissuesなどで色々漁った結果、公式で対応する予定は無さそうな雰囲気がしたので無理やり対応してみます。
https://github.com/vuejs/vue-router/issues/2040
特定ページにいる場合、クエリーも渡す
参考
How do I style an active route that has query params? - by stackoverflowちょっと強引な解決方法になりますが、もし他にいい方法が有ればコメントで教えて下さい!!
<script> export default { computed: { pageList () { const route = this.$route // NOTE: どうしてもクエリーパラメーターを使いたい場合は当該pathにいる場合にrouteからfullPathを取得して当てる const itemsPath = route.path === '/items' ? route.fullPath : '/items' return [{ icon: 'shopping_cart', title: '商品一覧', to: itemsPath, exact: true, }] } } } </script>
- 投稿日:2020-07-13T17:46:30+09:00
nuxt plugins では process.client は undefined なので注意(axios のクライアント側エラー処理を書く時のハマりどころ)
これは何
axios のクライアント側エラー処理を書く時にはまったところについて共有のための記事です。
process.client
は使えないので、そもそもクライアント用とサーバ用でファイルを分けようという話です。バージョン情報
{ "nuxt": "^2.11.0", "@nuxt/typescript-build": "^0.5.2", "@nuxt/typescript-runtime": "^0.3.3", "@nuxtjs/axios": "^5.9.5" }トラブルについて
クライアントサイドでその処理が実行されるか、というのを制御できる
process.client
プロパティがnuxtアプリケーションではよく使われます。plugins読み込みの時にはこのプロパティが設定されていないので
undefined
になってしまいます。plugins/plugin.tsif (process.client) { // 何か処理 }と書くと、一見動くように見えるのですが、実際には
process.client
がundefined
になるのでifの中の処理は常に実行されませんでした。具体的に起こった問題は以下のようなもの
- サーバ側処理もクライアント側処理も同じプラグインファイルに書いていた
- クライアント側の処理をして問題なければ
return
して処理おわり、その後にサーバサイドの処理をして問題あればnuxt error
を吐くみたいな処理を書いていた- そもそもifに入っていないので
return
で処理が止まらず、クライアント側のクライアントエラー(4xx)でもnuxt error
を吐いて死んでしまう解決策
clientとserverでそれぞれ実行する処理をファイル名を分けて、それぞれnuxt設定ファイルで読み込みます。
nuxt.config.tsplugins: [ '@/plugins/axios.client.ts', '@/plugins/axios.server.ts', ],他にも
mode
を書く方法もあるようですが、ファイル名だけをみた時にどちらで実行されるのか?というのがはっきり分かるので、ファイル名で分けたほうが個人的にはいいかなと思います。補足
process.client
はundefined
になってしまうのですが、process
オブジェクト自体は定義されているようでした。
これはnuxtがnodejsのインターフェースの拡張しているもののようです。plugin のなかで
console.log(process);
すると以下のようになりました。
.browser
を使えばフロントかサーバかの判定はできるようですが、すでにnuxtでは非推奨になっていました。参考
https://ja.nuxtjs.org/api/configuration-plugins
https://ja.nuxtjs.org/guide/plugins/
https://stackoverflow.com/questions/58146662/process-server-is-undefined-in-nuxt-js-modules
- 投稿日:2020-07-13T14:36:20+09:00
[SPA]自分なりに脆弱性を潰しながらフロントエンドに認証情報を扱う
最近、新規事業で完全0-1のプロダクト開発をした際に
Rails/Vue.jsのSPAでいかにセキュアに認証情報を扱うかについて調べた結果を書いていきます
「それおかしくない?」とか「脆弱性つぶせてなくない?」とかあればコメントで指摘していただけるととても助かりますどう実装するか
実装の意図や詳細については下の方で解説します
Rails側
- 認証情報を
secure
属性とhttponly
属性をつけたcookieでフロントエンドに返す
- domain属性はAPIのドメインのみにする(特に設定変えなければデフォルトでそうなってるはず)
- 独自ヘッダの有無のチェックを認証処理に組みこむ
- gem
rack-cors
を導入する
- originsをSPAのURLのみに制限する
credentials
をtrue
にしておくVue.js側
withCredentials
オプションをtrue
に設定する(axiosやxhr)
- fetchなら
credentials: 'include'
とかかな- リクエストに独自ヘッダをつける
- なんでもいいし中身も空文字でよくて、ヘッダがあることが重要
なぜ上記のような実装をするのか
以下の3つがポイントになります
httponly
属性とsecure
属性が有効でdomain
属性にはAPIのドメインのみを設定したcookieでアクセストークンを持たせる- 独自ヘッダが必要であること
- corsでoriginsを制限されていること
cookieについて
なぜhttponlyにするのか
- XSSや悪意あるパッケージなどのjsによりcookieにあるトークンを抜かれないようにするため
- jsから抜かれる危険性はlocalstorageを使わない理由でもある
- 代わりに、cookieにあるトークンを非同期通信でもリクエストするために
withCredentials
オプションを有効にするなぜsecureにするのか
- 非SSLのリクエストを盗聴されないように、とかっていうよくある理由
なぜdomainをAPIのドメインのみに制限するのか
- サブドメをワイルドカードにしたりとか他のドメインを許容する必要がないため
- 少なくとも今回のケースでは複数ドメインをまたいでcookieの共有は必要なかった
- withCredentialを有効にしてcookieとして飛ばすからフロントエンドのjsからcookieを操作できる必要はない
- フロントのurlからはcookie見えないけどAPIのドメインへのcookieとしてちゃんと存在しているのでwithCredential有効にしておけばちゃんとリクエストされる
- 恥ずかしながら自分は最初「フロントのドメインでも扱えなきゃいけないよなぁ」と思い込んでいました。。。
独自ヘッダについて
- htmlのformによるCSRF対策
- htmlのform(同期通信)では独自ヘッダを付与することはできないため
CORSについて
- 独自ヘッダと併用することでjsによるCSRF対策となる
- originsでちゃんと制限かけることで外からの不正なリクエストを弾く
- ただし、それだけでは不十分で、ブラウザ上ではエラーになるがリクエスト自体は飛んで処理されてしまう
- 不正なGETには有効だがPOSTは処理されてしまうので困る
- POSTは条件次第(formDataを使うこと)で単純リクエストになり、プリフライトリクエストが走らない場合があるため独自ヘッダによりプリフライトを強制させる
- これによりプリフライトリクエストで止める事ができるので不正なリクエストを飛ばせなくすることができる
- 独自ヘッダは2度美味しい
- ちなみに、rack-corsの設定で
credentials: true
にしないとフロントでwithCredential: true
のリクエストを弾いてしまう実装のサンプル
一部抜粋して書いていきます
参考記事などは下の方にまとめてますrails側
rack-corsなどの設定
config/application.rb# ...割愛 # apiモードのrailsでcookiesを使えるようにするため # コントローラ側に `include ActionController::Cookies` も必要 config.middleware.use ActionDispatch::Cookies config.middleware.insert_before 0, Rack::Cors do allow do # 複数のorigins制限するためドメインの配列 # 環境変数はjsonにすると配列が簡単に扱えるやんという最近見つけたtips origins JSON.parse(ENV.fetch('CORS_DOMAINS_JSON') { '[]' }) resource "*", # axiosなどで `withCredential: true` にした上で、そのcookieを受け取るため credentials: true, headers: :any, methods: [:get, :post, :patch, :delete] end end # ...割愛認証情報をcookieで返す
- アクセストークンの期限はDBにあるので
permanent
- 開発環境はsecure外したいので環境変数
COOKIE_SECURE
で操作# ...割愛 cookies.permanent[:access_token] = { value: access_token, httponly: true, secure: ENV['COOKIE_SECURE'].present?, } cookies.permanent[:refresh_token] = { value: refresh_token, httponly: true, secure: ENV['COOKIE_SECURE'].present?, } # ...割愛認証処理
access_token = cookies[:access_token] # ...割愛(access_tokenチェックの処理など) raise 適当な例外クラス if request.headers[:'X-REQUESTED-BY-MY-APP'].nil?Vue.js側
withCredentialsを有効にする
src/main.js// ...割愛 Vue.prototype.$http = axios.create( { baseURL: process.env.VUE_APP_API_URI, withCredentials: true }, ); // ...割愛独自ヘッダをつける
- Vue.prototype.$httpにインスタンス入れることでどこからでも
this.$http
で使える- interceptorsを使うことですべてのリクエストでヘッダ設定の処理を走らせる
src/main.js// ...割愛 Vue.prototype.$http.interceptors.request.use((request) => ({ ...request, headers: { ...request.headers, ...{ 'X-REQUESTED-BY-MY-APP': '' }, }, })); // ...割愛疑問
XSS脆弱性があっても認証情報を抜かれないための対策とか、外からのCSRFの対策はしたけど
XSSでXHRのコードをインジェクトされて外部ではなく本来のバックエンドに勝手にリクエストを飛ばされないようにするための対策ってどうすればいいんだろう。。。
根本的にXSSを塞ぐしかないってことになるのかな参考記事
rails-apiでcookieを使う
RailsでAPIにCORSを設定する
rack-corsでCORS設定をする
さよならCSRF(?) 2017
CORS: OPTIONSリクエスト(preflight request)を避ける
- 投稿日:2020-07-13T14:36:20+09:00
[SPA]脆弱性を潰して認証情報を扱う[アクセストークン]
最近、新規事業で完全0-1のプロダクト開発をした際に
Rails/Vue.jsのSPAでいかにセキュアに認証情報を扱うかについて調べた結果を書いていきます
「それおかしくない?」とか「脆弱性つぶせてなくない?」とかあればコメントで指摘していただけるととても助かりますどう実装するか
実装の意図や詳細については下の方で解説します
Rails側
- 認証情報を
secure
属性とhttponly
属性をつけたcookieでフロントエンドに返す
- domain属性はAPIのドメインのみにする(特に設定変えなければデフォルトでそうなってるはず)
- 独自ヘッダの有無のチェックを認証処理に組みこむ
- gem
rack-cors
を導入する
- originsをSPAのURLのみに制限する
credentials
をtrue
にしておくVue.js側
withCredentials
オプションをtrue
に設定する(axiosやxhr)
- fetchなら
credentials: 'include'
とかかな- リクエストに独自ヘッダをつける
- なんでもいいし中身も空文字でよくて、ヘッダがあることが重要
なぜ上記のような実装をするのか
以下の3つがポイントになります
httponly
属性とsecure
属性が有効でdomain
属性にはAPIのドメインのみを設定したcookieでアクセストークンを持たせる- 独自ヘッダが必要であること
- corsでoriginsを制限されていること
cookieについて
なぜhttponlyにするのか
- XSSや悪意あるパッケージなどのjsによりcookieにあるトークンを抜かれないようにするため
- jsから抜かれる危険性はlocalstorageを使わない理由でもある
- 代わりに、cookieにあるトークンを非同期通信でもリクエストするために
withCredentials
オプションを有効にするなぜsecureにするのか
- 非SSLのリクエストを盗聴されないように、とかっていうよくある理由
なぜdomainをAPIのドメインのみに制限するのか
- サブドメをワイルドカードにしたりとか他のドメインを許容する必要がないため
- 少なくとも今回のケースでは複数ドメインをまたいでcookieの共有は必要なかった
- withCredentialを有効にしてcookieとして飛ばすからフロントエンドのjsからcookieを操作できる必要はない
- フロントのurlからはcookie見えないけどAPIのドメインへのcookieとしてちゃんと存在しているのでwithCredential有効にしておけばちゃんとリクエストされる
- 恥ずかしながら自分は最初「フロントのドメインでも扱えなきゃいけないよなぁ」と思い込んでいました。。。
独自ヘッダについて
- htmlのformによるCSRF対策
- htmlのform(同期通信)では独自ヘッダを付与することはできないため
CORSについて
- 独自ヘッダと併用することでjsによるCSRF対策となる
- originsでちゃんと制限かけることで外からの不正なリクエストを弾く
- ただし、それだけでは不十分で、ブラウザ上ではエラーになるがリクエスト自体は飛んで処理されてしまう
- 不正なGETには有効だがPOSTは処理されてしまうので困る
- POSTは条件次第(formDataを使うこと)で単純リクエストになり、プリフライトリクエストが走らない場合があるため独自ヘッダによりプリフライトを強制させる
- これによりプリフライトリクエストで止める事ができるので不正なリクエストを飛ばせなくすることができる
- 独自ヘッダは2度美味しい
- ちなみに、rack-corsの設定で
credentials: true
にしないとフロントでwithCredential: true
のリクエストを弾いてしまう実装のサンプル
一部抜粋して書いていきます
参考記事などは下の方にまとめてますrails側
rack-corsなどの設定
config/application.rb# ...割愛 # apiモードのrailsでcookiesを使えるようにするため # コントローラ側に `include ActionController::Cookies` も必要 config.middleware.use ActionDispatch::Cookies config.middleware.insert_before 0, Rack::Cors do allow do # 複数のorigins制限するためドメインの配列 # 環境変数はjsonにすると配列が簡単に扱えるやんという最近見つけたtips origins JSON.parse(ENV.fetch('CORS_DOMAINS_JSON') { '[]' }) resource "*", # axiosなどで `withCredential: true` にした上で、そのcookieを受け取るため credentials: true, headers: :any, methods: [:get, :post, :patch, :delete] end end # ...割愛認証情報をcookieで返す
- アクセストークンの期限はDBにあるので
permanent
- 開発環境はsecure外したいので環境変数
COOKIE_SECURE
で操作# ...割愛 cookies.permanent[:access_token] = { value: access_token, httponly: true, secure: ENV['COOKIE_SECURE'].present?, } cookies.permanent[:refresh_token] = { value: refresh_token, httponly: true, secure: ENV['COOKIE_SECURE'].present?, } # ...割愛認証処理
access_token = cookies[:access_token] # ...割愛(access_tokenチェックの処理など) raise 適当な例外クラス if request.headers[:'X-REQUESTED-BY-MY-APP'].nil?Vue.js側
withCredentialsを有効にする
src/main.js// ...割愛 Vue.prototype.$http = axios.create( { baseURL: process.env.VUE_APP_API_URI, withCredentials: true }, ); // ...割愛独自ヘッダをつける
- Vue.prototype.$httpにインスタンス入れることでどこからでも
this.$http
で使える- interceptorsを使うことですべてのリクエストでヘッダ設定の処理を走らせる
src/main.js// ...割愛 Vue.prototype.$http.interceptors.request.use((request) => ({ ...request, headers: { ...request.headers, ...{ 'X-REQUESTED-BY-MY-APP': '' }, }, })); // ...割愛疑問
XSS脆弱性があっても認証情報を抜かれないための対策とか、外からのCSRFの対策はしたけど
XSSでXHRのコードをインジェクトされて外部ではなく本来のバックエンドに勝手にリクエストを飛ばされないようにするための対策ってどうすればいいんだろう。。。
根本的にXSSを塞ぐしかないってことになるのかな参考記事
rails-apiでcookieを使う
RailsでAPIにCORSを設定する
rack-corsでCORS設定をする
さよならCSRF(?) 2017
CORS: OPTIONSリクエスト(preflight request)を避ける
- 投稿日:2020-07-13T14:29:36+09:00
StorybookでCannot read property 'query' of undefinedだと?!
はじめに
あれ、最近ビジュアルリグレッションテストが落ちるな?と見てみると、なんとも見慣れないエラーが出ていました。それが
Cannot read property 'query' of undefined
です。storybookで確認してみるとこんな感じ↓↓
reg-suitとstorybookを利用して、PR度に自動差分が出るようにしているのですが、毎度引っかかるようになってしまいました。まぁ、ちゃんと毎回確認しなかった自分が悪いです。はい。
原因
どうやら起因となったのは、以下を含むコンポーネントを作ったことのようです
<nuxt-link :to="{ path: 'hoge', query: $route.query }">hoge</nuxt-link>そうコンポーネント内にrouteをみる記述があり、そのままだとstorybookではVueRouterを認識してくれないようです。
対応
これは絶対に誰かすでに詰まってるだろうし、
addon
あるっしょ!と思ってググってたらやはり見つけました。でも意外とスター少ない...(利用しているパッケージにはどんどんスターつけていきましょう!)デコレータは、実際には VueRouter インスタンスをラップする関数です。storybook内でナビゲーションのプロトタイプを構築したい場合や、 ルータ自体をより制御したい場合に使用することができます。
VueRouterをラップしたものであるとシンプルでありますが、こういうのあるってありがたいですね!
では、早速インストールして、以下のように対象の設定ファイルに追記するだけ。自分の場合はルートの
.storybook/preview.js
に設定記述をまとめてるのでそちらに書きます。コンポーネントストーリーファイルごとに設定している人は、そちらに書きましょう。(略) import { addDecorator } from '@storybook/vue' import StoryRouter from 'storybook-vue-router' addDecorator(StoryRouter())これで完了。特定のルートにだけ含めたいなどの場合には、
StoryRouter
の第2引数にオプションを渡せば良さそうです。さいごに
storybookを導入しているかつ、
this.$router.name
やthis.$router.query
を使っているコンポーネントがあってエラーが出る場合は、参考にしてみてください
- 投稿日:2020-07-13T10:35:04+09:00
Vue.js 削除ボタンを作ってスロットを理解する
スロットとは
スロット(slot)は、Vue.jsにおける親コンポーネントから子コンポーネントにデータを渡す手段の一つです。
スロットという名前はハーデスやリゼロなど様々な機種がある遊技マシン...のことではなく「差し込み口」という意味で使われています。つまり、コンポーネントに外からコンテンツの差し込みを受け付けるという目的で使用されます。
スロットはその性質上、再利用の高いコンポーネントによく使用されます。再利用の高いコンポーネントは、Atomic Designにおける
Atom
のようなものが代表されます。まずはさっそくスロットの基礎的な使い方を見ていきましょう。
スロットコンテンツ
スロットを使用コンポーネントとして、汎用的に利用する削除ボタンを実装します。
コンポーネントの内容は、Vuetifyのv-button
をラップしただけ簡単なものです。DeleteBtn.vue<template> <v-btn color="error" dark min-width="300" rounded > <slot></slot> </v-btn> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'DeleteBtn', }) </script>見た目はこんな感じです。
小さなコンポーネントですが、このボタンはいろんなページで使用するためコンポーネント化することでデザインを共通化することができます。
<slot></slot>
を置換する子コンポーネント側のテンプレートに
<slot>
タグを記述すると、その場所ではスロットコンテンツが埋め込まれます。
今はボタンのテキストが表示されていない状態ですが、親からスロットコンテンツを渡して表示させてみましょう。親からスロットコンテンツを渡すには、以下のようにスロットコンテンツをテンプレート上でタグで囲います。
App.vue<template> <v-row> <v-col class="text-center"> <delete-btn>削除ボタン</delete-btn> </v-col> </v-row> </template> <script lang="ts"> import Vue from 'vue' import DeleteBtn from '~/components/atom/DeleteBtn.vue' export default Vue.extend({ components: { DeleteBtn }, }) </script>これは次のように出力されます。
スロットには、HTML要素やコンポーネントを入れることもできます。
App.vue<template> <v-row> <v-col class="text-center"> <delete-btn><h1>削除ボタン</h1></delete-btn> </v-col> <v-col class="text-center"> <delete-btn><v-icon color="black">fab fa-github</v-icon></delete-btn> </v-col> </v-row> </template> <script lang="ts"> import Vue from 'vue' import DeleteBtn from '~/components/atom/DeleteBtn.vue' export default Vue.extend({ components: { DeleteBtn }, auth: false, layout: 'unauthorized' }) </script>フォールバックコンテンツ
スロットに対して、コンテンツがない場合に描画されるデフォルトのコンテンツを指定することができます。
フォールバックコンテンツを使用するには、<slot>
タグの中に記述します。DeleteBtn.vue<template> <v-btn color="error" dark min-width="300" rounded > <slot>DELETE</slot> </v-btn> </template>親コンポーネントでスロットを指定しなかった場合には
DELETE
というテキストが出力され、指定された場合にはその文字が出力されます。App.vue<template> <v-row> <v-col class="text-center"> <!-- スロットコンテンツを指定せず --> <delete-btn></delete-btn> </v-col> <v-col class="text-center"> <!-- スロットコンテンツを指定 --> <delete-btn>削除</delete-btn> </v-col> </v-row> </template> <script lang="ts"> import Vue from 'vue' import DeleteBtn from '~/components/atom/DeleteBtn.vue' export default Vue.extend({ components: { DeleteBtn } }) </script>名前付きスロット
この削除ボタンにさらに機能を追加しましょう、
ボタンをクリックしたときに、「本当に削除しますか?」というダイアログが出現するようにします。とりあえず簡単にVuetifyのダイアログを追加します。
DeleteBtn.vue<template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> <slot>DELETE</slot> </v-btn> <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title class="subtitle-1"> 削除します。よろしいですか? </v-card-title> <v-card-text> この操作は取り消せません。 </v-card-text> <v-divider></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> キャンセル </v-btn> <v-btn color="error" text @click="clickOK"> 削除する </v-btn> </v-card-actions> </v-card> </v-dialog> </span> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'DeleteButton', data() { return { dialog: false } }, methods: { openDialog() { this.dialog = true }, closeDialog() { this.dialog = false }, clickCancel() { this.closeDialog() this.$emit('clickCancel') }, clickOK() { this.closeDialog() this.$emit('clickOK') } } }) </script>このコンポーネントを汎用的に利用するために、ダイアログのメッセージもスロット化したいはずです。
しかし、次のようにそのまま<slot>
タグで囲むとうまく動作しません。DeleteBtn.vue<!-- 誤ったスロットの使いかた --> <template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> <slot>DELETE</slot> </v-btn> <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title class="subtitle-1"> <slot>削除します。よろしいですか?</slot> </v-card-title> <v-card-text> <slot>この操作は取り消せません。</slot> </v-card-text> <v-divider></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> <slot>キャンセル</slot> </v-btn> <v-btn color="error" text @click="clickOK"> <slot>削除する</slot> </v-btn> </v-card-actions> </v-card> </v-dialog> </span> </template>なぜなら、スロットが複数あるため親コンポーネントからどのスロットに対してコンテンツを差し込めばよいか判断することができないからです。
このように、スロットが複数ある場合にはスロットに名前を付けて利用します。
<slot>
要素はname
という属性を持っているのでこれを利用して名前を定義します。DeleteBtn.vue<template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> <slot>DELETE</slot> </v-btn> <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title> <slot name="dialogTitle">削除します。よろしいですか?</slot> </v-card-title> <v-card-text> <slot name="dialogText">この操作は取り消せません。</slot> </v-card-text> <v-divider></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> <slot name="cancelBtn">キャンセル</slot> </v-btn> <v-btn color="error" text @click="clickOK"> <slot name="okBtn">削除する</slot> </v-btn> </v-card-actions> </v-card> </v-dialog> </span> </template>
name
属性を持たない<slot>
は暗黙的にdefault
という名前を持ちます。親コンポーネントからスロットコンテンツを指定するには、
<template>
に対してv-slot
ディレクティブでスロット名を与えます。App.vue <template> <v-row> <v-col class="text-center"> <delete-btn> 削除 <template v-slot:dialogTitle> {{ item.id }}のデータを本当に削除しますか? </template> <template v-slot:okBtn> はい、削除します。 </template> </delete-btn> </v-col> </v-row> </template> <script lang="ts"> import Vue from 'vue' import DeleteBtn from '~/components/atom/DeleteBtn.vue' export default Vue.extend({ components: { DeleteBtn }, data() { return { item: { id: '12345' } } } }) </script>
<template>
で囲まれていな要素は、デフォルトスロットに対するものとして扱われます。
これは、次のように描画されます。名前付きスロットの省略記法
v-slot
ディレクティブにも省略記法が使用できます。
省略記法では、v-slot:
の代わりに#
を使用します。App.vue<template> <v-row> <v-col class="text-center"> <delete-btn> 削除 <template #dialogTitle> {{ item.id }}のデータを本当に削除しますか? </template> <template #okBtn> はい、削除します。 </template> </delete-btn> </v-col> </v-row> </template>Propsとスロットの違い
ここまでは基礎的なスロットの使い方をみてきました。しかし、ある程度Vue.jsを触ったことがある人なら、次のように思ったんじゃないでしょうか。
「結局これってPropsとやってることは変わらないんじゃ・・・わざわざスロットを使わなくてもいいのでは?」
例えば、先程の例では次のようにPropsを使ったコンポーネントに書き換えることもできそうです。
DeleteBtn.vue<template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> {{ btnText }} </v-btn> <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title> {{ dialogTitle }} </v-card-title> <v-card-text> {{ dialogText }} </v-card-text> <v-divider name="dialogText"></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> {{ cancelBtn }} </v-btn> <v-btn color="error" text @click="clickOK"> {{ okBtn }} </v-btn> </v-card-actions> </v-card> </v-dialog> </span> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'DeleteBtn', props: { btnText: { type: String, required: false, default: 'DELETE' }, dialogTitle: { type: String, required: false, default: '削除します。よろしいですか?' }, dialogText: { type: String, required: false, default: 'この操作は取り消せません。' }, cancalBtn: { type: String, required: false, default: 'キャンセル' }, okBtn: { type: String, required: false, default: '削除' } }, data() { return { dialog: false } }, methods: { openDialog() { this.dialog = true }, closeDialog() { this.dialog = false }, clickCancel() { this.closeDialog() this.$emit('clickCancel') }, clickOK() { this.closeDialog() this.$emit('clickOK') } } }) </script><template> <v-row> <v-col class="text-center"> <delete-btn btnText="削除" :dialogTitle="`${item.id}のデータを本当に削除しますか?`" okBtn="はい、削除します。" /> </v-col> </v-row> </template>このような仕事をさせるのなら、Propsとスロットに違いはないようにも思えます。
実際にはどのようにPropsとスロットを使い分けていくのでしょうか?Propsは値を渡し、スロットは描画内容を渡す
Propsと比較したときに挙げられるスロットの役割とし、親コンポーネントに描画内容を任せるという点があります。
例えば、作成中の削除ボタンに次のような機能を持たせたいとします。
- 重要なデータAを削除する際には、ダイアログの文字を大きく太文字で表示させる
- そこそこ大切なデータBを削除するときはダイアログの文字の前にアイコンを表示させる
- それ以外のデータを削除するときにはデフォルトのダイアログを表示
この機能をPropsを使って実装しようする場合、データの状態に関するロジックを子コンポーネントに記述しなくてはいけなくなってしまいます。
DeleteBtn.vue<v-card-title v-if="dataLevel === 3" class="display-1"> {{ dialogTitle }} </v-card-title> <v-card-title v-else-if="dataLevel === 2"> <v-icon>fas fa-exclamation-triangle</v-icon>{{ dialogTitle }} </v-card-title> <v-card-title v-else> {{ dialogTitle }} </v-card-title>このように、データの状態が増えるたびに分岐が増えてしまい、コンポーネントが肥大化してしまします。さらには親の状態の増加によって本来手を加えるべきではない子コンポーネントに記述しなければいけない状態となり、汎用的に使用できるコンポーネントとは言えなくなっていしまいます。
この時、スロットを利用すればコンポーネントを使う側が描画内容を決定すること可能です。
ただこれなら、それぞれのデータの状態に合わせた削除ボタンコンポーネントを作成すれば、子コンポーネント側の肥大化は解消することができます。(DataLevel3DeleteBtn.vueとDataLevel2DeleteBtn.vueを作成して、それぞれに描画内容をもたせる)
ただし、その場合には動作は同じだが描画内容だけが違うコンポーネントを作成することになり、DRY原則からしてあまりイケてない実装になってしまいます。
スロットは、再利用の高いコンポーネントを作成するときに効果を発揮するともいえるでしょう。
スコープ付きスロット
通常のスロットのスコープを確認する
ここからは、基礎的な内容から更に一歩進んだものとなります。
通常、v-slot
ディレクティブからアクセスできるデータはそのコンポーネント自身のデータになります。次の例で確認してみましょう。親コンポーネントと子コンポーネントは、同じ
user
というプロパティをもっています。Hello.vue<template> <div>Hello, <slot></slot></div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ data() { return { user: { name: 'aaa' } } } }) </script>App.vue<template> <div> <Hello>{{ user.name }}</Hello> </div> </template> <script lang="ts"> import Vue from 'vue' import Hello from '~/components/atom/Hello.vue' export default Vue.extend({ components: { Hello }, data() { return { user: { name: 'bbb' } } }, }) </script>この時、描画される内容は子のもつ
user
プロパティなのか、親の持つuser
プロパティなのかどちらでしょうか?
スロットは子コンポーネントに描画されるので、一見子コンポーネントプロパティが使われるようにも思えますが、実際に使用されるのは親コンポーネントの持つプロパティです。これは、デフォルトの動作では例えスロットを使用したとしても、子コンポーネントのプロパティにはアクセスできないことを意味します。
試しに、親コンポーネントのuser
プロパティを削除してみると、エラーが発生します。親から子のプロパティを参照する
一切子のプロパティを親から参照できないとなると不便になることも多いので、スロットには子のプロパティを参照するための機能が提供されています。
子コンポーネントから親コンポーネントのスロットコンテンツとしてプロパティを渡す場合、
<slot>
要素の属性としバインドします。Hello.vue<template> <div>Hello, <slot :user="user"></slot></div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ data() { return { user: { name: 'aaa' } } } }) </script>
<slot>
要素にバインドされた属性は、スロットプロパティ と呼ばれます。これは、親コンポーネント内でスロットの名前を指定することでスロットプロパティを受け取ることができます。App.vue<template> <div> <Hello> <!-- slotPropという名前はコンポーネント内で一意である名前であれば好きな名前を使用することができます。 --> <template #default="slotProp"> {{ slotProp.user.name }} </template> </Hello> </div> </template>子コンポーネントのプロパティが描画されています。
デフォルトスロットの省略記法
スロットがデフォルトスロットだけの場合には、
<template>
タグで名前を指定せずとも、次のように記述することができます。App.vue<template> <div> <Hello v-slot="slotProp"> {{ slotProp.user.firstName }} </Hello> </div> </template>スロットプロパティの分割代入
v-slot
では、JavaScriptの式を記述することができます。ですので、分割代入を利用すれば、よりきれいにプロパティを取得することができます。App.vue<template> <div> <Hello v-slot="{ user }"> {{ user.firstName }} </Hello> </div> </template>実践的な例
スコープ付きスロットを利用した、より実践的な例を見ていきましょう。
もう一度、先程の削除ボタンコンポーネントに登場していただきます。今度は、ダイアログ自体をスロットとして提供できるようにしましょう。
新たに、<v-dialog>
全体をスロットで囲んでいます。DeleteBtn.vue<template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> <slot>DELETE</slot> </v-btn> <slot name="dialog"> <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title> <slot name="dialogTitle">削除します。よろしいですか?</slot> </v-card-title> <v-card-text> <slot name="dialogText">この操作は取り消せません。</slot> </v-card-text> <v-divider name="dialogText"></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> <slot name="cancelBtn">キャンセル</slot> </v-btn> <v-btn color="error" text @click="clickOK"> <slot name="okBtn">削除する</slot> </v-btn> </v-card-actions> </v-card> </v-dialog> </slot> </span> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'DeleteBtn', data() { return { dialog: false } }, methods: { openDialog() { this.dialog = true }, closeDialog() { this.dialog = false }, clickCancel() { this.closeDialog() this.$emit('clickCancel') }, clickOK() { this.closeDialog() this.$emit('clickOK') } } }) </script>親コンポーネントから異なるタイプのダイアログを描画するために、次のように記述したいはずでしょう。
App.vue<template> <v-row> <v-col class="text-center"> <delete-btn> <template #dialog> <v-dialog v-model="dialog" persistent min-width="500"> <v-card> <v-card-title class="headline"> 本当に削除しますか? </v-card-title> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" @click="clickCancel"> キャンセル </v-btn> <v-btn color="error" @click="clickOK"> 削除する </v-btn> </v-card-actions> </v-card> </v-dialog> </template> </delete-btn> </v-col> </v-row> </template>お察しの通り、これはうまく動作しませんなぜならダイアログの表示を制御する
dialog
プロパティは子コンポーネントが持っているからです。(v-model
で渡しているところです。)
親にスロットプロパティとして渡してみます。DeleteBtn.vue<template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> <slot>DELETE</slot> </v-btn> <slot name="dialog" :dialog="dialog"> <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title> <slot name="dialogTitle">削除します。よろしいですか?</slot> </v-card-title> <v-card-text> <slot name="dialogText">この操作は取り消せません。</slot> </v-card-text> <v-divider name="dialogText"></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> <slot name="cancelBtn">キャンセル</slot> </v-btn> <v-btn color="error" text @click="clickOK"> <slot name="okBtn">削除する</slot> </v-btn> </v-card-actions> </v-card> </v-dialog> </slot> </span> </template>App.vue<template> <v-row> <v-col class="text-center"> <delete-btn> <template #dialog="{ dialog }"> <v-dialog v-model="dialog" persistent min-width="500"> <v-card> <v-card-title class="headline"> 本当に削除しますか? </v-card-title> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" @click="clickCancel"> キャンセル </v-btn> <v-btn color="error" @click="clickOK"> 削除する </v-btn> </v-card-actions> </v-card> </v-dialog> </template> </delete-btn> </v-col> </v-row> </template>しかし、残念なことにこれは好ましい実装ではありません。なぜなら、スロットプロパティの値を
v-model
で直接更新してしまっているからです。スロットプロパティで渡された値はあくまで参照だけに留めるべきで、直接値を更新してしまうのは禁じ手です。親のスロット内での変更を、どのように子に伝えればよいのでしょうか?
スロットプロパティでメソッドを渡す
実は、スロットプロパティには子のメソッドを渡すことも可能です。
スロットプロパティの中では渡したメソッドは子コンポーネントのものなので、子の値を変更することができるというわけです。メソッドの渡し方は通常の記法変わりません。
DeleteBtn.vue<template> <span> <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog"> <slot>DELETE</slot> </v-btn> <slot name="dialog" :dialog="dialog" :closeDialog="closeDialog" :clickCancel="clickCancel" :clickOK="clickOK" > <v-dialog v-model="dialog" max-width="290"> <v-card> <v-card-title> <slot name="dialogTitle">削除します。よろしいですか?</slot> </v-card-title> <v-card-text> <slot name="dialogText">この操作は取り消せません。</slot> </v-card-text> <v-divider name="dialogText"></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="clickCancel"> <slot name="cancelBtn">キャンセル</slot> </v-btn> <v-btn color="error" text @click="clickOK"> <slot name="okBtn">削除する</slot> </v-btn> </v-card-actions> </v-card> </v-dialog> </slot> </span> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'DeleteButton', data() { return { dialog: false } }, methods: { openDialog() { this.dialog = true }, closeDialog() { this.dialog = false }, clickCancel() { this.closeDialog() this.$emit('clickCancel') }, clickOK() { this.closeDialog() this.$emit('clickOK') } } }) </script>ついでにキャンセルボタンをOKボタンをクリックした際のイベントも渡しておきましょう。
親側の記述は次のようになります。
v-model
を分解して:value
と@input
を使用します。App.vue<template> <v-row> <v-col class="text-center"> <delete-btn> <template #dialog="{ dialog, closeDialog, clickCancel,clickOK }"> <v-dialog :value="dialog" @input="closeDialog" dark min-width="500"> <v-card> <v-card-title class="headline"> 本当に削除しますか? </v-card-title> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" @click="clickCancel"> キャンセル </v-btn> <v-btn color="error" @click="clickOK"> 削除する </v-btn> </v-card-actions> </v-card> </v-dialog> </template> </delete-btn> </v-col> </v-row> </template>これで、親からスロットを利用してダイアログを差し替えることができました。
終わりに
私自身も、Propsとスロットの違いがよくわかっておらず、スロットからは避けていました。
スロットを利用すると、再利用性の高いコンポーネントが作成できたりと、作成の幅が広がります。
さらに、ライブラリのコンポーネントを使用する機会も多いと思いますがそういったものはAPIとしてスロットを公開していることが多いです。
スロットを理解することで、ライブラリをより効果的に使用することも期待できます。
- 投稿日:2020-07-13T00:49:11+09:00
Nuxt.jsを使ってみた
前提
Node.jsインストール済み
本題
$ npx create-nuxt-app hellonpxは、npmパッケージのダウンロードと実行を1度に行います。
※ホットリローディングに対応しているのはdev(開発環境)のみ。
npm run build、npm run startは本番環境にアップロードする前などに使用するもので開発時には基本的には使わない。マスタッシュ構文
page/index.vue<template> <div class="container"> <p>{{ message }}</p> </div> </template> <script> export default { data() { return { message: "Hello Nuxt.js!" }; } }; </script> <style> </style>ページ遷移
pageフォルダ内に新規ファイルを作成
page/test.vue<template> <selection class="container"> <h1>Test</h1> <hr /> //追記 <router-link to="/">Test Page</router-link> </selection> </template>Vue Routerを使用。
Nuxt.jsでは自動で設定してくれている。page/index.vue<template> <div class="container"> <p>{{ message }}</p> <hr /> //追記 <router-link to="/test">Top Page</router-link> </div> </template>パラメーターのバリデーション
数字出ないといけない場合
<script> export default { data() { return { message: 'user/_id.vueを表示中' } }, validate({ params }) { return /^\d+$/.test(params.id) } }; </script>エラーページ
404の場合
layouts/error.vue<template> <div> <h1 v-if="error.statusCode === 404">Error! 404</h1> </div> </template> <script> export default { props: ['error'] }; </script>※エラーページには含めてはいけない。
HTMLヘッダーのカスタマイズ
ロゴを変える場合
nuxt.config.jshead: { //省略 link: [ { rel: 'stylesheet', href: 'URL'} ] }, //省略pages/index.vue<style> .title { font-family: '変更', //省略 } </style>非同期通信
axiosインストール
$ npm install axiosデータはJSONPlaceholderを使用。
/usersデータを使用。データ取得
pages/index.vue<template> <section class="container"> <div> <ul> <li v-for="user in users" :key="user.id"> {{ user.id }}, {{ user.name }}, {{ user.company.name }} </li> </ul> </div> </section> </template> <script> const axios = require('axios') let url = '/userのURLを添付' export default { asyncData({ params, error }) { return axios.get(url) .then((res) => { return { users: res.data } }) //エラー処理 .catch((e => { error({ users: e.response.status, message: e.message }) })) } } </script>asyncDataはNuxt.jsが用意しているメソッド。
コンポーネントを初期化する前に非同期の処理を行えるようにするメソッド。画像の表示
画像をassetsディレクトリ配下に設置。
pages/index.vue<template> <section class="container"> <div> <img src="~/assets/〇〇.jpg"> </div> </section> </template> <script> export default { } </script>ストア作成
storeディレクトリ配下にindex.jsを作成。
store/index.jsimport Vuex from "vuex"; const createStore = () => { return new Vuex.Store({ state: function() { return { message: "Hello Vuex!" }; } }); }; export default createStore;pages/index.vue<template> <div class="container"> <div> <p>{{ $store.state.message }}</p> </div> </div> </template> <script> export default {}; </script>ミューテーションの利用
store/index.js//省略 const createStore = () => { return new Vuex.Store({ state: function() { return { message: "Hello Vuex!" }; }, mutations: { updateMessage: function(state) { state.message = "Updated!"; } } }); }; //省略pages/index.vue<template> <div class="container"> <div> <p>{{ $store.state.message }}</p> <button @click="$store.commit('updateMessage')">Update</button> </div> </div> </template> <script> export default {}; </script>ミューテーションの機能をコンポーネントから呼び出すには、commitメソッドを利用する。
ミューテーションへ値を渡す
pages/index.vue<template> <div class="container"> <div> <p>{{ $store.state.message }}</p> <button @click="$store.commit('updateMessage', 'Commit with payload')">Update</button> </div> </div> </template>store/index.jsconst createStore = () => { return new Vuex.Store({ state: function() { return { message: "Hello Vuex!" }; }, mutations: { updateMessage: function(state, payload) { state.message = payload; } } }); };ミューテーションへ値を渡したい時は、commitメソッドの第二引数に渡したい値を入れ、
ミューテーション側(index.js)の第二引数で値を受け取り利用する。アクションの利用
これまでステートの値を操作するまでにアクションを飛ばし、ミューテーションを呼んでいたため、アクションを経由するようにする。
pages/index.vue<template> <div class="container"> <div> <p>{{ $store.state.message }}</p> <button @click="$store.dispatch('updateMessageAction', 'Dispatch with payload')">Dispatch</button> </div> </div> </template>store/index.jsconst createStore = () => { return new Vuex.Store({ state: function() { return { message: "Hello Vuex!" }; }, mutations: { updateMessage: function(state, payload) { state.message = payload; } }, actions: { updateMessageAction(context, payload) { context.commit("updateMessage", payload); } } }); };ストアのモジュールモードの利用
・クラシックモード
1つのファイル(index.js)に記述
・モジュールモード
複数のファイルに記述●モジュールモード動作の条件
・index.jsがストアオブジェクトをexportしない
・または、index.jsがstoreフォルダ配下に存在しないstoreディレクトリ配下にhello.jsを作成。
store/hello.jsexport const state = () => ({ message: "Hello Vuex!" }); export const mutations = { updateMessage: function(state, payload) { state.message = payload; } }; export const actions = { updateMessageAction(context, payload) { context.commit("updateMessage", payload); } };pages/index.vue<template> <div class="container"> <div> <p>{{ $store.state.hello.message }}</p> <button @click="$store.dispatch('hello/updateMessageAction', 'Dispatch with payload')" >Dispatch</button> </div> </div> </template>store/index.jsは削除。
SPA開発
firebaseインストール
$ npm install --save firebase@6.2.4 $ npm install --save vuexfire@3.0.1環境変数の設定
$ npm install --save @nuxtjs/dotenv@1.3.0.envファイルを作成。
FIREBASE_PROJECT_ID = 'IDを入力'nuxt.config.js/* ** Nuxt.js modules */ modules: ["@nuxtjs/dotenv"],Firebaseとの連携
pluginsにfirebase.jsファイルを作成。
plugins/firebase.jsimport firebase from "firebase"; const config = { projectId: process.env.FIREBASE_PROJECT_ID }; if (!firebase.apps.length) { firebase.initializeApp(config); } export default firebase; `` ##ストアの作成 storeフォルダ配下にindex.jsを作成。 ```store/index.js import { vuexfireMutations } from "vuexfire"; export const mutations = { ...vuexfireMutations };storeフォルダ配下にlist.jsを作成。
store/list.js//firebaseの初期化設定ファイル import firebase from "~/plugins/firebase"; //vuexfireのfirestoreActionをインポート import { firestoreAction } from "vuexfire"; //データベース設定 const db = firebase.firestore(); const listsRef = db.collection("lists"); export const state = () => ({ lists: [] }); export const actions = { //FirestoreActionを呼び出す init: firestoreAction(({ bindFirestoreRef }) => { bindFirestoreRef("lists", listsRef); }), add: firestoreAction((context, name) => { //未入力でなければ下記内容のデータをfirebaseへ保存 if (name.trim()) { listsRef.add({ name: name, done: false, created: firebase.firestore.FieldValue.serverTimestamp() }); } }), remove: firestoreAction((context, id) => { listsRef.doc(id).delete(); }), toggle: firestoreAction((context, todo) => { listsRef.doc(list.id).update({ done: !list.done }); }) };コンポーネントの作成
pagesフォルダ配下にlists.vueを作成。
pages/lists.vue<template> <div> <div class="form"> <form @submit.prevent="add"> <input v-model="name" /> <button>Add</button> </form> </div> </div> </template> <script> import moment from "moment"; export default { data: function() { return { name: "", done: false }; }, created: function() { this.$store.dispatch("lists/init"); }, methods: { add() { this.$store.dispatch("lists/add", this.name); this.name = ""; } } }; </script>firebaseに保存されていればOK。
listsリストを表示
保存したデータを取り出す。
pages/lists.vue<template> <div> <!-- 追記 --> <ul> <li v-for="list in lists" :key="list.id"> {{ list.done }} {{ list.name }} {{ list.created.toDate() | dateFilter }} </li> </ul> <div class="form"> <form @submit.prevent="add"> <input v-model="name" /> <button>Add</button> </form> </div> </div> </template> <script> import moment from "moment"; export default { data: function() { return { name: "", done: false }; }, created: function() { this.$store.dispatch("lists/init"); }, methods: { add() { this.$store.dispatch("lists/add", this.name); this.name = ""; } }, computed: { lists() { return this.$store.state.lists.lists; } }, filters: { dateFilter: function(date) { return moment(date).format("YYYY/MM/DD HH:mm:ss"); } } }; </script>削除機能
pages/lists.vue<template> <div> <ul> <li v-for="list in lists" :key="list.id"> {{ list.done }} {{ list.name }} {{ list.created.toDate() | dateFilter }} //追記 <button @click="remove(liste.id)">×</button> </li> </ul> <div class="form"> <form @submit.prevent="add"> <input v-model="name" /> <button>Add</button> </form> </div> </div> </template> <script> //省略 export default { //省略 methods: { add() { this.$store.dispatch("lists/add", this.name); this.name = ""; }, //追記 remove(id) { this.$store.dispatch("lists/remove", id); } }, //省略 }; </script>完了・未完了チェックボックス
pages/lists.vue<template> <div> <ul> <li v-for="list in lists" :key="list.id"> //追記 <span v-if="list.created"> <input type="checkbox" :checked="list.done" @change="toggle(list)" /> <span :class="{ done: list.done }">{{ list.name }} {{ list.created.toDate() | dateFilter }}</span> <button @click="remove(list.id)">×</button> </span> </li> </ul> <div class="form"> <form @submit.prevent="add"> <input v-model="name" /> <button>Add</button> </form> </div> </div> </template> <script> //省略 export default { //省略 methods: { add() { this.$store.dispatch("lists/add", this.name); this.name = ""; }, remove(id) { this.$store.dispatch("lists/remove", id); }, toggle(list) { this.$store.dispatch("lists/toggle", list); } }, //省略 }; </script> //追記 <style> li > span > span.done { text-decoration: line-through; } </style>
- 投稿日:2020-07-13T00:42:48+09:00
Vue.js3.0移行(is属性を使ったカスタム要素)
Vue.js 2.x の is 属性を使ったカスタム要素の移行
is
属性を使っている箇所をv-is
に置き換えます。Vue.js 2.x
<tbody> <tr is="my-component" v-for="(foo, index) in bar" :baz="foo" :key="index"> </tbody> components: { 'my-component': myComponent },Vue.js 3.x
<tbody> <tr v-is="'my-component'" v-for="(foo, index) in bar" :baz="foo" :key="index"> </tbody>※componentオプションを使ってコンポーネントを登録した場合
コンポーネントの登録方法によって、記述方法が変わることに注意。
詳細はRFC参照。
https://github.com/vuejs/rfcs/pull/149