20220322のvue.jsに関する記事は6件です。

Vue 状態管理変数を実装してみた

実現したかったこと aタグなどの要素としては同じだが遷移先が異なるものについてcomponent化 生成した数に応じて、表示を右寄せ・左寄せとする 実装 index.html <!-- // ... --> <body> <!-- // ... --> <vue_header></vue_header> <!-- // ... --> </body> <script type="text/javascript"> Vue.component('vue_a', { props: [ 'list', 'position_class', ], template: ` <a v-bind:class="position_class" v-bind:href="list.href" > {{ list.name }} </a> `, }); Vue.component('vue_header', { beforeCreate: function () { this.count = 0; }, data: function () { return { lists: [ { href: "/href1", name: "リンク1", }, { href: "/href2", name: "リンク2", }, { href: "/href3", name: "リンク3", }, ], }; }, methods: { getPositionClass: function () { if (this.count < 2) { this.count++; return "left"; } else { this.count++; return "right"; } } }, template: ` <header> <div> <vue_a v-for="(list, index) in this.lists" v-bind:key=index v-bind:list=list v-bind:position_class="getPositionClass()" > </vue_a> </div> </header> `, }); <!-- // ... --> </script> <!-- // ... --> つまずいたところ vue_headerのdata内にcountを保持していたが、 countの更新と共にtemplateが再描画されてしまう beforeCreate内で変数初期化することで、 処理内で更新してもtemplateは更新されない さいごに 他にいい対応方法・至らぬ点ありましたら、コメントで指摘して頂けるとありがたいです...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 状態管理用の変数を実装してみた

実現したかったこと aタグなどの要素としては同じだが遷移先が異なるものについてcomponent化 生成した数に応じて、表示位置を制御するクラスを適用する 実装 index.html <!-- // ... --> <body> <!-- // ... --> <vue_header></vue_header> <!-- // ... --> </body> <script type="text/javascript"> Vue.component('vue_a', { props: [ 'list', 'position_class', ], template: ` <a v-bind:class="position_class" v-bind:href="list.href" > {{ list.name }} </a> `, }); Vue.component('vue_header', { beforeCreate: function () { this.count = 0; }, data: function () { return { lists: [ { href: "/href1", name: "リンク1", }, { href: "/href2", name: "リンク2", }, { href: "/href3", name: "リンク3", }, ], }; }, methods: { getPositionClass: function () { if (this.count < 2) { this.count++; return "left"; } else { this.count++; return "right"; } } }, template: ` <header> <div> <vue_a v-for="(list, index) in this.lists" v-bind:key=index v-bind:list=list v-bind:position_class="getPositionClass()" > </vue_a> </div> </header> `, }); <!-- // ... --> </script> <!-- // ... --> つまずいたところ vue_headerのdata内にcountを保持していたが、 countの更新と共にtemplateが再描画されてしまう beforeCreate内で変数初期化することで、 処理内で更新してもtemplateは更新されない さいごに 他にいい対応方法・至らぬ点ありましたら、コメントで指摘して頂けるとありがたいです...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuetifyでoptgroup付きselectboxを作ってみる

