- 投稿日:2020-05-28T22:30:46+09:00
爆速でwh.imのゲームを実装する(NGワードゲーム編)
はじめに
こんにちは。普段は情報系の学科で大学生をしている者です。
この記事に紹介されている、wh.im(ウィム)というサービスの立ち上げに関わっているのですが、その一環でwh.im上で楽しめるゲームを開発しました。
このサービスの特徴として、誰でもゲームを投稿 できます!そのやり方を知っていただきたく、前回に引き続き記事を書きますので、興味を持った方はぜひゲーム開発を試してみてください!
前回のじゃんけんに引き続き、より複雑なゲームを実装していきます。
今回は実際にwh.im上で遊ぶことのできる「NGワードゲーム」を例に挙げて、実装方法を説明いたします。このゲームで出てくる、phaseやグローバルで使える関数などは、前回出てこなかったテクニックとなります。
まずは開発環境で実際に動かしてみる
実際にNGワードゲームを開発環境で動かしてみたいと思います。
まず
$ cd ダウンロードしたいディレクトリ $ git clone https://github.com/whimRTC/whim-ng_word.gitとします。そして
$ cd whim-ng_word $ yarn # or npm install $ yarn serve # or npm run serveとします。yarnまたはnpmがインストールされていない場合はインストールしてください。
するとlocalhost:3001
にゲームが起動します。そして、wh.imから「遊び場」へ入室し、そのアドレスの末尾に
&develop=true
をつけます。するとwh.imが開発者用のモードとなります。右上のメニューの「アプリを選ぶ」から「開発用(port:3001)」を選ぶことにより、自分の手元でゲームを試すことができます。このように表示されれば成功です!
実際のコードを見てみる
続いて、実際のコードを使いながら通信方法を説明していきます。
App
まず表示される画面が書かれている
src/App.vue
をご覧ください。src/App.vue<template> <div id="app"> <Main class="main" /> <Player v-for="user in $whim.users" :key="user.id" class="box" :class="`pos${user.positionNumber}`" :displayUser="user" /> </div> </template> <script> export default { name: "App", components: { Main: () => import("@/components/main/Index"), Player: () => import("@/components/player/Index") } }; </script> <!-- 以下略 -->このゲームではMain画面とPlayer画面に分かれて実装されています。そのため、
App.vue
内でMain
、Player
の2つのコンポーネントを呼び出しています。
Mainは画面中央部の画面を、Playerはそれぞれのユーザーのいる場所に表示される画面を表します。
Main
、Player
の実装はそれぞれ、src/components/main/Index.vue
、src/components/player/Index.vue
に実装があります。wh.imを経由した通信の方法
前回の復習です。
App.vue
で$whim.users
という呼び出しがあります(this.$whim.users
の省略形です)が、これはwhim-client-vue
というパッケージに入っています。このようにすることで、this.$whim
から始まる関数だけで、利用者間の非同期通信まわりは全てできるようになっています。ここに扱える関数一覧を示します。scriptタグ内では適宜
this
を先頭に付けてください。状態取得(呼び出すたびに通信する)
コード 型 説明 $whim.users [User] ルームに入っているユーザー一覧 $whim.room Room Room Object $whim.accessUser User 現在アクセスしているUser $whim.state State ゲームの状態(自由に設計可能) 状態変更
コード 引数 説明 $whim.assignState(Object) Object ゲーム情報を追記更新、
存在しないキーの場合:追記
存在するキーの場合:更新$whim.replaceState(Object) Object ゲーム情報を渡されたObjectにすべて変える $whim.deleteState ゲーム情報を空にする これでは分かりにくいと思うので、後ほどのコードで使っている部分を見ながら、理解していただけると助かります。
より詳細な説明は、開発者ドキュメントをご覧ください。このゲームのデータ構造
stateはゲームに合わせて自由に設計することができます。
今回のゲームでは、次のような設計です。state ├── phase // ゲームのフェーズ: "shuffling" | "playing" | "answer" └── ngWords // NGワード: {ユーザーID: そのユーザーのNGワード}phaseを用いることで、ゲームの状態を整理しながらコードを書くことができます。
これらの変数は開発環境ではどんな時でも見ることができます。右上のメニューから「SHOW APP STATE」を選択すると
白い部分には私のuserIdが表示されています。
このように、現在の
phase
や自分のNGワードを確認することができます(ngWordsは畳み込まれて表示されるので一度クリックして展開してください)。このようにして、カンニングすることができます(友達とやるときはやめましょう)。Vueのグローバル変数
wh.imで通信したいときに、別のコンポーネント内で処理を共通化したいことがあると思います。そういったときには、次のように、
main.js
に追記します。src/main.jsconst NG_WORD_PATTERNS = require("@/assets/ng_word_patterns.json"); Vue.prototype.$gameStart = () => { const shuffledPattern = shuffle( NG_WORD_PATTERNS[Math.floor(Math.random() * NG_WORD_PATTERNS.length)] ); let ngWords = {}; Vue.prototype.$whim.users.forEach((user, i) => { ngWords[user.id] = shuffledPattern[i]; }); Vue.prototype.$whim.assignState({ phase: "shuffling", ngWords: ngWords }); };Vue.jsではvueファイル内で
this.$hoge
で表されるグローバル関数は、Vue.prototype
に定義されています(whim-client-vueの実装もそのようになっています)。だから、上のようにVue.prototype.$gameStart
に関数を代入しておけば、vueファイル内でthis.$gameStart
のように呼び出すことができます(Vueには様々な$
から始まるメソッドがあるのでゲームで使う関数だとわかるように$game
から始まる関数名にしました)。また、
Vue.prototype.$whim
でwhim-client-vueの関数を呼び出せるようになっています(この関数定義より前にVue.use(whimClientVue, { store });
が必要になります)。この
$gameStart
という関数には、ゲームの開始時の処理を定義しています。具体的にはランダムにお題をstate.ngWords
に格納して、state.phase
を'shuffling'
に切り替えています。Player
次にPlayer画面のコードについて説明していきます。Player画面は各プレイヤーの上に表示されます。ここにはゲームの状態に応じて、NGワードを表示するかどうかを変えています。
src/components/player/Index.vue<template> <div class="container"> <div v-if="status === 'hidden'" class="card hidden"> <span class="text--subtitle"> NGワード</span> </div> <div v-else-if="status === 'shuffling'" class="card"> <img :src="require('@/assets/shuffling.gif')" class="shuffling" /> </div> <div v-else-if="status === 'visible'" class="card"> <span class="text--subtitle">{{ appState.ngWords[displayUser.id] }}</span> </div> </div> </template> <script> export default { name: "Player", props: { displayUser: Object // 表示されているUserの情報 }, computed: { phase() { return this.$whim.state.phase; }, isMe() { return this.displayUser.id === this.$whim.accessUser.id; }, appState() { return this.$whim.state; }, status() { if (this.phase === "shuffling") { return "shuffling"; } if ((this.phase === "playing" && !this.isMe) || this.phase === "answer") { return "visible"; } return "hidden"; } } }; </script> <!-- 以下略 -->
computed
のstatus
で何を画面に表示すべきかを決めています。status
が'hidden'
の場合にはNGワードが隠れている状態が表示され、status
が'shuffling'
の場合にはシャッフルの演出がされ、status
が'visible'
の場合には答えが表示されます。Main
Main画面は画面中央部に表示されます。
state.phase
によって表示するコンポーネントを切り替えています。src/components/main/Index.vue<template> <div> <Shuffling v-if="phase === 'shuffling'" /> <Playing v-else-if="phase === 'playing'" /> <Answer v-else-if="phase === 'answer'" /> <GenreSelection v-else /> </div> </template> <script> export default { name: "Main", components: { GenreSelection: () => import("@/components/main/GenreSelection"), Shuffling: () => import("@/components/main/Shuffling"), Playing: () => import("@/components/main/Playing"), Answer: () => import("@/components/main/Answer") }, computed: { phase() { return this.$whim.state.phase; } } }; </script> <style lang="scss" scoped></style>GenreSelection
ここではゲームのスタート画面を定義しています。ジャンル選択は未実装です。クリックすることで
start
関数が呼ばれます。start
関数の内部で先程定義した$gameStart
関数が呼ばれます。src/components/main/GenreSelection.vue<template> <div> <a class="fuwatto_btn_yellow" @click="start">スタート</a> </div> </template> <script> export default { name: "GenreSelection", props: { msg: String }, data() { return { genre: "random" }; }, methods: { start() { this.$gameStart(); } } }; </script> <!-- 以下略 -->Shuffling
ここではお題をシャッフルしている画面を定義しています。
mounted
関数は、このコンポーネントが表示されたとき呼ばれ、2000ミリ秒後にstate.phase
を'playing'
に切り替えています。src/components/main/Shuffling.vue<template> <div> <a class="fuwatto_btn_yellow">シャッフル中...</a> </div> </template> <script> export default { name: "Shuffling", mounted() { setTimeout(() => { this.$whim.assignState({ phase: "playing" }); }, 2000); } }; </script> <!-- 以下略 -->Playing
ここでは残り時間を中央に表示しています。NGワードやALLシャッフルをクリックすると
goAnswer
関数が呼ばれ、state.phase
をanswer
に切り替えます。src/components/main/Playing.vue<template> <div> <div class="text--subtitle title">タイムリミットまで</div> <countdown :time="10 * 60 * 1000" @end="goAnswer" class="countdown" :transform="transform" ref="countdown" > <template slot-scope="props" >{{ props.minutes }}:{{ props.seconds }}</template > </countdown> <a class="fuwatto_btn yellow" @click="goAnswer">NGワード!</a> <a class="fuwatto_btn grey" @click="goAnswer">ALLシャッフル</a> </div> </template> <script> export default { name: "Playing", methods: { goAnswer() { this.$refs.countdown.abort(); this.$whim.assignState({ phase: "answer" }); }, transform(props) { props.seconds = props.seconds.toString().padStart(2, "0"); return props; } } }; </script> <!-- 以下略 -->Answer
答えを表示するフェーズですが、中央にはシャッフルで始めに戻るようにしています。
start
関数で$gameStart
が呼ばれます。src/components/main/Answer.vue<template> <div> <a class="fuwatto_btn_yellow" @click="start">シャッフル開始</a> </div> </template> <script> export default { name: "Answer", methods: { start() { this.$gameStart(); } } }; </script> <!-- 以下略 -->最後に
いかがでしたでしょうか。自分でゲームを作れるような気がしてきましたか?
引き続き、ゲーム作りのTipsのようなものは投稿し続けたいと思いますので、よろしくお願いします!
- 投稿日:2020-05-28T21:37:25+09:00
【BootstrapVueコピペのみ】導入から画像一覧画面の実装まで
Vueバージョン確認
npm list vueまずは上記コマンドでバージョンの確認
twinzlabo@0.1.0 /Users/twinzlabo ── vue@2.6.11BootstrapVueの導入
BootstrapVueの導入がまだの方のために念のため導入方法書いときますね
とりあえずコピペして環境を整えてください
main.jsimport BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' Vue.use(BootstrapVue)npm install vue bootstrap-vue bootstrap以上でBootstrapVueの導入は完了です
画像一覧画面の実装
説明は抜きにしてコードを下に貼ってあるのでどんどんコピペして
自分のプロジェクトに合った修正を加えてみてください
一応先に完成イメージです(モバイルに合わせてコーディングしてます)
<template> <b-container> <b-row> <b-col> <div v-for="(item, i) in items" class="images" :key="i"> <b-img thumbnail fluid :src="item.imageUrl"></b-img> </div> </b-col> </b-row> </b-container> </template><script> export default { data () { return { items: [ { imageUrl: require('@/assets/images/1.png') }, { imageUrl: require('@/assets/images/2.png') }, { imageUrl: require('@/assets/images/3.png') }, { imageUrl: require('@/assets/images/4.png') }, { imageUrl: require('@/assets/images/5.png') }, { imageUrl: require('@/assets/images/6.png') }, { imageUrl: require('@/assets/images/7.png') }, { imageUrl: require('@/assets/images/8.png') }, { imageUrl: require('@/assets/images/9.png') }, { imageUrl: require('@/assets/images/10.png') } ] } } } </script>いかがでしたでしょうか?
見た目はこれからですが画像一覧画面にはなったかと思います以上です
こちらの記事にてstyleのコードまで詳しく参照できます
【Vue/BootstrapVueコピペのみ】Bootstarap導入からシンプルな画像一覧画面の実装方法までを徹底解説
- 投稿日:2020-05-28T20:27:33+09:00
Vue.js で作ったサイトを 無償の Azure Static Web Apps で CI/CD も含めて数分で構築する方法
Microsoft Build 2020 で Public Preview になったスタティックサイト向けの新しいホスティングサービスである Azure Static Web Apps を使うと Vue.js のアプリケーションを数分でデプロイしてクラウド上に公開できるようになります。
Vue.js はプログレッシブフレームワークというコンセプトで作られているため、最初は小さく始めて徐々に本格的な構成のアプリケーションに育てていくことができるようになっています。この記事では、 Vue.js と Azure Static Web Apps を使ってスケーラブルなアプリケーションを作るための最初の一歩を紹介したいと思います。
Vue CLI を使ったアプリケーションの新規作成
Azure Static Web Apps では、アプリケーションのコードが GitHub に存在していることが前提です。なので、まず基本的なコードを書いてしまいましょう。
Vue.js アプリケーションを新規に作るには Vue CLI を使うのが近道です。Vue CLI を使えば、例えば Router の導入や TypeScript の開発などを適切な方法で開始することができます。では、早速 Vue CLI を実行してみましょう。ターミナルを使ってアプリケーションを作成するディレクトリに移動して以下のコマンドを実行します。
npm i -g @vue/cli vue create .Vue CLI のプロンプトには以下のように良く使う設定でプロジェクトを新規作成してみます。TypeScript は Vue の開発でも今後主流になると思うので選択しておきます。また Router や ESLint + Prettier 、そして Jest も多くのプロジェクトで使われているので Vue CLI で最初から導入しておきます。
なお、 TypeScript のUse class-style component syntax?
は、 Vue 3.x ではメインストリームから外れたので、ここではNo
を選択したほうが良いでしょう。Vue CLI v4.3.1 ? Please pick a preset: Manually select features ? Check the features needed for your project: TS, Router, Linter, Unit ? Use class-style component syntax? No ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Prettier ? Pick additional lint features: Lint on save ? Pick a unit testing solution: Jest ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? (y/N) Nしばらくすると Vue.js のプロジェクトが生成されます。まずローカルでアプリケーションが起動することを確認します。
npm run serve無事に Vue.js の緑のアイコンとともに初期画面が表示されれば、Vue.js のアプリケーション基本構成はできあがりです。TypeScript のコンパイルはもちろん、ユニットテストやフォーマッティングも既に実行できるようになっています。あとはプロダクト固有の機能を追加するコードを書くだけです。
この時点で GitHub にコミットして Azure にデプロイする準備をしておきましょう。
Azure Static Web Apps のプロビジョニング
Azure Static Web Apps は Azure Portal の Create a resource で
static web apps
と入力し、以下のように必要事項を入力します。この時、さきほど作成した GitHub Repo の情報をリンクさせます。次の Build 設定画面では
App artifact location
にdist
と入力します。dist
は、Vue CLI 標準のビルドコマンドであるnpm run build
を実行した際に HTML, CSS, Javascript などホスティングの対象となるファイルが出力されるディレクトリです。あとは、
Review + create
をクリックして数分待てば、デプロイが完了です。デプロイのために CI サービスを設定したり、YAML でビルドスクリプトを書いたりする必要はありません!なお、ビルドとデプロイはバックグラウンドで GitHub Actions のビルドワークフローが自動で実行されます。ビルドの状況を見るには、リポジトリの
Actions
タブを開いて確認できます。アイコンがグリーンになっていればビルドとデプロイが成功しています。Azure Portal に戻って サイトの URL をクリックしてみましょう。ローカルで実行したアプリケーションと同じページが公開されているのを見ることができるはずです。
なお、この時点で GitHub リポジトリの
.github/workflows
に YAML ファイルが追加されているので、ローカルに Pull するのを忘れないようにしましょう。フォールバックを設定する
Vue CLI で Router を導入した際に、
History Mode
を選択しました。SPA ではサーバー側に何も設定をしなければ、特定のページ URL を直接指定すると 404 が発生してしまいます。History Mode をホスティング環境でも有効にするには、その環境に適した設定を行う必要があります。Azure Static Web Apps では以下のような
routes.json
ファイルを/public
以下に作成します。{ "routes": [ { "route": "/*", "serve": "/index.html", "statusCode": 200 } ] }では、この変更をブランチにプッシュしてプルリクエストを作成しましょう。
プルリクエストをトリガーに Static Web Apps へのデプロイが実行されています。ビルドプロセスが完了すると、ポータルの Environments から変更がデプロイされたアプリケーションを実際に確認することができるようになります。
ステージングされた変更から
Browse
をクリックして実際にページを確認してみると、about
ページをリロードしても404ページが表示されないように改善されていることを確認できました。あとは、プルリクエストをマージして
production
環境にデプロイされるのを待つだけです。なお、プルリクエストがクローズされると、ステージング環境は自動的に削除されます。このように、スケーラブルな構成の Vue.js アプリケーションがとても簡単に Azure でホスティングができるようになることが確認できたのではないでしょうか?
ちなみに Azure Static Web Apps は現在 Public Preview で無償で利用できます。
- 投稿日:2020-05-28T20:27:33+09:00
Vue.js で作るサイトを 無償の Azure Static Web Apps に CI/CD も含めて数分でホスティングする方法
Microsoft Build 2020 で Public Preview になったスタティックサイト向けの新しいホスティングサービスである Azure Static Web Apps を使うと Vue.js のアプリケーションを数分でデプロイしてクラウド上に公開できるようになります。
Vue.js はプログレッシブフレームワークというコンセプトで作られているため、最初は小さく始めて徐々に本格的な構成のアプリケーションに育てていくことができるようになっています。この記事では、 Vue.js と Azure Static Web Apps を使ってスケーラブルなアプリケーションを作るための最初の一歩を紹介したいと思います。
Vue CLI を使ったアプリケーションの新規作成
Azure Static Web Apps では、アプリケーションのコードが GitHub に存在していることが前提です。なので、まず基本的なコードを書いてしまいましょう。
Vue.js アプリケーションを新規に作るには Vue CLI を使うのが近道です。Vue CLI を使えば、例えば Router の導入や TypeScript の開発などを適切な方法で開始することができます。では、早速 Vue CLI を実行してみましょう。ターミナルを使ってアプリケーションを作成するディレクトリに移動して以下のコマンドを実行します。
npm i -g @vue/cli vue create .Vue CLI のプロンプトには以下のように良く使う設定でプロジェクトを新規作成してみます。TypeScript は Vue の開発でも今後主流になると思うので選択しておきます。また Router や ESLint + Prettier 、そして Jest も多くのプロジェクトで使われているので Vue CLI で最初から導入しておきます。
なお、 TypeScript のUse class-style component syntax?
は、 Vue 3.x ではメインストリームから外れたので、ここではNo
を選択したほうが良いでしょう。Vue CLI v4.3.1 ? Please pick a preset: Manually select features ? Check the features needed for your project: TS, Router, Linter, Unit ? Use class-style component syntax? No ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Prettier ? Pick additional lint features: Lint on save ? Pick a unit testing solution: Jest ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? (y/N) Nしばらくすると Vue.js のプロジェクトが生成されます。まずローカルでアプリケーションが起動することを確認します。
npm run serve無事に Vue.js の緑のアイコンとともに初期画面が表示されれば、Vue.js のアプリケーション基本構成はできあがりです。TypeScript のコンパイルはもちろん、ユニットテストやフォーマッティングも既に実行できるようになっています。あとはプロダクト固有の機能を追加するコードを書くだけです。
この時点で GitHub にコミットして Azure にデプロイする準備をしておきましょう。
Azure Static Web Apps のプロビジョニング
Azure Static Web Apps は Azure Portal の Create a resource で
static web apps
と入力し、以下のように必要事項を入力します。この時、さきほど作成した GitHub Repo の情報をリンクさせます。次の Build 設定画面では
App artifact location
にdist
と入力します。dist
は、Vue CLI 標準のビルドコマンドであるnpm run build
を実行した際に HTML, CSS, Javascript などホスティングの対象となるファイルが出力されるディレクトリです。あとは、
Review + create
をクリックして数分待てば、デプロイが完了です。デプロイのために CI サービスを設定したり、YAML でビルドスクリプトを書いたりする必要はありません!なお、ビルドとデプロイはバックグラウンドで GitHub Actions のビルドワークフローが自動で実行されます。ビルドの状況を見るには、リポジトリの
Actions
タブを開いて確認できます。アイコンがグリーンになっていればビルドとデプロイが成功しています。Azure Portal に戻って サイトの URL をクリックしてみましょう。ローカルで実行したアプリケーションと同じページが公開されているのを見ることができるはずです。
なお、この時点で GitHub リポジトリの
.github/workflows
に YAML ファイルが追加されているので、ローカルに Pull するのを忘れないようにしましょう。フォールバックを設定する
Vue CLI で Router を導入した際に、
History Mode
を選択しました。SPA ではサーバー側に何も設定をしなければ、特定のページ URL を直接指定すると 404 が発生してしまいます。History Mode をホスティング環境でも有効にするには、その環境に適した設定を行う必要があります。Azure Static Web Apps では以下のような
routes.json
ファイルを/public
以下に作成します。{ "routes": [ { "route": "/*", "serve": "/index.html", "statusCode": 200 } ] }では、この変更をブランチにプッシュしてプルリクエストを作成しましょう。
プルリクエストをトリガーに Static Web Apps へのデプロイが実行されています。ビルドプロセスが完了すると、ポータルの Environments から変更がデプロイされたアプリケーションを実際に確認することができるようになります。
ステージングされた変更から
Browse
をクリックして実際にページを確認してみると、about
ページをリロードしても404ページが表示されないように改善されていることを確認できました。あとは、プルリクエストをマージして
production
環境にデプロイされるのを待つだけです。なお、プルリクエストがクローズされると、ステージング環境は自動的に削除されます。このように、スケーラブルな構成の Vue.js アプリケーションがとても簡単に Azure でホスティングができるようになることが確認できたのではないでしょうか?
ちなみに Azure Static Web Apps は現在 Public Preview で無償で利用できます。
- 投稿日:2020-05-28T20:01:25+09:00
Vue.jsでinputフォームの文字数制限をする
今回のゴール
- Vue.jsで実装するinputフォームで入力できる文字数を制限し、特定の文字数以上は入力できないようにする
実際のコード
サンプル
CodeSandboxに実際に動くもの作りました。
https://codesandbox.io/s/input-form-chara-limit-o9qp1?fontsize=14&hidenavigation=1&theme=dark<template> <div id="app"> <p>5文字以上入力ができないinputフォーム</p> <input type="text" v-model="inputText"> <p>{{ inputText }}</p> </div> </template> <script> export default { data() { return { inputText: "" }; }, watch: { inputText(inputText) { this.inputText = this.charaLimit(inputText); } }, methods: { charaLimit(inputText) { return inputText.length > 5 ? inputText.slice(0, -1) : inputText; } } }; </script>watchによってv-modelで文字列
inputText
が新しく入力された時に、methodのcharaLimit
が動くようにします。
cahraLimmit
ではinputText
が5文字より多い場合は文字列の末尾を削除し、そうでなければ入力された文字列をそのまま
returnしてinputText
に代入するようにします。
inputText
が入力される度に文字列の末尾を削除するので実質的に、文字列が5文字より多い場合はinputText
に値が入らなくなります。所感
入力された文字のバリデーションは正規表現に任して、入力文字数は
slice
で対応すると簡単でした。参考
- 投稿日:2020-05-28T19:21:32+09:00
【Vue/Font Awesome導入時のエラー解決】error There should be no space after this paren space-in-parens
Vueバージョン確認
npm list vueまずは上記コマンドでバージョンの確認
twinzlabo@0.1.0 /Users/twinzlabo ── vue@2.6.11エラーメッセージ
Failed to compile. ./src/main.js Module Error (from ./node_modules/eslint-loader/index.js): /Users/twinzlabo/src/main.js 17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces 17:15 error There should be no space after this paren space-in-parens 17:53 error Multiple spaces found before ‘)’ no-multi-spaces 17:53 error There should be no space before this paren space-in-parens解決策
今回のエラー文で注目すべき箇所は
17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces 17:15 error There should be no space after this paren space-in-parens 17:53 error Multiple spaces found before ‘)’ no-multi-spaces 17:53 error There should be no space before this paren space-in-parensつまりmain.js17行目でスペースがおかしい使い方されているよというエラーです
ではまず実際のエラー箇所を確認してみましょう
main.js// about fontawesome--- import { library } from '@fortawesome/fontawesome-svg-core' import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' // ------------------- import App from './App.vue' import router from './router' import store from './store' library.add(faCoffee, faSpinner, faAngleDoubleUp) Vue.component( 'font-awesome-icon', FontAwesomeIcon )どうやらFont Awesome導入時にエラーが発生してしまったようですね
Vue.component( ‘font-awesome-icon’, FontAwesomeIcon )こちらぱっと見は全然問題なさそうですが、
‘font-awesome-icon’, FontAwesomeIconこの前後にスペースが1つずつ存在してしまっています
細かいですがESlintではこの程度でもエラーを表示してしまうので気をつけましょう
修正後のコードはこのようになります
// about fontawesome--- import { library } from '@fortawesome/fontawesome-svg-core' import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' // ------------------- import App from './App.vue' import router from './router' import store from './store' library.add(faCoffee, faSpinner, faAngleDoubleUp) Vue.component('font-awesome-icon', FontAwesomeIcon)これで無事解決しましたね
以上です
参考記事
【Vue/Font Awesome導入】Font Awesomeを導入してアイコンを使用するまでの流れを徹底解説
- 投稿日:2020-05-28T19:18:19+09:00
VuexFireでバインドしたデータをcomputedやmethodsで使う
Nuxt.js+Firebaseの勉強を始めたばかりです。
前回: Nuxt.js + Firebase + VuexFireでデータ表示をソートする
やりたいこと
Vuexfireでバインドしたデータをcomputedやmethodsで使いたいです。
Webページにリストボックスを用意しています。リスト項目の内容をFirestoreで管理します。itemsへの設定はcomputedで行います。
環境
- Firebase 8.3.0
- Vue CLI 4.0.5
- Nuxt.js 2.11.1
- VuexFire 3.2.0
失敗したこと
例えば、Firestoreの構成は次のようにしておいて、
コレクション ドキュメント フィールド pages (document) title pages/index.vueの
cumputed
で...mapGetters({ pages: 'getPages' })
としておいたとします。template
の中で下記コードを書けばtitleフィールドのデータが表示されますよね。<v-flex v-for="(page, i) in pages" :key="i" xs12 sm4 mb-2 pa-2> <v-card> <v-card-text> {{page.title}} </v-card-text> </v-card> </v-flex>ネットで調べると同様の記事が多く出てきますので、フロント技術に疎い私でもコピペすれば期待通り動かすことができました。
だから私は最初、computedやmethodsで利用するときも下記のようなコードで利用できるのではないかと考えました。しかしこれは失敗でした。
これではうまくいきませんfor (let page in this.pages) { console.log(page.title); }この例では、pageには 0,1,2.. が入っているので、
this.pages[page].title
でデータを取り出します。これはうまくいきますfor (let page in this.pages) { console.log(this.pages[page].title); }私が実装したこと
コレクション ドキュメント フィールド categories (document) A001: カテA
A002: カテB
A003: カテCpages/index.vue<template> <v-layout justify-center align-start row wrap> <v-flex xs12 sm9 mb-2 > <v-card> <v-card-text> <v-select v-model="selectedCategories" item-text="label" item-value="value" :items="categoriesList" label="カテゴリーを選んでください" return-object /> </v-card-text> </v-card> </v-flex> </v-layout> </template> <script> import { mapGetters } from 'vuex'; import { db } from '../plugins/firebase'; export default { data () { return { selectedCategories: { label: 'カテA', value: 'A001' }, } }, computed: { ...mapGetters({ categories: 'getCategories' }), categoriesList: function () { let arr = []; for (let i in this.categories) { let doc = this.categories[i]; let keys = Object.keys(doc); for (let j in keys) { let key = keys[j]; arr.push({ label: doc[key] , value: key }); } } return arr; }, }, mounted () { this.$store.dispatch('setCategoriesRef', db.collection('categories')); }, } </script>store/index.jsimport { vuexfireMutations, firestoreAction } from 'vuexfire'; export const state = () => ({ categories: [], }); export const mutations = { ...vuexfireMutations }; export const getters = { getCategories: (state) => { return state.categories; }, }; export const actions = { setCategoriesRef: firestoreAction(({ bindFirestoreRef }, ref) => { bindFirestoreRef('categories', ref); }), };
- 投稿日:2020-05-28T18:57:12+09:00
rails: ActionView::Template::Error http error 500 対処法について
現在、ポートフォリオを作成している途中のゆーた(@onoblog)です。
そこそこ、ローカル環境で、ポートフォリオのデザインも整ってきたし、1回、デプロイしとこうかなと思い、デプロイしたときのエラーをまとめておきます。環境
- rails 5.2.3
- carrierwave
- fog-aws
- jquery-rails (4.3.5)
- vue/cli 4.1.2
- yarn 1.21.1
- webpacker (5.0.1)
- Docker 19.03.5
- docker-compose 1.25.2
- nginx 1.15.8
対処法
assetへのパイプラインを通してあげる設定にしてあげます。
config/enviroments/production.rbconfig.assets.compile = true状況
このように、デプロイしたものを表示しようとするとhttp error 500が起きてしまいました。
最初に、nginxのlogかawsのec2の設定を疑い、無駄な時間を使ってしまいました。
docker-compose -f staging.yml exec app tail -f log/production.logその後、上記コマンドで本番環境のログを見ると
ActionView::Template::Error
が起きている状況でした。原因
ローカル環境では、trueになっていたことにより、動的にコンパイルができていましたが、本番環境では、falseになってしまっていました。
precompileしていないファイルを動的にコンパイルするので、本番環境では、負荷がかかってしまうので、良くないと思いますが、一時的に、適宜変更します。
development production true false 参照
- 投稿日:2020-05-28T17:51:21+09:00
VuejsでGoogleMapAPIを使用して現在地を中心にしたマップを表示する
VuejsでGoogleMapAPIを使用して現在地を中心にしたマップを表示する
概要
- VuejsのGoogleMapのプラグインを使わないで現在地を中心としたマップを表示する
環境
- Mac Catalina(10.15.2)
- Node (v12.7.0)
- VSCode
前提
- VueCLIを使用する
- GoogleMapAPIのKeyは取得済み
手順
プロジェクトを立ち上げる
- 以下のコマンドでプロジェクトフォルダ新規作成する
vue create google-map-vue-practice
- プロジェクトフォルダをVSCodeで開く
ファイルを変更する
- public/index.htmlに以下のコードを追加する
- API_KEYは自身のGoogleMapAPIKeyに置き換える
index.html<div id="app"></div> <!-- built files will be auto injected --> <script async defer src="https://maps.googleapis.com/maps/api/js?key=API_KEY&callback=initMap"> </script> </body>
- App.vueを以下のようにする
- navigator.geolocation.getCurrentPosition
- 現在地を取得する関数
- window.google.maps.LatLng
- 緯度経度のオブジェクトを作る関数
- window.google.maps.Map
- GoogleMapを作成する関数
- window.google.maps.Marker
- マーカーを作成する関数
App.vue<template> <div id="app"> <div id="map" ref="map" /> </div> </template> <script> export default { name: "App", data: () => ({ map:null }), mounted() { if (navigator.geolocation) { // callback関数内でthis使えないため let vm = this navigator.geolocation.getCurrentPosition( function(position){ let latlng = new window.google.maps.LatLng( position.coords.latitude, position.coords.longitude ); vm.map = new window.google.maps.Map(vm.$refs["map"], { center: latlng, zoom: 4 }) new window.google.maps.Marker({ position: latlng, map: vm.map }) } ) } } }; </script> <style> #map { height: 600px; background: gray; } </style>実行する
- 以下のコマンドでローカル実行する
npm run serve
http://localhost:8080/
でブラウザを開くと以下の画面が出る終わりに
- 今回のプロジェクトを私のリポジトリで公開
- 上記のコード以外も少し混ざってますのでご了承ください
- 投稿日:2020-05-28T16:33:55+09:00
ヒープ領域制限によりビルドが通らない場合
環境
インスタンス:t3a.nano
OS: AmazonLinux2
node.js:v14.3.0上記環境でnpm run serveを実施してVueアプリの動作確認をした後に
npm run buildでビルドを実行したところFATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memoryのエラーが表示されてビルドが通らなかった。
ビルドするためのメモリが足らないと判断できるため
スワップ領域を確保したうえで
https://qiita.com/nakamto/items/5e78e9caceeff6b9e2b4$ export NODE_OPTIONS="--max-old-space-size=1024" $ npm run build※1024は確保できるメモリ量に応じて変更してください。
max-old-space-sizeの値を指定することでビルドが通りました。メモリの少ないインスタンスを利用していることが原因
と推測しますので、メモリが大きいインスタンスへ変更しても良いかもしれません。
- 投稿日:2020-05-28T15:27:03+09:00
Laravel+Vue propsでの親から子へのデータの渡し方
親から子へのデータ流れ
- 子コンポーネントにpropsを定義する
<!-- Like.vue(子コンポーネント) --> <script> export default { props: ["postId", "userId", "defaultLiked", "defaultCount"], } </script>2 Laravelならこのデータ渡し方をコントローラー(親)で定義する。
// PostController class class PostController extends Controller{ public function show(Post $post) { $userAuth = \Auth::user(); $post->load('likes'); $defaultCount = count($post->likes); $defaultLiked = $post->likes->where('user_id', $userAuth->id)->first(); if (count($defaultLiked) === 0) { $defaultLiked == false; } else { $defaultLiked == true; } return view('posts.show', [ 'post' => $post, 'userAuth' => $userAuth, 'defaultLiked' => $defaultLiked, 'defaultCount' => $defaultCount ]); } }3 親コンポーネントにv-bindを使い、2(コントローラー)で受けとったデータを参照し子コンポーネント(Like.vue)に受け渡す。
<!-- show.blade.php(親コンポーネント) --> <like :post-id="{{ json_encode($post->id) }}" :user-id="{{ json_encode($userAuth->id) }}" :default-Liked="{{ json_encode($defaultLiked) }}" :default-Count="{{ json_encode($defaultCount) }}" ></like>アドバイスあればよろしくお願いします!
- 投稿日:2020-05-28T14:41:18+09:00
vue nuxt 個人メモ
vue nuxt 個人メモ
データセット
dataメソッドで行う
index.vue<script> export default Vue.extend({ data() { return { modal: false, email: '', password: '' } } }) </script>ハンドラー、イベント
methodsで行う
index.vue<script> export default Vue.extend({ data() { return { modal: false, email: '', password: '' } }, methods: { openModal() { this.modal = true }, closeModal() { this.modal = false } } }) </script>api取得 ライフサイクル
画面の描画前にデータを取得する場合
asyncData
画面の描画後にデータを取得する場合
mounted
index.vue<script> export default Vue.extend({ data() { return { modal: false, email: '', password: '' } }, async asyncData({ query, params, $axios }) { try { const response = await $axios .$get( `/api/~~${query.id}`, {...params}, { withCredentials: true } ) .catch((error) => error.response) if (response === undefined) { throw new Error('undefined') } if (response.status !== 'success') { throw new Error(response.data.message) } return { error: false } } catch (error) { console.error(error.message) return { error: true } } }, async mounted() { await this.getAPI() } }) </script>大きな違いはSSRで画面を表示するかどうか??
https://medium.com/veltra-engineering/in-ssr-vue-js-is-created-twice-7f9122de9b77Nuxtでは特にユニバーサルなコードを書くことを意識するのが重要なので、
違いを理解する必要がある(まだ理解してない)props
v-forなどでデータを渡す際はv-bindでそのまま渡す
index.vue<ul> <ListData v-for="item in data" :key="item.id" v-bind="item" /> </ul>渡されたデータがスネークケースの場合で、
キャメルケースに変換したい場合はハイフンでつなぐindex.vue<ul> <ListData v-for="item in data" :key="item.id" :index="index + 1" :start-date="item.start_date" :end-date="item.end_date" /> </ul>watch method
dataで定義した値に変更が加わった時にフックして処理を実行したい場合などに使う
<script> export default { props: { id: { type: Number, default: 0 } }, data() { return { value: '' } }, watch: { value(newValue, oldValue) { console.log( }, id(newValue, oldValue) { console.log( } } } </script>nuxt link
to=""
でリンク先を指定するけど
変数を入れたい場合など:to="`/myPage/${user.id}`"親から子へ渡す props
親コンポーネント
書き方は下記
<template> <Child hoge1="ABC" hoge2="DEF"> </template>値が変動する場合はデータバインドで渡す
<template> <Child :hoge1="dat1" :hoge2="dat2"> </template>子コンポーネント
配列形式で受けとる場合
<script> export default { props: ['hoge1', 'hoge2'] } </script>オブジェクトで受け取る場合
<script> export default { props: { hoge1: { type: String, default: 'abc', required: true }, hoge2: { type: String, default: '', required: false } } } </script>子から親に伝達するemit
v-on:
または@
でイベントを紐づける親コンポーネント<Child @click="onClick"> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ methods: { onClick(e: Event) { e.preventDefault() console.log(e) } } }) </script>子コンポーネント<template> <button type="button" @click="$emit('click')" /> </template>methodsなどで使用する場合<script lang="ts"> import Vue from 'vue' export default Vue.extend({ methods: { onClick(e: Event) { e.preventDefault() this.$emit('click') this.$emit('click', e) // 引数を渡す場合は第二引数 } } }) </script>
@click
に引数を渡す忘れガチなのでメモ
<template> <div id="app"> <button @click="click">button</button> <button @click="superClick('引数です')">param button</button> </div> </template> <script> export default { methods: { superClick(value) { alert(value); } } };propsType
Typescript interface でpropsの受け取る型を指定
index.vue<script lang="ts"> interface Schedule { id: Number, startDate: String, endDate: String, deliveryType: Number, afterTalkFlag: Number } export default Vue.extend({ props: { userName: String, schedule: { type: Object as PropType<Schedule>, default: null } } }) </script>nuxt での form入力
input checkbox
まずデータを用意
<script> export default { data() { return { dataList: [ // api から取得してきたデータ {name: "hoge"}, {name: "fuga"}, {name: "fugafuga"} ], formData: [] // form送信用 } } } </script>用意したデータをバインドする
<template> <div v-for="(item, index) in dataList" :key="'dataItem' + index" > <input :id="'dataItem_' + item.name" type="checkbox" :value="item.name" v-model="formData" name="'dataItem_' + item.name" /> <label :for="'dataItem_' + item.name"> {{ item.name }} </label> </div> </template>デフォルトでチェックを入れる場合
デフォルトでチェックを入れる場合はv-modelで紐付ける
form送信用のdataオブジェクトに初期値をセットする<script> export default { data() { return { dataList: [ // api から取得してきたデータ { name: "hoge", value: true }, { name: "fuga", value: true }, { name: "hogege", value: false } ], formData: [] // form送信用 } }, mounted() { this.formData = this.dataList.map((item: any) => item.value) } } </script>子のプロパティまたはメソッドを参照する(refs)
親のresetボタンを押した際に、子供のフォームの内容をresetする場合など
ChildForm.vue<script> export default { name: 'ChildForm', data() { value: '' }, methods: { onClear() { this.value = '' } } } </script>parent.vue<template> <div> <ChildForm ref="childForm"/> <button @click="clear"> Clear </base-button> </div> </template> <script> import ChildForm from '@/components/ChildForm.vue' export default { components: ChildForm, methods: { clear() { this.$refs.childForm.onClear() } } } </script>v-for での特定回数の繰り返し
記事データを表示する際、1 ~ 10 件のデータを表示する場合がある
他に、ページャーなどの機能で配列の10 ~ 20件の間のデータを抽出したいなど
v-for
に整数値を与えることで繰り返しの回数を制御できるがそれだと実現したい機能とは違うのでまとめておく<template> <div v-for="item in pages"> <div>{{ item.name }}</div> <div>{{ item.age }}</div> </div> </template> <script> export default { data() { return { currentPage: 1, //現在のページ pageLimit: 50, // 表示できるページ数 limitItem: 10, // 表示できる要素数 customerList: [ { id:1, name: 'hoge', age: 20 }, { id:2, name: 'fuga', age: 22 }, { id:3, name: 'aaaa', age: 30 }, { id:4, name: 'fugafuga', age: 10 } // 以降 200件のデータが続く ] } }, computed: { pages() { const maxItem = Number(this.itemLimit) const start = this.currentPage === 1 ? 0 : (this.currentPage - 1) * maxItem const end = this.currentPage === 1 ? maxItem : this.currentPage * maxItem return this.customerList.filter((_element, index) => { return index >= start && index < end }) } } } </script>
- 投稿日:2020-05-28T13:53:21+09:00
vueでチェックボックス全選択の作り方
概要
タイトルの通り、以下のようなチェックボックスを全て選択するチェックボックスを実装する方法
コード
テンプレート側<el-row :gutter="24"> <el-col :span="21" class="disability_name_area_allcheck" ><el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" :disabled="!selectRireki.isEditable" @change="checkAllObstacles" >すべて選択</el-checkbox > </el-col> </el-row> <el-row :gutter="24"> <el-col :span="24" class="disability_name_area"> <original-check-list v-model="selectedDisabilityCertificateData.body_disability_name" item-physical-name="body_disability_name" item-display-name=" " :option-list="obstaclesList" :input-required="false" :disabled-condition="!selectRireki.isEditable" ></original-check-list> </el-col> </el-row>スクリプト側private isIndeterminate: boolean = false; private checkAll: boolean = false; checkAllObstacles(val: boolean) { if (!this.selectedDisabilityCertificateData) { return; } this.selectedDisabilityCertificateData.body_disability_name = val ? this.getObstaclesListKeys() : []; this.isIndeterminate = false; } @Watch("selectedDisabilityCertificateData.body_disability_name") checkedObstaclesChange(value: Array<boolean>) { let checkedCount = value.length; this.checkAll = checkedCount === this.obstaclesList.length; this.isIndeterminate = checkedCount > 0 && checkedCount < this.obstaclesList.length; }
- 投稿日:2020-05-28T09:42:55+09:00
LaravelでVue.jsのscaffoldを生成した時の違和感
Laravelで
php artisan ui vue
をするとVue.jsのscaffold(「スカフォールド」とか「スキャフォールド」とか言って、ひな形を意味するらしい)を生成できるのだが...Vue.component( 'example-component', require('./components/ExampleComponent.vue').default );これは違和感を感じないか??
ExampleComponent.vue側で
module.exports
して、Vue.component( 'example-component', require('./components/ExampleComponent.vue') );とするか、
import ExampleComponent from './components/ExampleComponent'; Vue.component('example-component', ExampleComponent);としたくない?
Laravel公式にも堂々と.default
って書いてるし、敢えてなのか?
- 投稿日:2020-05-28T03:59:38+09:00
【Vuetify】NuxtのSSRでVuetifyのDatePickerを使用したら「The client-side rendered virtual DOM tree is not matching server-rendered content. 」というエラー発生
現象
SSRでVuetifyのDatePickerを使用したら
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
というエラーが発生解決策
調べてみると
tableにtbodyを入れると解決する
とのこと。
https://qiita.com/k5690033/items/d1e436fd3b92fcc1fe61しかし今回はコンポーネントを丸ごと使用しているためこれができない。
no-ssrを使う
DatePickerに関してはssrする必要がそもそもないので
no-ssr
で囲って解決した。
https://github.com/nuxt/nuxt.js/issues/1700<no-ssr> <v-date-picker v-model="picker" :landscape="landscape" :reactive="reactive" :full-width="fullWidth" :show-current="showCurrent" :type="month ? 'month' : 'date'" :multiple="multiple" :readonly="readonly" :disabled="disabled" :events="enableEvents ? functionEvents : null" ></v-date-picker> </no-ssr>
- 投稿日:2020-05-28T02:00:10+09:00
Vue.jsでAPIのBasic認証をした
はじめに
Vue.jsによる開発においてAPIのBasic認証の実装をしました。
エラーについての検索をした際に類似のケースで苦しんでいる方が国内外問わず多くいらっしゃったので、本記事では私が嵌ったエラーと解決策の一例について書いていきます。
当記事の使用言語はVue.jsとNode.jsです。課題の切り分け
実装に当たって私が悩んだ点は大きく分けて下記二つとなります。
・CORSの不一致
・非同期処理の中でBasic認証が出来ない(404エラーが返ってくる)◎CORSの不一致
APIのBase URLに対してGETでデータの取得を初めて求めた際に、
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.以上のエラーが返ってきた。
エラーはどうやら「same-origin」と呼ばれるセキュリティポリシーにより、ブラウザが同一のドメインを持つアプリケーションのみと対話するように保護されているためであり、
Google先生に質問をしたところ、VueではProxyの設定でCORSのエラーを回避出来ることが分かり、configを設定するファイルに下記のような記述をした。
vue.config.jsmodule.exports = { devServer : { // proxy all requests starting with /api to jsonplaceholder "/testapi": { target: "https://api.test.hogehoge.jp/", changeOrigin: true, pathRewrite: { "^/testapi": "" } } }公式「フロントエンドアプリとバックエンドAPIサーバーが同じホスト上で実行されていない場合は、開発中にAPIリクエストをAPIサーバーにプロキシする必要がある」・・・。
改めてドキュメントを読み込む大切さを思い知った・・・?ちなみにCORSについてはこちらの記事が大変分かりやすかったです、ありがとうございます!
◎非同期処理の中でBasic認証が出来ない(404エラーが返ってくる)
CORSの問題を解決して一件落着!と思いきや、GETして拾ってきたのはJsonではなく404エラーだった。
エラーはBasic認証の部分で発生しており、axiosを利用した非同期処理のロジックを組みながら最終的に下記のようなコードで実装をした。
getApiData.jsgetApiData({ commit }) { const clientId = "hoge"; const clientSecret = "test"; //Basic認証のために、base64でエンコードする const encodedData = Buffer.from(clientId + ":" + clientSecret).toString( "base64" ); axios .get("/testapi", { withCredentials: true, headers: { Authorization: "Basic " + encodedData } }) .then(function(response) { console.log("成功"); }) .catch(function(error) { console.log("失敗"); }); }これは定型文的に決まってルールとして、
Basic認証がかかったURLにアクセスする場合にはAuthorizationヘッダーへ向けて、Authorization: Basic <Base64エンコードしたユーザ名:パスワード>以上をリクエストをする必要があるようだった。
おわりに
今回APIを扱うに当たって、はじめてポストマン を使ってみたところ、素早くテストをすることが出来ました。学習コストはほとんどなく、直感的に扱えたので次回以降も継続して利用をしていこうと思います。
- 投稿日:2020-05-28T00:49:31+09:00
.sync修飾子を使って複数のフォームの変更監視をする
.sync修飾子とは
https://jp.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A3%BE%E5%AD%90
上記をご覧ください。
.sync修飾子を使用すれば複数のフォームで入力した内容を編集したりできる便利。どうやって使うか
下記のようにモーダルを呼び出すスクリプトを用意しました。
やっていることはボタンを押した際にopenModal(todo)
が発火されtodoオブジェクトをスクリプトに渡しつつモーダルを開くような形にしています。親コンポーネント<!-- モーダルを開く際todoObjectを渡す --> <b-button variant="outline-info" @click="openModal(todo)"> 編集 </b-button> <transition name="modal"> <!-- .syncで子コンポーネントの変更監視 --> <!-- モーダルを動的にするために子コンポーネントにtodoの情報を渡している --> <show-modal :id="todoId" :timelimit.sync="todoTimelimit" :content.sync="todoContent" v-if="showContent" @close="modalClose" v-model="editContent" @edit="editTodo($event)" /> </transition> <script> export default { name: 'todo-list', components: { ShowModal, }, // 送られてきたtodoObjectを変数に代入 openModal (todo) { this.showContent = true this.todoId = todo.id this.todoTimelimit = todo.timelimit this.todoContent = todo.content }, editTodo (e) { this.$store.commit('editTodo', { newContent: this.todoContent, newTimelimit: this.todoTimelimit, id: e }) </script>続いて子コンポーネントです。
$emitに注目していただけるとわかる通りpropsで受けとっている物の後ろにupdate:
をつけて$emitで渡しています。第二引数は$event
じゃないと親コンポーネントに渡すことができませんでした。
.sync修飾子を利用することでフォームの中に書かれている内容を検知しそれぞれが親のコンポーネントに渡す為、v-modelによって変更感知する必要が無くなります。それにv-modelと違って複数の変更が感知できるようになった為、さらに子コンポーネント内でのフォーム作成の自由度が上がると思います。子コンポーネント<template> <div> <div class="modal-mask"> <div class="modal-wrapper" @click.self="$emit('close')"> <div class="modal-container"> <div class="modal-header"> 編集 </div> <div class="modal-form"> <b-input-group class="mb-2"> <!-- $emitで入力内容を親コンポーネントに渡している --> <b-form-input type="text" placeholder="やりたいことを入力してください" @input="$emit('update:content', $event)" :value="content" ></b-form-input> <!-- $emitで入力内容を親コンポーネントに渡している --> <b-form-datepicker class="mb-2" placeholder="何日までに行いますか?" :value="timelimit" @input="$emit('update:timelimit', $event)" ></b-form-datepicker> </b-input-group> </div> <div class="modal-footer"> <button @click="edit(id)">OK</button> <button @click="$emit('close')">Close</button> </div> </div> </div> </div> </div> </template> <script> export default { name: 'show-modal', props: ['id', 'timelimit', 'content'] , deta () { return { closeModal: false, text: '', } }, methods: { edit(id) { this.$emit('edit', id) this.$emit('close') } } } </script>最後に.sync修飾子によって送られてきたフォームの内容を下記のスクリプトで送信しています。
<script> editTodo (e) { this.$store.commit('editTodo', { newContent: this.todoContent, newTimelimit: this.todoTimelimit, id: e }) </script>複数のフォームに対してv-model以外で検知する方法を探していたらsync修飾子というものを見つけました。どん詰まって何時間も浪費するかと思いきや以外とあっさり解決できたのでよかったです。
お役に立てれば幸いです。
- 投稿日:2020-05-28T00:18:17+09:00
未来の観光業をLINEBotで支えたい。
はじめに
緊急事態宣言が解除されましたが、全く同じ生活に戻ることはまずないでしょう。そんな中僕たちが救いたいと思ったのは日本全国にある観光地です。各地の特産品やお土産品は、色々なインターネットサービスを使って格安で在庫処分が行われています。このままではなかなか利益が見込めず、一刻も早く観光客が安心して旅行をできる環境を作らなければいけません。今回作った作品は後ほど紹介するハッカソンで奨励賞をいただいた作品の記録用記事です。開発構成/使用技術などについても少し書いています。
また、未来を見据えた物なので自粛警察はやめてください。メンバー
今回は広島県等が主催するレッドハッカソン2020にしんぶんぶん(@shinbunbun_)とゆうせい(@ItyuJ)と僕(@inoue2002)の高校3年生3人でチームを組んで参加しました。
アイデア出しは一応始まる前から初めてはいたのですが、なかなかいいアイデアが出ず、実質、他の参加者の方のグループより出発は遅くなってしまいました。それぞれ普段からよく開発している分野がLINEBot/フロントエンド/バックエンドと綺麗に別れていたので、自分の担当する部分をメインにゴリゴリ開発しました。(LINEbotはぶんぶん君に力を貸してもらった部分もあります)作ったもの(登壇動画)(5分)
実際の同級生の声をヒントに
自粛に疲れた高校生は今、何を従っているのか。聞いてみました。そこで、カラオケなど以外に、「県外に遊びに行きたい」「旅行に行きたい」との声も多くあったので、観光業に何かアプローチしようと思ったのがきっかけです。
観光業の今の現状
観光していて外せないものといえば、その土地の名産品やお土産品ですよね。
しかし誰も観光客が来なくなった今、それらの行き場を失った物達はネット通販等で格安に在庫処分されるようになりました。
いくら在庫処分が全てできたとしても利益が出たり、観光客が増えたりする訳でも無く、解決の方向には進んでいません。
そんなこれらの状況から生まれたのがこの「トラべる!」です。「トラべる!」は何がどうなってどうしたいの?
完結に言うと 「LINEグループにBotを招待して、みんなで旅のしおりを作るサービスです」
これを使うことで、みんなが行きたい場所を取り入れながらそれらをすぐに可視化できます。
こうして、旅行計画を立てるきっかけを提供することが観光客の増加に繋がるのではないかと考えました。このサービスの特徴
・観光情報は広島県/広島市のオープンデータを活用した。
→広島県
→広島市・DBを操作するAPIを作ったりして、水平展開しやすい構成を作った
・実際に観光に行った人から感染症対策などが取られていて、安心できたかなどを聞くことができる。
開発体制
連絡
基本的にはハッカソンでオールだったので常にZoomを繋いでいました。
だから、分からない事や聞きたいことはいつでも聞ける体制を取っていました。アイデア出し
Miroを使って、"社会の変化"が何で"必要とされるもの"は何なのかみたいなブレストから入りました。
仕様策定
基本的に、HackMDに書くようにしました。APIだと以下のような感じです。
以下は一部の切り抜きですが、全機能のPOST/GETについて策定してくれました。
やりとり
基本的にはSlackに専用のチャンネルを作成し、そちらで行いました。
開発構成/仕様技術
AWSにBotのコードやらマップのページやらぽんぽん置いて完成させました。
作ったものについて
※基本的に現在の開発段階ではグループでのイベントにしか対応していません。
※グループに招待→やりたいことを選択して記録する→みんなのやりたいことを確認する。というのが現在ユーザーに使っていただける機能になります。実際に試して見る(現在止めてます)
ぜひ一度試してみてください。APIの利用回数が異常だったので現在は利用を停止しています。開発を終えて
今回作ってみたものはプロトタイプに過ぎません。実装したい機能がまだまだ沢山あるので、もし時間があるならばもう少し開発に時間を割いて、よりユーザー体験の良いサービスにしたいなと思いました。
開発の裏話ですが、徹夜ハッカソンだったものの、色々と個人がこだわっていたりして時間が足りなかったです。発表の1時間前までハッピーパスが通っていないと言うなかなかギリギリの戦いでした。なんとか作った即席のスライドで最終の審査に挑みました。
結果発表では奨励賞と言う形をいただき、2日間それぞれの力を合わせて開発したものがこのように目に見える形で評価を頂き嬉しい限りです。これからもすごくなりたいがくせいぐるーぷのメンバーでハッカソンなどに参加し、色々な技術に挑戦したいです。一緒に開発してくれた@shinbunbun_と@ItyuJに感謝です。後日得た教訓
無料枠に感謝wwwww pic.twitter.com/03RtFFQmxj
— ようかん (Yosuke Inoue) / (@inoue2002) May 27, 2020
後日、APIの利用利用料金がどうなってるんだろうとコンソールを見ると上のツイートのようにPlaceAPIに7000リクエストも入って請求が¥16,000を超えていました。幸い初心者クレジットで$300あったので負担してもらえたのですが、おそらく実際に開発で使ったのは2000ほど。どこかでシークレットキーがバレたのかな。。。
クラウド破産には気をつけてこれからも開発を続けて行きます。。今後の展望
面白そう!私も観光業助けたいのでこのプロジェクトやりたいです!みたいな声が多くなると実際に動くかもしれないです。
APIやフロントエンドの技術的記事はメンバーの2人がこの後書いてくれるかもしれないです!