概要
Nuxt3がすごいらしいので、引き続きToDoアプリを作りながら学んでいきたいと思います。
前回作成したコードをコンポーネント化していきたいと思います。
また、ディレクトリに関しても見ていきたいと思います。
前回の記事
この記事のゴール
前回作成したapp.vueの機能をコンポーネントに分割して実現する。
完成コード
<template>
<div>
<NuxtPage />
</div>
</template>
<template>
<div>
<h1>ToDoApp</h1>
<AddTask @add="addTask" />
<TaskList :task-name-list="taskNameList" @complete="completeTask" />
</div>
</template>
<script setup lang="ts">
const taskNameList = ref<string[]>([]);
const addTask = (taskName: string) => {
taskNameList.value.push(taskName);
}
const completeTask = (newTaskNameList: string[]) => {
taskNameList.value = newTaskNameList;
}
</script>
<template>
<h2>タスク追加</h2>
<input v-model="taskName">
<button @click="addTask">追加</button>
</template>
<script setup lang="ts">
interface Emit {
(event: 'add', value: string): void;
}
const emit = defineEmits<Emit>()
const taskName = ref<string>('')
const addTask = () => {
if (taskName.value !== '') {
emit('add', taskName.value)
}
taskName.value = ''
}
</script>
<template>
<h2>タスク名</h2>
<div v-for="taskName in taskNameList" :key="taskName">
<p>
{{ taskName }}<button @click="completeTask(taskName)">完了</button>
</p>
</div>
</template>
<script setup lang="ts">
interface Props {
taskNameList: string[]
}
interface Emit {
(event: 'complete', value: string[]): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emit>();
const completeTask = (completedTaskName: string) => {
const newTaskNameList = props.taskNameList.filter((taskName: string) => {
return completedTaskName !== taskName;
});
emit('complete', newTaskNameList)
}
</script>
ディレクトリ構成
.
├── pages
| └── index.vue
├── components
| ├── AddTask.vue
| └── TaskList.vue
└── app.vue
ディレクトリについて
Nuxt3ではディレクトリ構成が事前に決められています。環境構築時には存在していなかったディレクトリでも、追加することでディレクトリの名前に沿った役割を与えることができます。
今回使用する2つのディレクトリについて、説明しておきたいとおもいます。
Nuxt3で用意されているディレクトリ・ファイル一覧
- .nuxt
- .output
- assets
- components
- composables
- content
- layouts
- middleware
- node_modules
- pages
- plugins
- public
- server
- utils
- .env
- .gitignore
- .nuxtignore
- app.config.ts
- app.vue
- nuxt.config.ts
- package.json
- tsconfig.json
pagesディレクトリ
Nuxtでは内部でVue Routerを使用しており、ファイルベースのルーティングを提供しています。
基本的な内容はNuxt2から変わりはありません。
pages
├── index.vue
├── vegetable.vue
└── fruits
├── index.vue
├── apple.vue
└── banana
├── index.vue
└── cake.vue
例えば、上記のようなディレクトリ構成となっている場合、
6つのファイルそれぞれにアクセスするためのパスが以下のように自動的に作成されます。
-
www.example.com
-
www.example.com/vegetable
-
www.example.com/fruits
-
www.example.com/fruits/apple
-
www.example.com/fruits/banana
-
www.example.com/fruits/banana/cake
ファイル名、ディレクトリ名がそのまま階層となってパスが作成されます。
ただし、index.vueはそのディレクトリ名と同じ(一番最上位の場合は、トップページ)となります。
その他にも、動的なルーティングへの対応なども行えるため、とても便利です。
補足
pageディレクトリにファイルを作成したからといって、即使用できるわけではないです。
エントリポイントとなっているapp.vueに対して、pages以下のファイルをルーティングして読み込むことを明示しなけらばなりません。
といっても、設定は全く難しいものではなく、<NuxtPage />
を埋め込むだけで完了です。
<template>
<div>
<NuxtPage />
</div>
</template>
NuxtPageコンポーネントが用意されており、ここにルーティングされたファイルの中身が表示されます。
componentsディレクトリ
componentsディレクトリ内はコンポーネント専用のディレクトリです。
各ファイルでimportする必要なく、ディレクトリ内のコンポーネントを使用できます。
書いていく
pagesディレクトリに移動させる
前回までに記述した内容をpages/index.vueに移動させます。
また、先ほど紹介した通り、app.vue内にNuxtPageコンポーネントを設置します。
画面が移動前と変わらずに表示されていれば成功です。
コンポーネント分割
コンポーネント分割はページ内で使用されている機能を再利用しやすくするために行われます。
共通の機能を切り分け、再利用性を高めることで、AページとBページどちらにも同じ内容を記述する必要がなくなります。
componentsディレクトリを作成し、その中にコンポーネントを設置していきましょう。
コンポーネントの構成
以下の形で分割します。
追加機能: AddTaskコンポーネント
タスク名の表示: TaskNmaeListコンポーネント
TaskNmaeList.vue
分割したコンポーネントに対して、値を受け渡します。
まずはcomponents内にTaskNameListコンポーネントを作成します。
まず、タスク名を表示していた部分をコンポーネント化します。
この時に:task-name-list="taskNameList"
と記述して、scriptで定義されているtaskNameListをコンポーネントに受け渡します。
関数でいうところの引数のようなイメージです。
<template>
<div>
<h1>ToDoApp</h1>
<h2>タスク追加</h2>
<input v-model="taskName"><button @click="addTask()">追加</button>
<TaskNameList :task-name-list="taskNameList" /> // ←コンポーネント化
</div>
</template>
components内に作成したTaskNameListコンポーネントを見ていきます。
template内には切り取った内容をそのまま貼り付けます。
<template>
<h2>タスク名</h2>
<div v-for="taskName in taskNameList" :key="taskName">
<p>
{{ taskName }} <button @click="completeTask(taskName)">完了</button>
</p>
</div>
</template>
次はscriptです。
先ほど、index.vueから受け渡された値をコンポーネントで受け取る処理が必要になります。
この辺りは従来のVue2, Nuxt2とは書き方が異なります。
Vue2, Nuxt2での書き方
props: {
taskNameList: {
type: Array as () => string[],
requires: true,
},
taskNameList2: {
type: Array as PropType<string[]>,
required: true,
}
}
受け渡しに必要なのが、defineProps
です。
こちらを使用することで、受け取るプロパティを宣言します。
宣言方法にはいくつか方法があるようですが、TypeScriptでの型定義を活かすため、interfaceを用いて記述します。
interfaceでProps型を宣言します。(命名はPropsでなくても問題ありません)
Props内に呼び出し元から受け取る変数名と、型名を記述します。
先ほど定義したPropsをdefinePropsに設置しすることで、受け取ることができるようになります。
あとは、script内でもtemplate内でもtaskNameList
を使用することができます。
<script setup lang="ts">
interface Props {
taskNameList: string[]
}
defineProps<Props>();
</script>
次は、<button @click="completeTask(taskName)">完了</button>
のcompleteTaskメソッドに関してです。
こちらのコンポーネントでやりたいことは、「完了ボタンを押されたデータを削除する」ことですが、propsにより取得している、taskNameList
内のデータを削除することはできません。
コンポーネント内でプロパティを変更することは禁止されています。
ただし、データ定義を行なっている呼び出し元ではデータの修正が可能となるので、そちらでデータを修正します。ここで活躍するのがemit
です。
emit
はコンポーネント内で発火したイベントを呼び出し元に知らせる役割があります。
呼び出しもとでは、v-onディレクティブを使って発火されたイベントを検知します。
こちらも、vue2, nuxt2とでは定義方法が変わります。
Propsと同様にinterfaceでEmit型を宣言します。(命名はなんでもいいです)
呼び出し元で指定できるように、event名、引数、戻り値を定義します。
それをdefineEmits
に使用することで、emit
として定義することができます。
コンポーネント内でのemitの発火方法は、emit('event名', '親コンポーネントで受け取る引数')
となります。
emiteは関数内で定義します。
「コンポーネント内でイベント発火→sampleメソッド呼び出し→emit→呼び出し元」
というような流れになるように定義します。
<script setup>
interface Emit {
(event: 'event', value: string): void;
}
const emit = defineEmits<Emit>();
const sample = () => {
emit('event', '親コンポーネントで受け取る引数')
}
</script>
親コンポーネントでの受け取りは以下のようになります。
event名は@event
部分の命名になります。
<template>
<Sample @event="sample" />
</template>
<script>
const sample = (value: string) => { // 定義されていた引数
// 処理を行う
}
</script>
具体的に見ていきます。
また、emitは関数内で定義するため、コンポーネントで加工したデータを呼び出し元に渡すこともできます。
ここでは、完了済みのデータを削除した新しいtaskNameList
を引数に定義しています。
<script>
interface Emit {
(event: 'complete', value: string[]): void;
}
const emit = defineEmits<Emit>();
const completeTask = (completedTaskName: string) => {
const newTaskNameList = props.taskNameList.filter((taskName: string) => {
return completedTaskName !== taskName;
});
emit('complete', newTaskNameList)
}
</script>
呼び出しもとでは、Propsとして渡すデータを修正可能なので、emitで渡されてきたデータを直接taskNameListに定義します。
<template>
<TaskList :task-name-list="taskNameList" @complete="completeTask" />
</template>
<script setup lang="ts">
const completeTask = (newTaskNameList: string[]) => {
taskNameList.value = newTaskNameList;
}
</script>
これでTaskNameListは完成です。
AddTask.vue
AddTask.vueでも同様に、emitを使ってデータを追加します。
<script setup lang="ts">
interface Emit {
(event: 'add', value: string): void;
}
const emit = defineEmits<Emit>()
const taskName = ref<string>('')
const addTask = () => {
if (taskName.value !== '') {
emit('add', taskName.value)
}
taskName.value = ''
}
</script>
<template>
<AddTask @add="addTask" />
</template>
<script setup lang="ts">
const taskNameList = ref<string[]>([]);
const addTask = (taskName: string) => {
taskNameList.value.push(taskName);
}
</script>
長くなってしまいましたが、以上で完了です。
説明途中のコードは省略されている部分があります。完成系のコードは最初に載せているので、参考にしてください。
まとめ
コンポーネントの分割はこれで完了です。
長くなってしまいました。分かりづらい部分があれば申し訳ないです。
Props,Emitの定義方法が変わり、個人的には違和感がありますね。
また、interface, defineProps, defineEmit, 変数、関数と定義するものが増えているため、コードがゴチャつかないようにすることを考えていかないといけないかもしれないですね。
次回は、UIフレームワークを見ていきたいと思います。