vuetifyを使用してoptgroup付きのselectboxを作成したときのメモです。 やりたいこと multiple selectの場合はチェックボックスを表示し、optgroupも選択可能にする optgroupをクリックしたときには配下のitemを全て選択したことにして非活性にする 検索処理や更新処理など幅広く使えるようにしたいので、状況に合わせて使い分けられるように、以下の3パターンのデータを返却させる groupの選択状況 itemの選択状況 groupもitemに置き換えた場合のitemの選択状況 参考元 v-select Slots list-item-groups selection-controls GroupedSelect.vue template <template> <v-autocomplete v-bind="$attrs" v-model="selectedValues" color="primary" item-text="text" item-value="value" class="grouped-select" :class="{'-dark': $vuetify.theme.dark}" :items="innerItems" :multiple="multiple" chips > <template #label> <slot name="label"/> </template> <template #selection="data"> <v-chip v-if="multiple" class="px-2" :input-value="data.selected" :color="isGroupOption(data.item) ? `secondary ${$vuetify.theme.dark ? 'lighten-2' : 'darken-2}'` : 'primary'" close outlined small @click:close="removeItem(data.item)" > {{ data.item.text }} </v-chip> <template v-else> {{ data.item.text }} </template> </template> <template #item="{ item, attrs, on }"> <v-list-item v-slot="{ active }" v-bind="attrs" class="px-1" :class="{'ml-6': isItemOption(item) && hasGroup}" :disabled="isGroupOption(item) ? !multiple : isGroupParentSelected(item)" v-on="on" > <v-list-item-action v-if="multiple" class="mr-2" > <v-checkbox :disabled="isGroupOption(item) ? !multiple : isGroupParentSelected(item)" :input-value="active" /> </v-list-item-action> <v-list-item-content class="ml-2 text-caption" @click="clickItem(item)" v-text="item.text" /> </v-list-item> </template> </v-autocomplete> </template> type export interface GroupedSelectValues { groupIdList: string[] itemIdList: string[] idList: string[] } interface GroupedSelectOption { optionType: 'group' | 'item' itemId: string groupId: string text: string value: string } export type GroupedSelectItem = Omit<GroupedSelectOption, 'value'> props, data groupIdList, itemIdListでそれぞれの選択状況を指定します。 props: { multiple: { type: Boolean, default: false }, items: { type: Array as PropType<GroupedSelectItem[]>, default: () => [] }, groupIdList: { type: Array as PropType<string[]>, default: () => [] }, itemIdList: { type: Array as PropType<string[]>, default: () => [] }, }, data () { return { selected: this.multiple ? [] : undefined } }, computed 選択状況の getter, setter selectedValues: { get (): string[] | string | undefined { return this.selected }, set (v: string[] | string | undefined): void { if (v instanceof Array && v.length === this.selected?.length) { this.selected = v } else { this.$emit('change', this.getResult(v)) this.selected = v } } }, ${e.optionType}-${e.itemId}のルールで内部管理用のidを割り振ります。 innerItems (): GroupedSelectOption[] { return (this.items || []).map((e) => ({ ...e, value: `${e.optionType}-${e.itemId}` })) }, 状態管理系処理 hasGroup (): boolean { return !!(this.items || []).find(this.isGroupOption) }, isGroupParentSelected () { const selected = this.selected return (item: GroupedSelectOption) => castArray(selected).includes(`group-${item.groupId}`) }, watch 選択状況の変更監視 groupIdList (val) { this.refreshSelectedValues( castArray(this.itemIdList), castArray(val)) }, itemIdList (val) { this.refreshSelectedValues( castArray(val), castArray(this.groupIdList)) } created created () { this.refreshSelectedValues( castArray(this.itemIdList), castArray(this.groupIdList)) }, methods 選択状況を内部管理idに変換します。 refreshSelectedValues (itemIdList: string[], groupIdList: string[]) { if (this.multiple) { this.selectedValues = [ ...groupIdList.map(v => `group-${v}`), ...(itemIdList).map(v => `item-${v}`) ] } else { this.selectedValues = itemIdList[0] ? `item-${itemIdList[0]}` : undefined } }, 状態管理系処理 isGroupOption (item: GroupedSelectItem) { return item.optionType === 'group' }, isItemOption (item: GroupedSelectItem) { return item.optionType === 'item' }, groupの子itemを取得します。 getGroupedItemList (group: GroupedSelectItem): GroupedSelectOption[] { if (this.isGroupOption(group)) { return this.innerItems.filter((item) => this.isItemOption(item) && item.groupId === group.groupId) } else { return [] } }, 選択削除処理 removeItem (item: GroupedSelectOption) { if (this.selectedValues instanceof Array) { this.selectedValues = this.selectedValues.filter((v) => v !== item.value) } else { this.selectedValues = undefined } }, 選択時処理 groupの場合、子itemの選択を外します。 clickItem (item: GroupedSelectItem) { if (this.multiple === true && this.isGroupOption(item)) { this.getGroupedItemList(item).forEach(this.removeItem) } }, 選択状況を作成します。 groupIdList: groupの選択中idリスト itemIdList: itemの選択中idリスト idList: 選択状況を全てitemで表現した場合の選択中idリスト getResult (selected: string[] | string | undefined): GroupedSelectValues { return castArray(selected).reduce((prev: GroupedSelectValues, value) => { const item = this.innerItems.find((item) => item.value === value) if (item && this.isGroupOption(item)) { prev.groupIdList.push(item.groupId) prev.idList = union(prev.idList, this.getGroupedItemList(item).map(e => e.itemId)) } else if (item && this.isItemOption(item)) { prev.itemIdList.push(item.itemId) prev.idList = union(prev.idList, [item.itemId]) } return prev }, {groupIdList: [], itemIdList: [], idList: []}) }, style <style lang="scss" scoped> @import '~vuetify/src/styles/main'; .grouped-select { ::v-deep .v-select__slot .v-input__append-inner { @extend .my-auto; } ::v-deep .v-select__selections { > *:not(input) { @extend .mt-1; } > input { @extend .px-2; @extend .mx-2; @extend .rounded; opacity: 0.5; } } &:not(.-dark) ::v-deep .v-select__selections input { background-color: var(--v-secondary-lighten2); } &.-dark ::v-deep .v-select__selections input { background-color: var(--v-secondary-darken2); } } </style> 利用側 template <template> <grouped-select v-bind="$attrs" :items="items" :group-id-list="castArray(value.group)" :item-id-list="castArray(value.item)" :multiple="multiple" @change="change" > <template #label> <slot name="label"/> </template> </grouped-select> </template> model, props model: { prop: 'value', event: 'change', }, props: { multiple: { type: Boolean, default: false }, value: { type: Object as PropType<Value>, default: undefined }, }, computed 選択肢の取得 items (): GroupedSelectItem[] { // sample items return [ { optionType: 'group', itemId: 'g1', groupId: 'g1', text: 'Group1' }, { optionType: 'item', itemId: 'i1', groupId: 'g1', text: 'Item1' }, { optionType: 'item', itemId: 'i2', groupId: 'g1', text: 'Item2' }, { optionType: 'item', itemId: 'i3', groupId: 'g1', text: 'Item3' }, { optionType: 'group', itemId: 'g2', groupId: 'g2', text: 'Group2' }, { optionType: 'item', itemId: 'i5', groupId: 'g2', text: 'Item5' }, { optionType: 'item', itemId: 'i6', groupId: 'g2', text: 'Item6' }, ] }, methods multiple selectの場合はstring[]、single selectの場合はstringで返却します。 change (values: GroupedSelectValues) { if (this.multiple === true) { const value: Value = { group: values.groupIdList, item : values.itemIdList, } this.$emit('change', value) } else { const value: Value = { group: '', item : values.idList[0], } this.$emit('change', value) } }, その他パーツ import _castArray from "lodash/castArray" export function castArray <T> (v: T[] | T | undefined) { return V ? _castArray(v) : [] }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 入門2 v-on:click

