はじめに
以前Laravel10、vue3より、簡易な動くものを作った。
今回は、上記環境にて、もう少しvueの動きを入れる。
内容
題材は、techpit教材を用いる。
第4章のコードを参考にしている。
題材はVueCLIで動いているが、今回の環境に書き換えている。
(vueの動作の調査学習であり、style等は割愛している。)
本環境はvueのバージョンは3であるが、バージョン2の書き方で対応する。data、methods、mounted といったオプションからなるオブジェクトを使用したvue2から使用していればお馴染みの書き方となる。
追加、削除、変更を行い、親と子のコンポーネントを作り、親と子の状態の伝え方(イベントの発火)を復習する。
Vueの基本である以下の機能を使用する。
・クラスとスタイルのベインディング(v-bind:class 省略形:class)
・レンダリング(v-if、 v-for)
・イベントハンドリング(v-on 省略形 @)
まずはvueの機能を復習することを重点に置き、後でバージョン3に書き換えてみる。
(イベント発火について)
イベントの発火について少し触れる。
Vue.jsでは、子コンポーネントから親コンポーネントにデータを渡す方法として、「イベント発火」が一般的に用いられる。これはVueの「データフローの一方向性」という原則から来ている。つまり、親コンポーネントから子コンポーネントへはpropsを通じてデータが渡される一方で、子コンポーネントから親コンポーネントへはイベントを発火することで情報を伝えるという考え方である。
この原則を適用することで、データの流れが予測しやすくなり、コードの理解やデバッグが容易になる。なお、特定の状況下ではVuexのような状態管理ライブラリを使用して、複数のコンポーネント間でデータを共有するといったアプローチもある。
準備
今回環境にはBootstrap、Font Awesome、Saasが必要であり、そのインストールを行う。
今回の環境だと以下で対応する。(npm使用)
npm install bootstrap
npm install @fortawesome/fontawesome-free
npm install -D sass
app.jsに追加
import './bootstrap';
import '../css/app.css';
import { createApp } from 'vue';
import App from '../views/App.vue';
import router from './router';
import { createVuetify } from 'vuetify';
import 'vuetify/dist/vuetify.min.css';
import 'bootstrap/dist/css/bootstrap.css'←追加
import 'bootstrap/dist/js/bootstrap.js'←追加
import '@fortawesome/fontawesome-free/css/all.css'←追加
const app = createApp(App);
const vuetify = createVuetify();
app.use(router);
app.use(vuetify)
app.mount('#app');
// createApp(App).use(router).mount("#app")
Vueコンポーネント内でSCSSを使用する場合、<style>タグ
にlang="scss"属性を追加する。
コンポーネントMainPageを作り、その子コンポーネントNoteItemを作り、コンポーネント同士で動作させる。
route.jsに追加
/mainで動くようにする。
import { createRouter, createWebHistory } from "vue-router";
import AboutView from "../views/AboutView.vue"
import HomeView from "../views/HomeView.vue"
import MainPage from "../views/MainPage.vue"
const routes = [
{
path: "/about",
component: AboutView,
name: "about"
},
{
path: "/",
component: HomeView,
name: "home"
},
↓追加
{
path: "/main",
component: MainPage,
name: "main"
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes
})
export default router
実装
実装内容
/mainに接続し、ノートを追加ボタンを押下すると下記のようになる。
以下を行えるようにする。
・ノート追加
ノート追加ボタン押下で、新規ノートを追加していく。
・ノート追加に対し、コンポーネントを分離する。
propsを使ってコンポーネントへ変数を受け渡す
・マウスーオーバで処理対応
マウスオーバー時に背景色を変化させ、メニューボタンを表示する。
・ノートの削除
削除ボタン押下時にノートを削除できるようにする。
$emitを使ってコンポーネント→メインページ側にイベント発火
・ノート名の変更
ノート名を変更できるよう入力可能、表示のみの表示をeditflgを持たせコントロールする。
ノート追加
今回の目的を達成している箇所のみ記載。
ボタンクリックで、ノートデータとしてnoteListに追加し、それを表示させている。
v-on(@は省略形)、表示にあたりv-forを使用している。
<template>
<!-- ノート追加ボタン -->
↓v-on(@)でクリックイベントを発生させる
<button class="transparent" @click="onClickButtonAdd">
<i class="fas fa-plus-square"></i>ノートを追加
</button>
<!-- ノートリスト -->
↓v-forでnoteを表示する
<div class="note" v-for="note in noteList" v-bind:key="note.id">
<div class="note-icon">
<i class="fas fa-file-alt"></i>
</div>
<div class="note-name">{{note.name}}</div>
</div>
</template>
<script>
export default {
↓データ追加
data() {
return {
noteList : [],
}
},
methods: {
↓クリックしたイベントのメソッド(noteをpushにて追加)
onClickButtonAdd : function() {
this.noteList.push({
id : new Date().getTime().toString(16),
name : `新規ノート`,
})
},
},
}
</script>
コンポーネントを分離
親コンポーネントをMainPage.vueとする。
ノートリスト表示箇所をNoteItemコンポーネントとして書き換える。
NoteItemをimportで読み込み、コンポーネントとして使用できるよう相対パスで指定し追加する。
v-bindはコンポーネントに値を受け渡しを行うディレクティブ。
v-bind:keyについてはおまじない。
・親コンポーネント
resources/views/MainPage.vue
<template>
<div class="main-page">
<div class="left-menu">
<!-- ノートリスト -->
↓コンポーネント化
<NoteItem
v-for="note in noteList"
v-bind:note="note"
v-bind:key="note.id"
/>
<!-- ノート追加ボタン -->
<button class="transparent" @click="onClickButtonAdd">
<i class="fas fa-plus-square"></i>ノートを追加
</button>
</template>
<script>
↓コンポーネント呼び出し
import NoteItem from './parts/NoteItem.vue'
export default {
data() {
return {
noteList : [],
}
},
methods: {
onClickButtonAdd : function() {
this.noteList.push({
id : new Date().getTime().toString(16),
name : `新規ノート`,
})
},
},
components: {
NoteItem,
},
}
</script>
・子コンポーネント
resources/views/parts/NoteItem.vue
本来、コード内で使用する変数はdataにてまとめて定義するが、子コンポーネントとして使用することにより、propsで渡された変数も同様にコード内で使用できるようになる。
今回はnoteという変数がpropsで定義し、dataで定義することなく、{{note.name}}のように即時使用することできる。
<template>
<div class="note">
<div class="note-icon">
<i class="fas fa-file-alt"></i>
</div>
<div class="note-name">{{note.name}}</div>
</div>
</template>
<script>
↓親コンポーネントから子コンポーネントへデータを渡すときには、propsオプションを使用する
nameオプションは、必須ではなく省略可能(デバッグで役にたつ)
export default {
name: 'NoteItem',
props: [
'note',
],
}
</script>
<style scoped lang="scss">
略
</style>
マウスオーバで処理対応
以下2点を行う。
- まずノートのマウスオーバー状態を制御する。
- 次にマウスオーバー時に、追加、削除、修正等が行えるボタンを表示する。
1.ノート作成し、各ノートのマウスオーバー状態を制御する。
noteオブジェクト作成時にmouseoverフラグを持たせる。
マウスオーバした時に、そのmouseoverフラグにて、NoteItemでclassを割り当てて、表示を制御する。
・resources/views/MainPage.vue
<template>
<NoteItem v-for="note in noteList" v-bind:note="note" v-bind:key="note.id" />
︙
</template>
<script>
︙
export default {
︙
methods: {
onClickButtonAdd: function () {
console.log('aaaa')
this.noteList.push({
id: new Date().getTime().toString(16),
name: `新規ノート`,
mouseover: false,←追加
})
},
︙
}
resources/views/parts/NoteItem.vue
<template>
<div class="note"
@mouseover="onMouseOver"
@mouseleave="onMouseLeave"
v-bind:class="{mouseover: note.mouseover}"
︙
>
</template>
<script>
name: 'NoteItem',
props: [
'note',
],
methods: {
onMouseOver : function() {
this.note.mouseover = true;
},
onMouseLeave : function() {
this.note.mouseover = false;
},
},
</script>
<style scoped lang="scss">
.note {
︙
&.mouseover {
background-color: rgb(232, 231, 228);
cursor: pointer;
}
︙
</style>
新しいノートが作成されるたびにnoteListに新しいオブジェクトが追加される。
この新しいオブジェクトはid、name、およびmouseoverという3つのプロパティを持って、mouseoverに関しては、mouseoverプロパティは初期値としてfalseが設定されており、その後、ユーザーがノート要素にマウスを移動させると、onMouseOver関数が実行されてmouseoverがtrueに設定される。ユーザーがノート要素からマウスを離すと、onMouseLeave関数が実行されてmouseoverが再びfalseに設定される。
ノート要素に対するv-bind:class指令は、mouseoverプロパティの値に基づいてmouseoverクラスを動的に適用または削除する。つまり、ユーザーがノート要素にマウスを移動させると、その要素にmouseoverクラスが適用され、背景色が変わる。ユーザーがノート要素からマウスを離すと、mouseoverクラスが削除され、背景色が元に戻る。この挙動により、ユーザーがどのノートを選択しているのか視覚的にわかるようになる。
2.マウスオーバー時にメニューを表示する。
マウスオーバー時に4つのボタンを表示するようにする。
v-showを使ってnote.mouseoverの状態に応じて表示/非表示を切り替えるようにしている。4つのボタンには.button-iconというクラスを付与し、それぞれFontAwesomeを使って別々のアイコンを表示している。
・resources/views/parts/NoteItem.vue
<template>
<div class="note"
@mouseover="onMouseOver"
@mouseleave="onMouseLeave"
v-bind:class="{mouseover: note.mouseover}"
>
<div class="note-icon">
<i class="fas fa-file-alt"></i>
</div>
<div class="note-name">{{note.name}}</div>
↓ここを追加
<div v-show="note.mouseover" class="buttons">
<div class="button-icon">
<i class="fas fa-sitemap"></i>
</div>
<div class="button-icon">
<i class="fas fa-plus-circle"></i>
</div>
<div class="button-icon">
<i class="fas fa-edit"></i>
</div>
<div class="button-icon">
<i class="fas fa-trash"></i>
</div>
</div>
</div>
</template>
︙
(略)
︙
<style scoped lang="scss">
.note {
width: 100%;
margin: 10px 0;
display: flex;
align-items: center;
padding: 5px;
color: rgba(25, 23, 17, 0.6);
&.mouseover {
background-color: rgb(232, 231, 228);
cursor: pointer;
}
.note-icon {
margin-left: 10px;
}
.note-name {
width: 100%;
padding: 3px 10px;
}
↓ボタンクラス
.buttons {
display: flex;
flex-direction: row;
.button-icon {
padding: 3px;
margin-left: 3px;
border-radius: 5px;
}
}
}
</style>
ノートを削除
削除ボタンをクリックし、該当ノートを削除する。
子コンポーネント側からイベントの発火を親が受け取り、nodeListより削除する。
・resources/views/MainPage.vue
<template>
<!-- ノートリスト -->
<NoteItem
v-for="note in noteList"
v-bind:note="note"
v-bind:key="note.id"
@delete="onDeleteNote"←イベント処理
/>
<!-- ノート追加ボタン -->
</div>
<div class="right-view">
右ビュー
</div>
</div>
</template>
<script>
import NoteItem from './parts/NoteItem.vue'
export default {
data() {
return {
noteList : [],
}
},
methods: {
↓イベント処理
onDeleteNote : function(deleteNote) {
const index = this.noteList.indexOf(deleteNote);
this.noteList.splice(index, 1);
},
},
components: {
NoteItem
}
}
</script>
(略)
resources/views/parts/NoteItem.vue
<template>
<div v-show="note.mouseover" class="buttons">
<div class="button-icon">
<i class="fas fa-sitemap"></i>
</div>
<div class="button-icon">
<i class="fas fa-plus-circle"></i>
</div>
<div class="button-icon">
<i class="fas fa-edit"></i>
</div>
<div class="button-icon">
↓削除ボタン箇所
<div class="button-icon" @click="onClickDelete(note)">
<i class="fas fa-trash"></i>
</div>
</div>
</div>
</template>
<script>
export default {
methods: {
↓削除を親に伝える
onClickDelete : function(note) {
this.$emit('delete', note);
},
},
}
</script>
(略)
NoteItemコンポーネント内のonClickDeleteメソッドは、削除ボタンがクリックされたときに実行される。このメソッド内でthis.$emit('delete', note)が実行されると、親コンポーネントに対してdeleteという名前のイベントが発生し、削除するノートオブジェクトがそのイベントのデータとして送信される。
MainPageコンポーネント内では、各noteItemに対して@delete="onDeleteNote""という形でdeleteイベントリスナが設定されている。これにより、子コンポーネントから発生したdeleteイベントがキャッチされ、そのイベントデータ(削除するノート)がonDeleteNoteメソッドの引数として渡される。
onDeleteNoteメソッド内では、削除するノートがnoteList内のどの位置にあるかをindexOfメソッドで取得し、そのインデックスを使ってspliceメソッドで該当のノートをnoteListから削除する。
以上が、削除ボタンのクリックからノートの削除までの一連の流れになる。
つまり、Vueのカスタムイベントと配列の操作を用いて、特定のノートの削除を実現している。
ノートの名前を変える
ノートの名前を変えれるようにする。
修正が可能な状態にすること、編集不可の状態にすること、
noteに編集フラグを追加する。
見た目を変えるようにする。
編集中の場合は、他のボタンを表示しない、というようにv-ifで表示を変える。
resources/views/MainPage.vue
<template>
<div class="left-menu" @click.self="onEditNoteEnd()">←クリックした時
<!-- ノートリスト -->
<NoteItem
v-for="note in noteList"
v-bind:note="note"
v-bind:key="note.id"
@delete="onDeleteNote"
@editStart="onEditNoteStart"←子コンポーネントからのイベントを対応
@editEnd="onEditNoteEnd"←子コンポーネントからのイベントを対応
/>
<!-- 追加ボタン -->
</div>
</template>
<script>
export default {
methods: {
onClickButtonAdd : function() {
this.noteList.push({
id : new Date().getTime().toString(16),
name : `新規ノート`,
mouseover : false,
editing : false,←編集状態として追加する
})
console.log(this.noteList);
},
↓イベント発火で動くメソッドであり、編集状態にする(フラグで操作)
onEditNoteStart : function(editNote) {
for (let note of this.noteList) {
note.editing = (note.id === editNote.id);
}
},
↓イベント発火で動くメソッドであり、編集状態を止める(フラグで操作)
onEditNoteEnd : function() {
for (let note of this.noteList) {
note.editing = false;
}
},
components: {
NoteItem
}
}
</script>
(略)
・resources/views/parts/NoteItem.vue
<template>
<div class="note"
@mouseover="onMouseOver"
@mouseleave="onMouseLeave"
v-bind:class="{mouseover: note.mouseover && !note.editing}"←マウスオーバ以外に編集状態で表示を変える
>
<template v-if="note.editing">←編集状態の時
<input v-model="note.name" class="transparent" @keypress.enter="onEditEnd" />
</template>
<template v-else>←以下編集状態ではないとき、今までと同様
・・・ 表記割愛
</template>
</template>
<script>
export default {
name: 'NoteItem',
props: [
'note',
],
methods: {
↓クリックすると、持っている状態noteを、親にeditStartイベントを発火する
onClickEdit : function(note) {
this.$emit('editStart', note);
},
↓クリックすると、持っている状態noteを、親にeditEndイベントを発火する
onEditEnd : function() {
this.$emit('editEnd');
},
},
}
</script>
(略)
MainPage.vueでは、ノートが作成される度にそのノートが編集中か否かを判断するeditingフラグを各ノートオブジェクトに追加している。
onEditNoteStartメソッドでは、引数で渡されたノートのidが一致するノートのeditingフラグをtrueに設定し、それ以外のノートのeditingフラグはfalseに設定する
NoteItem.vueでは、各ノートがマウスオーバーされたときかつそのノートが編集中ではない時、特定のクラスが適用される(style記載は割愛している)。また、ノートが編集中であるかどうかによって、表示するテンプレートを切り替えている。
onClickEditメソッドとonEditEndメソッドでは、それぞれeditStartとeditEndのイベントを発火する。これにより親コンポーネントに状態変化を伝える。
これにより、各ノートの編集状態の制御と、その状態に基づく表示の変更が行われ、これらの変更はVueのリアクティブシステムにより自動的に反映される。
終わりに
今回はLaravel10でのデータやVue3の扱いの前に、そもそものvueの機能を整理した。vueの機能の復習にはなったと考えている。
ただ引用箇所記載の少なさや、題材の扱い方について懸念が残る。不都合あればご指摘ください。