Skip to content
This repository was archived by the owner on May 1, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions content/1.vue/4.composition-api/index.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Composables とは何か ?
# コンポーザブルとは何か ?

[Composables](https://ja.vuejs.org/guide/reusability/composables.html) とは、Vue の Composition API を活用して再利用可能な状態やロジックを定義するための機能です。Options API で主流な [mixins](https://ja.vuejs.org/api/options-composition.html#mixins) と類似したコンセプトですが、より柔軟で再利用性の高い機能を提供します。Composition API の詳しい説明は [こちら](https://ja.vuejs.org/guide/extras/composition-api-faq.html) をご参照ください。
[コンポーザブル](https://ja.vuejs.org/guide/reusability/composables.html) とは、Vue の Composition API を活用して再利用可能な状態やロジックを定義するための機能です。Options API で主流な [mixins](https://ja.vuejs.org/api/options-composition.html#mixins) と類似したコンセプトですが、より柔軟で再利用性の高い機能を提供します。Composition API の詳しい説明は [こちら](https://ja.vuejs.org/guide/extras/composition-api-faq.html) をご参照ください。

Composables の主な特徴は以下の通りです
コンポーザブルの主な特徴は以下の通りです

- **再利用可能なロジック**: Composables を使用すると、コンポーネント間で共有したいロジックや状態をモジュールとして定義し、それを簡単にインポートして使用できます。
- **関数として定義**: Composables は通常、関数として定義され、必要な状態やメソッドを返します。この関数は Vue の Composition API を使用して内部で状態管理や副作用を処理します。
- **明示的な依存関係**: Composables を使うことで、依存関係が明示的になり、どのロジックや状態がどのコンポーネントで使用されているかが明確になります。
- **再利用可能なロジック**: コンポーザブルを使用すると、コンポーネント間で共有したいロジックや状態をモジュールとして定義し、それを簡単にインポートして使用できます。
- **関数として定義**: コンポーザブルは通常、関数として定義され、必要な状態やメソッドを返します。この関数は Vue の Composition API を使用して内部で状態管理や副作用を処理します。
- **明示的な依存関係**: コンポーザブルを使うことで、依存関係が明示的になり、どのロジックや状態がどのコンポーネントで使用されているかが明確になります。

Nuxt では、`composables/` ディレクトリに Composables なロジックを格納することが多く、[自動インポート](https://nuxt.com/docs/examples/features/auto-imports) の対象になります。
Nuxt では、`composables/` ディレクトリにコンポーザブルなロジックを格納することが多く、[自動インポート](https://nuxt.com/docs/examples/features/auto-imports) の対象になります。

## チャレンジ

それでは、これらの特徴を踏まえて以下のステップでロジックを Composables として切り出し、再利用してみましょう。
それでは、これらの特徴を踏まえて以下のステップでロジックをコンポーザブルとして切り出し、再利用してみましょう。

1. 既存の vue ファイル(`app.vue`)の確認してください。
2. カウンターロジックを `composables/useCounter.ts` に切り出してください。
Expand Down
6 changes: 3 additions & 3 deletions content/1.vue/5.components/.template/files/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
import Child from './Child.vue'

const name = ref('John Doe')

Expand All @@ -12,9 +12,9 @@ function updateName(value: string) {
<div>
<h1>Parent Component</h1>
<p>Hi, {{ name }} 👋</p>
<ChildComponent
<Child
message="Hello from Parent!"
:name="name"
:name
@update:name="updateName"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
<script setup lang="ts">
defineProps<{
message: string
name: string
}>()

const name = defineModel<string>()
const emit = defineEmits<{
'update:name': [name: string]
}>()

defineSlots<{
paragraph: () => any
}>()
</script>

<template>
<div class="child-component">
<h2>Child Component</h2>
<p>{{ message }}</p>
<p><slot name="paragraph" /></p>
<input
v-model="name"
type="text"
:value="name"
@input="emit('update:name', $event.target.value)"
>
</div>
</template>
Expand Down
24 changes: 19 additions & 5 deletions content/1.vue/5.components/.template/solutions/app.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
import Child from './Child.vue'

const name = ref('John Doe')

function updateName(value: string) {
name.value = value
}
</script>

<template>
<div>
<h1>Parent Component</h1>
<p>Hi, {{ name }} 👋</p>
<ChildComponent
v-model="name"
message="Hello from Parent!"
/>
<Child
:name
@update:name="updateName"
>
<template #paragraph>
Hello from <span class="red--text">Parent!</span>
</template>
</Child>
</div>
</template>

<style scoped>
.red--text {
color: red;
}
</style>
128 changes: 103 additions & 25 deletions content/1.vue/5.components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Vue.js のコンポーネントは、UI を小さな再利用可能な部分に

## 基本的な SFC の構造

SFC は基本的に以下のような `<script>`, `<template>`, `<style>` の 3 つのセクションで構成されます。
SFC は基本的に以下のような `<script setup>`, `<template>`, `<style>` の 3 つのセクションで構成されます。

```vue
<script setup lang="ts">
Expand All @@ -36,7 +36,7 @@ p {
</style>
```

この例では、`<script>`, `<template>`, `<style>` の 3 つのセクションが使われています。
この例では、`<script setup>`, `<template>`, `<style>` の 3 つのセクションが使われています。

- `<script setup>`: コンポーネントのロジック部分を定義します。`<script setup>` を使用することで、Composition API を簡潔に書くことができます。
- `<template>`: コンポーネントのビュー部分を定義します。
Expand All @@ -48,70 +48,148 @@ p {

```vue
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
import Child from './Child.vue'
</script>

<template>
<ChildComponent />
<Child />
</template>
```

## コンポーネント間のデータの受け渡し

Vue コンポーネント間でデータをやり取りする基本的な方法として、`props` と `emit` を使用します。

- `props`: 親コンポーネントから子コンポーネントにデータを渡すための方法です。
- `emit`: 子コンポーネントから親コンポーネントにイベントを発火するための方法です。
### Props

それぞれ `defineProps`, `defineEmits` で登録します。\
使い方は右側のプレイグラウンド、または [API ドキュメント](https://ja.vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)から確認できます。
親コンポーネントから子コンポーネントにデータを渡すための方法です。

## 双方向バインディング

コンポーネント上で `v-model` を使うことで双方向バインディングを実装できます。\
以下の例では、コンポーネント中で宣言された `value` が `<input>` の値とバインドされ、`<input>` の入力値が `value` に反映されます。
まずは子コンポーネント側で `defineProps` マクロを使用し、受け取りたいデータを定義します。

```vue
<!-- Child.vue -->
<script setup lang="ts">
const value = ref('')
defineProps<{ message: string }>()
</script>
```

次に親コンポーネント側で、子コンポーネントにデータを渡すために `v-bind` ディレクティブを使用します。\
`:props名="データ"` という形式で、子コンポーネントにデータを渡すことができます。

```vue
<!-- Parent.vue -->
<template>
<input v-model="value" type="text">
<Child :message="message" />
</template>
```

また、SFC で `defineModel` を使うことで、親コンポーネントから `v-model` 経由で使用できる双方向バインディングの `props` を宣言できます。
また、props 名とデータの変数名が同名の場合は省略記法を使うことができます。

```vue
<!-- Parent.vue -->
<template>
<Child :message />
</template>
```

### Emit

子コンポーネントから親コンポーネントにイベントを発火するための方法です。

まずは子コンポーネント側で `defineEmits` マクロを使用し、発火したいイベントを定義します。
emit 関数を用いて、イベントを発火することができます。

```vue
<!-- Child.vue -->
<script setup lang="ts">
const localValue = defineModel<string>()
const emit = defineEmits<{ sendMessage: [] }>()
</script>

<template>
<input v-model="localValue" type="text">
<button type="button" @click="emit('sendMessage')">
Click me
</button>
</template>
```

発火されたイベントは親コンポーネント側で `v-on` ディレクティブを使用して受け取ることができます。

```vue
<!-- Parent.vue -->
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
function handleSendMessage() {
console.log('Message sent!')
}
</script>

const parentValue = ref('Initial Value')
<template>
<Child @send-message="handleSendMessage" />
</template>
```

以下のように、イベント発火時に子コンポーネントからデータを受け渡すこともできます。

```vue
<!-- Child.vue -->
<script setup lang="ts">
const emit = defineEmits<{ sendMessage: [string] }>()
</script>

<template>
<ChildComponent v-model="parentValue" />
<button type="button" @click="emit('sendMessage', 'Hello, Vue!')">
Click me
</button>
</template>
```

## チャレンジ
```vue
<!-- Parent.vue -->
<script setup lang="ts">
function handleSendMessage(message: string) {
console.log(message)
}
</script>

プレイグラウンドの `ChildComponent.vue` は `props` と `emit` を使って双方向バインディングを実現しています。\
これを `defineModel` を使って簡潔に書き直してみましょう。
<template>
<Child @send-message="handleSendMessage" />
</template>
```

もし手詰まりになったら、解決策を確認するためのボタンをクリックして、ヒントを得ることができます。
それぞれの詳しい [API ドキュメント](https://ja.vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)から確認することができます。

## チャレンジ

:ButtonShowSolution{.bg-faded.px4.py2.rounded.border.border-base.hover:bg-active.hover:text-primary.hover:border-primary:50}
右のプレイグラウンドでは、props と emit を使ってコンポーネント間のデータの受け渡しを行っています。\
Vue.js では [スロット](https://ja.vuejs.org/guide/components/slots.html) という機能を利用することで、親コンポーネントからコンポーネントにテンプレートを挿入することができます。\
右のプレイグラウンドを編集して、スロットを使ったテンプレートの挿入を行ってみましょう。

1. 子コンポーネント (`Child.vue`) でスロットの定義を行う\
[defineSlot マクロ](https://ja.vuejs.org/api/sfc-script-setup.html#defineslots) を使うことにより、型安全なスロットを定義することができます。\
定義ができたら、template 内で `slot` タグを配置することで渡されたテンプレートの挿入を行うことができます。

```vue
<script setup lang="ts">
defineSlots<{ paragraph: () => any }>()
</script>

<template>
<h2>Child Component</h2>
<p><slot name="paragraph" /></p>
</template>
```

2. 親コンポーネント (`app.vue`) で slot にテンプレートを挿入する\
親コンポーネント側で、子コンポーネントにテンプレートを挿入するために `v-slot` ディレクティブを使用します。\
(ここでは `v-slot` の省略記法の `#` を使用しています)

```vue
<template>
<Child>
<template #paragraph>
Hello from <span class="red--text">Parent!</span>
</template>
</Child>
</template>
```

:ButtonShowSolution{.bg-faded.px4.py2.rounded.border.border-base.hover:bg-active.hover:text-primary.hover:border-primary:50}