v-on:clickの使い方のサンプルになります。 ボタンをクリックするとカウントが1ずつ上がります。 index.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>カウントアップ</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app1"> <p> {{ count }}回クリックしました。</p> <button v-on:click="increment">カウントを増やす</button> </div> <script> const App1 = { data() { return { //初期値を定義 count: 0 } }, methods: { increment: function(){ //count変数に+1する this.count += 1; } } } app1 = Vue.createApp(App1) app1.mount('#app1') </script> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1ページのGithub Pagesポートフォリオを(わざわざ)Vueで作ってみた

背景 就職活動に使うために、簡単なポートフォリオを自作してみました。 どうせなら何か新しい技術を学びたい、ということでVueを使ってみました。 記事の目的と対象者 僕は強いて言うならC#やC++の方が専門なので、JSはあまりよくわかっていません。 本記事はHTML/JS初心者で1ページポートフォリオを作ってみたい方の中で、Vueを使ってちょっとだけ楽ができないかな~と思っている方に、ざっくりとしたやり方と僕がハマった部分を伝える記事です。 Vue JavaScriptのフレームワークであり、いろいろなことができるらしいです。 僕はその中で、ページ内の各パーツを別々に書くことができるという利点を利用しようと考えました。 以前ホームページを作った際に、同じパーツを何度も間違いなくコピー・ペーストしたり、共通部分の修正のためにすべてのページを書きなおすことに苦戦したため、この機能はとても魅力的に見えました。 Pug・テンプレートエンジン ざっと調べた限りでは、この用途では本来Pugなどの「テンプレートエンジン」を用いるのが一般的なのだろうと思います。 今回はVueを使ってみることを主目的としていたので利用しませんでしたが、おそらく素直にPugなどを使った方が簡単に目的を達成できたと思います。 テンプレートエンジンとは簡単に言えば、Webページの「HTMLの雛形(テンプレート)」と「記事やタイトルといった具体的な表示内容」を分けて書き、後から合成してHTMLを生成する機能です。 開発環境 Windows 10 VSCode 成果物 ページ:KiryuHare's Portfolio ソースコード:KiryuHare_kiryuHare.github.io 手順 ページ制作の手順について解説します。 導入 以下の記事を参考にしつつ、設定を行っていきました。 以下のコマンドを使ってvueをダウンロードします。 yarn global add @vue/cli プロジェクトを作成します。 vue create test1 問題と解決ーnodeのバージョン 一度上述コマンドを実行した際、エラーが発生してプロジェクトを作成できませんでした。 nodeのバージョンを疑い、公式ページから再インストールを行って、再び上述コマンドを打ち込むことでプロジェクトを作成できました。 https://nodejs.org/ja/ 16.14.0 lts 動作確認 test1フォルダにVSCodeを移動し、以下のコマンドを打ってローカルのサーバーを立てて動作確認をします。 yarn run serve 初回は1分ほどサーバーの起動まで待たされた気がします。 http://localhost:8080/にアクセスし、以下のようなページが表示されるのを確認しました。 GitHub Pagesへのアップロード 以下のページを参考に、GitHub Pagesへのページのアップロードを行いました。 Github Pagesで表示するためには、Vueファイルからhtmlファイルを生成しなければなりません。 なので、yarn run buildコマンドでページをビルドし、生成したファイルをGitHub Pagesで表示可能な場所にgit pushしなければなりません。 push先の候補は複数ありますが、今回は/docs/フォルダ内にビルドすることにしました。 リポジトリのルートにvue.config.jsというファイルを作成し、次の記述を追加しました。 outputDir: で指定したフォルダにページが生成されるようになっています。 vue.config.js module.exports = { outputDir: "docs", assetsDir: "./", publicPath: "./", }; そして、前述のコマンドを実行し、 yarn run build GitHub Pagesを使うGitリポジトリにPushします。 また、GitHub PagesでMasterブランチのdocsファイル内を表示するように設定します。 以下記事などが参考になります。 ページ名の変更 プロジェクト名とページ名が同じだと不便なので、ページ名を変更します。 vue.config.jsを以下のページを参考にし、 以下のように書き換えました。 vue.config.js module.exports = { outputDir: "docs", assetsDir: "./", publicPath: "./", pages: { index: { entry: "src/main.js", title: "KiryuHare's Portfolio", } }, }; ページ内容を書く あとはページ全体・各コンポーネントとvueファイルを書き込んでいきます。 僕は初めのvueファイルの内容を一回消してしまって何を書けるのかわからず困ってしまったので、作ったコンポーネントの一つをここに書いておきます。 まずはじめにscriptタグを見て、コンポーネントが要求しているプロパティを把握しておくとよいかもしれないです。 Example.vue <template> <div class="example border" :style="{ backgroundImage: background_style }"> <div class="example-header"> <h3>{{ contents.title }}</h3> <p>{{ contents.single_explain }}</p> </div> <div class="example-body"> <div class="movie"> <iframe v-if="contents.movie_url" width="560" height="315" v-bind:src="contents.movie_url" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen ></iframe> </div> <slot></slot> </div> </div> </template> <script> export default { props: { contents: Object, }, computed: { background_style() { return ( "linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7))," + "url(" + this.contents.image + ")" ); }, }, }; </script> <style scoped> .example { width: 80%; /* background-color: #222; */ background-size: auto; } img { width: 600px; } .example-header { padding: 10px; } .example-header p { margin-left: 10px; } .example-body { padding: 30px; } .movie { display: flex; justify-content: center; } </style> css scopedの利用 各コンポーネントのcssを書く際に、css scopedを利用すればある程度class名の被りなどを気にせずにcssを書けるようなので、利用しました。 Example.vue <style scoped> .example { width: 80%; /* background-color: #222; */ background-size: auto; } img { width: 600px; } .example-header { padding: 10px; } .example-header p { margin-left: 10px; } .example-body { padding: 30px; } .movie { display: flex; justify-content: center; } </style> ただ、この機能には欠点というか、非直感的な挙動があるようです。今後は、これに悩まされることもあるかもしれません。 必要なファイルの追加 僕の場合、google adsのapp-adsをgithub pagesを利用して配置しているので、https://kiryuhare.github.io/app-ads.txtで指定したtxtファイルにアクセスできるようにする必要がありました。 /public直下にファイルを置いておくことで、ビルド後のルートにも置かれるようですので、そこに前述のファイルを置いておきました。 画像の使用で詰む 当初は、画像ファイルをassetフォルダに入れて参照しようと考えていました。 アドレスの参照方法が誤っていたため、以下のページから指定方法を変更しました。 ページ内で<img src="@/assets/img/img1.jpg"などと書いて参照しました。 しかし、同じ画像ファイルをcssでbackground-imageとして利用しようとし、方法が分からずに詰みました。 あきらめて、欲しい場所のdiv要素にcssを直書きし、画像ファイル自体はpublicに追加しました。 感想と考察 かなりオーバーパワーなことをしている感覚がありましたが、ページを生成することができました。 考察:gh-pagesを使った方法 GitHub Pagesで利用可能なアップロード場所の一つに、gh-pagesブランチがあります。 しかし、ビルドの度にわざわざ別ブランチに移動するなどの操作が面倒だったため、僕はこれまで利用したことがありませんでした。 しかし、Node JSで利用可能なgh-pagesというパッケージを利用することで、そのあたりは簡単になるようです。 ビルド前とビルド後を別々に管理できるのは魅力的なので、次回以降は検討してみたいと思っています。 考察:参照するテンプレートについて vue.config.js module.exports = { outputDir: "docs", assetsDir: "./", publicPath: "./", pages: { index: { entry: "src/main.js", title: "KiryuHare's Portfolio", template: 'public/index.html' } }, }; vue.config.jsのpages.index.templete要素では、上記のようにテンプレートを指定することができます。そこで、この指定を変えると何が起こるのか気になったので実験してみました。 pages.Aの場合、public/A.htmlがあればそれを、なければpublic/index.htmlをhtmlの雛形として利用するようです。 public/*.htmlのうち、一回も雛形として利用されなかったhtmlファイルは他のファイルと同様、docsフォルダのルートにコピーして配置されるようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

