- 投稿日:2021-06-09T22:54:24+09:00
【Vue.js】親・子のコンポーネント間でのv-model
はじめに 仕事で使う事になったので1からVue.jsについて学んだ。 ちゃんと覚えておかないとまずそうな事を備忘録として1つ1つ残しておく。 親・子のコンポーネント間でv-modelを使う(双方向データバインディングする) ソースコードとしては以下。 親・子 SFC名 親 App.vue 子 EventTitle.vue App.vue <template> <div class="container-sm"> <!-- 省略 --> <div> <h2>イベントのフォーム</h2> <EventTitle :value="eventData.title" @input="eventData.title = $event" ></EventTitle> <!-- <EventTitle v-model="eventData.title"></EventTitle> ←v-modelは上記のように書き換えられる --> </div> </div> </template> <script> // 省略 import EventTitle from "./components/EventTitle.vue"; export default { data() { return { // 省略 eventData: { title: "", }, }; }, components: { // 省略 EventTitle, }, // 省略 }; </script> EventTitle.vue <template> <div> <p>タイトル</p> <div class="form-floating mb-3"> <input type="text" class="form-control" id="title" placeholder="タイトル" :value="value" @input="$emit('input', $event.target.value)" /> <label for="title">タイトル</label> </div> <p>{{ value }}</p> </div> </template> <script> export default { props: ["value"], }; </script> ソースコード全体は以下。 どのようにしてv-modelが動作する流れを詳細に見ていくと、、、(どこからスタートするかは鶏と卵であるが) 親コンポーネントの:valuev-bind:valueの事であり、value属性を動的にバインド(value属性の値に設定する)するという事をする最初は空文字なので空文字がバインドされる 子コンポーネントのprops: ["value"]親→子のコンポーネント間のデータ渡しの仕組みであるpropsを使い、親コンポーネントの属性名と同じ名前にする事で親コンポーネントの値を受け取る 子コンポーネントの:value="value"propsで受け取った値をinputタグのvalue属性にv-bindでバインドするこれにより親コンポーネントで定義していたeventData.titleが子コンポーネントのinput要素に反映されるようになる 子コンポーネントの@input="$emit('input', $event.target.value)"子→親のコンポーネント間のデータ渡しの仕組みである$emit()を用いて、inputに入力があった際に親コンポーネントのカスタムイベントを発火させる今回は親コンポーネントの@inputを発火させたいので第一引数がinputになっており、$emitで副次的に渡せる値としてはinputに入力された値を渡したいので、第二引数が$event.target.valueになっている※$event.target.valueの$eventはDOMに備わっているイベントオブジェクトで、今回はinput要素への入力で発火したイベントオブジェクトが格納される(子コンポーネントの@input) 親コンポーネントの@input="eventData.title = $event"子コンポーネントから$emit()で発火されたイベントが親コンポーネントで発火し、eventData.titleに$event($emitを使っている場合は$emitの第二引数に渡しているもの)が代入されるこれにより子コンポーネントの入力内容が親コンポーネントのオブジェクトに格納される このような流れで、親・子のコンポーネント間でのv-modelは実現される。 ※ソースコードの注意事項としては、 <EventTitle :value="eventData.title" @input="eventData.title = $event" ></EventTitle> と <EventTitle v-model="eventData.title"></EventTitle> は全く同じもので、v-modelが:value="eventData.title"・@input="eventData.title = $event"というのを書かないでいいようにしてくれているだけ。 実際に内部的に動いているのは:value="eventData.title"・@input="eventData.title = $event"を書いた方なので、上記のような「v-modelが動作する流れ」になる。そのため、props: ["value"]のvalueをtestに書き換えて、v-bindの代入元のvalueもtestにしても動作しない。これは暗黙的に:value="eventData.title"・@input="eventData.title = $event"で処理されているから。 以下は動作する場合と動作しない場合の動画。 Vue.jsの勉強メモ一覧記事へのリンク Vue.jsについて勉強した際に書いた勉強メモ記事のリンクを集約した記事。 https://qiita.com/yuta-katayama-23/items/dabefb59d16a83f1a1d4
- 投稿日:2021-06-09T22:54:24+09:00
【Vue.js】親・子のコンポーネント間でのv-modle
はじめに 仕事で使う事になったので1からVue.jsについて学んだ。 ちゃんと覚えておかないとまずそうな事を備忘録として1つ1つ残しておく。 親・子のコンポーネント間でv-modleを使う(双方向データバインディングする) ソースコードとしては以下。 親・子 SFC名 親 App.vue 子 EventTitle.vue App.vue <template> <div class="container-sm"> <!-- 省略 --> <div> <h2>イベントのフォーム</h2> <EventTitle :value="eventData.title" @input="eventData.title = $event" ></EventTitle> <!-- <EventTitle v-model="eventData.title"></EventTitle> ←v-modelは上記のように書き換えられる --> </div> </div> </template> <script> // 省略 import EventTitle from "./components/EventTitle.vue"; export default { data() { return { // 省略 eventData: { title: "", }, }; }, components: { // 省略 EventTitle, }, // 省略 }; </script> EventTitle.vue <template> <div> <p>タイトル</p> <div class="form-floating mb-3"> <input type="text" class="form-control" id="title" placeholder="タイトル" :value="value" @input="$emit('input', $event.target.value)" /> <label for="title">タイトル</label> </div> <p>{{ value }}</p> </div> </template> <script> export default { props: ["value"], }; </script> ソースコード全体は以下。 どのようにしてv-modelが動作する流れを詳細に見ていくと、、、(どこからスタートするかは鶏と卵であるが) 親コンポーネントの:valuev-bind:valueの事であり、value属性を動的にバインド(value属性の値に設定する)するという事をする最初は空文字なので空文字がバインドされる 子コンポーネントのprops: ["value"]親→子のコンポーネント間のデータ渡しの仕組みであるpropsを使い、親コンポーネントの属性名と同じ名前にする事で親コンポーネントの値を受け取る 子コンポーネントの:value="value"propsで受け取った値をinputタグのvalue属性にv-bindでバインドするこれにより親コンポーネントで定義していたeventData.titleが子コンポーネントのinput要素に反映されるようになる 子コンポーネントの@input="$emit('input', $event.target.value)"子→親のコンポーネント間のデータ渡しの仕組みである$emit()を用いて、inputに入力があった際に親コンポーネントのカスタムイベントを発火させる今回は親コンポーネントの@inputを発火させたいので第一引数がinputになっており、$emitで副次的に渡せる値としてはinputに入力された値を渡したいので、第二引数が$event.target.valueになっている※$event.target.valueの$eventはDOMに備わっているイベントオブジェクトで、今回はinput要素への入力で発火したイベントオブジェクトが格納される(子コンポーネントの@input) 親コンポーネントの@input="eventData.title = $event"子コンポーネントから$emit()で発火されたイベントが親コンポーネントで発火し、eventData.titleに$event($emitを使っている場合は$emitの第二引数に渡しているもの)が代入されるこれにより子コンポーネントの入力内容が親コンポーネントのオブジェクトに格納される このような流れで、親・子のコンポーネント間でのv-modelは実現される。 ※ソースコードの注意事項としては、 <EventTitle :value="eventData.title" @input="eventData.title = $event" ></EventTitle> と <EventTitle v-model="eventData.title"></EventTitle> は全く同じもので、v-modelが:value="eventData.title"・@input="eventData.title = $event"というのを書かないでいいようにしてくれているだけ。 実際に内部的に動いているのは:value="eventData.title"・@input="eventData.title = $event"を書いた方なので、上記のような「v-modelが動作する流れ」になる。そのため、props: ["value"]のvalueをtestに書き換えて、v-bindの代入元のvalueもtestにしても動作しない。これは暗黙的に:value="eventData.title"・@input="eventData.title = $event"で処理されているから。 以下は動作する場合と動作しない場合の動画。 Vue.jsの勉強メモ一覧記事へのリンク Vue.jsについて勉強した際に書いた勉強メモ記事のリンクを集約した記事。 https://qiita.com/yuta-katayama-23/items/dabefb59d16a83f1a1d4
- 投稿日:2021-06-09T21:02:19+09:00
Vue.js todoリスト 理解用
todoリストで調べた点 todoリストのチュートリアルを通して疑問に感じた点を備忘録とこれから学んでいく人のために記している。 ディレクティブ"v-"で始まる属性である。基本的にJavaScriptの記法と同じだが、"v-for"と"v-on"は例外となる。 v-vind属性を動的にできる。クリック時のリンク切り替えなどができる。todoリストの場合はリスト一覧から一つずつ取ってくるため、for文とともに切り替えていると考えられる。 v-onイベントを発動するトリガーとして使う。ボタンクリック時に関数を呼び出したりする。todoリストではsubmitイベントはformで起こるのでformに挿入する。preventはページがリロードされないようにしている。 pushJavaScriptでの配列の追加方法 watchオブジェクトの追加、削除を監視する役割を持つ。deepをtrueにすることで中身の変更も監視する。 ? :(三項演算子)A = B ? 0 : 1 においてA=Bなら0を返し、A!=Bなら1を返す。todoリストでは三項演算子でフラグを反転している。item.state(現状) = item.state(== 1) ?となっている。item.stateが両辺で別ということを意識してほしい。 splice配列の要素を取り除く方法。index番目の要素から1つ取り除く。 参考 ディレクティブ: https://v3.vuejs.org/guide/template-syntax.html#directives v-bind: https://kinocolog.com/vue_v_bind/ v-on: https://jp.vuejs.org/v2/guide/events.html watch:https://qiita.com/Keitaro/items/8e3f8448d1a0fe281648 フラグの反転: https://qiita.com/day-1/items/737523837e3803f3d3e9
- 投稿日:2021-06-09T17:57:34+09:00
Vue.jsのサイトをAWS S3にCodePiplineで自動デプロイしてみた
今回は最近Vue.jsを始めたのでサイト公開にかかる固定費を安く抑えようとAWS S3のWebサイトホスティング機能を使います。あと自動デプロイもしたかったのでAWSのCodePiplineを使います。構成は下図の通りです。 Vue.jsのプロジェクトの配下にbuildspec.ymlを配置します。 このファイルでCodeBuildがファイルに基づきビルドを実行してくれます。インデントをきちんとしていないとビルドの時にエラーが出ます。 buildspec.yml version: 0.2 phases: pre_build: commands: - if [ -e /tmp/node_modules.tar ]; then tar xf /tmp/node_modules.tar; fi - npm install build: commands: - npm run build post_build: commands: - tar cf /tmp/node_modules.tar node_modules artifacts: files: - '**/*' base-directory: dist cache: paths: - /tmp/node_modules.tar S3の設定 まず、バケットを任意の名前で作成します。そしてプロパティ欄に静的ウェブサイトホスティングを有効化しましょう。 次にアクセス許可の欄でブロックパブリックアクセスをオフにしましょう。 あと、バケットポリシーを編集しましょう。バケット名は自分のバケット名に直しましょう。 { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadForGetBucketObjects", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::バケット名/*" } ] } CodePipelineの作成 任意の名前でパイプライン名を作成してください。 ソースプロバイダーにGithubを選択して、「Githubに接続する」をクリックしての自分のリポジトリと連携します。 プロバイダービルドを AWS CodeBuildを選択して、任意の名前でプロジェクト名を作成してください。 デプロイプロバイダーにAmazon S3を選択して、保存したいバケット名を選択し、バケットは空欄にしておいて、デプロイする前にファイルを抽出するにチェックを入れます。 そして実際にGithubにpushすると自動でデプロイされているのがわかります。デプロイが成功したらS3の静的ウェブサイトホスティングに書かれているURLを開くと自分のサイトが公開されます!
- 投稿日:2021-06-09T16:05:01+09:00
【個人開発】�アレンジ飯を共有するサービスArrangy(アレンジー)をリリースしました
サービス概要 コンビニ商品や外食店の料理を使用したアレンジ飯を共有するサービスです。 URL: https://arrangy.jp/ Github: https://github.com/kazu-2020/arrangy サービスを制作した背景 一人暮らしをしていた際、食事を外食店やスーパー、コンビニの惣菜で済ませることが多くありました。そのため、次第に同じ物を食べてばかりの食生活になり、結果、好きで食べていた物を「食べ飽きてしまった」という経験がありました。また、昨今のコロナによって外食が難しくなり、「食の選択肢」そのものが少なくなっているという印象を受けていました。 そこで、これらの問題を解決する手段として私がサービスを通じて提案したのが「アレンジ飯」という選択肢です。アレンジ飯とは既に出来上がっている料理に一手間加えることで別の料理に変えることを言います。アレンジ飯の良さを一人でも多くの方に知ってもらいたいと思い、Arrangy(アレンジー)を制作しました。 サービスの使い方 注: タブレットサイズではデザインが崩れます! まずはトップページです。 ログインしなくても、投稿されたアレンジ飯を確認することができ、ページ上部には新しく投稿されたアレンジ飯をカールセルを用いて表示しています。 また、ページをスクロールして頂くとアレンジ飯の一覧が表示され、「いいね数」の多い順に表示されるようになっています。 ログイン後は「新規投稿、コメント機能、いいね機能」を使用することができます。 新規投稿する際は、下記の項目を設定してもらうようになっています。 ・ 画像投稿 ・・・ アレンジ前の写真とアレンジ後の写真を1枚ずつ選択する。 ・ タイトル ・ 投稿内容・・・ 材料や作り方を記入する ・ アレンジ度 ・・・ アレンジの度合いを「ちょい足し」「激変」の2択から選択する。 ・ おすすめ度 ・・・ 5段階で設定する。 いいねボタンは各投稿ページで選択することができます。 いいねした投稿はお気に入り一覧に追加されます。 苦労したこと ①RailsからVueに画像ファイルどうやって渡すの問題 Railsのみ使用していた時は、form_withがよしなにやってくれていたので、何も意識せずに画像ファイルを扱うことができました。しかし、フロントにVueを使用した場合はよしなにやってくれていた箇所を自分で実装しなければなりません。 そこで今回はVueからRailsへエンコードしたデータURLを渡し、Rails側でデコードを行うことでこの問題を解決しました。しかし、この実装は次の問題で泣く泣く変更することになりました(笑)。 ②本番環境で投稿にめちゃくちゃ時間かかる問題 ①ではgem carrierwaveを使用し、画像ファイル自体はs3に保存する形にしていました。開発環境で確認した際は問題なかったのですが、本番環境で投稿時間を測定すると画像1枚を含めて投稿するのに4sもかかっていました... 色々調べた結果、herokuを使用する際はブラウザからs3に直接アップロードする方法が推奨されていました。この方法を採用したところ、画像2枚を含めて投稿するのにかかる時間を1sまで抑えることができました。 ③TwitterのOGPが動的に変更されない問題 フィードバックを元に各投稿をtwitterでシェアできる機能を実装する際に発生した問題です。これはVue側でmetaタグが動的に変更されるようにしていても、twitterのクローラーがJavaScriptを実行してくれないのが原因でした。この問題を解決するためにNuxtを使用するといる案もありましたが、既にサービスがほとんど完成していた状態でしたので別の案を採用しました。 それがtwitterクローラーがアクセスしてきた場合のみ、静的なHTMLを返すという手法です。 まず判別方法ですが、twitterクローラーの場合は、Twitterbot/1.0という情報がユーザーエージェントに含まれます。そのため、routingのオプションにあるconstraintsを使用して下記のようなルーティングを設定してあげることで実現しました。 get '/arrangements/:id', to: 'crawlers#show', constraints: { user_agent: /Twitterbot\/1.0/ } 後は、gem metatagsを使用してmetaタグを動的に変更してあげることで解決しました。 工夫した点 ①画像をアップロードする際、トリミングを行えるようにする サイトに表示される際にアスペクト比1:1で表示されるのですが、cssのcontainやcoverでは不自然な画像に整形されることがあったので、ユーザーにアスペクト比1:1でトリミングした画像を投稿してもらうようにしました。 ②各投稿にアレンジ前の写真を載せるようにする 投稿一覧を閲覧する際、使用した商品が一目で分かるようにしました。 ER図 インフラ構成 さいごに 今回初めてのサービスを無事にリリースすることができました。 個人的にはサービスのアイディア出しに非常に苦労したので、就活用の1ポートフォリオではなく、大変思い入れのあるサービスになりました。 今後も引き続き、改善していきたいと思っているのでフィードバックなど貰えると幸いです。 是非、ご家族やご友人にアレンジ飯を振る舞ってみてください!
- 投稿日:2021-06-09T15:23:15+09:00
Leaflet での矩形などの図形を最前面・最背面へ移す
Rectangle など、 Path クラスを継承する図形は、 bringToFront() で最前面へ、 bringToBack() で最背面へ移動できる // map や bounds の定義は省略しています const rectangle1 = L.rectangle(bounds1) .addTo(map) .bringToFront() const rectangle2 = L.rectangle(bounds2) .addTo(map) .bringToBack() Vue 向けのラッパーライブラリである Vue Leaflet でもコンポーネントから mapObject を取得することでメソッドを利用できる(下記サンプルは Vue 2 向けのものです。 Vue 3 向けのもので利用可能かどうかは調査できてません・・・) <!-- l-map 等マップのセッティングについては省略しています --> <template> <l-rectangle :bounds="bounds" @click="toBack" ref="rectangle"></l-rectangle> </template> <script> import { LRectangle } from 'vue2-leaflet' export default { components: { LRectangle }, computed: { bounds () { return [[35.0, 134.0], [35.2, 134.2]] } }, methods: { toBack () { this.$refs.rectangle.mapObject.bringToBack() } } } </script>
- 投稿日:2021-06-09T11:43:56+09:00
【Vue × Firestore】検索機能の実装
検索機能の実装 search.vue(子コンポーネント) <template> <div class="search-inner flex"> <h2 class="search-tll neon flex">Cinemaryを検索する</h2> <hr class="separate" /> <div class="search-main-contens flex"> <input placeholder="例)アクション 恋愛 ミステリー SF ホラー ミュージカル etc.." class="search-main-item" type="search" v-model="inputValue" /> </div> </div> </template> <script> export default { data() { return {}; }, props: { value: { type: String, required: true //文字列型を必須で要求する } }, computed: { inputValue: { get() { return this.value; //getterとは、変数から値を取得して呼び出し元に返す関数。ここではpropsで親より受けた値(value)を返している。 }, set(value) { this.$emit("input", value); //setterとは、変数に値(value)を設定する関数。ここでは入力フォーム(input)に入力された値(value)を返している。 } } } }; </script> まず以下記事を参考に親子間のコンポーネントのデータ伝播を実装していきます。 board.vue(親コンポーネント) <template> <div> <Header /> <Search v-model="searchWord" /> <Post /> <div class="post"> <h2 id="top" class="post-tll neon">投稿一覧</h2> <div class="post-inner"> <div class="post-items"> <paginate name="paginate-log" tag="ol" :list="filteredPostData" :per="12" v-if="filteredPostData.length !== 0" > <!-- filteredPostDataにて該当する投稿がある場合、表示。 --> <List v-for="(list, index) in paginated('paginate-log')" :index="index" :list="list" :userDatas="userDatas" :key="list.id" /> </paginate> <div v-else class="nothing">" {{searchWord}} " に該当する投稿はありませんでした。</div> <!-- filteredPostDataにて該当する投稿がない場合、上記を表示させる。 --> <paginate-links for="paginate-log" class="pagination flex" v-scroll-to="postTop" :show-step-links="true" :style="filteredPostData.length !== 0 == '' ? 'display:none;' : 'display:flex;'" ></paginate-links> </div> </div> </div> </div> </template> export default { data() { return { searchWord: "" }; }, components: { List, Search }, computed: { filteredPostData() { if (this.searchWord != "") { return this.postData.filter(v => { return ~v.genre.indexOf(this.searchWord); //検索内容(this.searchWord)と同じ内容(genre)を持つ要素の位置を返す。存在しない場合、-1を返す。 //しかし、-1は今回ない為、「~v」とビット反転演算子(符号を反転してマイナス1した数)を使って、-1 → 0となる。 }); } else { return this.postData; //サーチ内容がない場合はそのまま、それ以外はフィルタ結果を返す } } }, created() { firebase .firestore() .collection("posts") .orderBy("time", "desc") .get() .then(snapshot => { snapshot.forEach(doc => { this.postData.push({ ...doc.data(), id: doc.id }); }); }); } }; </script> search.vue(子コンポーネント)から受けたデータをもとにfilteredPostData()メソッドを使って 今回はgenreのデータで条件を絞って表示しております。
- 投稿日:2021-06-09T10:30:12+09:00
Vue で編集、リバート可能なテキスト
普段はテキストだけどダブルクリックするとテキストボックスになるよ Enterかフォーカスアウトで確定されるよ 渡した値はリアクティブに変更されるよ 大丈夫!リバートもできるよ 値が変更された時は一応通知が行くよ 値が空になった時はプレースホルダが表示されるよ リバートボタンとプレースホルダは <tamplate> でカスタマイズできるよ そのうちnpm化してちゃんとドキュメント書きます <template> <span> <!-- bootstrap-vue 使ってたからこれになってるけど普通のinputに差し替えてもいいよ --> <b-form-input v-if="isEditable" ref="content-input" :value="value" size="sm" class="box" type="text" :style="inputWidth" @keypress.enter.native="editableOff" @blur="editableOff" @input="$emit('input', $event)" /> <span v-else ref="plain-text" class="box" @dblclick="editableOn()" > <span v-if="value" > {{ value }} </span> <slot v-else name="placeholder" > :none: </slot> </span> <span v-if="isEdited()" @dblclick="revertValue()" > <slot name="revert" > :rev: </slot> </span> </span> </template> <script> export default { props: { value: { type: String, require: true, default: '' }, identify: { type: Object, require: false, default: () => {} } }, data () { return { isEditable: false, originText: '', textWidth: null, inputWidth: { 'min-width': '' } } }, mounted () { this.originText = this.value }, methods: { editableOn () { this.textWidth = this.$refs['plain-text'].clientWidth this.isEditable = true this.$nextTick(() => { // table の中とかにあると input になった時に縮んじゃう事があるから元のテキストのサイズに合わせるようにしている this.inputWidth['min-width'] = (this.textWidth * 1.1) + 'px' this.$refs['content-input'].select() }) }, editableOff () { // Enterした時にフォーカスアウトで2回ここに来るからガードつけておく if (!this.isEditable) { return } this.isEditable = false if (this.isEdited()) { this.$emit('editedValue', this.identify, this.value, this.originText) } }, isEdited () { return this.value !== this.originText }, revertValue () { this.$emit('input', this.originText) this.$emit('revert') this.isEditable = false } } } </script> <style> </style>