- 投稿日:2020-05-20T23:44:41+09:00
【Vue】v-slot簡単にわかりやすく!!
v-slot それはニュータイプ
技術的な詳しい話はわからないので、
使い方!いきます!
あと、ガンダムが好きなのでガンダムネタぶち込みます!デフォルトの使い方
この段階ではまだニュータイプではありません。
それが効力するのはまだ先です。SlotSample.vue<template> <div> 君の<slot>兄上</slot>がいけないのだよ </div> </template> <script> export default { name: 'serifu', data () { return { } } } </script> <style> </style>出力
君の兄上がいけないのだよ
セリフいじり放題なcomponentができました。
これを使いましょう
main.vue<template> <serifu>父上</serifu> </template> <script> import Serifu from './SlotSample.vue' export default { components: { 'serifu': Serifu } } </script>出力
君の父上がいけないのだよ
完璧ですね。
これがslotです。確かに優秀なパイロットではありましょうが、まだまだです。なんてったってネームドではありません。名前付きslot
連邦の白い ...
いつからでしょう彼には二つ名がありました。
彼はニュータイプなのではないか、、、そう信じているでしょうがまだです。
今の彼ではアクシズショックは起こせません。Amuro.vue<template> <p><slot name="serifu1"></slot></p> </template> <script> export default { name: 'amuro', data () { return { } } } </script> <style> </style>残念ながら彼にはまだ見えていないようです。
ニュータイプへの一歩を踏み出してもらいましょう。main.vue<template> <amuro> <template v-slot:serifu1>あぁ、ララァ ... 時が見える ...</template> </amuro> </template> <script> import Amuro from './Amuro.vue' export default { components: { 'amuro': Amuro } } </script>出力
あぁ、ララァ ... 時が見える ...
無事にニュータイプの片鱗を出すことができたようです。
ここまでくると安心ですね。サイコフレームの共振を起こしていただきましょう。データを渡せちゃうslot
サイコフレームの共振
新たな力 νガンダムを手に入れた彼ならば、サイコフレームの共振は起こせます。
たかが石ころ一つ、ガンダムで押し出してやるAxisShock.vue<template> <p><slot :data="axisShock"></slot></p> </template> <script> export default { name: 'axisShock', data () { return { axisShock: { cause: 'PsychoField', date: '宇宙世紀0093年3月12日' } } } } </script> <style> </style>アクシズショックをみてみましょう
main.vue<template> <axisShock v-slot:default="data"> 第二次ネオ・ジオン抗争 アクシズ落とし : {{ data.axisShock.date }} </axisShock> </template> <script> import AxisShock from './AxisShock.vue' export default { components: { 'axisShock': AxisShock } } </script>出力
第二次ネオ・ジオン抗争 アクシズ落とし : 宇宙世紀0093年3月12日
無事にニュータイプになりアクシズを押し出すことに成功したようですね。
slotでニュータイプなvueライフを送りましょう!
- 投稿日:2020-05-20T23:19:31+09:00
web初心者が気分転換できそうなトリビアボタンを作ってみた(Vue.js + Trivia API)
概要
普段は組込みソフトエンジニアをしており、設計やデバッグが思うように進まず、集中が続かなかったり脳をリフレッシュしたいことが多々あります。
そこで、脳をリフレッシュするためにブラウザさえ使えれば気分転換できる
「トリビアボタン」
を作ってみました。デモ
「気分転換トリビアボタン」を公開しました!
— まえぷー@出窓菜園ティスト (@kmaepu) May 20, 2020
頭が疲れた時や、電車移動中の隙間時間に見ると気分転換になるかなと思い作りました!
私はフロントエンドのド素人なので、HTMLがお粗末なのは目瞑っていただけると嬉しいです。
こちらから遊ぶことができます☺https://t.co/bvl25aDtu9#protoout pic.twitter.com/nCeC9ChetOこちらから利用できますのでお試しあれ~?
※2020/5/20現在、エスケープ文字がおかしな表示になっている不具合があります。
中身はどうなっているか
構成
トリビアのデータは「OPEN TRIVIA DATABASE」のTrivia APIで取得て利用しています。
お気づきの方もいらっしゃるかもしれないですが、ドメインがherokuではないです。これは、「freenom」という無料でドメイン取得ができるサービスを利用しています。
開発環境
2020/5/20現在の開発環境です。
- Windows10
- VS code 1.45.0
- Node.js v10.15.3ソースコード解説
inde.html<html> <head> <title>Refreshing trivia</title> </head> <body> <p>Refreshing trivia</p> <div id="app"> Question:<br> <br> {{ message }}<br> <br> Answer:<br> {{ ans }}<br> <br> <button v-on:click="getData();">次のトリビア</button> <br> ※ネット環境によりロードに数秒かかる事があります。 </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> const app = new Vue({ el: '#app', data: { message: '', ans: '' }, methods: { getData: async function () { const response = await axios.get('/api') console.log(response.data); this.message = response.data.results[0].question; this.ans = response.data.results[0].correct_answer; }, }, mounted: function () { this.getData(); } }) </script> </body> </html>index.htmlでは、Vue.jsでボタンを押したときの処理を実装しています。こうすることでシングルページアプリケーション(SPA)を実現しています。
app.js// expressライブラリを呼び出す var express = require('express'); var app = express(); // axiosライブラリを呼び出す const axios = require('axios'); // public というフォルダに入れられた静的ファイル(HTMLファイル・CSSファイル・ブラウザ上のJavaScriptファイル)はそのまま表示 app.use(express.static(__dirname + '/public')); app.get('/api', async function(req, res) { let response; try { // Trivia APIにリクエストして、データを取得する response = await axios.get('https://opentdb.com/api.php?amount=1'); console.log(response.data); } catch (error) { console.error(error); } // 結果をJSONに割り当てる res.json(response.data); }); //app.listen(8080); app.listen(process.env.PORT || 8080);app.jsではTrivia APIにリクエストを出してデータを取得し、index.htmlに渡しています。
問題発覚!
ここで、Trivia APIの癖が発覚しました。Trivia APIから取得したデータをそのまま利用すると、次のようにエスケープ文字が変換されてしまっています。
こちらの方法でアンエスケープしてやれば修正できるのではないかとトライしています...。
おわりに
トリビアは英語限定なので、翻訳機能があると嬉しいですね。これでも英語の勉強になる...?からよしとします。
参考
- 投稿日:2020-05-20T23:19:31+09:00
web初心者が、気分転換トリビアボタンを作ってみた(Vue.js + Trivia API)
概要
普段は組込みソフトエンジニアをしており、設計やデバッグが思うように進まず、集中が続かなかったり脳をリフレッシュしたいことが多々あります。
そこで、脳をリフレッシュするためにブラウザさえ使えれば気分転換できる
「トリビアボタン」
を作ってみました。デモ
「気分転換トリビアボタン」を公開しました!
— まえぷー@出窓菜園ティスト (@kmaepu) May 20, 2020
頭が疲れた時や、電車移動中の隙間時間に見ると気分転換になるかなと思い作りました!
私はフロントエンドのド素人なので、HTMLがお粗末なのは目瞑っていただけると嬉しいです。
こちらから遊ぶことができます☺https://t.co/bvl25aDtu9#protoout pic.twitter.com/nCeC9ChetOこちらから利用できますのでお試しあれ~?
※2020/5/20現在、エスケープ文字がおかしな表示になっている不具合があります。
中身はどうなっているか
構成
トリビアのデータは「OPEN TRIVIA DATABASE」のTrivia APIで取得て利用しています。
お気づきの方もいらっしゃるかもしれないですが、ドメインがherokuではないです。これは、「freenom」という無料でドメイン取得ができるサービスを利用しています。
開発環境
2020/5/20現在の開発環境です。
- Windows10
- VS code 1.45.0
- Node.js v10.15.3ソースコード解説
inde.html<html> <head> <title>Refreshing trivia</title> </head> <body> <p>Refreshing trivia</p> <div id="app"> Question:<br> <br> {{ message }}<br> <br> Answer:<br> {{ ans }}<br> <br> <button v-on:click="getData();">次のトリビア</button> <br> ※ネット環境によりロードに数秒かかる事があります。 </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> const app = new Vue({ el: '#app', data: { message: '', ans: '' }, methods: { getData: async function () { const response = await axios.get('/api') console.log(response.data); this.message = response.data.results[0].question; this.ans = response.data.results[0].correct_answer; }, }, mounted: function () { this.getData(); } }) </script> </body> </html>index.htmlでは、Vue.jsでボタンを押したときの処理を実装しています。こうすることでシングルページアプリケーション(SPA)を実現しています。
app.js// expressライブラリを呼び出す var express = require('express'); var app = express(); // axiosライブラリを呼び出す const axios = require('axios'); // public というフォルダに入れられた静的ファイル(HTMLファイル・CSSファイル・ブラウザ上のJavaScriptファイル)はそのまま表示 app.use(express.static(__dirname + '/public')); app.get('/api', async function(req, res) { let response; try { // Trivia APIにリクエストして、データを取得する response = await axios.get('https://opentdb.com/api.php?amount=1'); console.log(response.data); } catch (error) { console.error(error); } // 結果をJSONに割り当てる res.json(response.data); }); //app.listen(8080); app.listen(process.env.PORT || 8080);app.jsではTrivia APIにリクエストを出してデータを取得し、index.htmlに渡しています。
問題発覚!
ここで、Trivia APIの癖が発覚しました。Trivia APIから取得したデータをそのまま利用すると、次のようにエスケープ文字が変換されてしまっています。
こちらの方法でアンエスケープしてやれば修正できるのではないかとトライしています...。
おわりに
トリビアは英語限定なので、翻訳機能があると嬉しいですね。これでも英語の勉強になる...?からよしとします。
参考
- 投稿日:2020-05-20T23:19:31+09:00
気分転換にトリビアボタンをポチろう(Vue.js + Trivia API)
概要
どうしても集中が続かないとき、脳をリフレッシュしたいですよね。
ブラウザさえ使えれば気分転換できる
「トリビアボタン」
を作ってみました。デモ
「気分転換トリビアボタン」を公開しました!
— まえぷー@出窓菜園ティスト (@kmaepu) May 20, 2020
頭が疲れた時や、電車移動中の隙間時間に見ると気分転換になるかなと思い作りました!
私はフロントエンドのド素人なので、HTMLがお粗末なのは目瞑っていただけると嬉しいです。
こちらから遊ぶことができます☺https://t.co/bvl25aDtu9#protoout pic.twitter.com/nCeC9ChetOこちらから利用できますのでお試しあれ~?
※2020/5/20現在、エスケープ文字がおかしな表示になっている不具合があります。
中身はどうなっているか
構成
トリビアのデータは「OPEN TRIVIA DATABASE」のTrivia APIで取得て利用しています。
お気づきの方もいらっしゃるかもしれないですが、ドメインがherokuではないです。これは、「freenom」という無料でドメイン取得ができるサービスを利用しています。
開発環境
2020/5/20現在の開発環境です。
- Windows10
- VS code 1.45.0
- Node.js v10.15.3ソースコード解説
inde.html<html> <head> <title>Refreshing trivia</title> </head> <body> <p>Refreshing trivia</p> <div id="app"> Question:<br> <br> {{ message }}<br> <br> Answer:<br> {{ ans }}<br> <br> <button v-on:click="getData();">次のトリビア</button> <br> ※ネット環境によりロードに数秒かかる事があります。 </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> const app = new Vue({ el: '#app', data: { message: '', ans: '' }, methods: { getData: async function () { const response = await axios.get('/api') console.log(response.data); this.message = response.data.results[0].question; this.ans = response.data.results[0].correct_answer; }, }, mounted: function () { this.getData(); } }) </script> </body> </html>index.htmlでは、Vue.jsでボタンを押したときの処理を実装しています。こうすることでシングルページアプリケーション(SPA)を実現しています。
app.js// expressライブラリを呼び出す var express = require('express'); var app = express(); // axiosライブラリを呼び出す const axios = require('axios'); // public というフォルダに入れられた静的ファイル(HTMLファイル・CSSファイル・ブラウザ上のJavaScriptファイル)はそのまま表示 app.use(express.static(__dirname + '/public')); app.get('/api', async function(req, res) { let response; try { // Trivia APIにリクエストして、データを取得する response = await axios.get('https://opentdb.com/api.php?amount=1'); console.log(response.data); } catch (error) { console.error(error); } // 結果をJSONに割り当てる res.json(response.data); }); //app.listen(8080); app.listen(process.env.PORT || 8080);app.jsではTrivia APIにリクエストを出してデータを取得し、index.htmlに渡しています。
問題発覚!
ここで、Trivia APIの癖が発覚しました。Trivia APIから取得したデータをそのまま利用すると、次のようにエスケープ文字が変換されてしまっています。
こちらの方法でアンエスケープしてやれば修正できるのではないかとトライしています...。
おわりに
トリビアは英語限定なので、翻訳機能があると嬉しいですね。これでも英語の勉強になる...?からよしとします。
参考
- 投稿日:2020-05-20T21:37:07+09:00
Vue.jsでAPIを返してみる
概要
LINEで写真を撮るBotがGyazoに写真を溜めていくので、お友達ではない人にも見せられるような簡易なWebサービスをVue.jsで作成してVercelとfreenomで独自ドメインのサイトを作るというものです。
できたもの
vue.jsでnekoBotで表示する写真をパラパラと見れるものを作った。
— 3yaka (@3yaka4) May 20, 2020
Deployしたら動かなくなったけど。。。https://t.co/ifiicmRP6b#protoout pic.twitter.com/bzu9fOOGHg
https://ouchineko.ga/環境
macOS Catalina Visual Studio Code 1.45.1 Node.js: 12.8.1構成
app.js public - index.html - style.cssコード
<html> <head> <title>Hello My WebSite!</title> <link rel="stylesheet" media="all" href="style.css"> </head> <body> <div id="app" class="waku"> <h1>3yakaさん家のにゃんこはなにしてる?</h1> <p>猫カメラが撮った写真の最新10件がランダムに表示されるよ</p> <button id="testbutton" v-on:click="getData()"></button> <p class="mes"> {{ message }} <img class="imgsiz" v-bind:src="src" /><!-- データバインディングの場合はカッコをくくらなくて呼び出せます --> </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> const app = new Vue({ el: '#app', data: { message: 'Hello Vue!', src:'https://i.gyazo.com/9360a06096a20ab93a79a4793f7670dd.jpg' // 画像のメッセージ初期値 }, methods: { getData: async function(){ let e; const response = await axios.get('/api') for (var i = 0 ; i < 9 ; i++){ e = Math.floor(Math.random () * 10); } const date = new Date(response.data[e].created_at); let datef = date.toLocaleString(); this.message = `この写真を撮った時間は${datef}だよ`; this.src = response.data[e].url; // 取得した画像差し替え console.log(response.data[e].url); }, }, mounted: function(){ this.getData(); } }) </script> </body> </html>node.jsconst express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; const Gyazo = require('gyazo-api'); const gyazoclient = new Gyazo('***'); // axiosライブラリを呼び出す const axios = require('axios'); // public というフォルダに入れられた静的ファイル(HTMLファイル・CSSファイル・ブラウザ上のJavaScriptファイル)はそのまま表示 app.use(express.static(__dirname + '/public')); app.get('/api', async function(req, res) { let response; try { response = await gyazoclient.list(); } catch (error) { console.error(error); } //結果をJSONに割り当てる res.json(response.data); }); (process.env.NOW_REGION) ? module.exports = app : app.listen(PORT); console.log(`Server running at ${PORT}`);上記でローカルのサーバーだと動くけどVercellにDeployするとapiが読めないと返ってくる。
vercelから出るIntended Nameserversの番号が4日くらい前にProtoOut Studioでやったときとだいぶ違う感じになってた。a.zeit-world.co.uk c.zeit-world.org e.zeit-world.com f.zeit-world.netこれが、こんな感じに
ns1.vercel-dns.com ns2.vercel-dns.comきっと色々変更中なんだろうな、、、now.shからvercelってだいぶ違う感じだしな。。。
きっとこれが理由ではないけど、Deployしたらダメになるって辛い。参考サイト
爆速!Vercelとfreenomで独自ドメインのサイトを無料で作成する - Qiita
感想
久々にコンソールログが真っ赤なサイトを見た。
- 投稿日:2020-05-20T19:07:37+09:00
Nuxt + universal-cookieでクライアント、サーバーサイド両方でcookieを扱えるようにする
universal-cookieというcookieを扱うライブラリがあり、それをNuxtと組み合わせて使用するときはサーバーサイドでも正しくcookieを取得、設定できるようにする必要がある。
そこでこの記事ではその設定方法を記載していく。universal-cookieにはexpressで動作するuniversal-cookie-expressというものもある。
Nuxtの設定ではuniversal-cookie-expressのコードを参考に設定してみた。コードはこちら https://github.com/igayamaguchi/universal-cookie-nuxt-example
まずはNuxtのインストール
特に変わったことはしていないが、TypeScriptを選択しているので以降に続くサンプルコードはTypeScriptで記述していく。
$ create-nuxt-app create-nuxt-app v2.15.0 ✨ Generating Nuxt.js project in . ? Project name universal-cookie-nuxt-example ? Project description My first-rate Nuxt.js project ? Author name igayamaguchi ? Choose programming language TypeScript ? Choose the package manager Npm ? Choose UI framework None ? Choose custom server framework None (Recommended) ? Choose the runtime for TypeScript @nuxt/typescript-runtime ? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose linting tools ESLint, Prettier, StyleLint ? Choose test framework Jest ? Choose rendering mode Universal (SSR) ? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection) $ npm install universal-cookie + universal-cookie@4.0.3実際に設定をしていく
設定していく前にuniversal-cookieの使い方
universal-cookieをdefault importしてインスタンス化、そのインスタンスのメソッドからcookieの操作が可能になる。
この例はcookieの設定と取得。
ただし、サーバーサイドでcookieの設定をしたとしてもクライアントにSet-Cookie
ヘッダーを送るわけではないのでブラウザがcookieの値を保存することはない。import UniversalCookie from 'universal-cookie' // set const cookie = new UniversalCookie() cookie.set('hoge', 'fuga') // get const cookie = new UniversalCookie() const value = cookies.get('hoge') console.log(value) // cookieの値 - fugaVueコンポーネント内でuniversal-cookieを使えるように
このset、getをVueコンポーネント内で
this.$cookie.set
、this.$cookie.get
という形でアクセスできるようにする。以下ページコンポーネントでの使用例。
asyncData、fetchではapp.$cookie
、それ以外ではthis.$cookie
という形でアクセスする。<script lang="ts"> import Vue from 'vue' import Logo from '~/components/Logo.vue' export default Vue.extend({ asyncData({ app }) { app.$cookie.set('hoge', true) }, fetch({ app }): Promise<void> | void { app.$cookie.set('hoge2', true) }, components: { Logo }, created() { console.log(this.$cookie.get('hoge')) }, mounted() { console.log(this.$cookie.get('hoge')) } }) </script>この
this.$cookie
という形を実現するために、Nuxtのplugin機構を活かしてinjectの設定をしていく。
以下プラグインのコード。plugins/cookieimport { OutgoingMessage } from 'http' import { serialize } from 'cookie' import { Plugin } from '@nuxt/types' import UniversalCookie, { CookieChangeOptions } from 'universal-cookie' declare module 'vue/types/vue' { interface Vue { $cookie: UniversalCookie } } declare module '@nuxt/types' { interface NuxtAppOptions { $cookie: UniversalCookie } } declare module 'vuex/types/index' { interface Store<S> { $cookie: UniversalCookie } } /** * Vueでthis.$cookieからクッキーの操作を行える処理を追加する * サーバー、クライアント両方で同じインターフェースで扱えるように */ const plugin: Plugin = ({ req, res }, inject) => { let cookie: UniversalCookie if (process.server) { cookie = createServerCookie(req.headers.cookie || '', res) } else { cookie = new UniversalCookie() } inject('cookie', cookie) } /** * universal-cookieはサーバーでcookieを追加、変更、削除する場合、それをクライアントに知らせる仕組みがない * そのため自前でレスポンスヘッダーにSet-Cookieヘッダーを追加してクッキーの情報をクライアントに送る必要がある * その仕組みを備えたインスタンスを生成する */ export function createServerCookie( cookie: string, res: OutgoingMessage ): UniversalCookie { const universalCookie = new UniversalCookie(cookie) universalCookie.addChangeListener((change: CookieChangeOptions) => { if (res.headersSent) { return } let cookieHeader = res.getHeader('Set-Cookie') if (typeof cookieHeader === 'string') { cookieHeader = [cookieHeader] } else if (typeof cookieHeader === 'number') { cookieHeader = [cookieHeader.toString()] } cookieHeader = (cookieHeader as string[]) || [] // cookieの削除時にはvalueにundefinedが入る if (change.value === undefined) { cookieHeader.push(serialize(change.name, '', change.options)) } else { cookieHeader.push(serialize(change.name, change.value, change.options)) } res.setHeader('Set-Cookie', cookieHeader) }) return universalCookie } export default plugin上から解説していく。
まずはインスタンスの生成。
クライアントサイドでの実行では特にすることもなくそのままuniversal-cookieをインスタンス化。
サーバーサイドではヘッダーに送られているクッキーの文字列を取得してそれを用いてuniversal-cookieをインスタンス化。
res
をuniversal-cookieのインスタンスを作成する関数に送っているのは、レスポンスヘッダーの取得、設定を行うため。import UniversalCookie from 'universal-cookie' if (process.server) { cookie = createServerCookie(req.headers.cookie || '', res) } else { cookie = new UniversalCookie() } // ------------------ function createServerCookie( cookie: string, res: OutgoingMessage ): UniversalCookie { const universalCookie = new UniversalCookie(cookie) // ------------------universal-cookieにはaddChangeListenrというcookieを設定、または削除したときによびだされるフック関数を設定することができるメソッドが備えられている。
サーバーサイドでクッキーを設定する場合はそのフック関数にSet-Cookie
ヘッダーを返す仕組みを導入することでサーバーで設定したクッキーがブラウザに送信され正しく保存されるようにしている。
フック関数は第一引数に設定されたcookieの値、オプションが渡されるようになっている。universalCookie.addChangeListener((change: CookieChangeOptions) => { /* ... */ })Nuxt上であり得るのか分からないが既にヘッダーが送られ始めた後にcookieの設定をしている場合はサーバーサイドのcookieの設定は行わないようにしている。
if (res.headerSent) { return }送る予定のレスポンスの中に既に
Set-Cookie
ヘッダーが設定していた場合その値を取り出して加工していく。
res.getHeader('xxxxx')
で任意のレスポンスヘッダーを取得することが出来る。今回はSet-Cookie
ヘッダーを使用するのでres.getHeader('Set-Cookie')
。
res.getHeader('Set-Cookie')
では配列以外で返ってくるパターンを想定して文字列の配列形式に変換しておく。let cookieHeader = res.getHeader('Set-Cookie') if (typeof cookieHeader === 'string') { cookieHeader = [cookieHeader] } else if (typeof cookieHeader === 'number') { cookieHeader = [cookieHeader.toString()] } cookieHeader = (cookieHeader as string[]) || []先ほど作成した配列に設定、または削除するcookieを追加してヘッダーに設定する。
値はフック関数の第一引数に{ name: クッキー名, value: クッキーの値, options: クッキーのオプション(期限など)}
という形で入っているのでそれを取り出し、cookie
パッケージのserializeメソッドを使用して文字列に加工してあげることでヘッダーに設定するのに適した文字列となる。削除時にはoptionsには
{ expires: 1970-01-31T15:00:01.000Z, maxAge: 0 }
のような過去の値、期限切れになるような値が入るようになっているので、それをそのままヘッダーとして送ればブラウザ側のcookieを削除することができる。
res.setHeader('xxxxx')
でレスポンスヘッダーの追加、更新ができるので、res.setHeader('Set-Cookie', cookieHeader)
でSet-Cookie
ヘッダーの設定を行っている。import { serialize } from 'cookie' // cookieの削除時にはvalueにundefinedが入る if (change.value === undefined) { cookieHeader.push(serialize(change.name, '', change.options)) } else { cookieHeader.push(serialize(change.name, change.value, change.options)) } res.setHeader('Set-Cookie', cookieHeader)これでプラグインの記述が完成するので、
nuxt.config.ts
にプラグインファイルを読み込む記述を追加してNuxtの設定が完了する。plugins: ['~/plugins/cookie'],確認
最初に載せたコードを
page/index.vue
に適用してそのページにアクセスしてみる。<script lang="ts"> import Vue from 'vue' import Logo from '~/components/Logo.vue' export default Vue.extend({ asyncData({ app }) { app.$cookie.set('hoge', true) }, fetch({ app }): Promise<void> | void { app.$cookie.set('hoge2', true) }, components: { Logo }, created() { console.log(this.$cookie.get('hoge')) }, mounted() { console.log(this.$cookie.get('hoge')) } }) </script>アクセスしてChromeの開発ツールのNetworkタブでページのレスポンスヘッダーを除いてみると以下のように
Set-Cookie
ヘッダーが設定されていることが確認できる。またCookieも正しく設定されていることが確認できる。
値の設定だけではなくもちろん値の取得も機能している。
以下のコードのconsole.logが正しく出力されている。created() { console.log(this.$cookie.get('hoge')) }, mounted() { console.log(this.$cookie.get('hoge')) }この情報が誰かの役に立てば幸いです。
- 投稿日:2020-05-20T18:49:30+09:00
Nuxt.js+Firebaseでログイン情報を保持したい
Nuxt.js+Firebaseの勉強を始めたばかりです。
みなさんの記事を見よう見まねで、Googleアカウントでのログイン認証ができるWebページを作成しました。やりたいこと
ログイン後に、ログイン画面
https://xxx.firebaseapp.com/login/
に遷移したところ、画面が真っ白になりました。ログイン情報を保存して、ログインしていればリダイレクトするようにしたいです。環境
- 開発環境 WindowsPC
- node 12.13.0
- npm 6.12.0
- npx 6.12.0
- Firebase 8.3.0
- Vue CLI 4.0.5
- Nuxt.js 2.11.1
リダイレクト処理+ログイン情報の保持
- ~/pages
- login.vue
- ~/middleware
- notAuthenticated.js
- ~/store
- auth.js
ログイン画面
login.vue
のmiddlewareにリダイレクト処理を含むミドルウェアnotAuthenticated
を追加しました。pages/login.vue<template> <div> <FirebaseAuth/> </div> </template> <script> import FirebaseAuth from '@/components/FirebaseAuth.vue'; import { mapState, mapGetters, mapActions } from "vuex"; export default { middleware: 'notAuthenticated', name: 'Login', components: { FirebaseAuth } } </script>リダイレクト処理を含むミドルウェア
notAuthenticated
は次のようにしました。middleware/notAuthenticated.jsexport default function ({ store, redirect }) { if (store.getters["auth/isLoggedIn"]) { return redirect('/'); } }これでログイン画面にアクセスした時に、ミドルウェアで認証情報の有無をチェックし、適切なページへリダイレクトしてくれます。
補足ですが、ミドルウェアの実行順序は以下の順とのこと。
- nuxt.config.js
- Layout Middleware
- Page Middleware
続いて、ログイン情報を保持するための処理です。
store/auth.jsimport Vue from 'vue'; import { auth } from '~/plugins/firebase'; export const state = () => ({ user: {}, status: "" }); export const mutations = { setUser(state, user) { state.status = "loggedIn"; state.user = user; }, logout(state) { state.status = "loggedOut"; state.user = {}; } }; export const getters = { isLoggedIn: (state) => { return state.status === "loggedIn"; }, getUsername: (state) => state.user.displayName }; export const actions = { gotUser({ commit }, user) { commit("setUser", user); }, logout({ commit }) { auth.signOut().then(() => { commit("logout"); }) }, };このコードを書くだけでは
state.status
の値は保存されません。
リロード・画面遷移でデータが消えてしまうので、vuex-persistedstateを試すことにしました。vuex-persistedstate
まずはインストールします。コマンドは
npm install vuex-persistedstate --save
です。
- ~/plugins
- persistedstate.js
- ~/store
- index.js
- ~/nuxt.config.js
plugins/persistedstate.jsexport default async (context) => { await context.store.dispatch("nuxtClientInit", context); };store/index.jsimport { vuexfireMutations } from 'vuexfire'; import createPersistedState from "vuex-persistedstate"; export const mutations = { ...vuexfireMutations }; export const actions = { nuxtClientInit ({ commit, state, dispatch }, { req }) { createPersistedState()(this); }, };nuxt.config.jsplugins: [ '~/plugins/firebase', { src: "~plugins/persistedstate.js", ssr: false }, ],
- 投稿日:2020-05-20T18:45:01+09:00
Vue Composition API×vue-konva+自作サービスで解説するcanvas操作
初めに
本記事では、canvas上でテキストを操作するサービスをリリースするにあたり得られた、以下の知見を紹介します
- canvasを操作するライブラリ、konva(vue-konva)について
- Vue Composition APIについて
Vue composition APIとは、次期メジャーバージョンのVueにて追加予定のAPI群です。
このAPI群を使うことで、これまでのVueの書き方ががらっと変わります(従来の書き方もできます)。
この機能は現在(2020年5月時点)でも、Vueにパッケージを追加することで試用できます。本記事では、このAPIのポイントについても紹介します。本記事にて取り上げる内容
- 自作サービスの紹介
- Vue(Vue2.0+Vue Composition API)
- vue-konva
前提条件
本記事は、以下の読者を想定しています。
- canvasでの自在な描画技術や、vueに興味がある人
- vueの入門程度の知識があるとなおよい
自作サービスの紹介
今回、Netlify+Nuxt+Veautifyという構成でWebサービスを作りました。
筆の海 https://seaofbrush.netlify.app/
どんなサービスかは、下の動画を見たほうが早いです
こんなふうに、テキストボックスに入力した文章を筆として扱い、キャンバス上に文を描く(?)ことができます。
「とにかく字をぐりぐり描きたい」というのが一番の動機のため、あまり凝った機能は設けていませんが、フォントや文字サイズを変更したり、アンドゥやダウンロードをしたりといった字描き(?)のための最低限の機能は備えています。konva、vue-konvaとは
html5のcanvas上で図形を便利に操作するため、konvaというライブラリがあります。
vue-konvaはそれをvueのコンポーネントとして扱えるようにしたものです。
導入方法については、既に素晴らしい記事がありましたのでそちらを参照してください。
- Nuxt.jsにvue-konvaを追加する
https://qiita.com/SatoshiTanoue/items/43270a462407e3561cb8
konvaによる図形の描画
konvaの基本的な使い方ですが、まずkonvaにはstage・layer・その他オブジェクト(基本図形やテキストなど)という三種類のオブジェクトがあり、
- stageにlayerを登録
- layerにその他オブジェクトを登録
- layerを描画
……という3ステップで図形を描画するようになっています。
実行例は以下の通り
( https://konvajs.org/docs/overview.html を元に少し改変)
円を描画するサンプルコード
// ステージを作成 let stage = new Konva.Stage({ container: 'container', // 描画するdivのidを指定 width: 500, height: 500 }); // レイヤーを新規作成 let layer = new Konva.Layer(); // 図形(ここでは円)を作成 let circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4 }); // レイヤーに図形を追加 layer.add(circle); // ステージにレイヤーを追加 stage.add(layer); // レイヤーを描画 layer.draw();vue-konvaによる図形の描画
続いてvue-konvaによる図形の描画方法です
上記のkonvaの例をベースとしつつ、Vue Composition APIの解説も兼ねるため、vueのコードとしてやや無理がある例になっていますがご了承ください。
単一ファイル(.vue)本体サンプルコード
<template> <div> <client-only placeholder="Loading..."> <v-stage ref="stage" :config="myState.stageConfig" @mouseenter.native="onMouseEnter" @mouseleave.native="onMouseLeave" > <v-layer ref="layer"> <v-circle ref="circle" :config="myState.circleConfig" /> </v-layer> </v-stage> </client-only> 今の円のサイズは{{myState.text}}です </div> </template> <script> // composition-apiで使用する一連のapiをインポート import { ref, reactive, computed, watch, } from "@vue/composition-api"; export default { name: "test", setup(_, context) { //ref const stage = ref(null); const layer = ref(null); const circle = ref(null); const size = ref(250) //reactive const myState = reactive({ stageConfig: computed(() => { return { width: size.value, height: size.value, } }), circleConfig: computed(() => { return { x: size.value / 2, y: size.value / 2, radius: size.value / 2, fill: 'red', stroke: 'black', strokeWidth: 4 } }), text: "", }) //function function onMouseEnter(event) { size.value = 500; }; function onMouseLeave() { size.value = 250; }; watch(() => size.value, (val, prevVal) => { myState.text = String(val); }) return { //const stage, layer, circle, size, myState, //func onMouseEnter, onMouseLeave, }; } } </script>上記のコードを使いwebページを作成すると、以下のようにマウスカーソルを当てると巨大化する円が描画されます。
上記の例を元に、ポイントをいくつか紹介します。
vue-konvaにおけるポイント
- new Konva.hogehogeオブジェクトが
<v-hogehoge>
というコンポーネントに置き換わっている- Konvaオブジェクトの初期値は:configディレクティブで渡す
- refを指定する
これはどこのドキュメントやサイトにも載っていないのですが、vue-konvaのコンポーネントはとりあえずref="hogehoge"という形で、何かしらのコンポーネント名を指定しておくと良いです。※例
<v-layer ref="layer"></v-layer>理由については後述します。
Vue composition APIのポイント
ref()とreactive()
Vueの特徴といえばリアクティブ、すなわち変数の変更が他の変数と動的に連動する点にあります。
従来のAPIではdata()内に記述していたリアクティブな変数は、ref()かreactive()関数を使うように変更されました。//従来の書き方 data() { return { size: 250 } } //Composition APIの書き方 const size=ref(250); return { size } //または const state = reactive({ size:250 }) return { state }ref()とreactive()、どちらの関数で書いても、もう一方の書き方で表現しなおすことができます。
どちらの表現も一長一短があり、特性やユースケースで使い分ける必要があるかと思います。
- ref()のメリット
- 宣言が楽
- ref()のデメリット
- 値を参照するときや代入するとき、関数名そのままではなく「関数名.value」でアクセスする必要がある
※例
const size=ref(500) console.log(size.value)//=>"500"この、使うときに.valueを付けなければならないという特性は何かと忘れがちで、バグの原因にもなりがちです。
- reactiveのメリット
- 関連する変数を一つのstate関数名にまとめられ、コードが分かりやすくなる
上記vue-konvaのコードでは、一連のconfig設定をmyStateという関数名でまとめている例がそれにあたります。
アクセスする際は「ステート名.変数名」という形式でアクセスします。こちらは.valueを付ける必要はありません。※例
const myState = reactive({ size:500 }) console.log(myState.size)//=>"500"
- reactiveのデメリット
- 宣言が若干手間
function
従来methodで指定していた各メソッドは、functionという形でそのまま記述するようになりました。
//従来の書き方 methods: { onMouseEnter(event) { //(中略) }; } //Composition APIの書き方 function onMouseEnter(event) { //(中略) };watch
watchも若干書き方が変わりました。
//従来の書き方 watch: { size(value) { //(中略) } }, //Composition APIの書き方 //(1)引数に値を指定する watch(() => size.value, (val, prevVal) => { //(中略) }) //または //(2)特に値を指定しない watch(() => { if(size.value==500){ //(中略) } })なお、新しい書き方の場合、さらに(1)引数に値を指定する書き方と(2)しない書き方があるようです。
指定する書き方のほうが、ウォッチする内容が少ないぶん速いはず(要確認)ですし、コードが分かりやすくなるため、引数に値を指定したほうが良いのではないかと思います。return
コンポーネント内で使用する変数や関数は、return内で指定しないと使えませんのでお気をつけください。
return{ //constを指定 size, myState, //functionを指定 onMouseEnter, }ref()についてのtips
1. コンポーネントにアクセスする
コンポーネント内のrefディレクティブと同名のref関数を用意することで、コンポーネントがマウントされた際にそのコンポーネントオブジェクトが自動的に代入されます。
従来の$refに相当する使い方ですね。<template> (中略) <v-layer ref="myLayer"/> </template> <script> (中略) const myLayer=ref(null);//←同じ名前にしておくと、<v-layer>の実体が後で勝手に入る </script>2. konvaのnodeオブジェクトを取得して図形を操作
一度画面に表示した図形にアニメーションを付ける場合など、後から図形に何か操作する場合、操作対象をnodeオブジェクトとして指定する必要があります。
nodeオブジェクトは、上記1.で取得したコンポーネントからgetNode()メソッドで取得します。※例:円が移動するvue-konvaアニメーション(Tween)の場合
<template> (中略) <v-circle ref="myCircle"/> </template> <script> (中略) const myCircle=ref(null); (中略) if(myCircle.value){ //コンポーネントからnodeオブジェクトを取得 const nodeObj=myCircle.value.getNode(); //nodeオブジェクトを引数に、Konva.Tween()でアニメーションを設定 let tweenObj = new Konva.Tween({ node: nodeObj, duration: 1.0, x: myState.X y: myState.Y easing: Konva.Easings.BackEaseOut, }); //アニメーションを実行 tweenObj.play(); } </script>「vue-konvaのコンポーネントはとりあえずrefを指定しておけ」と前述した理由はここにあります。
konvaを使う動機として、単に図形を表示するだけでなく、何かしらの凝った操作やアニメーションを付けたいというケースが大半だと思います。そのため、refしてコンポーネント取得してgetNodeしてアニメーションを設定、というケースが非常に多いです。3. ref()で配列を扱う
ref()は配列も扱うことができます。配列内の各要素にアクセスするときは「関数名.value[index]」という形式です。
const refArray=ref([]); refArray.value.push("A") refArray.value.push("B") refArray.value.push("C") console.log(refArray.value[1])//=>"B"(1~3の応用)複数の図形を自在に操る
vueの場合、v-forディレクティブで複数のコンポーネントを一括して管理できるわけですが、
これと上記1~3を組み合わせると、複数の図形の一括したアニメーションが非常に容易になります。
例を以下に示します。前述した円のアニメーションのコードを踏襲していますが、今度の例ではリアクティブにする必要がなくなったconfig設定は単なる定数や関数になっている点にのみご注意ください。
単一ファイル(.vue)本体
<template> <div> <client-only placeholder="Loading..."> <v-stage ref="stage" :config="stageConfig" @mouseenter.native="onMouseEnter" > <v-layer ref="layer"> <v-circle v-for="index in numberArray" :config="circleConfig(index)" ref="circle"/> </v-layer> </v-stage> </client-only> </div> </template> <script> import { ref, reactive, computed, watch, } from "@vue/composition-api"; export default { name: "test", //data setup(_, context) { //ref const stage = ref(null); const layer = ref(null); const circle = ref(null); const stageConfig={ width: 1000, height: 1000, } //0~99の連番を作成 const numberArray = [...Array(100).keys()]; function circleConfig(index) { return { x: 500, y: 500, radius: index + 25, fill: 'red', stroke: 'black', strokeWidth: 1 } }; function onMouseEnter(event) { for (let index of numberArray) { if(circle.value[index]){ const nodeObj = circle.value[index].getNode(); let tweenObj = new Konva.Tween({ node: nodeObj, duration: 1.0, x: Math.random() * 1000, y: Math.random() * 1000, easing: Konva.Easings.BackEaseOut, }); tweenObj.play(); } } ; } return { //const stage, layer, circle, stageConfig, numberArray, //func circleConfig, onMouseEnter, }; } } </script>上記の例を実行すると……
さまざまな円が、マウス操作に従って動き出すアニメーションができました!
このアニメーション、描画を担っているコードは実質、<template>
内の<v-circle>
コンポーネントと<script>
内のcircleConfig()とonMouseEnter()。合計でたったの二十行程度となります。
"vue-konvaは使える"ということが分かってもらえましたでしょうか。おわりに
自作サービスの紹介から始まり、vue-konvaの紹介からVue composition APIのtipsまで、やや散漫な内容となってしまいました。
しかしこれを機にvueやvue-konvaの魅力が伝わり、新たなサービス開発の一助となれば幸いです。
- 投稿日:2020-05-20T18:42:03+09:00
【RailsAPI + Vue.js】Pagyを用いたページネーションの実装
はじめに
RailsAPIとVuetifyでページネーションを作りました。
gemをどれにしようか調べてみたところ、Pagyがやたらとシンプル!軽い!ということらしいので、Pagyを使いました。環境、使用技術
- Rails 5.2.4.2
- Pagy 3.8.1
- Vue.js 4.3.1
- Vuetify 2.2.21
- axios 0.19.2
Vuetifyやaxiosは他のものでも置き換え可能かなと思います。
Rails側
Pagyの初期設定
How To | Pagyに書いてある通りです。
Gemfilegem 'pagy', '~> 3.5'毎度おなじみ
$ bundle install
を実行し、config/initializers/pagy.rb
に設定ファイルを作成します。
テンプレートをコピペして、必要なところだけコメントアウトを外します。config/initializers/pagy.rbPagy::VARS[:items] = 3 # 1ページに3件取得するコントローラ
app/controllers/api/v1/tweets_controller.rbclass Api::V1::UsersController < Api::V1::BaseController + include Pagy::Backend def index - users = User.all + pagy, users = pagy(User.all) render json: users end endAPIモードでない場合はこれだけで完成です。
PostmanでAPIを叩いてレスポンスを確認してみます。
このように、userのデータが3件ずつ取得できていました(シリアライザーを使っているので、カラム名がキャメルケースになっていますが、デフォルトではスネークケースのはずです)。しかし、これだけでは現在のページや総ページ数がわかりません。フロント側のページネーションコンポーネントではそれらのデータが必要なので、追加で記述していきます。
ヘッダーにページの情報を入れる
app/controllers/api/v1/tweets_controller.rb+ require 'pagy/extras/headers' class Api::V1::UsersController < Api::V1::BaseController include Pagy::Backend def index pagy, users = pagy(User.all) + pagy_headers_merge(pagy) render json: users end endこの記述により、レスポンスヘッダーに以下の情報が格納されます。
Left align Right align Link 最初・最後のページ、前・次のページのリンク Current-Page 現在のページ番号 Page-Items 1ページのuserの数 Total-Pages 全てのページ数 Total-Count 全てのuserの数 "Link"の中身(実際は一行)↓
<http://127.0.0.1:3000/api/v1/users?page=1>; rel="first", <http://127.0.0.1:3000/api/v1/users?page=1>; rel="prev", <http://127.0.0.1:3000/api/v1/users?page=3>; rel="next", <http://127.0.0.1:3000/api/v1/users?page=3>; rel="last"これでRails側の処理は終わりです。
共通化する場合は、after_action
を使う方法もあります(see 公式)。Vue側
Vue-routerは使っていません。
テンプレート部分
Pagination component — Vuetify.jsを少しカスタマイズします。
<template> <div class="text-center"> <v-pagination v-model="currentPage" :length="page.totalPages" ></v-pagination> </div> </template> <script> export default { data () { return { requestUrl: "/api/v1/users", page: { currentPage: 1, totalPages: 5, } } }, } </script>
これでひとまずページネーションを表示することはできましたが、まだ、ボタンを押してもpage.currentPage
の値が変わるだけです。ボタンを押したときの挙動
コンポーネントから
@input
イベントを受け取り、changePage
メソッドで処理を行います。<template> <div class="text-center"> <v-pagination v-model="currentPage" :length="page.totalPages" + @input="changePage" ></v-pagination> </div> </template> <script> export default { data () { return { + requestUrl: "/api/v1/users", page: { currentPage: 1, totalPages: 5, } } }, + methods: { + changePage(val) { + // 処理 + } + } } </script>methods: { async changePage(val) { // "/api/v1/users?page=2"などにGETリクエストを送る const response = await this.$axios.get(`${this.requestUrl}?page=${val}`) // 受け取ったusersデータを格納する const { users } = response.data this.users = users } }ページ読み込み時のデータ取得
mounted
で最初の画面描画時の動きを記述します。async mounted() { try { // "/api/v1/users"にGETリクエストを送る const response = await this.$axios.get(this.requestUrl) // それぞれのdataにレスポンスの値を代入する this.page.totalPages = Number(response.headers["total-pages"]) const { users } = response.data this.users = users } }最終的なコード
<template> <!-- usersの表示部分。省略 --> <div class="text-center"> <v-pagination v-model="page.currentPage" :length="page.totalPages" @input="changePage" /> </div> </template> <script> import goTo from "vuetify/es5/services/goto" // しれっと追加している export default { data() { return { requestUrl: "/api/v1/users", page: { currentPage: 1, totalPages: 1, }, users: [] } }, async mounted() { try { const response = await this.$axios.get(this.requestUrl) this.page.totalPages = Number(response.headers["total-pages"]) const { users } = response.data this.users = users } }, methods: { async changePage(val) { goTo(0) // ページ最上部までスクロール。Vuetifyのメソッド const res = await this.$axios.get(`${this.requestUrl}?page=${val}`) const { users } = res.data this.users = users } } } </script>ちなみに
追加でヘッダーに情報を渡す場合
以下のように書くことで追加できます。
requestUrl
を初期値のdataで設定するのが難しい場合は、このようにヘッダーに渡して受け取る方法もあります。def index pagy, users = pagy(User.all) pagy_headers_merge(pagy) response.headers.merge!({ 'Request-Url' => request.path_info }) render json: users end参考リンク
rails APIでページネーションを実装する
【vue.js】 Vuetifyで簡単ページネーション(Paginations)
- 投稿日:2020-05-20T18:08:43+09:00
【nuxt】【style-resources】「Semicolons aren't allowed in the indented syntax. というエラーがでて、buildできない
やりたかった事
scssの変数や、mixin等を一括で各コンポーネントに読み込みたい。そしてそれをbuildしたい。
手順
下記を参考に、scssの変数や、mixin等を一括で各コンポーネントに読み込んだ。
https://github.com/nuxt-community/style-resources-modulenpm i @nuxtjs/style-resourcesもしくは
yarn add -D @nuxtjs/style-resourcesその後、
nuxt.config.js
にて追記export default { buildModules: [ '@nuxtjs/style-resources', ], styleResources: { // それぞれの環境に合わせて設定を記述 sass: [], scss: [], less: [], stylus: [] } }僕の場合は、scssファイルを使用していたので以下に変更
export default { buildModules: ['@nuxtjs/style-resources'], styleResources: { scss: [ // scssの変数や、mixin等のファイル名を記述 '~assets/scss/variables.scss', '~assets/scss/mixin.scss', '~assets/scss/style.scss', ] } }build時に、vuetify-moduleのエラーが発生
これで開発環境では特に問題起こらず一括読み込みができるようになった。
しかし、なぜがbuildができない。。。
Semicolons aren't allowed in the indented syntax
v-〇〇でのエラーなので、vuetify関連でエラーが発生してるぽい。
解決策
全く同じ現象に遭遇した人がいたので参考にしてみた。
https://github.com/nuxt-community/vuetify-module/issues/82話を追っていくと、 一旦、node_modulesを削除、再インストールしてみると良いとのこと。
node_modeluesの削除&再インストール
試しに
node_modeluesの削除
npm i
を行ってみると、無事解決できた。詰まったらこれ大事ですね。
- 投稿日:2020-05-20T15:41:14+09:00
VSCodeでvue.jsのデバッグ(console.logを使用)
console.logでvue.jsのデバッグを行う為、こちらの記事を参考に修正しました。
参照URL
https://www.atmarkit.co.jp/ait/articles/1807/10/news033_3.htmllaunch.jsonを開き、下記を追記します。
"internalConsoleOptions": "openOnSessionStart"
デバッグのアイコンをクリックし、実行ボタンを押します。
console.logでコンソール画面に出力することができました。
- 投稿日:2020-05-20T12:58:53+09:00
Vue.jsのプラグイン機能を使用してWebページをちょっぴり進化させる
Vue.jsのプラグイン機能であるVue Carouselを使用して、
Webページをちょっとだけ進化させてみました。今回追加した機能の動き
このような形で画像等のスライド移動が出来るようになります。
「Vue Carousel」とは
Vue.jsで簡単にカルーセルスライダーを導入できるプラグインです。
導入はnpmコマンドで導入する方法とCDNより読み込む方法があります。
今回はCDNより読み込む方法で実施しています。Webページ
公開ページ(現在申請中)
コード
HTML
index.html<body> <header> <link rel="stylesheet" href="main.css"> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <h1> <a href="index.html"><img src="town4.png" alt="街を作ろう"></a> </h1> <nav id="global_navi"> <ul> <li class="current"><a href="index.html">HOME</a></li> <li><a href="index.html">ブロックを作成する</a></li> <li><a href="index.html">作品ギャラリー</a></li> <li><a href="index.html">IoTブロック作品</a></li> <li><a href="index.html">AIで設計書作成</a></li> <li><a href="index.html">お問い合わせ</a></li> </ul> </nav> </header> <div id="VueCarousel-slide"> <carousel :per-page="4" :navigation-enabled="true" navigation-prev-label="< PREV" navigation-next-label="NEXT > " :speed="900"> <slide> <img src="nanoblock1.jpg" width="300" alt="フリーザー"> </slide> <slide> <img src="nanoblock2.jpg" width="300" alt="ピカチュウ"> </slide> <slide> <img src="nanoblock3.jpg" width="300" alt="カビゴン"> </slide> <slide> <img src="nanoblock4.jpg" width="300" alt="サンダー"> </slide> <slide> <img src="nanoblock5.jpg" width="300" alt="ゲンガー"> </slide> <slide> <img src="nanoblock6.jpg" width="300" alt="ラッキー"> </slide> <slide> <img src="nanoblock8.jpg" width="300" alt="ファイヤー"> </slide> </carousel> </div> <script src="https://ssense.github.io/vue-carousel/js/vue-carousel.min.js"></script> <script src="main.js"></script> </div>CSS
main.css.VueCarousel-slide { position: relative; min-height: 200px; text-align: center; font-size: 18px; background-color: #666666; /* グレー */ color: #333; /* 黒 */ } .label { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }JavaScript
main.jsnew Vue({ el: '#VueCarousel-slide', components: { carousel: VueCarousel.Carousel, slide: VueCarousel.Slide, }, });コードの説明
次に詳細を説明します。
HTML
<div id="VueCarousel-slide">
の直後の部分では、
カルーセルスライドが何枚なのか、スライド速度はどれくらいなのか等を定義しています。具体的には次の通りです。
- per-page="4" : 1スライドに何枚表示するかの設定です。今回は4枚を表示しています。
navigation-enabled="true" : navigationラベルを使用できるようにする設定です。具体的には以下が使用できるようになります。
- navigation-prev-label="< PREV" : 戻る場合の表示内容を設定します。
- navigation-next-label="NEXT > " : 進む場合の表示内容を設定します。
speed="900" : スライドが遷移する速度です。500がデフォルト値で数字が大きいと速度がゆっくりになります。
CSS
スライドの縦横の長さや背景色を定義しています。JavaScript
'#VueCarousel-slide'
と、HTML上の'div id="VueCarousel-slide'
の部分は、内容自体は任意ですが整合している必要があるため、値が同じことを確認してください。以上です。
参考リンク
- https://www.keycoxs.com/entry/2018/09/20/175909
- https://qiita.com/weekendhikach/items/667aa5eee521f6bbd8ef
終わりに
次に機会があれば、参考リンクに記載されているようにボタンの押下でスライド移動ができるような実装も試してみたいです。
- 投稿日:2020-05-20T12:03:01+09:00
Laravel6.x + Vue.js(TypeScript)のSPAでページネーション付きのテーブル作成
冒頭
- 個人開発でLaravel6.x + Vue.js(TypeScript)のSPA作ったので一部抜粋
- CSSフレームワークはVuetify2を採用
- 環境構築については一旦スルーします
- ソースコード例はこちら
ディレクトリ構造
フロント側の構造のみ抜粋
resources/ts ├── App.vue ├── app.ts ├── components │ └── pages │ └── Search.vue ├── laravel-data-entity │ └── PaginateObject.d.ts ├── plugins │ └── http.ts ├── router.ts ├── types │ └── index.d.ts └── vue-data-entity └── DataObject.d.tsソースコード
型定義ファイル
- データベースのテーブルに用意してるテーブルの構造を定義する
resources/ts/vue-data-entity/DataObject.d.tsexport interface FileDataObject { id: number; hoge: string; }
- Laravelのページネーションオブジェクトを定義する
resources/ts/laravel-data-entity/PaginateObject.d.tsimport { DataObject } from "../vue-data-entity/FDataObject" export interface PaginateObject { current_page: number; data: DataObject[]; first_page_url: string; from: number; last_page: number; last_page_url: string; next_page_url: string; path: string; per_page: number; prev_page_url: string | null; to: number; total: number; }Vue Routerの設定
resources/ts/router.tsimport Vue from 'vue' import Router from 'vue-router' import Search from './components/pages/Search.vue' Vue.use(Router) const router = new Router({ mode: 'history', routes: [ { path: '/search', name: 'Search', component: Search, }, ] }); export default router;Axiosをラップしたプラグインを作る
resources/ts/plugins/http.tsimport _Vue from 'vue'; import axios from 'axios'; export default { install(Vue: typeof _Vue): void { const http = axios.create({ responseType: "json" }); Vue.prototype.$http = http; }, };プラグインをインストール、ルーターを設定
resources/ts/app.tsimport Vue from "vue"; import router from './router' import App from "./App.vue"; ~~省略~~ import http from './plugins/http'; Vue.use(http); ~~省略~~ new Vue({ router: router, render: h => h(App), ~~省略~~ }).$mount('#app')Vue側のソースコード例
resources/ts/components/pages/Search.vue<template> <v-content> <v-container fluid> <v-simple-table dense> <template v-slot:default> <thead> <tr> <th class="text-left">id</th> <th class="text-left">hoge</th> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="index"> <td>{{ item.id }}</td> <td>{{ item.hoge }}</td> </tr> </tbody> </template> </v-simple-table> <v-pagination v-model="page" :length="pageLength"></v-pagination> </v-container> <v-overlay :value="overlay"> <v-progress-circular color="primary" indeterminate size="64"></v-progress-circular> </v-overlay> </v-content> </template> <script lang="ts"> import { DataObject } from "../../vue-data-entity/DataObject"; import { PaginateObject } from "../../laravel-data-entity/PaginateObject"; import { Vue, Component, Watch } from "vue-property-decorator"; import VueRouter from "vue-router"; import { AxiosError, AxiosResponse } from "axios"; // registerHooksを登録しないとbeforeRouteUpdateが定義できません Component.registerHooks(["beforeRouteUpdate"]); export default class Search extends Vue { data: FileDataObject[] = []; overlay: boolean = false; pageLength: number = 1; page: number = 1; /** * created */ public created() { this.search(); } /** * watch * ルーティングに反映するためにWatchを使います。 */ @Watch("page") onPageChanged() { this.$router .push({ name: "Search", query: { page: this.page.toString() } }) } /** * search */ public search() { this.page = isNaN(Number(this.$route.query.page)) ? 1 : Number(this.$route.query.page); this.overlay = true; Vue.prototype.$http .get( `/api/search?page=${this.page}` ) .then((res: AxiosResponse<PaginateObject>): void => { this.data = res.data.data; this.pageLength = res.data.last_page; this.overlay = false; }) .catch((error: AxiosError): void => { alert("検索実行時にエラーが発生しました"); this.overlay = false; }); } /** * beforeRouteUpdate * ルーティングが更新される毎に(this.pageが変更する毎に検索を実行します) */ public beforeRouteUpdate(to: VueRouter, from: VueRouter, next: any) { next(); this.search(); } } </script>resources/ts/App.vue<template> <v-app id="inspire"> <router-view /> </v-app> </template>Laravel側
APIのルーティングを定義
routes/api.phpRoute::get('search', 'Api\SearchController@search');APIのコントローラーでページネーションオブジェクトを返却する
app/Http/Controllers/Api/SearchController.php<?php namespace App\Http\Controllers\Api; use App\Data; //モデルを定義しといてね use App\Http\Controllers\Controller; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; class SearchController extends Controller { /** * * * @return */ public function search(Request $request) { return Data::select('id', 'hoge')->paginate(10); } }resources/views/index.blade.php<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#"> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="robots" content="index,follow"> <title>~~~~</title> </head> <body> <div id="app" > </div> <script src="{{ mix('/js/app.js') }}"></script> </body> </html>動作例
- 投稿日:2020-05-20T11:30:52+09:00
独自フォームとGoogleFormを連携してかっこよくスマートにアンケート回収する
はじめに
1週間vue.jsを勉強して、昨日うポートフォリオサイトが完成しました。ぜひみてください。ポートフォリオ
そこでお問い合わせフォームをVuetifyで作って、その回答をGoogleFormに送信できたら便利だな。と思い、色々調べたのでまとめます。(現在僕のポートフォリオサイトではお問い合わせフォーム機能は停止しています)大まかな仕組み
仕組みとしては簡単です。各記入欄に値を入力してもらい、送信ボタンが押されると各記入欄の値を取得しそれをパラメーターにくっつけてaxiosでGET通信するだけです。と言っても想像だけだと難しいので画像で解説していきます。
実際にやってみる(この記事のゴール)
1.フォームに行く
2 記入&送信
3 送信完了ページに移る
4 スプレッドシートに記入される
5 メールが送信されプッシュで通知される
作り方
1.作りたいフォームと同じ形式でGoogleFormを作る&プレビューする
2.開発者ツールからformタグを探し、URLをメモする(action='<メモする>')
https://docs.google.com/forms/u/xxxxxxx/formResponse
3 各質問の全ての質問idを取得する(name='entry.xxxxxxx')
entry.xxxxxxx
4 formのコンポーネントを作って行く(※入門1週間のコードです)
コードの下に解説載せてます
form.vue<template> <v-app> <div class="top-card"> <v-card-title class=" justify-center top-title" color="#26c6da" >Contact</v-card-title > </div> <div class="title"> <v-card width="80%" height="auto" class="mx-auto" style="padding-bottom:100px; margin-bottom:200px" > <v-card width="80%" height="auto" class="mx-auto" style="padding-top:20px" flat > <v-card-title color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > 件名 </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="subject"></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > 団体名/会社 </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="organization" ></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > お名前 </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="name"></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > メール </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="mail"></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > 内容 </span></v-card-title > <v-textarea outlined auto-grow rows="4" row-height="30" shaped id="content" ></v-textarea> <v-card max-width="10%" class="mx-auto" flat> <v-btn rounded color="#e4007f" dark v-on:click="request" style="font-size:20px" clas="justify-center" >送信</v-btn > <Loading v-show="loading"></Loading> </v-card> </v-card> </v-card> </div> </v-app> </template> <script scaped> import axios from "axios"; export default { methods: { request: async function() { var Sub = "entry.xxxxxxx" + "=" + document.getElementById("subject").value; var Org = "entry.xxxxxxxxx" + "=" + document.getElementById("organization").value; var Nam = "entry.xxxxxxxx" + "=" + document.getElementById("name").value; var Mai = "entry.xxxxxxxx" + "=" + document.getElementById("mail").value; var Con = "entry.xxxxxxx" + "=" + document.getElementById("content").value; var url = "https://docs.google.com/forms/u/0/d/xxxxxxxxx/formResponse?" + Sub + "&" + Org + "&" + Nam + "&" + Mai + "&" + Con; axios.get(url); setTimeout(() => { window.location.href = "/thank"; }, 1000); }, }, }; </script> <style scaped> .top-card { padding-top: 50px; padding-bottom: 80px; } .top-title { padding-top: 50px; font-size: 30px; color: #0075c2; font-weight: bold; } </style>コード解説
書く項目の
v-textarea
にidを振っておいて送信ボタンが押されたらdocument.getElementById("content").value;
回答を取得、先ほどメモしておいた回答idと紐付けてaxios.get
します。
上のコードを丸コピする際には、1 プロジェクトにaxiosをインストール
npm i axios
2 formのURLを書き換える
url="https://docs.google.com/forms/u/0/d/xxxxxxxxx/formResponse?"
3 各項目の回答idを書き換え
4 ページの遷移先
/thank
の作成5シート名の変更
スプレッドシートを開いて一番下のシートタブから名前を変更します。
sample
6 gasを開く
7 gasのコードを書く
メールアドレスとsheet名をしっかり確認してください。
コード.gsfunction myFunction() { const recipient = 'xx自分のメールアドレスとか入れるxx`'; //送信先のメールアドレス var spreadsheet2 = SpreadsheetApp.getActiveSpreadsheet(); var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sample'); //シート名を指定 var sheet2 = spreadsheet2.getActiveSheet(); //①列の先頭行から下方向に取得する var lastRow1 = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow(); const recipientCompany = lastRow1;//7 var getVal = sheet2.getRange(recipientCompany,1).getValue(); var sub = sheet2.getRange(recipientCompany,2).getValue(); var org = sheet2.getRange(recipientCompany,3).getValue(); var name = sheet2.getRange(recipientCompany,4).getValue(); var mail = sheet2.getRange(recipientCompany,5).getValue(); var content = sheet2.getRange(recipientCompany,6).getValue(); const subject = getVal; //日時 const body = `件名:${sub}\n団体名:${org}\n名前:${name}\nメール:${mail}\n内容:${content}\n\nお問い合わせ一覧リスト↓↓\n\n\n https://docs.google.com/spreadsheets/d/`; const options = {name: 'ポートフォリオお問い合わせフォーム'}; GmailApp.sendEmail(recipient, subject, body, options); }8 トリガーを設定する
現在のプロジェクトトリガーを選択
右下のトリガーと追加をクリック
以下のように設定して保存すれば完成!
まとめ
GoofleFormはURLにパラーメータをつければでGET通信すれば回答できる!!
テキスト以外にもボタンとかでもできるみたいです。参考文献をみてください。最後に
from.vueでsetTimeoutを使っていたり色々と汚いところはいっぱいあります。初心者のため、今はまだ動くベースで勉強しています。それを理解した上で試してみてください。ありがとうございました。
参考文献
- 投稿日:2020-05-20T11:30:52+09:00
独自フォームとGoogleフォームを連携してかっこよくスマートにアンケート回収する
はじめに
1週間vue.jsを勉強して、昨日ポートフォリオサイトが完成しました。ぜひみてください。youkan.me
そこでお問い合わせフォームをVuetifyで作って、その回答をGoogleFormに送信できたら便利だな。と思い、色々調べたのでまとめます。(現在僕のポートフォリオサイトではお問い合わせフォーム機能は停止しています)大まかな仕組み
仕組みとしては簡単です。各記入欄に値を入力してもらい、送信ボタンが押されると各記入欄の値を取得しそれをパラメーターにくっつけてaxiosでGET通信するだけです。と言っても想像だけだと難しいので画像で解説していきます。
実際にやってみる(この記事のゴール)
1.フォームに行く
2 記入&送信
3 送信完了ページに移る
4 スプレッドシートに記入される
5 メールが送信されプッシュで通知される
作り方
1.作りたいフォームと同じ形式でGoogleFormを作る&プレビューする
2.開発者ツールからformタグを探し、URLをメモする(action='<メモする>')
https://docs.google.com/forms/u/xxxxxxx/formResponse
3 各質問の全ての質問idを取得する(name='entry.xxxxxxx')
entry.xxxxxxx
4 formのコンポーネントを作って行く(※入門1週間のコードです)
コードの下に解説載せてます
form.vue<template> <v-app> <div class="top-card"> <v-card-title class=" justify-center top-title" color="#26c6da" >Contact</v-card-title > </div> <div class="title"> <v-card width="80%" height="auto" class="mx-auto" style="padding-bottom:100px; margin-bottom:200px" > <v-card width="80%" height="auto" class="mx-auto" style="padding-top:20px" flat > <v-card-title color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > 件名 </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="subject"></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > 団体名/会社 </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="organization" ></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > お名前 </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="name"></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > メール </span></v-card-title > <v-textarea class="mx-2" rows="1" no-resize id="mail"></v-textarea> <v-card-title class="" color="#26c6da" style=" margin-top:10px;font-size: 30px;color: #0075c2; " ><span style="background:linear-gradient(transparent 60%, #ffff66 60%);" > 内容 </span></v-card-title > <v-textarea outlined auto-grow rows="4" row-height="30" shaped id="content" ></v-textarea> <v-card max-width="10%" class="mx-auto" flat> <v-btn rounded color="#e4007f" dark v-on:click="request" style="font-size:20px" clas="justify-center" >送信</v-btn > <Loading v-show="loading"></Loading> </v-card> </v-card> </v-card> </div> </v-app> </template> <script scaped> import axios from "axios"; export default { methods: { request: async function() { var Sub = "entry.xxxxxxx" + "=" + document.getElementById("subject").value; var Org = "entry.xxxxxxxxx" + "=" + document.getElementById("organization").value; var Nam = "entry.xxxxxxxx" + "=" + document.getElementById("name").value; var Mai = "entry.xxxxxxxx" + "=" + document.getElementById("mail").value; var Con = "entry.xxxxxxx" + "=" + document.getElementById("content").value; var url = "https://docs.google.com/forms/u/0/d/xxxxxxxxx/formResponse?" + Sub + "&" + Org + "&" + Nam + "&" + Mai + "&" + Con; axios.get(url); setTimeout(() => { window.location.href = "/thank"; }, 1000); }, }, }; </script> <style scaped> .top-card { padding-top: 50px; padding-bottom: 80px; } .top-title { padding-top: 50px; font-size: 30px; color: #0075c2; font-weight: bold; } </style>コード解説
書く項目の
v-textarea
にidを振っておいて送信ボタンが押されたらdocument.getElementById("content").value;
回答を取得、先ほどメモしておいた回答idと紐付けてaxios.get
します。
上のコードを丸コピする際には、1 プロジェクトにaxiosをインストール
npm i axios
2 formのURLを書き換える
url="https://docs.google.com/forms/u/0/d/xxxxxxxxx/formResponse?"
3 各項目の回答idを書き換え
4 ページの遷移先
/thank
の作成5シート名の変更
スプレッドシートを開いて一番下のシートタブから名前を変更します。
sample
6 gasを開く
7 gasのコードを書く
メールアドレスとsheet名をしっかり確認してください。
コード.gsfunction myFunction() { const recipient = 'xx自分のメールアドレスとか入れるxx`'; //送信先のメールアドレス var spreadsheet2 = SpreadsheetApp.getActiveSpreadsheet(); var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sample'); //シート名を指定 var sheet2 = spreadsheet2.getActiveSheet(); //①列の先頭行から下方向に取得する var lastRow1 = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow(); const recipientCompany = lastRow1;//7 var getVal = sheet2.getRange(recipientCompany,1).getValue(); var sub = sheet2.getRange(recipientCompany,2).getValue(); var org = sheet2.getRange(recipientCompany,3).getValue(); var name = sheet2.getRange(recipientCompany,4).getValue(); var mail = sheet2.getRange(recipientCompany,5).getValue(); var content = sheet2.getRange(recipientCompany,6).getValue(); const subject = getVal; //日時 const body = `件名:${sub}\n団体名:${org}\n名前:${name}\nメール:${mail}\n内容:${content}\n\nお問い合わせ一覧リスト↓↓\n\n\n https://docs.google.com/spreadsheets/d/`; const options = {name: 'ポートフォリオお問い合わせフォーム'}; GmailApp.sendEmail(recipient, subject, body, options); }8 トリガーを設定する
現在のプロジェクトトリガーを選択
右下のトリガーと追加をクリック
以下のように設定して保存すれば完成!
まとめ
GoofleFormはURLにパラーメータをつければでGET通信すれば回答できる!!
テキスト以外にもボタンとかでもできるみたいです。参考文献をみてください。最後に
from.vueでsetTimeoutを使っていたり色々と汚いところはいっぱいあります。初心者のため、今はまだ動くベースで勉強しています。それを理解した上で試してみてください。ありがとうございました。
参考文献
- 投稿日:2020-05-20T10:55:14+09:00
jest finding components with find is deprecatedの対処
この記事は何?
jestの次のメジャーバージョン(27.0系かな?)では、Componentを取る目的でのfindの仕様がdeprecatedになるそうなので対処方法を書きます。
対処方法
Componentをfindしたい時は
findComponent
メソッドを使いましょう。それ以外の用途ではfind
のままでOK。VueTest.spec.js// 略 expect(wrapper.find('.any-class').exists()).toBe(true) // OK expect(wrapper.find(AnyComponent).exists()).toBe(true) // OK, but this usage is deprecated expect(wrapper.findComponent(AnyComponent).exists()).toBe(true) // OK // 略参考
https://vue-test-utils.vuejs.org/api/wrapper/find.html
https://vue-test-utils.vuejs.org/ja/api/wrapper/find.html日本語docではまだdeprecation warningが出ていないんですね。(2020/5中旬時点)
- 投稿日:2020-05-20T10:55:14+09:00
vue-test-utils finding components with find is deprecatedの対処
この記事は何?
vue-test-utilsの次のメジャーバージョンでは、Componentを取る目的でのfindの使用がdeprecatedになるそうなので対処方法を書きます。
対処方法
Componentをfindしたい時は
findComponent
メソッドを使いましょう。それ以外の用途ではfind
のままでOK。VueTest.spec.js// 略 expect(wrapper.find('.any-class').exists()).toBe(true) // OK expect(wrapper.find(AnyComponent).exists()).toBe(true) // OK, but this usage is deprecated expect(wrapper.findComponent(AnyComponent).exists()).toBe(true) // OK // 略参考
https://vue-test-utils.vuejs.org/api/wrapper/find.html
https://vue-test-utils.vuejs.org/ja/api/wrapper/find.html日本語docではまだdeprecation warningが出ていないんですね。(2020/5中旬時点)
- 投稿日:2020-05-20T10:28:08+09:00
ASP.Net Core+RazorPage+Vue(ラジオボタンとチェックボタンの変更)
**** 2020.5.21 タイトル変えました ****
今回の内容
以前作った入力用のコンポーネントのラジオボタンとチェックボタンの修正と拡張をしました。
具体的にはプロパティーなどで不要なものを削除してソースを少しでも小さくする。(どうしても最初は不要なコードが多くなりますねー)
次にvue特有の属性値「v-bind」をタグヘルパーでは利用しない。別に使用しても問題ないのですが、なんとなくこの「v-xxx」についてはhtmlやvueのソース、javaスクリプト内のみで利用する方がいいかなという程度です。配列やオブジェクトをタグヘルパーで設定する場合、そこで「v-bind」を利用すると何も考えずにJSONのデータが利用できるのですが、これをやめるとvueのコード内でいったんJSONを変換する必要があり、ソースは増えてしまうのですが、タグヘルパーで「v-bind:list」とかに設定するのが気持ち悪いのでやめました。
あとは未選択の許容とその際に入力必須をできる用にしました。未選択を許容した場合、ラジオボタンは選択されているものを再度クリックすると未選択状態となります。前提
ASP.NetCore RazorPage+Vue+blumaの環境を利用します。
以前に書いた「Vue.jsを利用してみる(1)」と「Vue.jsを利用してみる(1)」を参照しての環境を構築します。必須バリデーション
必須バリデーションアトリビュートを作ります。
基本クラス(AnnotationBase)
バリデーションの基本クラスです。今後各種入力で使う為の共通の処理などを入れておくために作成しています。
今のところエラーメッセージとhtmlの属性の追加処理を作成しています。AnnotationBase.csusing System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace RazorPageVue.VueAnnotations { /// <summary> /// バリデーション属性用の基本クラス /// </summary> public class AnnotationBase : ValidationAttribute { /// <summary> /// コンストラクタ /// </summary> protected AnnotationBase() { } /// <summary> /// コンストラクタ /// </summary> /// <param name="errorMessageAccessor">エラーメッセージへのアクセサ</param> protected AnnotationBase(Func<string> errorMessageAccessor) : base(errorMessageAccessor) { } /// <summary> /// コンストラクタ /// </summary> /// <param name="errorMessage">エラーメッセージ</param> protected AnnotationBase(string errorMessage) : base(errorMessage) { } /// <summary> /// アトリビュートの設定 /// </summary> /// <param name="attributes">アトリビュート</param> /// <param name="key">追加するキー</param> /// <param name="value">追加する値</param> protected void MergeAttribute(IDictionary<string, string> attributes, string key, string value, bool setForce = false) { if (attributes.ContainsKey(key)) { // 属性が既に設定されている場合 if (string.IsNullOrWhiteSpace(attributes[key])) { // 値が未設定の場合は値の未設定する attributes[key] = value; } else { // 値が設定されており、設定しようとする値と異なる場合は例外を発生させる if (attributes[key] != value) { //throw new ValidationH5Error("タグの属性[" + key + "]に設定されている値[" + attributes[key] + "]が想定[" + value + "]と異なります。"); attributes.Remove("type"); attributes[key] = value; } } } else { // 属性が未設定の場合は、新たな属性を設定する attributes.Add(key, value); } } } }必須バリデーションクラス(RequiredAttribute)
必須バリデーションです。これによりレーザーページでモデルクラスのプロパティに[Required]を付けると必須入力となります。実際の処理はhtml出力時に「required='required'」と「required-err-msg='設定したエラーメッセージ'」が追加されます。エラーメッセージは未設定なら「required-err-msg」は追加されません。
サーバーサイドの処理は記述していますが、基本的にはこの後に記述するvueの中でえらーチェックするので実行されることはないはずです。(ダイレクトにpostを実行された場合の対処です)RequiredAttribute.csusing Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using System; using System.ComponentModel.DataAnnotations; namespace RazorPageVue.VueAnnotations { /// <summary> /// 入力必須属性 /// </summary> [AttributeUsage(AttributeTargets.Property)] public class RequiredAttribute : AnnotationBase, IClientModelValidator { /// <summary> /// コンストラクタ /// </summary> public RequiredAttribute(): base() { } /// <summary> /// バリデーション(サーバーサイド) /// </summary> /// <param name="value">値</param> /// <param name="validationContext">バリデーションコンテキスト</param> /// <returns></returns> protected override ValidationResult IsValid( object value, ValidationContext validationContext) { if (value == null) { return new ValidationResult(GetErrorMessage(validationContext.DisplayName)); } else { if (value.GetType() == typeof(string)) { if (string.IsNullOrWhiteSpace((string)value)) { return new ValidationResult(GetErrorMessage(validationContext.DisplayName)); } else { return ValidationResult.Success; } } else { return ValidationResult.Success; } } } /// <summary> /// クライアントでのバリデーション用の操作 /// </summary> /// <param name="context">クライアントのバリデーションコンテキスト</param> public void AddValidation(ClientModelValidationContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); // 以下のタグ属性を設定する // required "required" // required-err-msg バリデーションで設定されたエラーメッセージ MergeAttribute(context.Attributes, "required", "required"); if (!string.IsNullOrWhiteSpace(ErrorMessage)) MergeAttribute(context.Attributes, "required-err-msg", ErrorMessage); } /// <summary> /// サーバーバリデーション時のエラーメッセージ取得 /// </summary> /// <param name="displayName">表示名称(DisplayNameアトリビュートで変更できる)</param> /// <returns>必須エラーメッセージ</returns> string GetErrorMessage(string displayName) { if (string.IsNullOrEmpty(ErrorMessage)) { return displayName + "は入力必須です。"; } else { return ErrorMessage; } } } }ラジオボタン(改造)
必須バリデーションの対応とかしました。
VueRadioButtonsタグヘルパー(改造)
以前書いたものを改造しました。
一番大きなところは「Process」メソッドで、バリデーションに対応できるようにしています。この部分はinputタグ用のGeneratorを流用しています。
Enumの場合のリストの作成で、null許容型の判定が増えてます。VueRadioButtonsTagHelper.csusing Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Json; using System.Text; namespace RazorPageVue.VueTagHelpers { /// <summary> /// Vueで作ったVueRadioButtonコンポーネントをRazorで利用する為のタグヘルパー /// 以下のタグ属性を利用できるようにする /// asp-for:ラジオボタンの選択値 /// asp-items: ラジオボタンのリスト(KeyValuePair<string, string>のリスト)enumの選択の場合は設定不要 /// selectedclass:選択されているラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// notselectedclass:未選択のラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// [例] /// <vue-radio-buttons asp-for='@Model.SelectedList' asp-items='@Model.KeyValueList' selectedclass="has-background-primary"></radio-buttons> /// ModelのSelectedListは選択されたチェックボタンのリストの初期値を設定し、選択した結果リストが設定される /// Model.eyValueListはチェックボタンのリストをList<KeyValuePair<string,string>>型で設定する。(keyは表示する文字列、valueは設定される値) /// ただし、SelectedListがEnum型の配列の場合、リストを空にしておくと自動的にEnumのリストが設定される。Enumのリストを加工したい場合のみ上記のリストを設定する /// </summary> [HtmlTargetElement("vue-radio-buttons", TagStructure = TagStructure.NormalOrSelfClosing)] public class VueRadioButtonsTagHelper : TagHelper { /// <summary> /// asp-for属性が入るプロパティ /// </summary> public ModelExpression AspFor { get; set; } /// <summary> /// タグ属性「name」を受け取るプロパティ。(未設定で「asp-for」が設定されている場合はその変数を示す値が設定される) /// </summary> public string Name { get; set; } /// <summary> /// ラジオボタンのリスト(asp-items) /// kKeyValuePairのリストで、keyが表示される文字列で、valueが設定される値になる /// </summary> public List<KeyValuePair<string, string>> AspItems { get; set; } /// <summary> /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用 /// (コンストラクタのデータインジェクションで設定) /// </summary> protected IHtmlGenerator Generator { get; } /// <summary> /// コンストラクタ /// </summary> /// <param name="htmlHelper"></param> public VueRadioButtonsTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator) { Generator = generator; } /// <summary> /// htmlヘルパーを利用するために関連付けるビューのコンテキスト /// </summary> [ViewContext] [HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } /// <summary> /// タグの調整処理 /// </summary> /// <param name="context"></param> /// <param name="output"></param> public override void Process(TagHelperContext context, TagHelperOutput output) { // 引数のコンテキストと出力がnullならエラー if (context == null) throw new ArgumentNullException(nameof(context)); if (output == null) throw new ArgumentNullException(nameof(output)); // asp-forがEnum型でリストが未設定の場合はEnumのリストを作成する(Nullableの場合も対処している) if (AspFor.Metadata.ModelType.IsEnum) { if (AspItems == null) { AspItems = new List<KeyValuePair<string, string>>(); foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType)) { AspItems.Add(new KeyValuePair<string, string>(enumItemName, enumItemName)); } } } else if (AspFor.Metadata.ModelType.IsGenericType && AspFor.Metadata.ModelType.GetGenericTypeDefinition() == typeof(Nullable<>)) { if (AspItems == null) { AspItems = new List<KeyValuePair<string, string>>(); foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType.GenericTypeArguments[0])) { AspItems.Add(new KeyValuePair<string, string>(enumItemName, enumItemName)); } } } // 選択リストをJSONに変換して設定(Vueに配列を渡すにはv-bindを利用する必要がある) var serializer = new DataContractJsonSerializer(typeof(List<KeyValuePair<string, string>>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, AspItems); output.Attributes.SetAttribute("list", Encoding.UTF8.GetString(ms.ToArray())); } // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む // この時、すでに設定されているタグの属性は更新できないので注意 output.MergeAttributes(GenerateRadioButton()); } /// <summary> /// タグビルダーを作成 /// </summary> /// <returns>標準のinputタグを利用したタグビルダー</returns> private TagBuilder GenerateRadioButton() { // このタグで設定する属性リストを作成(NameがあってAspForが無い場合にNameが直接書かれていればそれを設定している) IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(AspFor.Name) && string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix)) { if (!string.IsNullOrEmpty(Name)) { htmlAttributes.Add("name", Name); } } else { htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } // inputの基本的なタグビルダーを生成して返す return Generator.GenerateTextBox( ViewContext, AspFor.ModelExplorer, AspFor.Name, AspFor.ModelExplorer.Model, null, htmlAttributes); } } }VueRadioButtonsコンポーネント(改造)
vueのコンポーネントです。この後の事を考えてvue-というプレフィックスを付けました。
必須バリデーションの対応をしています。その関係で、選択されている項目をもう一度クリックすると完全に選択外になるようにしました。
idやnameを以前はhiddenidやhiddennameとしていましたが、意味がないので普通にしました。最初に書いたように、タグヘルパーで「v-bind:」の記述をやめましたので、選択リストはdataのselectListにJSONを変換して取り込んでいます。v-bindを使うとプロパティの型と等を自動的に判定して行ってくれるようです。
クリック処理でいろいろ行っているのですが、キーボード操作でも同じ様に動いてくれていますので、キーボードのイベントは記述してません。おかげで助かった。vueRadioButtons.js//====================================================================== // ラジオボタンタグ // ラジオボタンをグループで設定し、選択されている一つのデータを返す // VueRadioButtonsTagHelperと連携して利用する // 使用方法は「VueRadioButtonsTagHelper」を参照 //====================================================================== Vue.component('vue-radio-buttons', { props: { id: String, // このコントロールのid。Razor Pageとasp-forで関連付けされている name: String, // このコントロールのname。Razor Pageとasp-forで関連付けされている list: String, // 選択リスト(key,value)のペアのリスト(JSONデータ)値がenum型の場合は列挙の配列で未設定ならすべての列挙 value: String, // 選択されている初期値(key)または列挙 selectedclass: String, // 選択されているアイテムのcssクラス notselectedclass: String, // 選択されていないアイテムのcssクラス required: Boolean, // 必須フラグ requiredErrMsg: String // 必須エラーメッセージ }, data: function () { return { selectList: JSON.parse(this.list), // 選択リスト selectedValue: this.value, // 選択値 groupName: "items_" + this.name // 選択ラジオボタンのグループ名 }; }, mounted: function () { this.checkRequired(); // マウント時に必須チェックを行う }, methods: { //------------------------------------------------------------ // 選択リストのid作成 //------------------------------------------------------------ itemid: function (index) { return this.id + '_' + index; }, //------------------------------------------------------------ // 選択操作。選択アイテムをクリックした時に実行 //------------------------------------------------------------ selectAction: function (e) { if (this.selectedValue === e.target.getAttribute("value")) { // 同じものを選択した場合は[cannoselect]が有効な場合は未選択状態にする(valueにnullが可能な場合のみ) if (!this.required) { this.selectedValue = ""; } } else { // 選択値を変更する this.selectedValue = e.target.getAttribute("value"); } this.checkRequired(); }, //------------------------------------------------------------ // その値が選択されているかどうか(自身が選択状態かどうかの判定用) //------------------------------------------------------------ isSelected: function (val) { return val === this.selectedValue; }, //------------------------------------------------------------ // 必須チェック //------------------------------------------------------------ checkRequired: function () { if (this.required && this.selectedValue === "") { // 変更エラーメッセージを取得 if (this.requiredErrMsg) { // 変更エラーメッセージがある場合は変更エラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(this.requiredErrMsg); } } else { // 変更エラーメッセージが無ければデフォルトのエラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity("どれか一つ入力してください。"); } } } else { // エラーが無いのでカスタムエラーを削除 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(""); } } } }, template: '<div>\ <input :id=id :name=name type="hidden" :value=selectedValue>\ <span v-for="(item, index) in selectList" :key="item.value">\ <span style="display:inline-block" :class="[isSelected(item.value) ? selectedclass : notselectedclass ]" :value=item.value>\ <input type="radio" ref="Items" :id=itemid(index) :name=groupName :checked=isSelected(item.value) v-on:click=selectAction :value=item.value >\ <label v-on:click=selectAction :value=item.value>{{ item.key }}</label>\ </span>\ </span>\ </div>' });チェックボタン(改造)
必須バリデーションの対応とかしました。
VueCheckButtonsタグヘルパー(改造)
以前書いたものを改造しました。
一番大きなところは「Process」メソッドで、バリデーションに対応できるようにしています。この部分はinputタグ用のGeneratorを流用しています。VueCheckButtonsTagHelper.csusing Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Json; using System.Text; namespace RazorPageVue.VueTagHelpers { /// <summary> /// Vueで作ったVueCheckButtonコンポーネントをRazorで利用する為のタグヘルパー /// 以下のタグ属性を利用できるようにする /// asp-for:チェックボタンの選択リスト /// asp-items: チェックボタンのリスト(KeyValuePair<string, string>のリスト)enumの選択の場合は設定不要 /// selectedclass:選択されているラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// notselectedclass:未選択のラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// [例] /// <vue-check-buttons asp-for='@Model.RadioSelected' asp-items='@Model.RadioKeyValueList' selectedclass="has-background-primary"></radio-buttons> /// ModelのRadioSelectedは選択されたラジオボタンの値が設定される /// Model.RadioKeyValueListはラジオボタンのリストをList<KeyValuePair<string,string>>型で設定する。(keyは表示する文字列、valueは設定される値) /// ただし、RadioSelectedがEnum型の場合、リストを空にしておくと自動的にEnumのリストが設定される。Enumのリストを加工したい場合のみ上記のリストを設定する /// なお、Enumの場合、Nullable(null許容型)にしていないと、Required未設定でも結果が空にはならないので注意 /// </summary> [HtmlTargetElement("vue-check-buttons", TagStructure = TagStructure.NormalOrSelfClosing)] public class VueCheckButtonsTagHelper : TagHelper { /// <summary> /// asp-for属性が入るプロパティ /// </summary> public ModelExpression AspFor { get; set; } /// <summary> /// タグ属性「name」を受け取るプロパティ。(未設定で「asp-for」が設定されている場合はその変数を示す値が設定される) /// </summary> public string Name { get; set; } /// <summary> /// ラジオボタンのリスト(asp-items) /// kKeyValuePairのリストで、keyが表示される文字列で、valueが設定される値になる /// </summary> public List<KeyValuePair<string, string>> AspItems { get; set; } /// <summary> /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用 /// (コンストラクタのデータインジェクションで設定) /// </summary> protected IHtmlGenerator Generator { get; } /// <summary> /// htmlヘルパーを利用するために関連付けるビューのコンテキスト /// </summary> [ViewContext] [HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="htmlHelper"></param> public VueCheckButtonsTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator) { Generator = generator; } /// <summary> /// タグの調整処理 /// </summary> /// <param name="context"></param> /// <param name="output"></param> public override void Process(TagHelperContext context, TagHelperOutput output) { // 引数のコンテキストと出力がnullならエラー if (context == null) throw new ArgumentNullException(nameof(context)); if (output == null) throw new ArgumentNullException(nameof(output)); //// RazorPageでPost時にモデルにバインドできるように、Vueのid,name,valueプロパティを設定 //output.Attributes.SetAttribute("name", AspFor.Name); // asp-forがEnum型でリストが未設定の場合はEnumのリストを作成する if (AspFor.Metadata.ModelType.GenericTypeArguments[0].IsEnum) { if (AspItems == null) { AspItems = new List<KeyValuePair<string, string>>(); foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType.GenericTypeArguments[0])) { AspItems.Add(new KeyValuePair<string, string>(enumItemName, ((int)Enum.Parse(AspFor.Metadata.ModelType.GenericTypeArguments[0],enumItemName)).ToString())); } } } // 選択リストをJSONに変換して設定 var serializer = new DataContractJsonSerializer(typeof(List<KeyValuePair<string, string>>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, AspItems); output.Attributes.SetAttribute("list", Encoding.UTF8.GetString(ms.ToArray())); } // 選択結果リストを設定 var jsonValueList = ""; if (AspFor.Metadata.ModelType.GenericTypeArguments[0].IsEnum) { // Enumの場合は一旦文字列リストに変換してからJSON形式にして設定する var workList = new List<string>(); foreach (var item in (System.Collections.IEnumerable)AspFor.Model) workList.Add(((int)item).ToString()); serializer = new DataContractJsonSerializer(typeof(List<string>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, workList); jsonValueList = Encoding.UTF8.GetString(ms.ToArray()); //output.Attributes.SetAttribute("v-bind:value", Encoding.UTF8.GetString(ms.ToArray())); } } else { // Enum以外の場合は文字列リストをJSON形式にして設定する serializer = new DataContractJsonSerializer(typeof(List<string>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, AspFor.Model); jsonValueList = Encoding.UTF8.GetString(ms.ToArray()); //output.Attributes.SetAttribute("v-bind:value", Encoding.UTF8.GetString(ms.ToArray())); } } //base.Process(context, output); // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む // この時、すでに設定されているタグの属性は更新できないので注意 output.MergeAttributes(GenerateRadioButton(jsonValueList)); } /// <summary> /// タグビルダーを作成 /// </summary> /// <returns>標準のinputタグを利用したタグビルダー</returns> private TagBuilder GenerateRadioButton(string value) { // このタグで設定する属性リストを作成(NameがあってAspForが無い場合にNameが直接書かれていればそれを設定している) IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(AspFor.Name) && string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix)) { if (!string.IsNullOrEmpty(Name)) { htmlAttributes.Add("name", Name); } } else { htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } // inputの基本的なタグビルダーを生成して返す return Generator.GenerateTextBox( ViewContext, AspFor.ModelExplorer, AspFor.Name, //AspFor.ModelExplorer.Model, value, null, htmlAttributes); } } }VueCheckButtonsコンポーネント(改造)
ラジオボタンと同じように変更しています。
vueCheckButtons.js//====================================================================== // チェックボタンタグ // チェックボタンをグループで設定し、選択されている一つのデータを返す // VueCheckButtonsTagHelperと連携して利用する // 使用方法は「VueCheckButtonsTagHelper」を参照 //====================================================================== Vue.component('vue-check-buttons', { props: { id: String, // このコントロールのid。Razor Pageとasp-forで関連付けされている name: String, // このコントロールのname。Razor Pageとasp-forで関連付けされている list: String, // 選択リスト(key,value)のペアのリスト(JSONデータ)値がenum型の場合は列挙の配列で未設定ならすべての列挙 value: String, // 選択されている項目のvalueの配列(JSONデータ)値が列挙の場合は列挙の配列 selectedclass: String, // 選択されているチェックボックスのcssクラス notselectedclass: String, // 選択されていないチェックボックスのcssクラス required: Boolean, // 必須フラグ requiredErrMsg: String // 必須エラーメッセージ }, data: function () { return { selectlist: JSON.parse(this.list), // 選択リスト selectedValues: JSON.parse(this.value), // 選択されている値のリスト groupName: "items_" + this.name // 選択チェックボックスのグループ名 }; }, mounted: function () { this.checkRequired(); // マウント時に必須チェックを行う }, methods: { //------------------------------------------------------------ // 選択リストのid作成 //------------------------------------------------------------ itemid: function (index) { return this.id + '_' + index; }, //------------------------------------------------------------ // 選択操作。選択アイテムをクリックした時に実行 //------------------------------------------------------------ selectAction: function (e) { var val = e.target.getAttribute("value"); var exitIndex = this.selectedValues.indexOf(val); if (exitIndex >= 0) { this.selectedValues.splice(exitIndex, 1); } else { this.selectedValues.push(val); } this.checkRequired(); }, //------------------------------------------------------------ // その値が選択されているかどうか(自身が選択状態かどうかの判定用) //------------------------------------------------------------ isSelected: function (val) { return this.selectedValues.indexOf(val) >= 0; }, //------------------------------------------------------------ // 必須チェック //------------------------------------------------------------ checkRequired: function () { if (this.required && this.selectedValues.length === 0) { // 変更エラーメッセージを取得 if (this.requiredErrMsg) { // 変更エラーメッセージがある場合は変更エラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(this.requiredErrMsg); } } else { // 変更エラーメッセージが無ければデフォルトのエラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity("どれか一つ入力してください。"); } } } else { // エラーが無いのでカスタムエラーを削除 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(""); } } } }, template: '<div :id=id>\ <input :name=name v-for="selected in selectedValues" :key=selected type="hidden" :value=selected>\ <span v-for="(item, index) in selectlist" :key="item.key">\ <span style="display:inline-block" :class="[isSelected(item.value) ? selectedclass : notselectedclass ]" :value=item.value>\ <input type="checkbox" ref="Items" :id=itemid(index) :name="groupName" :checked=isSelected(item.value) v-on:click=selectAction :value=item.value >\ <label v-on:click=selectAction :value=item.value>{{ item.key }}</label>\ </span>\ </span>\ </div>' });参考ページとテスト
以下は利用サンプルのページとそのテスト用のtescafeです。ちょっと適当ですがご容赦ください。
VueRadioButton利用サンプルのページ
RadioButton.cshtml@page @model RadioButtonModel @{ ViewData["Title"] = "RadioButton Check"; } @addTagHelper *,RazorPageVue @*利用するVueコンポーネントをここで取り込む*@ <script src="~/js/vueUtil.js" asp-append-version="true"></script> <script src="~/compornents/vueRadioButtons.js" asp-append-version="true"></script> <form method="post"> <div id="components-demo"> <h1 class="display-4">ラジオボタンチェック</h1> <div class="content box"> <label>通常のラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.RadioSelected' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須のラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.RadioRequiredSelected' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須(メッセージ変更)のラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.RadioRequiredSelectedErrChange' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>Enumのラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.EnumRadioSelected' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須のEnumのラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.EnumRequiredRadioSelected' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須(メッセージ変更)のEnumのラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.EnumRequiredRadioSelectedErrChange' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <input id="mainsubmit" type="submit" /> </div> <a id="returnLink" href="~/">戻る</a> </form> <script> // Vueコンポーネントを展開させる CreateVue('#components-demo'); </script>RadioButton.cshtml.csusing Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using RazorPageVue.VueAnnotations; using System.Collections.Generic; namespace RazorPageVue.Pages { public class RadioButtonModel : PageModel { /// <summary> /// ラジオボタンの選択リスト /// </summary> static public List<KeyValuePair<string, string>> RadioValues { get { if (_radioValues == null) { _radioValues = new List<KeyValuePair<string, string>>(); _radioValues.Add(new KeyValuePair<string, string>("第1項目", "1")); _radioValues.Add(new KeyValuePair<string, string>("第2項目", "2")); _radioValues.Add(new KeyValuePair<string, string>("第3項目", "3")); _radioValues.Add(new KeyValuePair<string, string>("第4項目", "4")); _radioValues.Add(new KeyValuePair<string, string>("最終項目", "5")); } return _radioValues; } } static List<KeyValuePair<string, string>> _radioValues = null; /// <summary> /// 結合データ /// </summary> [BindProperty] public InputModel BindData { get; set; } = new InputModel(); public class InputModel { // ラジオボタンの選択結果 public string RadioSelected { get; set; } = "2"; public string RadioSelectedText { get { foreach (var item in RadioValues) { if (item.Value == RadioSelected) return item.Key; } return ""; } } [Required] public string RadioRequiredSelected { get; set; } [Required(ErrorMessage ="必須変更")] public string RadioRequiredSelectedErrChange { get; set; } public string RadioRequiredSelectedText { get { foreach (var item in RadioValues) { if (item.Value == RadioRequiredSelected) return item.Key; } return ""; } } // Enumを利用したラジオボタンの選択結果 public SampleEnum? EnumRadioSelected { get; set; } = SampleEnum.sample2; // Enumを利用したラジオボタンの選択結果 [Required] public SampleEnum? EnumRequiredRadioSelected { get; set; } = null; [Required(ErrorMessage = "必須変更")] public SampleEnum? EnumRequiredRadioSelectedErrChange { get; set; } } public void OnGet() { } /// <summary> /// Post処理 /// </summary> /// <returns></returns> public IActionResult OnPost() { return RedirectToPage("RadioButtonResult", new { SelectedId = BindData.RadioSelected, SelectedName = BindData.RadioSelectedText, RequiredSelectedId = BindData.RadioRequiredSelected, RequiredSelectedName = BindData.RadioRequiredSelectedText, SelectedEnum = BindData.EnumRadioSelected, RequiredSelectedEnum = BindData.EnumRequiredRadioSelected }); } } }VueRadioButton利用サンプルのページを使ったtestcafeのテスト
VueRadioButton.test.tsimport "testcafe"; import {Selector} from "testcafe"; import { ClientFunction } from "testcafe"; fixture("Vue ラジオボタンテスト") .page("https://localhost:44357/RadioButton"); //---------------------------------------------------------------------- // バリデーションを実施しその結果を取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputValidity = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.checkValidity(); }); //---------------------------------------------------------------------- // バリデーションメッセージを取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputInvalidMessage = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.validationMessage; }); //---------------------------------------------------------------------- // バリデーション正常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- async function checkValid(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(true, message + "(バリデーション正常)"); await t.expect(getInputInvalidMessage(itemid)).eql("", message + "(エラーメッセージなし)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); await t.expect(getInputInvalidMessage(itemid)).contains("変更", message + "(バリデーションメッセージ有)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック(メッセージが変更されていないことを確認) // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageNoChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); let elementMessage = getInputInvalidMessage(itemid); await t.expect(elementMessage).notEql(""); await t.expect(elementMessage).notEql(null); await t.expect(elementMessage).notContains("変更", message + "(バリデーションメッセージ有)"); } } async function setDefatltNoErr(t) { await t.click(Selector("#BindData_RadioRequiredSelected_0")); await t.click(Selector("#BindData_RadioRequiredSelectedErrChange_0")); await t.click(Selector("#BindData_EnumRequiredRadioSelected_0")); await t.click(Selector("#BindData_EnumRequiredRadioSelectedErrChange_0")); } test("初期状態チェック", async (t) => { await t.click(Selector("#mainsubmit")); await checkValid(t, "BindData_RadioSelected",5); await checkInvalidMessageNoChange(t, "BindData_RadioRequiredSelected", 5); await checkInvalidMessageChange(t, "BindData_RadioRequiredSelectedErrChange", 5); await checkValid(t, "BindData_EnumRadioSelected", 4); await checkInvalidMessageNoChange(t, "BindData_EnumRequiredRadioSelected", 4); await checkInvalidMessageChange(t, "BindData_EnumRequiredRadioSelectedErrChange", 4); await setDefatltNoErr(t); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("2", "【正常】[通常ラジオボタン]初期選択(値=2)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第2項目", "【正常】[通常ラジオボタン]初期選択(テキスト=第2項目)"); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]初期選択(値=1)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]初期選択(テキスト=第1項目)"); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample2", "【正常】[enumラジオボタン]初期選択(値=sample2)"); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]初期選択(値=sample1)"); }); test("通常ラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]第1項目選択(値 = 1)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]第1項目選択(テキスト=第1項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("", "【正常】[通常ラジオボタン]第2項目選択(値 = )"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("", "【正常】[通常ラジオボタン]第2項目選択(テキスト=第2項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("3", "【正常】[通常ラジオボタン]第3項目選択(値 = 3)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第3項目", "【正常】[通常ラジオボタン]第3項目選択(テキスト=第3項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("4", "【正常】[通常ラジオボタン]第4項目選択(値 = 4)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第4項目", "【正常】[通常ラジオボタン]第4項目選択(テキスト=第4項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("5", "【正常】[通常ラジオボタン]第5項目選択(値 = 5)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("最終項目", "【正常】[通常ラジオボタン]第5項目選択(テキスト=第5項目)"); }); test("必須ラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]第1項目選択(値 = 1)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]第1項目選択(テキスト=第1項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("2", "【正常】[通常ラジオボタン]第2項目選択(値 = 2)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第2項目", "【正常】[通常ラジオボタン]第2項目選択(テキスト=第2項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("3", "【正常】[通常ラジオボタン]第3項目選択(値 = 3)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第3項目", "【正常】[通常ラジオボタン]第3項目選択(テキスト=第3項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("4", "【正常】[通常ラジオボタン]第4項目選択(値 = 4)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第4項目", "【正常】[通常ラジオボタン]第4項目選択(テキスト=第4項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("5", "【正常】[通常ラジオボタン]第5項目選択(値 = 5)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("最終項目", "【正常】[通常ラジオボタン]第5項目選択(テキスト=第5項目)"); await t.click(Selector("#returnLink")); }); test("Enumラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]第1項目選択(値=sample1)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("", "【正常】[enumラジオボタン]第2項目選択(値=)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample3", "【正常】[enumラジオボタン]第3項目選択(値=sample3)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sampleOther", "【正常】[enumラジオボタン]第4項目選択(値=sampleOther)"); }); test("必須Enumラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]第1項目選択(値=sample1)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample2", "【正常】[enumラジオボタン]第2項目選択(値=sample2)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample3", "【正常】[enumラジオボタン]第3項目選択(値=sample3)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sampleOther", "【正常】[enumラジオボタン]第4項目選択(値=sampleOther)"); });VueCheckButton利用サンプルのページ
CheckButton.cshtml@page @model CheckButtonModel @{ ViewData["Title"] = "RadioButton Check"; } @addTagHelper *,RazorPageVue @*利用するVueコンポーネントをここで取り込む*@ <script src="~/js/vueUtil.js" asp-append-version="true"></script> <script src="~/compornents/vueCheckButtons.js" asp-append-version="true"></script> <form method="post"> <div id="components-demo"> <h1 class="display-4">ラジオボタンチェック</h1> <div class="content box"> <label>通常のチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.NormalCheckList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons> </div> <div class="content box"> <label>必須のチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredCheckList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons> </div> <div class="content box"> <label>必須でエラーメッセージ変更のチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredErrChangeList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons> </div> <div class="content box"> <label>Enumのチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.NormalEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons> </div> <div class="content box"> <label>必須のEnumのチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons> </div> <div class="content box"> <label>必須でエラーメッセージ変更のEnumのチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredErrChangeEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons> </div> <input id="mainsubmit" type="submit" /> </div> <a id="returnLink" href="~/">戻る</a> </form> <script> // Vueコンポーネントを展開させる CreateVue('#components-demo'); </script>CheckButton.cshtml.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using RazorPageVue.VueAnnotations; namespace RazorPageVue.Pages { public class CheckButtonModel : PageModel { /// <summary> /// ラジオボタンの選択リスト /// </summary> public List<KeyValuePair<string, string>> CheckValues { get; set; } = new List<KeyValuePair<string, string>>(); /// <summary> /// 結合データ /// </summary> [BindProperty] public InputModel BindData { get; set; } = new InputModel(); public class InputModel { /// <summary> /// 通常のチェックボックスの選択結果 /// </summary> public List<string> NormalCheckList { get; set; } = new List<string>(); /// <summary> /// 入力必須のチェックボックスの選択結果 /// </summary> [Required] public List<string> RequiredCheckList { get; set; } = new List<string>(); /// <summary> /// 入力必須のエラーメッセージ変更チェックボックスの選択結果 /// </summary> [Required(ErrorMessage = "必須変更")] public List<string> RequiredErrChangeList { get; set; } = new List<string>(); /// <summary> /// Enumタイプのチェックボックスの選択 /// </summary> public List<SampleEnum> NormalEnumList { get; set; } = new List<SampleEnum>(); /// <summary> /// 入力必須のEnumタイプのチェックボックスの選択 /// </summary> [Required] public List<SampleEnum> RequiredEnumList { get; set; } = new List<SampleEnum>(); /// <summary> /// 入力必須のエラーメッセージ変更Enumタイプのチェックボックスの選択 /// </summary> [Required(ErrorMessage = "必須変更")] public List<SampleEnum> RequiredErrChangeEnumList { get; set; } = new List<SampleEnum>(); } public CheckButtonModel() { setCheckList(); } public void OnGet() { BindData.NormalCheckList = new List<string>() { "2" }; BindData.NormalEnumList = new List<SampleEnum>() { SampleEnum.sample3 }; } /// <summary> /// Post処理 /// </summary> /// <returns></returns> public IActionResult OnPost() { return RedirectToPage("CheckButtonResult", new { normalCheckList = BindData.NormalCheckList, requiredCheckList = BindData.RequiredCheckList, requiredErrChangeList = BindData.RequiredErrChangeList, normalEnumList = BindData.NormalEnumList, requiredEnumList = BindData.RequiredEnumList, requiredErrChangeEnumList = BindData.RequiredErrChangeEnumList }); } /// <summary> /// 選択用リストの初期化 /// </summary> void setCheckList() { CheckValues.Add(new KeyValuePair<string, string>("第1項目", "1")); CheckValues.Add(new KeyValuePair<string, string>("第2項目", "2")); CheckValues.Add(new KeyValuePair<string, string>("第3項目", "3")); CheckValues.Add(new KeyValuePair<string, string>("第4項目", "4")); CheckValues.Add(new KeyValuePair<string, string>("最終項目", "5")); } } }VueCheckButton利用サンプルのページを使ったtestcafeのテスト
VueCheckButton.test.tsimport "testcafe"; import {Selector} from "testcafe"; import { ClientFunction } from "testcafe"; fixture("Vue ラジオボタンテスト") .page("https://localhost:44357/CheckButton"); //---------------------------------------------------------------------- // バリデーションを実施しその結果を取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputValidity = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.checkValidity(); }); //---------------------------------------------------------------------- // バリデーションメッセージを取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputInvalidMessage = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.validationMessage; }); //---------------------------------------------------------------------- // バリデーション正常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- async function checkValid(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(true, message + "(バリデーション正常)"); await t.expect(getInputInvalidMessage(itemid)).eql("", message + "(エラーメッセージなし)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); await t.expect(getInputInvalidMessage(itemid)).contains("変更", message + "(バリデーションメッセージ有)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック(メッセージが変更されていないことを確認) // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageNoChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); let elementMessage = getInputInvalidMessage(itemid); await t.expect(elementMessage).notEql(""); await t.expect(elementMessage).notEql(null); await t.expect(elementMessage).notContains("変更", message + "(バリデーションメッセージ有)"); } } async function setDefatltNoErr(t) { await t.click(Selector("#BindData_RequiredCheckList_0")); await t.click(Selector("#BindData_RequiredErrChangeList_0")); await t.click(Selector("#BindData_RequiredEnumList_0")); await t.click(Selector("#BindData_RequiredErrChangeEnumList_0")); } test("初期状態および必須チェック", async (t) => { await t.click(Selector("#mainsubmit")); await checkValid(t, "BindData_NormalCheckList",5); await checkInvalidMessageNoChange(t, "BindData_RequiredCheckList", 5); await checkInvalidMessageChange(t, "BindData_RequiredErrChangeList", 5); await checkValid(t, "BindData_NormalEnumList", 4); await checkInvalidMessageNoChange(t, "BindData_RequiredEnumList", 4); await checkInvalidMessageChange(t, "BindData_RequiredErrChangeEnumList", 4); await setDefatltNoErr(t); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目"); await t.expect(Selector("#RequiredCheckList").textContent).eql("1:第1項目"); await t.expect(Selector("#RequiredErrChangeList").textContent).eql("1:第1項目"); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3"); await t.expect(Selector("#RequiredEnumList").textContent).eql("sample1"); await t.expect(Selector("#RequiredErrChangeEnumList").textContent).eql("sample1"); }); test("通常ラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,1:第1項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("未選択"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,3:第3項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,4:第4項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,5:第5項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_0")); await t.click(Selector("#BindData_NormalCheckList_2")); await t.click(Selector("#BindData_NormalCheckList_3")); await t.click(Selector("#BindData_NormalCheckList_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,1:第1項目,3:第3項目,4:第4項目,5:第5項目"); }); test("Enumラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sample1"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sample2"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("未選択"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sampleOther"); });今後の予定
実はすでに文字列、整数、実数、時刻あたりの入力は作ってみていますので、整理して出していきたいと思っています。以降の入力については基本的にieやEdgeでもChromeのような入力コントロールにすることと、バリデーションのレベルを統一するようにしています。
また、htmlと.Netのデータ型のインピーダンスミスマッチを吸収することも目指してます。
作っていく予定のコントロールは、inputタグのtypeごとに記述すると以下のようになります。
type 概要 サーバーのデータ型 備考 text 文字列 string 最小長のバリデーションが実装されていない物があるので対応。 number 整数 int系の全て 整数の対応。ieやEdgeでもChromeのような入力を目指す。 number 実数 double系の全て 実数の対応。ieやEdgeでもChromeのような入力を目指す。 range 月 datetimeかな これはどうするか思案中。 time 時刻 TimeSpan 時刻を示すデータ型はそもそも存在しないのでTimeSpanを利用して24時間表記で時分秒まで対応する。
ms以下の単位はそもそも整数ででも対応すればよい。date 日付 datetime 日付はdatetime型の「00:00:00」として扱う。 datetime 日時 - 一つの入力として扱うことは疑問。使いにくそうなので作らない。dateとtimeの2つを利用する方が現実的だと思う。 datetime-local 日時 - 上に同じ。 month 月 datetimeかな これはどうするか思案中。 week 週 - これは使うパターンが見えないので作らない。 password パスワード string これは基本的に通常のinputで対応する。 url URL - textのバリデーションとして実装。ただし正規表現を利用した簡易なものにするので、厳密ではない。 メールアドレス - textのバリデーションとして実装。ただし正規表現を利用した簡易なものにするので、厳密ではない。 color 色 stringかなー これはどうするか思案中。webの標準カラーを選択できるようにするかなー? 最後に
今回やたらとソースを張り付けてでかくなってしまいましたが、いずれ全てのソースをgithubにでも公開するつもりです。
ソースをさらしていますので、おかしなところとかアドバイスがあればお願いします。
いろいろな意見を聞くことで勉強になりますので、なんか指摘があればコメントしてもらえるとうれしいです。
- 投稿日:2020-05-20T10:28:08+09:00
ASP.Net CoreのRazorPageでVueのコンポーネントを作る(1.5)
今回の内容
以前作った入力用のコンポーネントのラジオボタンとチェックボタンの修正と拡張をしました。
具体的にはプロパティーなどで不要なものを削除してソースを少しでも小さくする。(どうしても最初は不要なコードが多くなりますねー)
次にvue特有の属性値「v-bind」をタグヘルパーでは利用しない。別に使用しても問題ないのですが、なんとなくこの「v-xxx」についてはhtmlやvueのソース、javaスクリプト内のみで利用する方がいいかなという程度です。配列やオブジェクトをタグヘルパーで設定する場合、そこで「v-bind」を利用すると何も考えずにJSONのデータが利用できるのですが、これをやめるとvueのコード内でいったんJSONを変換する必要があり、ソースは増えてしまうのですが、タグヘルパーで「v-bind:list」とかに設定するのが気持ち悪いのでやめました。
あとは未選択の許容とその際に入力必須をできる用にしました。未選択を許容した場合、ラジオボタンは選択されているものを再度クリックすると未選択状態となります。前提
ASP.NetCore RazorPage+Vue+blumaの環境を利用します。
以前に書いた「Vue.jsを利用してみる(1)」と「Vue.jsを利用してみる(1)」を参照しての環境を構築します。必須バリデーション
必須バリデーションアトリビュートを作ります。
基本クラス(AnnotationBase)
バリデーションの基本クラスです。今後各種入力で使う為の共通の処理などを入れておくために作成しています。
今のところエラーメッセージとhtmlの属性の追加処理を作成しています。AnnotationBase.csusing System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace RazorPageVue.VueAnnotations { /// <summary> /// バリデーション属性用の基本クラス /// </summary> public class AnnotationBase : ValidationAttribute { /// <summary> /// コンストラクタ /// </summary> protected AnnotationBase() { } /// <summary> /// コンストラクタ /// </summary> /// <param name="errorMessageAccessor">エラーメッセージへのアクセサ</param> protected AnnotationBase(Func<string> errorMessageAccessor) : base(errorMessageAccessor) { } /// <summary> /// コンストラクタ /// </summary> /// <param name="errorMessage">エラーメッセージ</param> protected AnnotationBase(string errorMessage) : base(errorMessage) { } /// <summary> /// アトリビュートの設定 /// </summary> /// <param name="attributes">アトリビュート</param> /// <param name="key">追加するキー</param> /// <param name="value">追加する値</param> protected void MergeAttribute(IDictionary<string, string> attributes, string key, string value, bool setForce = false) { if (attributes.ContainsKey(key)) { // 属性が既に設定されている場合 if (string.IsNullOrWhiteSpace(attributes[key])) { // 値が未設定の場合は値の未設定する attributes[key] = value; } else { // 値が設定されており、設定しようとする値と異なる場合は例外を発生させる if (attributes[key] != value) { //throw new ValidationH5Error("タグの属性[" + key + "]に設定されている値[" + attributes[key] + "]が想定[" + value + "]と異なります。"); attributes.Remove("type"); attributes[key] = value; } } } else { // 属性が未設定の場合は、新たな属性を設定する attributes.Add(key, value); } } } }必須バリデーションクラス(RequiredAttribute)
必須バリデーションです。これによりレーザーページでモデルクラスのプロパティに[Required]を付けると必須入力となります。実際の処理はhtml出力時に「required='required'」と「required-err-msg='設定したエラーメッセージ'」が追加されます。エラーメッセージは未設定なら「required-err-msg」は追加されません。
サーバーサイドの処理は記述していますが、基本的にはこの後に記述するvueの中でえらーチェックするので実行されることはないはずです。(ダイレクトにpostを実行された場合の対処です)RequiredAttribute.csusing Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using System; using System.ComponentModel.DataAnnotations; namespace RazorPageVue.VueAnnotations { /// <summary> /// 入力必須属性 /// </summary> [AttributeUsage(AttributeTargets.Property)] public class RequiredAttribute : AnnotationBase, IClientModelValidator { /// <summary> /// コンストラクタ /// </summary> public RequiredAttribute(): base() { } /// <summary> /// バリデーション(サーバーサイド) /// </summary> /// <param name="value">値</param> /// <param name="validationContext">バリデーションコンテキスト</param> /// <returns></returns> protected override ValidationResult IsValid( object value, ValidationContext validationContext) { if (value == null) { return new ValidationResult(GetErrorMessage(validationContext.DisplayName)); } else { if (value.GetType() == typeof(string)) { if (string.IsNullOrWhiteSpace((string)value)) { return new ValidationResult(GetErrorMessage(validationContext.DisplayName)); } else { return ValidationResult.Success; } } else { return ValidationResult.Success; } } } /// <summary> /// クライアントでのバリデーション用の操作 /// </summary> /// <param name="context">クライアントのバリデーションコンテキスト</param> public void AddValidation(ClientModelValidationContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); // 以下のタグ属性を設定する // required "required" // required-err-msg バリデーションで設定されたエラーメッセージ MergeAttribute(context.Attributes, "required", "required"); if (!string.IsNullOrWhiteSpace(ErrorMessage)) MergeAttribute(context.Attributes, "required-err-msg", ErrorMessage); } /// <summary> /// サーバーバリデーション時のエラーメッセージ取得 /// </summary> /// <param name="displayName">表示名称(DisplayNameアトリビュートで変更できる)</param> /// <returns>必須エラーメッセージ</returns> string GetErrorMessage(string displayName) { if (string.IsNullOrEmpty(ErrorMessage)) { return displayName + "は入力必須です。"; } else { return ErrorMessage; } } } }ラジオボタン(改造)
必須バリデーションの対応とかしました。
VueRadioButtonsタグヘルパー(改造)
以前書いたものを改造しました。
一番大きなところは「Process」メソッドで、バリデーションに対応できるようにしています。この部分はinputタグ用のGeneratorを流用しています。
Enumの場合のリストの作成で、null許容型の判定が増えてます。VueRadioButtonsTagHelper.csusing Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Json; using System.Text; namespace RazorPageVue.VueTagHelpers { /// <summary> /// Vueで作ったVueRadioButtonコンポーネントをRazorで利用する為のタグヘルパー /// 以下のタグ属性を利用できるようにする /// asp-for:ラジオボタンの選択値 /// asp-items: ラジオボタンのリスト(KeyValuePair<string, string>のリスト)enumの選択の場合は設定不要 /// selectedclass:選択されているラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// notselectedclass:未選択のラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// [例] /// <vue-radio-buttons asp-for='@Model.SelectedList' asp-items='@Model.KeyValueList' selectedclass="has-background-primary"></radio-buttons> /// ModelのSelectedListは選択されたチェックボタンのリストの初期値を設定し、選択した結果リストが設定される /// Model.eyValueListはチェックボタンのリストをList<KeyValuePair<string,string>>型で設定する。(keyは表示する文字列、valueは設定される値) /// ただし、SelectedListがEnum型の配列の場合、リストを空にしておくと自動的にEnumのリストが設定される。Enumのリストを加工したい場合のみ上記のリストを設定する /// </summary> [HtmlTargetElement("vue-radio-buttons", TagStructure = TagStructure.NormalOrSelfClosing)] public class VueRadioButtonsTagHelper : TagHelper { /// <summary> /// asp-for属性が入るプロパティ /// </summary> public ModelExpression AspFor { get; set; } /// <summary> /// タグ属性「name」を受け取るプロパティ。(未設定で「asp-for」が設定されている場合はその変数を示す値が設定される) /// </summary> public string Name { get; set; } /// <summary> /// ラジオボタンのリスト(asp-items) /// kKeyValuePairのリストで、keyが表示される文字列で、valueが設定される値になる /// </summary> public List<KeyValuePair<string, string>> AspItems { get; set; } /// <summary> /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用 /// (コンストラクタのデータインジェクションで設定) /// </summary> protected IHtmlGenerator Generator { get; } /// <summary> /// コンストラクタ /// </summary> /// <param name="htmlHelper"></param> public VueRadioButtonsTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator) { Generator = generator; } /// <summary> /// htmlヘルパーを利用するために関連付けるビューのコンテキスト /// </summary> [ViewContext] [HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } /// <summary> /// タグの調整処理 /// </summary> /// <param name="context"></param> /// <param name="output"></param> public override void Process(TagHelperContext context, TagHelperOutput output) { // 引数のコンテキストと出力がnullならエラー if (context == null) throw new ArgumentNullException(nameof(context)); if (output == null) throw new ArgumentNullException(nameof(output)); // asp-forがEnum型でリストが未設定の場合はEnumのリストを作成する(Nullableの場合も対処している) if (AspFor.Metadata.ModelType.IsEnum) { if (AspItems == null) { AspItems = new List<KeyValuePair<string, string>>(); foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType)) { AspItems.Add(new KeyValuePair<string, string>(enumItemName, enumItemName)); } } } else if (AspFor.Metadata.ModelType.IsGenericType && AspFor.Metadata.ModelType.GetGenericTypeDefinition() == typeof(Nullable<>)) { if (AspItems == null) { AspItems = new List<KeyValuePair<string, string>>(); foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType.GenericTypeArguments[0])) { AspItems.Add(new KeyValuePair<string, string>(enumItemName, enumItemName)); } } } // 選択リストをJSONに変換して設定(Vueに配列を渡すにはv-bindを利用する必要がある) var serializer = new DataContractJsonSerializer(typeof(List<KeyValuePair<string, string>>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, AspItems); output.Attributes.SetAttribute("list", Encoding.UTF8.GetString(ms.ToArray())); } // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む // この時、すでに設定されているタグの属性は更新できないので注意 output.MergeAttributes(GenerateRadioButton()); } /// <summary> /// タグビルダーを作成 /// </summary> /// <returns>標準のinputタグを利用したタグビルダー</returns> private TagBuilder GenerateRadioButton() { // このタグで設定する属性リストを作成(NameがあってAspForが無い場合にNameが直接書かれていればそれを設定している) IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(AspFor.Name) && string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix)) { if (!string.IsNullOrEmpty(Name)) { htmlAttributes.Add("name", Name); } } else { htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } // inputの基本的なタグビルダーを生成して返す return Generator.GenerateTextBox( ViewContext, AspFor.ModelExplorer, AspFor.Name, AspFor.ModelExplorer.Model, null, htmlAttributes); } } }VueRadioButtonsコンポーネント(改造)
vueのコンポーネントです。この後の事を考えてvue-というプレフィックスを付けました。
必須バリデーションの対応をしています。その関係で、選択されている項目をもう一度クリックすると完全に選択外になるようにしました。
idやnameを以前はhiddenidやhiddennameとしていましたが、意味がないので普通にしました。最初に書いたように、タグヘルパーで「v-bind:」の記述をやめましたので、選択リストはdataのselectListにJSONを変換して取り込んでいます。v-bindを使うとプロパティの型と等を自動的に判定して行ってくれるようです。
クリック処理でいろいろ行っているのですが、キーボード操作でも同じ様に動いてくれていますので、キーボードのイベントは記述してません。おかげで助かった。vueRadioButtons.js//====================================================================== // ラジオボタンタグ // ラジオボタンをグループで設定し、選択されている一つのデータを返す // VueRadioButtonsTagHelperと連携して利用する // 使用方法は「VueRadioButtonsTagHelper」を参照 //====================================================================== Vue.component('vue-radio-buttons', { props: { id: String, // このコントロールのid。Razor Pageとasp-forで関連付けされている name: String, // このコントロールのname。Razor Pageとasp-forで関連付けされている list: String, // 選択リスト(key,value)のペアのリスト(JSONデータ)値がenum型の場合は列挙の配列で未設定ならすべての列挙 value: String, // 選択されている初期値(key)または列挙 selectedclass: String, // 選択されているアイテムのcssクラス notselectedclass: String, // 選択されていないアイテムのcssクラス required: Boolean, // 必須フラグ requiredErrMsg: String // 必須エラーメッセージ }, data: function () { return { selectList: JSON.parse(this.list), // 選択リスト selectedValue: this.value, // 選択値 groupName: "items_" + this.name // 選択ラジオボタンのグループ名 }; }, mounted: function () { this.checkRequired(); // マウント時に必須チェックを行う }, methods: { //------------------------------------------------------------ // 選択リストのid作成 //------------------------------------------------------------ itemid: function (index) { return this.id + '_' + index; }, //------------------------------------------------------------ // 選択操作。選択アイテムをクリックした時に実行 //------------------------------------------------------------ selectAction: function (e) { if (this.selectedValue === e.target.getAttribute("value")) { // 同じものを選択した場合は[cannoselect]が有効な場合は未選択状態にする(valueにnullが可能な場合のみ) if (!this.required) { this.selectedValue = ""; } } else { // 選択値を変更する this.selectedValue = e.target.getAttribute("value"); } this.checkRequired(); }, //------------------------------------------------------------ // その値が選択されているかどうか(自身が選択状態かどうかの判定用) //------------------------------------------------------------ isSelected: function (val) { return val === this.selectedValue; }, //------------------------------------------------------------ // 必須チェック //------------------------------------------------------------ checkRequired: function () { if (this.required && this.selectedValue === "") { // 変更エラーメッセージを取得 if (this.requiredErrMsg) { // 変更エラーメッセージがある場合は変更エラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(this.requiredErrMsg); } } else { // 変更エラーメッセージが無ければデフォルトのエラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity("どれか一つ入力してください。"); } } } else { // エラーが無いのでカスタムエラーを削除 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(""); } } } }, template: '<div>\ <input :id=id :name=name type="hidden" :value=selectedValue>\ <span v-for="(item, index) in selectList" :key="item.value">\ <span style="display:inline-block" :class="[isSelected(item.value) ? selectedclass : notselectedclass ]" :value=item.value>\ <input type="radio" ref="Items" :id=itemid(index) :name=groupName :checked=isSelected(item.value) v-on:click=selectAction :value=item.value >\ <label v-on:click=selectAction :value=item.value>{{ item.key }}</label>\ </span>\ </span>\ </div>' });チェックボタン(改造)
必須バリデーションの対応とかしました。
VueCheckButtonsタグヘルパー(改造)
以前書いたものを改造しました。
一番大きなところは「Process」メソッドで、バリデーションに対応できるようにしています。この部分はinputタグ用のGeneratorを流用しています。VueCheckButtonsTagHelper.csusing Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Json; using System.Text; namespace RazorPageVue.VueTagHelpers { /// <summary> /// Vueで作ったVueCheckButtonコンポーネントをRazorで利用する為のタグヘルパー /// 以下のタグ属性を利用できるようにする /// asp-for:チェックボタンの選択リスト /// asp-items: チェックボタンのリスト(KeyValuePair<string, string>のリスト)enumの選択の場合は設定不要 /// selectedclass:選択されているラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// notselectedclass:未選択のラジオボタンのCSSクラス(ボタンとテキストをくくったspanタグ) /// [例] /// <vue-check-buttons asp-for='@Model.RadioSelected' asp-items='@Model.RadioKeyValueList' selectedclass="has-background-primary"></radio-buttons> /// ModelのRadioSelectedは選択されたラジオボタンの値が設定される /// Model.RadioKeyValueListはラジオボタンのリストをList<KeyValuePair<string,string>>型で設定する。(keyは表示する文字列、valueは設定される値) /// ただし、RadioSelectedがEnum型の場合、リストを空にしておくと自動的にEnumのリストが設定される。Enumのリストを加工したい場合のみ上記のリストを設定する /// なお、Enumの場合、Nullable(null許容型)にしていないと、Required未設定でも結果が空にはならないので注意 /// </summary> [HtmlTargetElement("vue-check-buttons", TagStructure = TagStructure.NormalOrSelfClosing)] public class VueCheckButtonsTagHelper : TagHelper { /// <summary> /// asp-for属性が入るプロパティ /// </summary> public ModelExpression AspFor { get; set; } /// <summary> /// タグ属性「name」を受け取るプロパティ。(未設定で「asp-for」が設定されている場合はその変数を示す値が設定される) /// </summary> public string Name { get; set; } /// <summary> /// ラジオボタンのリスト(asp-items) /// kKeyValuePairのリストで、keyが表示される文字列で、valueが設定される値になる /// </summary> public List<KeyValuePair<string, string>> AspItems { get; set; } /// <summary> /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用 /// (コンストラクタのデータインジェクションで設定) /// </summary> protected IHtmlGenerator Generator { get; } /// <summary> /// htmlヘルパーを利用するために関連付けるビューのコンテキスト /// </summary> [ViewContext] [HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="htmlHelper"></param> public VueCheckButtonsTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator) { Generator = generator; } /// <summary> /// タグの調整処理 /// </summary> /// <param name="context"></param> /// <param name="output"></param> public override void Process(TagHelperContext context, TagHelperOutput output) { // 引数のコンテキストと出力がnullならエラー if (context == null) throw new ArgumentNullException(nameof(context)); if (output == null) throw new ArgumentNullException(nameof(output)); //// RazorPageでPost時にモデルにバインドできるように、Vueのid,name,valueプロパティを設定 //output.Attributes.SetAttribute("name", AspFor.Name); // asp-forがEnum型でリストが未設定の場合はEnumのリストを作成する if (AspFor.Metadata.ModelType.GenericTypeArguments[0].IsEnum) { if (AspItems == null) { AspItems = new List<KeyValuePair<string, string>>(); foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType.GenericTypeArguments[0])) { AspItems.Add(new KeyValuePair<string, string>(enumItemName, ((int)Enum.Parse(AspFor.Metadata.ModelType.GenericTypeArguments[0],enumItemName)).ToString())); } } } // 選択リストをJSONに変換して設定 var serializer = new DataContractJsonSerializer(typeof(List<KeyValuePair<string, string>>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, AspItems); output.Attributes.SetAttribute("list", Encoding.UTF8.GetString(ms.ToArray())); } // 選択結果リストを設定 var jsonValueList = ""; if (AspFor.Metadata.ModelType.GenericTypeArguments[0].IsEnum) { // Enumの場合は一旦文字列リストに変換してからJSON形式にして設定する var workList = new List<string>(); foreach (var item in (System.Collections.IEnumerable)AspFor.Model) workList.Add(((int)item).ToString()); serializer = new DataContractJsonSerializer(typeof(List<string>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, workList); jsonValueList = Encoding.UTF8.GetString(ms.ToArray()); //output.Attributes.SetAttribute("v-bind:value", Encoding.UTF8.GetString(ms.ToArray())); } } else { // Enum以外の場合は文字列リストをJSON形式にして設定する serializer = new DataContractJsonSerializer(typeof(List<string>)); using (MemoryStream ms = new MemoryStream()) { serializer.WriteObject(ms, AspFor.Model); jsonValueList = Encoding.UTF8.GetString(ms.ToArray()); //output.Attributes.SetAttribute("v-bind:value", Encoding.UTF8.GetString(ms.ToArray())); } } //base.Process(context, output); // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む // この時、すでに設定されているタグの属性は更新できないので注意 output.MergeAttributes(GenerateRadioButton(jsonValueList)); } /// <summary> /// タグビルダーを作成 /// </summary> /// <returns>標準のinputタグを利用したタグビルダー</returns> private TagBuilder GenerateRadioButton(string value) { // このタグで設定する属性リストを作成(NameがあってAspForが無い場合にNameが直接書かれていればそれを設定している) IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(AspFor.Name) && string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix)) { if (!string.IsNullOrEmpty(Name)) { htmlAttributes.Add("name", Name); } } else { htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } // inputの基本的なタグビルダーを生成して返す return Generator.GenerateTextBox( ViewContext, AspFor.ModelExplorer, AspFor.Name, //AspFor.ModelExplorer.Model, value, null, htmlAttributes); } } }VueCheckButtonsコンポーネント(改造)
ラジオボタンと同じように変更しています。
vueCheckButtons.js//====================================================================== // チェックボタンタグ // チェックボタンをグループで設定し、選択されている一つのデータを返す // VueCheckButtonsTagHelperと連携して利用する // 使用方法は「VueCheckButtonsTagHelper」を参照 //====================================================================== Vue.component('vue-check-buttons', { props: { id: String, // このコントロールのid。Razor Pageとasp-forで関連付けされている name: String, // このコントロールのname。Razor Pageとasp-forで関連付けされている list: String, // 選択リスト(key,value)のペアのリスト(JSONデータ)値がenum型の場合は列挙の配列で未設定ならすべての列挙 value: String, // 選択されている項目のvalueの配列(JSONデータ)値が列挙の場合は列挙の配列 selectedclass: String, // 選択されているチェックボックスのcssクラス notselectedclass: String, // 選択されていないチェックボックスのcssクラス required: Boolean, // 必須フラグ requiredErrMsg: String // 必須エラーメッセージ }, data: function () { return { selectlist: JSON.parse(this.list), // 選択リスト selectedValues: JSON.parse(this.value), // 選択されている値のリスト groupName: "items_" + this.name // 選択チェックボックスのグループ名 }; }, mounted: function () { this.checkRequired(); // マウント時に必須チェックを行う }, methods: { //------------------------------------------------------------ // 選択リストのid作成 //------------------------------------------------------------ itemid: function (index) { return this.id + '_' + index; }, //------------------------------------------------------------ // 選択操作。選択アイテムをクリックした時に実行 //------------------------------------------------------------ selectAction: function (e) { var val = e.target.getAttribute("value"); var exitIndex = this.selectedValues.indexOf(val); if (exitIndex >= 0) { this.selectedValues.splice(exitIndex, 1); } else { this.selectedValues.push(val); } this.checkRequired(); }, //------------------------------------------------------------ // その値が選択されているかどうか(自身が選択状態かどうかの判定用) //------------------------------------------------------------ isSelected: function (val) { return this.selectedValues.indexOf(val) >= 0; }, //------------------------------------------------------------ // 必須チェック //------------------------------------------------------------ checkRequired: function () { if (this.required && this.selectedValues.length === 0) { // 変更エラーメッセージを取得 if (this.requiredErrMsg) { // 変更エラーメッセージがある場合は変更エラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(this.requiredErrMsg); } } else { // 変更エラーメッセージが無ければデフォルトのエラーメッセージを設定 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity("どれか一つ入力してください。"); } } } else { // エラーが無いのでカスタムエラーを削除 for (let i = 0; i < this.$refs.Items.length; i++) { this.$refs.Items[i].setCustomValidity(""); } } } }, template: '<div :id=id>\ <input :name=name v-for="selected in selectedValues" :key=selected type="hidden" :value=selected>\ <span v-for="(item, index) in selectlist" :key="item.key">\ <span style="display:inline-block" :class="[isSelected(item.value) ? selectedclass : notselectedclass ]" :value=item.value>\ <input type="checkbox" ref="Items" :id=itemid(index) :name="groupName" :checked=isSelected(item.value) v-on:click=selectAction :value=item.value >\ <label v-on:click=selectAction :value=item.value>{{ item.key }}</label>\ </span>\ </span>\ </div>' });参考ページとテスト
以下は利用サンプルのページとそのテスト用のtescafeです。ちょっと適当ですがご容赦ください。
VueRadioButton利用サンプルのページ
RadioButton.cshtml@page @model RadioButtonModel @{ ViewData["Title"] = "RadioButton Check"; } @addTagHelper *,RazorPageVue @*利用するVueコンポーネントをここで取り込む*@ <script src="~/js/vueUtil.js" asp-append-version="true"></script> <script src="~/compornents/vueRadioButtons.js" asp-append-version="true"></script> <form method="post"> <div id="components-demo"> <h1 class="display-4">ラジオボタンチェック</h1> <div class="content box"> <label>通常のラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.RadioSelected' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須のラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.RadioRequiredSelected' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須(メッセージ変更)のラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.RadioRequiredSelectedErrChange' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>Enumのラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.EnumRadioSelected' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須のEnumのラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.EnumRequiredRadioSelected' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <div class="content box"> <label>入力必須(メッセージ変更)のEnumのラジオボタン</label> <vue-radio-buttons asp-for='@Model.BindData.EnumRequiredRadioSelectedErrChange' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons> </div> <input id="mainsubmit" type="submit" /> </div> <a id="returnLink" href="~/">戻る</a> </form> <script> // Vueコンポーネントを展開させる CreateVue('#components-demo'); </script>RadioButton.cshtml.csusing Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using RazorPageVue.VueAnnotations; using System.Collections.Generic; namespace RazorPageVue.Pages { public class RadioButtonModel : PageModel { /// <summary> /// ラジオボタンの選択リスト /// </summary> static public List<KeyValuePair<string, string>> RadioValues { get { if (_radioValues == null) { _radioValues = new List<KeyValuePair<string, string>>(); _radioValues.Add(new KeyValuePair<string, string>("第1項目", "1")); _radioValues.Add(new KeyValuePair<string, string>("第2項目", "2")); _radioValues.Add(new KeyValuePair<string, string>("第3項目", "3")); _radioValues.Add(new KeyValuePair<string, string>("第4項目", "4")); _radioValues.Add(new KeyValuePair<string, string>("最終項目", "5")); } return _radioValues; } } static List<KeyValuePair<string, string>> _radioValues = null; /// <summary> /// 結合データ /// </summary> [BindProperty] public InputModel BindData { get; set; } = new InputModel(); public class InputModel { // ラジオボタンの選択結果 public string RadioSelected { get; set; } = "2"; public string RadioSelectedText { get { foreach (var item in RadioValues) { if (item.Value == RadioSelected) return item.Key; } return ""; } } [Required] public string RadioRequiredSelected { get; set; } [Required(ErrorMessage ="必須変更")] public string RadioRequiredSelectedErrChange { get; set; } public string RadioRequiredSelectedText { get { foreach (var item in RadioValues) { if (item.Value == RadioRequiredSelected) return item.Key; } return ""; } } // Enumを利用したラジオボタンの選択結果 public SampleEnum? EnumRadioSelected { get; set; } = SampleEnum.sample2; // Enumを利用したラジオボタンの選択結果 [Required] public SampleEnum? EnumRequiredRadioSelected { get; set; } = null; [Required(ErrorMessage = "必須変更")] public SampleEnum? EnumRequiredRadioSelectedErrChange { get; set; } } public void OnGet() { } /// <summary> /// Post処理 /// </summary> /// <returns></returns> public IActionResult OnPost() { return RedirectToPage("RadioButtonResult", new { SelectedId = BindData.RadioSelected, SelectedName = BindData.RadioSelectedText, RequiredSelectedId = BindData.RadioRequiredSelected, RequiredSelectedName = BindData.RadioRequiredSelectedText, SelectedEnum = BindData.EnumRadioSelected, RequiredSelectedEnum = BindData.EnumRequiredRadioSelected }); } } }VueRadioButton利用サンプルのページを使ったtestcafeのテスト
VueRadioButton.test.tsimport "testcafe"; import {Selector} from "testcafe"; import { ClientFunction } from "testcafe"; fixture("Vue ラジオボタンテスト") .page("https://localhost:44357/RadioButton"); //---------------------------------------------------------------------- // バリデーションを実施しその結果を取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputValidity = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.checkValidity(); }); //---------------------------------------------------------------------- // バリデーションメッセージを取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputInvalidMessage = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.validationMessage; }); //---------------------------------------------------------------------- // バリデーション正常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- async function checkValid(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(true, message + "(バリデーション正常)"); await t.expect(getInputInvalidMessage(itemid)).eql("", message + "(エラーメッセージなし)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); await t.expect(getInputInvalidMessage(itemid)).contains("変更", message + "(バリデーションメッセージ有)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック(メッセージが変更されていないことを確認) // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageNoChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); let elementMessage = getInputInvalidMessage(itemid); await t.expect(elementMessage).notEql(""); await t.expect(elementMessage).notEql(null); await t.expect(elementMessage).notContains("変更", message + "(バリデーションメッセージ有)"); } } async function setDefatltNoErr(t) { await t.click(Selector("#BindData_RadioRequiredSelected_0")); await t.click(Selector("#BindData_RadioRequiredSelectedErrChange_0")); await t.click(Selector("#BindData_EnumRequiredRadioSelected_0")); await t.click(Selector("#BindData_EnumRequiredRadioSelectedErrChange_0")); } test("初期状態チェック", async (t) => { await t.click(Selector("#mainsubmit")); await checkValid(t, "BindData_RadioSelected",5); await checkInvalidMessageNoChange(t, "BindData_RadioRequiredSelected", 5); await checkInvalidMessageChange(t, "BindData_RadioRequiredSelectedErrChange", 5); await checkValid(t, "BindData_EnumRadioSelected", 4); await checkInvalidMessageNoChange(t, "BindData_EnumRequiredRadioSelected", 4); await checkInvalidMessageChange(t, "BindData_EnumRequiredRadioSelectedErrChange", 4); await setDefatltNoErr(t); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("2", "【正常】[通常ラジオボタン]初期選択(値=2)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第2項目", "【正常】[通常ラジオボタン]初期選択(テキスト=第2項目)"); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]初期選択(値=1)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]初期選択(テキスト=第1項目)"); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample2", "【正常】[enumラジオボタン]初期選択(値=sample2)"); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]初期選択(値=sample1)"); }); test("通常ラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]第1項目選択(値 = 1)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]第1項目選択(テキスト=第1項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("", "【正常】[通常ラジオボタン]第2項目選択(値 = )"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("", "【正常】[通常ラジオボタン]第2項目選択(テキスト=第2項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("3", "【正常】[通常ラジオボタン]第3項目選択(値 = 3)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第3項目", "【正常】[通常ラジオボタン]第3項目選択(テキスト=第3項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("4", "【正常】[通常ラジオボタン]第4項目選択(値 = 4)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第4項目", "【正常】[通常ラジオボタン]第4項目選択(テキスト=第4項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioSelected_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("5", "【正常】[通常ラジオボタン]第5項目選択(値 = 5)"); await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("最終項目", "【正常】[通常ラジオボタン]第5項目選択(テキスト=第5項目)"); }); test("必須ラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]第1項目選択(値 = 1)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]第1項目選択(テキスト=第1項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("2", "【正常】[通常ラジオボタン]第2項目選択(値 = 2)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第2項目", "【正常】[通常ラジオボタン]第2項目選択(テキスト=第2項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("3", "【正常】[通常ラジオボタン]第3項目選択(値 = 3)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第3項目", "【正常】[通常ラジオボタン]第3項目選択(テキスト=第3項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("4", "【正常】[通常ラジオボタン]第4項目選択(値 = 4)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第4項目", "【正常】[通常ラジオボタン]第4項目選択(テキスト=第4項目)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_RadioRequiredSelected_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("5", "【正常】[通常ラジオボタン]第5項目選択(値 = 5)"); await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("最終項目", "【正常】[通常ラジオボタン]第5項目選択(テキスト=第5項目)"); await t.click(Selector("#returnLink")); }); test("Enumラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]第1項目選択(値=sample1)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("", "【正常】[enumラジオボタン]第2項目選択(値=)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample3", "【正常】[enumラジオボタン]第3項目選択(値=sample3)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRadioSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sampleOther", "【正常】[enumラジオボタン]第4項目選択(値=sampleOther)"); }); test("必須Enumラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]第1項目選択(値=sample1)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample2", "【正常】[enumラジオボタン]第2項目選択(値=sample2)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample3", "【正常】[enumラジオボタン]第3項目選択(値=sample3)"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_EnumRequiredRadioSelected_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sampleOther", "【正常】[enumラジオボタン]第4項目選択(値=sampleOther)"); });VueCheckButton利用サンプルのページ
CheckButton.cshtml@page @model CheckButtonModel @{ ViewData["Title"] = "RadioButton Check"; } @addTagHelper *,RazorPageVue @*利用するVueコンポーネントをここで取り込む*@ <script src="~/js/vueUtil.js" asp-append-version="true"></script> <script src="~/compornents/vueCheckButtons.js" asp-append-version="true"></script> <form method="post"> <div id="components-demo"> <h1 class="display-4">ラジオボタンチェック</h1> <div class="content box"> <label>通常のチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.NormalCheckList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons> </div> <div class="content box"> <label>必須のチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredCheckList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons> </div> <div class="content box"> <label>必須でエラーメッセージ変更のチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredErrChangeList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons> </div> <div class="content box"> <label>Enumのチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.NormalEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons> </div> <div class="content box"> <label>必須のEnumのチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons> </div> <div class="content box"> <label>必須でエラーメッセージ変更のEnumのチェックボックス</label> <vue-check-buttons asp-for='@Model.BindData.RequiredErrChangeEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons> </div> <input id="mainsubmit" type="submit" /> </div> <a id="returnLink" href="~/">戻る</a> </form> <script> // Vueコンポーネントを展開させる CreateVue('#components-demo'); </script>CheckButton.cshtml.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using RazorPageVue.VueAnnotations; namespace RazorPageVue.Pages { public class CheckButtonModel : PageModel { /// <summary> /// ラジオボタンの選択リスト /// </summary> public List<KeyValuePair<string, string>> CheckValues { get; set; } = new List<KeyValuePair<string, string>>(); /// <summary> /// 結合データ /// </summary> [BindProperty] public InputModel BindData { get; set; } = new InputModel(); public class InputModel { /// <summary> /// 通常のチェックボックスの選択結果 /// </summary> public List<string> NormalCheckList { get; set; } = new List<string>(); /// <summary> /// 入力必須のチェックボックスの選択結果 /// </summary> [Required] public List<string> RequiredCheckList { get; set; } = new List<string>(); /// <summary> /// 入力必須のエラーメッセージ変更チェックボックスの選択結果 /// </summary> [Required(ErrorMessage = "必須変更")] public List<string> RequiredErrChangeList { get; set; } = new List<string>(); /// <summary> /// Enumタイプのチェックボックスの選択 /// </summary> public List<SampleEnum> NormalEnumList { get; set; } = new List<SampleEnum>(); /// <summary> /// 入力必須のEnumタイプのチェックボックスの選択 /// </summary> [Required] public List<SampleEnum> RequiredEnumList { get; set; } = new List<SampleEnum>(); /// <summary> /// 入力必須のエラーメッセージ変更Enumタイプのチェックボックスの選択 /// </summary> [Required(ErrorMessage = "必須変更")] public List<SampleEnum> RequiredErrChangeEnumList { get; set; } = new List<SampleEnum>(); } public CheckButtonModel() { setCheckList(); } public void OnGet() { BindData.NormalCheckList = new List<string>() { "2" }; BindData.NormalEnumList = new List<SampleEnum>() { SampleEnum.sample3 }; } /// <summary> /// Post処理 /// </summary> /// <returns></returns> public IActionResult OnPost() { return RedirectToPage("CheckButtonResult", new { normalCheckList = BindData.NormalCheckList, requiredCheckList = BindData.RequiredCheckList, requiredErrChangeList = BindData.RequiredErrChangeList, normalEnumList = BindData.NormalEnumList, requiredEnumList = BindData.RequiredEnumList, requiredErrChangeEnumList = BindData.RequiredErrChangeEnumList }); } /// <summary> /// 選択用リストの初期化 /// </summary> void setCheckList() { CheckValues.Add(new KeyValuePair<string, string>("第1項目", "1")); CheckValues.Add(new KeyValuePair<string, string>("第2項目", "2")); CheckValues.Add(new KeyValuePair<string, string>("第3項目", "3")); CheckValues.Add(new KeyValuePair<string, string>("第4項目", "4")); CheckValues.Add(new KeyValuePair<string, string>("最終項目", "5")); } } }VueCheckButton利用サンプルのページを使ったtestcafeのテスト
VueCheckButton.test.tsimport "testcafe"; import {Selector} from "testcafe"; import { ClientFunction } from "testcafe"; fixture("Vue ラジオボタンテスト") .page("https://localhost:44357/CheckButton"); //---------------------------------------------------------------------- // バリデーションを実施しその結果を取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputValidity = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.checkValidity(); }); //---------------------------------------------------------------------- // バリデーションメッセージを取得する // 【変数】 // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- const getInputInvalidMessage = ClientFunction((id) => { var element = document.getElementById(id) as HTMLInputElement; return element.validationMessage; }); //---------------------------------------------------------------------- // バリデーション正常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 //---------------------------------------------------------------------- async function checkValid(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(true, message + "(バリデーション正常)"); await t.expect(getInputInvalidMessage(itemid)).eql("", message + "(エラーメッセージなし)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); await t.expect(getInputInvalidMessage(itemid)).contains("変更", message + "(バリデーションメッセージ有)"); } } //---------------------------------------------------------------------- // バリデーション異常チェック(メッセージが変更されていないことを確認) // 【変数】 // t: TestCafeの基本オブジェクト // id: 対象のinputタグのid属性 // messsage: メッセージ //---------------------------------------------------------------------- async function checkInvalidMessageNoChange(t, id, count, message = '') { for (let i = 0; i < count; i++) { let itemid = id + '_' + i; await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)"); let elementMessage = getInputInvalidMessage(itemid); await t.expect(elementMessage).notEql(""); await t.expect(elementMessage).notEql(null); await t.expect(elementMessage).notContains("変更", message + "(バリデーションメッセージ有)"); } } async function setDefatltNoErr(t) { await t.click(Selector("#BindData_RequiredCheckList_0")); await t.click(Selector("#BindData_RequiredErrChangeList_0")); await t.click(Selector("#BindData_RequiredEnumList_0")); await t.click(Selector("#BindData_RequiredErrChangeEnumList_0")); } test("初期状態および必須チェック", async (t) => { await t.click(Selector("#mainsubmit")); await checkValid(t, "BindData_NormalCheckList",5); await checkInvalidMessageNoChange(t, "BindData_RequiredCheckList", 5); await checkInvalidMessageChange(t, "BindData_RequiredErrChangeList", 5); await checkValid(t, "BindData_NormalEnumList", 4); await checkInvalidMessageNoChange(t, "BindData_RequiredEnumList", 4); await checkInvalidMessageChange(t, "BindData_RequiredErrChangeEnumList", 4); await setDefatltNoErr(t); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目"); await t.expect(Selector("#RequiredCheckList").textContent).eql("1:第1項目"); await t.expect(Selector("#RequiredErrChangeList").textContent).eql("1:第1項目"); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3"); await t.expect(Selector("#RequiredEnumList").textContent).eql("sample1"); await t.expect(Selector("#RequiredErrChangeEnumList").textContent).eql("sample1"); }); test("通常ラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,1:第1項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("未選択"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,3:第3項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,4:第4項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,5:第5項目"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalCheckList_0")); await t.click(Selector("#BindData_NormalCheckList_2")); await t.click(Selector("#BindData_NormalCheckList_3")); await t.click(Selector("#BindData_NormalCheckList_4")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,1:第1項目,3:第3項目,4:第4項目,5:第5項目"); }); test("Enumラジオボタンチェック", async (t) => { await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_0")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sample1"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_1")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sample2"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_2")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("未選択"); await t.click(Selector("#returnLink")); await setDefatltNoErr(t); await t.click(Selector("#BindData_NormalEnumList_3")); await t.click(Selector("#mainsubmit")); await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sampleOther"); });今後の予定
実はすでに文字列、整数、実数、時刻あたりの入力は作ってみていますので、整理して出していきたいと思っています。以降の入力については基本的にieやEdgeでもChromeのような入力コントロールにすることと、バリデーションのレベルを統一するようにしています。
また、htmlと.Netのデータ型のインピーダンスミスマッチを吸収することも目指してます。
作っていく予定のコントロールは、inputタグのtypeごとに記述すると以下のようになります。
type 概要 サーバーのデータ型 備考 text 文字列 string 最小長のバリデーションが実装されていない物があるので対応。 number 整数 int系の全て 整数の対応。ieやEdgeでもChromeのような入力を目指す。 number 実数 double系の全て 実数の対応。ieやEdgeでもChromeのような入力を目指す。 range 月 datetimeかな これはどうするか思案中。 time 時刻 TimeSpan 時刻を示すデータ型はそもそも存在しないのでTimeSpanを利用して24時間表記で時分秒まで対応する。
ms以下の単位はそもそも整数ででも対応すればよい。date 日付 datetime 日付はdatetime型の「00:00:00」として扱う。 datetime 日時 - 一つの入力として扱うことは疑問。使いにくそうなので作らない。dateとtimeの2つを利用する方が現実的だと思う。 datetime-local 日時 - 上に同じ。 month 月 datetimeかな これはどうするか思案中。 week 週 - これは使うパターンが見えないので作らない。 password パスワード string これは基本的に通常のinputで対応する。 url URL - textのバリデーションとして実装。ただし正規表現を利用した簡易なものにするので、厳密ではない。 メールアドレス - textのバリデーションとして実装。ただし正規表現を利用した簡易なものにするので、厳密ではない。 color 色 stringかなー これはどうするか思案中。webの標準カラーを選択できるようにするかなー? 最後に
今回やたらとソースを張り付けてでかくなってしまいましたが、いずれ全てのソースをgithubにでも公開するつもりです。
ソースをさらしていますので、おかしなところとかアドバイスがあればお願いします。
いろいろな意見を聞くことで勉強になりますので、なんか指摘があればコメントしてもらえるとうれしいです。
- 投稿日:2020-05-20T04:41:58+09:00
vue.jsとlocalStrageで閲覧履歴とお気に入り履歴を作ってみた
やったこと
ユーザの閲覧履歴やお気に入り履歴をlocal Strageに溜め込んでブラウザ側だけで履歴情報を表示するページを作りました。
中の処理ではVue.jsを使っています。使っているもの / できること
- 犬の画像をランダムで取得するAPI
- 直近5枚の見た画像の履歴を表示
- お気に入りへの追加、およびその削除
動いているページ
http://shima-07.ml/ソースコード
<html> <head> <title>Hello My WebSite!</title> <style> img.pic1 { width: 50%; height: auto; } </style> <style> img.pic2 { width: 96px; height: 65px; } </style> </head> <body> <div id= "app"> <a v-bind:href="src" target="_blank"> <img v-bind:src="src" class="pic1"/> </a> <p><button v-on:click="getData()">次へ</button></p> <p> <button v-if="good" v-on:click="delfavo()">お気に入りから削除する</button> <button v-else v-on:click="favo()">お気に入りに追加する</button> </p> <p>最大5件まで過去閲覧画像を表示</p> <!-- 画像URLが存在するときのみ表示する--> <a v-bind:href="his_1" target="_blank"> <img v-if="his_1" v-bind:src="his_1" class="pic2"/> </a> <a v-bind:href="his_2" target="_blank"> <img v-if="his_2" v-bind:src="his_2" class="pic2"/> </a> <a v-bind:href="his_3" target="_blank" > <img v-if="his_3" v-bind:src="his_3" class="pic2"/> </a> <a v-bind:href="his_4" target="_blank" > <img v-if="his_4" v-bind:src="his_4" class="pic2"/> </a> <a v-bind:href="his_5" target="_blank" > <img v-if="his_5" v-bind:src="his_5" class="pic2"/> </a> <p>お気に入りのわんちゃんを表示</p> <!-- 画像URLが存在するときのみ表示する--> <a v-bind:href="fav_1" target="_blank"> <img v-if="fav_1" v-bind:src="fav_1" class="pic2"/> </a> <a v-bind:href="fav_2" target="_blank"> <img v-if="fav_2" v-bind:src="fav_2" class="pic2"/> </a> <a v-bind:href="fav_3" target="_blank" > <img v-if="fav_3" v-bind:src="fav_3" class="pic2"/> </a> <a v-bind:href="fav_4" target="_blank" > <img v-if="fav_4" v-bind:src="fav_4" class="pic2"/> </a> <a v-bind:href="fav_5" target="_blank" > <img v-if="fav_5" v-bind:src="fav_5" class="pic2"/> </a> <a v-bind:href="fav_6" target="_blank"> <img v-if="fav_6" v-bind:src="fav_6" class="pic2"/> </a> <a v-bind:href="fav_7" target="_blank"> <img v-if="fav_7" v-bind:src="fav_7" class="pic2"/> </a> <a v-bind:href="fav_8" target="_blank" > <img v-if="fav_8" v-bind:src="fav_8" class="pic2"/> </a> <a v-bind:href="fav_9" target="_blank" > <img v-if="fav_9" v-bind:src="fav_9" class="pic2"/> </a> <a v-bind:href="fav_10" target="_blank" > <img v-if="fav_10" v-bind:src="fav_10" class="pic2"/> </a> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> var favlist =[]; var u0,u1,u2,u3,u4,u5,u6; var strage = []; var s = localStorage.getItem('imgs'); /// お気に入りようの配列/// if(localStorage.getItem('fav')){ // JSON.parse(data) の形で取り出す必要がある。 // ocalStorage.getItem('imgs').lengthにすると文字の長さになってしまうからダメ。JSON.parse()する for(let i = 0 ; i < JSON.parse(localStorage.getItem('fav')).length -1 ; i++){ favlist.push(JSON.parse(localStorage.getItem('fav'))[i]); } } /// 過去画像ようの配列//// if(localStorage.getItem('imgs')){ // JSON.parse(data) の形で取り出す必要がある。 // ocalStorage.getItem('imgs').lengthにすると文字の長さになってしまうからダメ。JSON.parse()する for(let k = 0 ; k < JSON.parse(localStorage.getItem('imgs')).length -1 ; k++){ strage.push(JSON.parse(localStorage.getItem('imgs'))[k]); } } ////////// メイン処理 ////////// const app = new Vue({ el: '#app', data: { src:'' , his_1: '', his_2: '', his_3: '', his_4: '', his_5: '', good: false, fav_1: '', fav_2: '', fav_3: '', fav_4: '', fav_5: '', fav_6: '', fav_7: '', fav_8: '', fav_9: '', fav_10: '' }, /// 過去の履歴を出すところ methods: { getData: async function(){ const URL = 'https://dog.ceo/api/breeds/image/random'; const response = await axios.get(URL); this.message = response.data; this.src = response.data.message; // local strageにため込む処理 strage.unshift({url:this.src}); //先頭に追加 // 5こ以上は消す strage = strage.slice(0,6); localStorage.removeItem('imgs'); //imgsだけ消す localStorage.setItem('imgs',JSON.stringify(strage)); // JSON.stringify(data) の形が需要。 console.log(strage); u0 = this.src; // 過去見たものの表示をする if(localStorage.getItem('imgs')){ // エラーを防ぐ為に、過去履歴が存在するときだけ、その分だけ表示する for(let j = 0 ; j < JSON.parse(localStorage.getItem('imgs')).length ; j++){ eval("this.his_"+ j + "= JSON.parse(localStorage.getItem('imgs'))[" + j + "].url"); // 普通に this.his_j = JSON.parse(localStorage.getItem('imgs'))[j].url;とは書けない } } console.log(this.his_1); this.good = false; // 画像が変わったらボタンを変える }, ///お気に入り登録する機能 favo: function(){ this.url = u0; favlist.unshift({url:this.url}); localStorage.setItem('fav',JSON.stringify(favlist)); this.good = true ; console.log(favlist); console.log(this.good); ///表示する if(localStorage.getItem('fav')){ // 存在確認とあまりにお気に入りが多い場合は10個にする var len1 = JSON.parse(localStorage.getItem('fav')).length; if(len1 > 10){ len1 = 10; } // エラーを防ぐ為に、過去履歴が存在するときだけ、その分だけ表示する for(let a = 0 ; a < len1 ; a++){ eval("this.fav_"+ (a+1) + "= JSON.parse(localStorage.getItem('fav'))[" + a + "].url"); // a番目のものをfav_a+1に格納する // 普通に this.his_j = JSON.parse(localStorage.getItem('imgs'))[j].url;とは書けない } } }, /// お気に入りから削除する機能 delfavo: function(){ favlist.shift(); //JSON.parse(localStorage.getItem('fav')).shift(); localStorage.setItem('fav',JSON.stringify(favlist)); this.good = false ; // falseに戻す console.log(favlist); if(localStorage.getItem('fav')){ // 存在確認とあまりにお気に入りが多い場合は10個にする var len2 = JSON.parse(localStorage.getItem('fav')).length; if(len2 > 10){ len2 = 10; } // エラーを防ぐ為に、過去履歴が存在するときだけ、その分だけ表示する for(let b = 0 ; b < len2 ; b++){ eval("this.fav_"+ (b+1) + "= JSON.parse(localStorage.getItem('fav'))[" + b + "].url"); // b番目のものをfav_b+1に格納する // 普通に this.his_j = JSON.parse(localStorage.getItem('imgs'))[j].url;とは書けない } } } }, mounted: function(){ this.getData(); this.favo(); this.delfavo(); } }) </script> </body> </html>
- 投稿日:2020-05-20T02:31:20+09:00
Vue.js+TypeScript+Nuxt.js環境で、『Property 'XXX' does not exist on type 'CombinedVueInstance~'�』を解消するためのtips
はじめに
Vue.js+TypeScript+Nuxt.js環境におけるScriptの書き方には下記の3種類があります。
- Vue.extend(OptionsAPI)
- defineComponent(CompositionAPI)
- vue-property-decorator(ClassAPI)
https://typescript.nuxtjs.org/ja/cookbook/components/#template
今回は、Vue.extend(OptionsAPI)を使用している場合の『Property 'XXX' does not exist on type 'CombinedVueInstance~'』というエラーを解消するためのtipsをまとめたものです。
しばしば遭遇するエラーだと思われますが、記事が少ないので取り急ぎまとめることにしました。
そのため、記事の内容については追加と修正を随時していきます。どんな時に発生するか
Vue.js+TypeScriptでcomputed、watch、methodsなどを書いていて、TypeScriptの型推論が効かなくなった時に発生します。
解消方法
- 返り値と引数の型を定義する
- TypeScript標準の型で定義できない場合はInterface、Typeで定義する
- 強制的にエラーをなくす
基本的には1と2で解決しますが、状況によっては3を使う場面があるかもしれませんので、参考までに入れておきます。
1.返り値と引数の型を定義する
methods: { someMethod (arg: number): number { return arg }, }返り値については、TypeScriptの型推論が行われる仕様になっているのですが、今回のように型推論が効かずにエラーの原因になり得るということと、可読性の観点から定義しておくべきだと考えています。
引数はそもそも型推論が行われませんので定義しないと他のエラーにも引っ掛かります。【 型定義の参考記事 】
https://qiita.com/is_ryo/items/6fc799ba4214db61d8ab2.TypeScriptの標準の型で定義できない場合はinterface、typeで定義する
number、string、voidなどの標準の型では定義できない複雑なものに関しては、下記のようにtypeやinterfaceを使用することによって定義することができます。
export type Hoge = { name: string price: number ,,,,省略 } ,,,,省略 methods: { someMethod (arg: Hoge): Hoge { return arg }, }【 InterfaceとTypeの違いの参考記事 】
https://qiita.com/tkrkt/items/d01b96363e58a7df830e3.強制的にエラーをなくす
本質的な解決ではありませんが、下記の2つのように書くと解消することができます。
computed: { ① return (this as any).hoge.method() ② // @ts-ignore return this.hoge.method() }①は(this as any)でany型にキャストすることができます。
②はts-ignoreによって次の行の型検査をスキップすることができます。
最後に
この他にも型定義関連のエラーは多々ありますので、随時投稿してまいります。
ナレッジを共有していって開発を効率化させましょう!