がんばってEventBusがイベントを複数回発火させてしまうのを食い止める

どうしてこの記事を書くことになったのか ページ送り機能があるアプリの中で、共通ヘッダーのコンポーネントにあるページ送りボタンから、コンテンツ部分のコンポーネントにあるページ送り+APIを呼ぶメソッドを叩く実装がありました。 そこで、以下のような事象が発生しました APIが複数回飛んでいる ページも二つ先、三つ先のページに飛んでしまう なんでこうなっちゃうの! というのを解消した記録です。 なお、ここで触れているEventBusはVue3.x系では消滅しました。 前提 node.js v14.15.5 vue.js v2.6.12 vue/cli 4.5.7 コード(ざっくり) app.vue <template> <Header /> <Content /> </template> <script> import Header ... import Content ... header.vue <template> <button @click="prev"></button> <button @click="next"></button> </template> <script> method: { prev():{ EventBus.$emit('prev'); } next():{ EventBus.$emit('next'); } </script> content.vue method: { created() { EventBus.$on('prev'); // 前のページへ行くイベントのリスナー EventBus.$on('next'); // 次のページへ行くイベントのリスナー this.getData(); }, prev(): { // postAPIを叩く処理 this.$router.push(prev); }, next(): { // postAPIを叩く処理 this.$router.push(next); }, getData(): { // getAPIを叩く処理 }, }, watch: { $route: { this.getDate(); }, } 原因調査 日本語で検索しても情報が少なそうだったので、EventBus dupulicateとかで検索してました。 引っかかったのがこのStack Overflow。 他にも似たような質問はいくつかあり、EventBusを使うとまあまあ起こる現象のようです。 こちらをもとに検証してみたところ、今回私が直面した事象は、前のページのコンポーネントのイベントリスナーが生きていて、そちらでもメソッドが実行されているということのようでした。 解消方法 結論としては、以下の4行をコンテンツ部分のコンポーネントに入れて解消しました。 コンポーネントが破棄される直前にEventBusのイベントリッスンを止めるようにする、ということですね。 beforeDestroy() { EventBus.$off('prev'); // 前のページへ行くイベントのリスナー EventBus.$off('next'); // 次のページへ行くイベントのリスナー }, 原因の原因調査 というわけで解消はしたわけですが、疑問は残ります。 コンポーネントってページ送りごとに破棄されてるんじゃないの…?(実装上は画面パスが変わったときにgetAPIを叩いて表示データを上書きしている状態だが、beforeDestroyの時に$offして解決しておりページ送りをした際にdestroyとcreatedはされている) という疑問に解消してくれるのが以下の記事でした。(上記StackOverflowのコメントの中に紹介がありましたが、元のリンクは切れているようなので、Internet Archiveに残っていたものへのリンクを貼ります。) こちらの記事によれば、 イベントバスのリスナーはグローバルに管理されているので、コンポーネントが作成されるたびに新しいリスナーが増えてしまう それぞれのリスナーはイベントが発火したときに、元のコンポーネントを再生成する。(読解に自信なし…状況的には合ってると思いますが) 複数回実行を避けるためには、コンポーネントが破棄されるときに手動でイベントリスナーも破棄する必要がある Vueの設計上、他のコンポーネントの状態は知ることができないのが基本なので、それを無理やりどうにかするような実装はやめよう とのことです。 コンポーネントが破棄されたときにイベントリスナーが破棄されない疑問が解消されました。 ちょっとEventBusの使い方を学んだだけでこの仕様を理解するのは無理があるように思いますが、そもそもEventBusを使うこと自体がVueの設計思想上のイレギュラーであると思うとやむなしのように思います。 (Vue3.x系で消されたのも納得…) まとめ EventBusのリスナーはグローバルに管理されているので、コンポーネントを破棄しただけでは破棄されず、手動で破棄する必要がある 忘れずにEventBus.$off('hoge') 賢い人「最初からVuexを使って実装すればよかったのでは?」 その通りだと思います…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む