Vue3でVuetifyをインストールしよう

Vuetifyのインストール

Vueを使い始めてUIライブラリを探すと色々出てきますね。PrimeVue、BootstrapVue、Quasar、Buefyなど他にも選択肢はあります。今日はそのうちでも人気のVuetifyのインストールから基本的な使い方まで説明します。 Vue3の良いところ VueはReactと比べて初心者向けという位置づけで認識されがちです。しかし機能的にはReactに劣らず、書くコードの数も減ることがよくあります。個人的な意見になりますが、ReactのようにFacebookのような大企業のブランドがバックアップしていないというだけでマーケティングで負けてしまったように受けられます。 Vueは素晴らしいフレームワークで仕事でReactに縛られるという事がなければVueを使っていくべきだと思います。 なぜVuetify ①無料で使える Vuetifyはソースコードを公開しているオープンソースになります。MITライセンスなので安心して商用で使えます。必要があればソースコードをいじることもOKです。 ②フレキシブルなコンポーネント コンポーネントのカスタマイズがしやすいように作られているため個性を出しやすいです。また、Googleのマテリアルデザインをテーマに作成されているのでスッキリとした見た目でデザインができます。 ③ツールが豊富 FigmaのUIコンポーネントキットが使えます。(Figmaの使い方はこちら) Viteのビルドツールで作成されたVueにVuetifyを実装できます。 TypeScriptとJavaScriptの両方で使えます。 もし、わからに事があればDiscordのコミュニティに参加して質問ができる。などなど ViteでVueプロジェクトの作成 今回はVue CLIではなくViteを使います。Viteがスタンダードになってきているので是非使ってください。 yarnを使ってインストールする場合は次の記事を見てください。 アプリ名は自分の好きなものを付けてください。 ではプロジェクトフォルダに移動してライブラリをインストールします。 YarnでVuetifyをインストール yarnを使っている人は下記のコマンドで一発でVueとVuetifyがインストールできます。 このあとにプロジェクトフォルダに移動してテストサーバーを起動できます。 Vuetifyのインストール ではnpmを使ってVuetifyをインストールします。yarnを使わない人はこちらを使ってください。最初にも書いたようにViteでVueのプロジェクトを先に作成しておいてください。 Vuetifyの設定 では、VuetifyをインストールしたところでVue3で使えるように設定していきます。 ではsrc/main.jsを下記の様に上書きしましょう。 これで全部のコンポーネントをインポートしていますが、好みで必要なコンポーネントだけインポートすることも可能です。 ではテストサーバーを実行しておきましょう。 コンポーネントの使い方 ではsrc/App.vueにVuetifyのコンポーネントを入れてみます。 このようにグローバルでVuetifyのコンポーネントが使えるようになりました。 コンポーネントごとで使えるprosや設定が変わってくるので詳しくはドキュメンテーションを確認するようにしましょう。 スペーシング またスタイルを変更したい場合は下記の様にクラスを追加することでmarginやpaddingを変更することができます。 このようにma-5(マージン オール)のクラスを追加することで簡単にスタイルが変更することができましたね。 詳しいスペースの使い方です。 このma-5は下記の要素が結合したものです。 {property}{direction}-{size} propertyの箇所は: directionの箇所は: sizeの箇所は 4pxずつ増えていきます: Breakpoints(ブレイクポイント) Flexboxを使ったリスポンシブなクラスを設定することができます。 デバイス コード タイプ レンジ エキストラスモール xs モバイル < 600px … Read more

[Vue入門] asyncコンポーネント

基本的な使い方

大規模なアプリケーションでは、アプリを小さなチャンクに分割し、必要なときにのみサーバーからコンポーネントを読み込む必要があるかもしれません。これを実現するために、Vue には defineAsyncComponent 関数があります:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...サーバーからコンポーネントを読み込む
    resolve(/* 読み込まれたコンポーネント */)
  })
})
// ... `AsyncComp` を普通のコンポーネントと同じように使用する

このように、defineAsyncComponent は Promise を返すローダー関数を受け取ります。Promise の resolve コールバックは、コンポーネントの定義をサーバーから取得したときに呼ばれます。読み込みが失敗したことを示すために、reject(reason) を呼ぶこともできます。

ES モジュールの動的インポート も Promise を返すためにほとんどの場合には defineAsyncComponent と合わせて使用します。Vite や webpack などのバンドラーもこの構文をサポートしているため、の Vue SFC をインポートするためにも使用できます。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

結果的に得られる AsyncComp は、実際にページ上にレンダリングされるときにローダー関数を呼ぶだけのラッパーコンポーネントです。さらに、内側のコンポーネントに任意の props を渡せるため、非同期ラッパーを使用すると、コンポーネントをシームレスに置換するとともに、遅延読み込みも実現できます。

ローディングとエラーの状態

非同期の操作は必然的にローディングとエラーの状態に関係してきます。そのため、defineAsyncComponent() ではこれらの状態のハンドリングを高度なオプションによりサポートしています。

const AsyncComp = defineAsyncComponent({
  // ローダー関数
  loader: () => import('./Foo.vue'),

  // 非同期コンポーネントの読み込み中に使用するコンポーネント
  loadingComponent: LoadingComponent,
  // ローディングコンポーネント表示前の遅延。デフォルト: 200ms。
  delay: 200,

  // 読み込みに失敗した場合に使用するコンポーネント
  errorComponent: ErrorComponent,
  // エラーコンポーネントは timeout が与えられて
  // その時間を超えた場合に表示される。デフォルト: Infinity。  
  timeout: 3000
})

ローディングコンポーネントが与えられた場合、内側のコンポーネントが読み込まれている間に表示されます。ローディングコンポーネントが表示されるまでに、デフォルトで 200ms の遅延があります。このようになっているのは、高速なネットワークではローディング状態が短く、置き換えが速すぎて、ちらつきのように見えてしまう恐れがあるためです。

エラーコンポーネントが与えられた場合、ローダー関数から返された Promise が reject されたときに表示されます。リクエストが長すぎる場合にエラーコンポーネントを表示するために、timeout を指定することもできます。

Suspense とともに使用する

非同期コンポーネントは、ビルトインコンポーネント <Suspense> とともに使用することもできます。

[Vue入門]Provide / Inject

Prop の過剰的な使用(Prop Drilling)

通常、親コンポーネントから子コンポーネントにデータを渡す必要がある場合、props を使用します。しかし、大きなコンポーネントツリーがあり、深くネスト化されたコンポーネントが遠い場合に親元から複数先のコンポーネントにデータを飛ばしたいシナリオを想定してください。

propsのやり方だと、親コンポーネントからつながるすべてのcomponentに同じ prop を渡さなければなりません:

<Footer> コンポーネントは親から受け取るの props を全く使わない場合でもデータの渡し役として、コンポーネントを記載する必要があります。それから<DeepChild> が<Footer>コンポーネントからこのデータにアクセスできるようにしてやっと<Root>コンポーネントのデータを受け取ることができるようになります。これでは、コードが煩雑になり、デバッグの作業も面倒になりますね。

このPropsを何階層も下に投げる作業を省くには provide と inject を使うことで解決できます。親コンポーネントは、そのすべての子コンポーネントに対して 依存関係を提供するプロバイダー (dependency provider) として機能することができます。子ツリー内のどのコンポーネントも、その深さに関係なく、親チェーン内の上位コンポーネントが提供する依存性を注入 (inject) することができます。

Provide

親コンポーネントから子コンポーネントにデータを提供するには provide() 関数を使います:

<script setup>
import { provide } from 'vue'

provide(/* key */ 'message', /* value */ 'hello!')
</script>

<script setup> を使わない場合、setup() 内で provide() が同期的に呼び出されていることを確認してください:

import { provide } from 'vue'

export default {
  setup() {
    provide(/* key */ 'message', /* value */ 'hello!')
  }
}

provide() 関数は 2 つの引数を受け付けます。第 1 引数はインジェクションキーと呼ばれ、文字列または Symbol となります。provideで使用するデータを投げるために使うニックネームのようなものですね。

このインジェクションキーは、子のコンポーネントが、インジェクション(注入)に必要な値を探すのに使われます。1 つのコンポーネントが異なる値を提供するために、異なるインジェクションキーで provide() を複数回呼び出すことができます。

第 2 引数は提供される値です。この値は refs のようなリアクティブな状態を含む、任意の型にすることができます:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

リアクティブな値を提供することで、提供された値を使用する子孫コンポーネントが、プロバイダーコンポーネントとのリアクティブな接続を確立することができます。

アプリケーションレベルの Provide

コンポーネント内だけでなく、アプリケーションレベルでデータを提供することも可能です:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

アプリケーションレベルの Provide は、アプリケーションでレンダリングされるすべてのコンポーネントで利用可能です。これは特にプラグインを書くときに便利です。プラグインは通常、コンポーネントを使ってデータを提供することができないからです。

Inject

親コンポーネントが提供するデータを注入するには inject() 関数を使用します:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

提供された値が ref である場合、そのまま注入され、自動的にアンラップされることはありません。これにより、インジェクターコンポーネントはプロバイダーコンポーネントとのリアクティビティの接続を保持することができます。

繰り返しますが、もし <script setup> を使用しないのであれば、inject() は setup() の内部でのみ同期的に呼び出す必要があります:

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}

インジェクションのデフォルト値

デフォルトでは、inject は注入されるキーが親チェーンのどこかで提供されることを想定しています。キーが提供されていない場合、実行時が出ます。

インジェクトされたプロパティをオプションのプロバイダーで動作させたい場合は、props と同様にデフォルト値を宣言する必要があります:

// もし "message" にマッチするデータがなかった場合は、
// `value` は "default value" になります
const value = inject('message', 'default value')

場合によっては、関数を呼び出したり、新しいクラスをインスタンス化したりして、デフォルト値を作成する必要があるかもしれません。オプションの値が使用されないケースで不要な計算や副作用を避けるために、デフォルト値を作成するためのファクトリー関数を使用することができます:

const value = inject('key', () => new ExpensiveClass())

リアクティビティと共に利用する

リアクティブな値を provide / inject する場合、可能な限り、リアクティブな状態への変更を provider の内部で維持することが推奨されます。これは、提供されるステートとその可能な変更が同じコンポーネントに配置されることを保証し、将来のメンテナンスをより容易にするためです。

インジェクターコンポーネントからデータを更新する必要がある場合があります。そのような場合は、状態の変更を担当する関数を使うことをおすすめします:

<!-- プロバイダーコンポーネント内部 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- インジェクターコンポーネント内部 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最後に、provide を通して渡されたデータが注入されたコンポーネントによって変更されないようにしたい場合は、提供された値を readonly() でラップすることができます。

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

シンボルキーと共に利用する

今までの例では、文字列のインジェクションキーを使っていました。もしあなたが多くの依存関係を提供するプロバイダーを持つ大規模なアプリケーションで作業していたり、他の開発者が使用する予定のコンポーネントを作成している場合は、名前の重複を避けるためにシンボルインジェクションキーを使用するのがベストです。

シンボルは専用のファイルに書き出しておくことをおすすめします:

// keys.js
export const myInjectionKey = Symbol()
// プロバイダーコンポーネント内
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, {
  /* 提供するデータ */
})
// インジェクターコンポーネント内
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

[Vue入門] スロット

このページは、コンポーネントの基本をすでに読んでいることを前提としています。コンポーネントを初めて使用する場合は、最初にそれをお読みください。

スロットコンテンツとその出力先

コンポーネントはpropsを受け入れることができることを学びましたね。propは色々なタイプのJavaScriptのデータを格納することができます。しかし、テンプレートコンテンツはどうでしょうか?再利用可能なコンポーネントを作成するうえで、親のコンポーネントから自在に意図した場所にデータをレンダーしたい場合にスロットが役に立ちます。

たとえば、次の<FancyButton>ような使用法をサポートするコンポーネントがある場合があるとします。

<FancyButton>
  Click me! <!-- slot content -->
</FancyButton>

のテンプレートは<FancyButton>次のようになります。

<button class="fancy-btn">
  <slot></slot> <!-- slot outlet -->
</button>

この<slot>エレメントは、親が提供するスロットコンテンツをレンダリングする場所を示すスロットアウトレットです。

いわば枠組みを作ってあげてその中(スロット)に意図したデータを投げてあげるわけですね。

そして、最終的にレンダリングされたDOMはこのようになります。:

<button class="fancy-btn">
  Click me!
</button>

スロットを使用すると、<FancyButton>は外側(およびその派手なスタイリング)のレンダリングを担当し、<button>内側のコンテンツは親コンポーネントによって提供されます。

スロットを理解するもう1つの方法として、スロットをJavaScript関数と比較することができます。

// parent component passing slot content
FancyButton('Click me!')

// FancyButton renders slot content in its own template
function FancyButton(slotContent) {
  return (
    `<button class="fancy-btn">
      ${slotContent}
    </button>`
  )
}

スロットコンテンツはテキストだけに限定されません。有効なテンプレートコンテンツであれば何でもOKです。たとえば、複数のHTMLエレメント、または他のコンポーネントを渡すことができます。

<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

スロットを使用することで、<FancyButton>をより柔軟で再利用可能なコンポーネントとして使用できるようになります。

Vueコンポーネントのスロットメカニズムは、ネイティブのWebコンポーネント<slot>要素で記載されていますが、後で説明する他の機能も備えています。

レンダリングスコープ

スロットコンテンツは、親で定義されているため、親コンポーネントのデータスコープにアクセスできます。例えば:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

ここでは、両方の{{ message }}補間で同じコンテンツがレンダリングされます。

スロットコンテンツは、子コンポーネントのデータにアクセスできません。原則として、次の点に注意してください。

親テンプレートのすべてが親スコープでコンパイルされ、子テンプレートのすべてが子スコープでコンパイルされます。

スロット内容が指定されていない場合の初期値

コンテンツが提供されていない場合にのみレンダリングされるように、スロットのフォールバック(つまりデフォルト)コンテンツを指定できるという便利な機能があります。たとえば、<SubmitButton>コンポーネントでは次のようになります。

<button type="submit">
  <slot></slot>
</button>

<button>親コンポーネントでスロットコンテンツを提供しなかった場合は、「Submit」というテキストをの中に表示したい場合を想定します。<slot>タグの間にフォールバックコンテンツ(スロット内容が指定されていない場合の初期値)を記載することで初期値を「Submit」にできます。

<button type="submit">
  <slot>
    Submit <!-- fallback content -->
  </slot>
</button>

親コンポーネントで使用する場合<SubmitButton>、スロットにコンテンツを提供なかった場合。

<SubmitButton />

これにより、フォールバックコンテンツ(デフォルト値)「Submit」がレンダリングされます。

<button type="submit">Submit</button>

ただし、スロットコンテンツを提供する場合:

<SubmitButton>Save</SubmitButton>

次に、提供されたコンテンツが代わりにレンダリングされます。

<button type="submit">Save</button>

複数のスロットがある場合

1つのコンポーネントに複数のスロットがあると便利な場合があります。次のテンプレート<BaseLayout>を使用するコンポーネントでは、次のようになります。

<div class="container">
  <header>
    <!-- We want header content here -->
  </header>
  <main>
    <!-- We want main content here -->
  </main>
  <footer>
    <!-- We want footer content here -->
  </footer>
</div>

このような場合、<slot>エレメントには特別な属性として、nameというものがあります。これを使用して、コンテンツをレンダリングする場所を指定できるようになり、さまざまなスロットに意図したIDを割り当てることができます。

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

例を見てわかるようにスロットネームのないデフォルト用のの<slot>があります。name slotを使用する親コンポーネント<BaseLayout>では、それぞれが異なるスロット出力先をターゲットとする複数のスロット出力先に提供する方法が必要です。ここで名前付きスロットの出番です。

名前付きスロットを渡すには、ディレクティブで<template>要素を使用してから、v-slotを使ってスロットの名前を引数として次のように渡す必要があります。

<BaseLayout>
  <template v-slot:header>
    <!-- content for the header slot -->
  </template>
</BaseLayout>

v-slotは省略して#記載できるので、<template v-slot:header>から<template #header>と記載することができます。

3つのスロットすべてのコンテンツを<BaseLayout>省略構文を使用して渡す場合は次のとおりになります。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

コンポーネントがデフォルトスロットと名前付きスロットの両方を受け入れる場合、すべての上階層の<template>ではないノード(HTML)は自動的にデフォルトスロットのコンテンツとして扱われます。したがって、上記は次のように書くこともできます。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- implicit default slot -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

これで、エレメント内のすべて<template>が対応するスロットに渡されます。最終的にレンダリングされるHTMLは次のようになります。

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

繰り返しになりますが、JavaScript関数の例を使用すると、名前付きスロットをよりよく理解するのに役立つ場合と思います。

// passing multiple slot fragments with different names
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renders them in different places
function BaseLayout(slots) {
  return (
    `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
  )
}

ダイナミックなスロット名

動的ディレクティブ引数はv-slotでも使用可能で、動的スロット名の定義をすることができます。

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- with shorthand -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

式は動的ディレクティブ引数の構文制約に従うことに注意してください。

スコープスロット

レンダリングスコープで説明したように、スロットコンテンツは子コンポーネントの状態にアクセスできません。

ただし、スロットのコンテンツが親スコープと子スコープの両方のデータを利用できると便利な場合があります。これを実現するには、子がデータをレンダリングするときにスロットにデータを渡す方法が必要になります。

これは、Propsをコンポーネントに渡すのと同じように、属性をスロットアウトレットに渡すことがで実現可能になります。:

<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

単一のデフォルトスロットを使用する場合と名前付きスロットを使用する場合では、スロットプロップの受信の仕方が少し異なります。v-slot子コンポーネントタグを直接使用して、最初に単一のデフォルトスロットを使用してPropsを受け取る方法を記載します。

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

子によってスロットに渡されるpropsは、対応するv-slotディレクティブの値として使用でき、スロット内の式からアクセスできます。

スコープスロットは、子コンポーネントに渡される関数と考えることができます。次に、子コンポーネントはそれを呼び出し、Propsを引数として渡します。

MyComponent({
  // passing the default slot, but as a function
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return (
    `<div>${
      // call the slot function with props!
      slots.default({ text: greetingMessage, count: 1 })
    }</div>`
  )
}

実際、これはスコープ付きスロットのコンパイル方法、および手動レンダリング関数でスコープ付きスロットを使用する方法に非常に近いものです。

スロット機能のシグネチャとv-slot=”slotProps”がどのように一致するかに注意してください。関数の引数と同じように、v-slot内で次の場所で各データを使用できます。

<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

名前付きスコープスロット

名前付きスコープスロットも同様に機能します。スロットのPropsは、v-slotディレクティブの値としてアクセスできますv-slot:name="slotProps"。省略した記載方法を使用すると、次のようになります。

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

名前付きスロットにPropsを渡す:

<slot name="header" message="hello"></slot>

nameスロットでnameというストリングが使用されるため、nameというプロップは制限され、Propsには含まれないことに注意してください。そのため、結果headerPropsは{ message: ‘hello’ }になります。

ファンシーリストの例

スコープ付きスロットの良い使い方は何か疑問に思われるかもしれません。次に例を記載します。<FancyList>アイテムのリストをレンダリングするコンポーネントを想像してみてください。リモートデータの読み込み、データを使用したリストの表示、さらにはページネーションや無限スクロールなどの高度な機能のロジックをカプセル化できます。ただし、各アイテムの外観に柔軟性を持たせ、各アイテムのスタイルをそれを使用する親コンポーネントに任せる必要があります。したがって、望ましい使用法は次のようになります。

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

<FancyList>内部では、異なるアイテムデータを使用して同じものを複数回レンダリングできます(オブジェクトをスロットプロップとして渡すために<slot>v-bindを使用していることに注意してください)。

<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

レンダーレスコンポーネント

上で説明した<FancyList>ユースケースは、再利用可能なロジック(データのフェッチ、ページネーションなど)とビジュアル出力の両方をカプセル化し、スコープ付きスロットを介してビジュアル出力の一部をコンシューマーコンポーネントとして使用したものになります。

この概念をもう少しひねると、ロジックのみをカプセル化し、それ自体では何もレンダリングしないコンポーネントを作ることができます。ビジュアル出力は、スコープスロットを備えたコンシューマーコンポーネントに完全に任されることになります。このタイプのコンポーネントをレンダーレスコンポーネントと呼びます。

レンダーレスコンポーネントの例としては、現在のマウス位置を追跡するロジックをカプセル化したものがあります。

<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

興味深いパターンですが、レンダリングレスコンポーネントで実現できることのほとんどは、余分なコンポーネントのネストのオーバーヘッドを発生させることなく、CompositionAPIを使用してより効率的な方法で実現できます。後で、Composableと同じマウス追跡機能を実装する方法を説明します。

まとめとしてスコープスロットは、<FancyList>の例のように、ロジックの格納化とビジュアル出力の作成の両方が必要な場合で役立つことが分かりますね。

[Vue入門] フォールスルー属性

このページは、コンポーネントの基礎をすでに読んでいることを想定して説明しています。初めてコンポーネントに触れる方は、まずそちらをお読みください。

属性の継承

“フォールスルー属性”とは、あるコンポーネントに渡されたものの、受け取ったコンポーネントの props や emits で明確に宣言されていない属性、または v-on イベントリスナーを指します。よくある例としては、classstyleid 属性などがあります。

コンポーネントが単一のルート要素をレンダリングする時、フォールスルー属性は自動的にルート要素の属性に追加されます。例えば、次のようなテンプレートを持つ <MyButton> コンポーネントがあったとします:

<!-- <MyButton> のテンプレート -->
<button>click me</button>

そして、このコンポーネントを使う親が以下です:

<MyButton class="large" />

最終的に DOM は以下のようにレンダリングされます:

<button class="large">click me</button>

class と style のマージ

もし、子コンポーネントのルート要素にすでに class や style 属性がある場合は、親から継承された class や style の値にマージされます。先ほどの例の <MyButton> のテンプレートを次のように変更するとします:

<!-- <MyButton> の テンプレート -->
<button class="btn">click me</button>

そうすると、最終的にレンダリングされる DOM は、こうなります:

<button class="btn large">click me</button>

v-on リスナーの継承

同じルールが v-on イベントリスナーにも適用されます:

<MyButton @click="onClick" />

click リスナーは <MyButton> のルート要素、つまりネイティブの <button> 要素に追加されます。ネイティブの <button> がクリックされた時、親コンポーネントの onClick メソッドがトリガーされます。もし、ネイティブの <button> が既に v-on でバインドされた click リスナーを持っている場合、両方のリスナーがトリガーされます。

ネストされたコンポーネントの継承

あるコンポーネントが他の 1 つのコンポーネントをルートノードとしてレンダリングする場合を考えてみましょう。例として、<MyButton> をルートとして <BaseButton> をレンダリングするようにリファクタリングしました:

<!-- シンプルに他の 1 つのコンポーネントをレンダリングする <MyButton/> のテンプレート -->
<BaseButton />

この時、<MyButton> が受け取ったフォールスルー属性は、自動的に <BaseButton> に転送されます。

以下の点に注意してください:

  1. 転送された属性には、<MyButton> が props として宣言した属性や、宣言したイベントの v-on リスナーは含まれません。言い換えると、宣言した props とリスナーは <MyButton> によって “消費” されています。
  2. 転送された属性は、 <BaseButton> が宣言していれば、 props として受け取ることができます。

属性の継承の無効化

コンポーネントに自動的な属性の継承をさせたくない場合は、コンポーネントのオプションで inheritAttrs: false を設定することができます。

<script setup> を使用するなら、このオプションは別の通常の <script> ブロックを使って宣言する必要があります:

<script>
// 通常の <script> でオプションを宣言
export default {
  inheritAttrs: false
}
</script>

<script setup>
// ロジックのセットアップ
</script>

属性の継承を無効にする一般的なシナリオは、ルートノード以外の要素に属性を適用する必要がある場合です。 inheritAttrs オプションを false に設定することで、フォールスルー属性を適用する場所を完全に制御することができます。

これらのフォールスルー属性は、テンプレート式で $attrs として直接アクセスすることができます:

<span>Fallthrough attributes: {{ $attrs }}</span>

$attrs オブジェクトには、コンポーネントの props や emits オプションで宣言されていないすべての属性 (例えば classstylev-on リスナーなど) が含まれます。

備考:

  • props とは異なり、フォールスルー属性は JavaScript では元のケーシングを保持します。したがって、 foo-bar のような属性は $attrs['foo-bar'] としてアクセスされる必要があります。
  • @click のような v-on イベントリスナーは、オブジェクトで $attrs.onClick という関数として公開されます。

前のセクションで紹介した <MyButton> コンポーネントの例では、スタイリングのために実際の <button> 要素を <div> でラップする必要がある場合があります:

<div class="btn-wrapper">
  <button class="btn">click me</button>
</div>

class や v-on リスナーなどのすべてのフォールスルー属性を、外側の <div> ではなく、内側の <button> に適用されるようにしたいです。これは、 inheritAttrs: false と v-bind="$attrs" で実現できます:

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>

引数なしの v-bind はオブジェクトのすべてのプロパティをターゲット要素の属性としてバインドすることを覚えておきましょう。

複数のルートノードでの属性継承

ルートノードが 1 つのコンポーネントと異なり、複数のルートノードを持つコンポーネントは、自動的に属性をフォールスルーするふるまいがありません。 $attrs が明示的にバインドされていない場合は、実行時に警告が出ます。

<CustomLayout id="custom-layout" @click="changeValue" />

もし <CustomLayout> が以下のようなマルチルートのテンプレートを持っている場合、 Vue はどこにフォールスルー属性を適用すればよいか分からないため、警告されます:

<header>...</header>
<main>...</main>
<footer>...</footer>

警告は $attrs が明示的にバインドされている場合は抑制されます:

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

JavaScript 内でフォールスルー属性にアクセスする

必要であれば、<script setup> 内で useAttrs() API を使用してコンポーネントのフォールスルー属性にアクセスすることができます:

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

もし <script setup> を使用していない場合、 attrs は setup() コンテキストのプロパティとして公開されます:

export default {
  setup(props, ctx) {
    // フォールスルー属性が ctx.attrs として公開される
    console.log(ctx.attrs)
  }
}

ここで attrs オブジェクトは常に最新のフォールスルー属性を反映していますが、リアクティブではないことに注意してください(パフォーマンス上の理由です)。ウォッチャーを使ってその変更を監視することはできません。リアクティビティが必要であれば、 prop を使ってください。または、 onUpdated() を使用して、更新されるたびに最新の attrs による副作用を実行することもできます。

[Vue入門] Emitの使い方

このページは、コンポーネントの基本をすでに読んでいることを前提としています。コンポーネントを初めて使用する場合は、最初にそれをお読みください。

イベントの送信とリスニング

$emitを使用してコンポーネントからv-onでバインドし、テンプレートにカスタムイベントを直接設定することができます。

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

次に、親はv-onを使用して子のcomponentから発信されたイベントを聞くことができます。

<MyComponent @some-event="callback" />

修飾子は、.onceコンポーネントイベントリスナーでもサポートされています。

<MyComponent @some-event.once="callback" />

コンポーネントやPropsと同様に、イベント名はケースの自動変換を提供します。キャメルケースイベントを使用しても、親のケバブケースのリスナーを使用してそれをどちらでもVueで同じものとして扱われることに注意してください。Propsのケーシングと同様に、テンプレートではケバブケースのイベントリスナーを使用することをお勧めします。

ヒント

ネイティブDOMイベントとは異なり、コンポーネントが発行するイベントはバブルしません。直接の子コンポーネントによって実行されたイベントのみを聞くことができます。

イベント引数

イベントで特定の値を実行すると便利な場合があります。たとえば<BlogPost>、テキストをどれだけ拡大するかをコンポーネントが担当するようにしたい場合があります。$emitそのような場合、この値を提供するために追加の引数を渡すことができます。

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

次に、親でイベントをリッスンするときに、インライン関数をリスナーとして使用できます。これにより、イベント引数にアクセスできます。

<MyButton @increase-by="(n) => count += n" />

または、イベントハンドラーがメソッドの場合:

<MyButton @increase-by="increaseCount" />

次に、その値がそのメソッドの最初のパラメーターとして渡されます。

function increaseCount(n) {
  count.value += n
}

ヒント

イベント名の後に渡されたすべての追加の引数は$emit()、リスナーに転送されます。たとえば$emit('foo', 1, 2, 3)、listener関数では3つの引数を受け取ります。

Emitイベントの宣言

defineEmits()放出されたイベントは、マクロを介してコンポーネントで明示的に宣言できます。

<script setup>
defineEmits(['inFocus', 'submit'])
</script>

で使用した$emitメソッドは、コンポーネント<template>のセクション内ではアクセスできませんが、代わりに使用できる同等の関数を返します。<script setup>defineEmits()

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

defineEmits()マクロは関数内では使用できません<script setup>。上記の例のように、マクロ内に直接配置する必要があります。

setupの代わりに明示的な関数を使用している場合は、オプション<script setup>を使用してイベントを宣言する必要があります。

export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}

setup()をしようした場合はコンテキストは他のプロパティと同様です。

export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {
    emit('submit')
  }
}

このemitsオプションは、オブジェクト構文もサポートしています。これにより、実行されたイベントのペイロードの実行時検証を実行できます。

<script setup>
const emit = defineEmits({
  submit(payload) {
    // return `true` or `false` to indicate
    // validation pass / fail
  }
})
</script>

TypeScriptをで使用している場合は<script setup>、純粋な型アノテーションを使用して発行されたイベントを宣言することもできます。

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

詳細:コンポーネントエミットの入力 

オプションですが、コンポーネントがどのように機能するかをより適切に文書化するために、発行されたすべてのイベントを定義することをお勧めします。また、Vueが既知のリスナーをフォールスルー属性から除外することもできます。

ヒント

オプションでネイティブイベント(たとえばclick)が定義されているemits場合、リスナーはコンポーネントから送信されたイベントのみをリッスンし、ネイティブイベントに応答しなくなります。

イベントの検証

プロップタイプの検証と同様に、発行されたイベントは、配列構文ではなくオブジェクト構文で定義されている場合に検証できます。

検証を追加するために、イベントには、呼び出しに渡された引数を受け取り、イベントが有効かどうかを示すブール値を返す関数が割り当てられます。emit

<script setup>
const emit = defineEmits({
  // No validation
  click: null,

  // Validate submit event
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

v-modelとEmitの使い方

カスタムイベントを使用して、v-modelと連携できるカスタムインプットを作ることもできます。

<input v-model="searchText" />

上記のコードは下のものと同じことを意味します。

<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

コンポーネントで使用する場合、少し違う動きが発生し、下記のようになります。

<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

ただし、これを実際に機能させるには、<input>のコンポーネントの内部で次のことを行う必要があります。

  • value属性をmodelValueプロップにバインドします
  • で、新しい値でイベントを発行inputしますupdate:modelValue

これが実際の動作です。

<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

これv-modelで、このコンポーネントで完全に機能するはずです。

<CustomInput v-model="searchText" />

遊び場で試してみてください

v-modelこのコンポーネント内に実装する別の方法はcomputed、ゲッターとセッターの両方で書き込み可能なプロパティを使用することです。メソッドはプロパティをget返し、メソッドは対応するイベントを発行する必要があります。modelValueset

<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

<template>
  <input v-model="value" />
</template>

v-model引数

デフォルトでv-modelは、コンポーネントmodelValueで小道具およびupdate:modelValueイベントとして使用されます。引数を渡してこれらの名前を変更できますv-model

<MyComponent v-model:title="bookTitle" />

この場合、子コンポーネントはtitle小道具を期待しupdate:title、親の値を更新するイベントを発行する必要があります。

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

遊び場で試してみてください

複数v-modelのバインディング

v-model以前に引数で学習した特定の小道具とイベントをターゲットにする機能を活用することで、単一のコンポーネントインスタンスに複数のvモデルバインディングを作成できるようになりました。

各v-modelは、コンポーネントに追加のオプションを必要とせずに、異なるプロップに同期します。

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

遊び場で試してみてください

v-model修飾子(モディファイヤ)

フォーム入力バインディングについて学習していたときに、.trim.number.lazyのような修飾子v-modelが組み込まれていることを確認しました。ただし、場合によっては、独自のカスタム修飾子を追加することもできます。

バインディングcapitalizeによって提供される文字列の最初の文字を大文字にするカスタム修飾子の例を作成しましょう。v-model

<MyComponent v-model.capitalize="myText" />

コンポーネントに追加されたモディファイヤは、プロップv-modelを介してコンポーネントに提供されます。次の例では、デフォルトで空のオブジェクトにmodelModifiersなるpropを含むコンポーネントを作成しました。

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

defineEmits(['update:modelValue'])

console.log(props.modelModifiers) // { capitalize: true }
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

コンポーネントのmodelModifierspropにが含まれていてcapitalize、その値がtrueであることに注意してください。これは、v-modelバインディングに設定されているためv-model.capitalize="myText"です。

プロップが設定されたので、modelModifiersオブジェクトキーを確認し、出力された値を変更するハンドラーを作成できます。<input />以下のコードでは、要素がイベントを発生させるたびに文字列を大文字にしますinput

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

遊び場で試してみてください

v-model引数と修飾子の両方を持つバインディングの場合、生成されるプロップ名はarg + "Modifiers"になります。例えば:

<MyComponent v-model:title.capitalize="myText">

対応する宣言は次のようになります。

const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

console.log(props.titleModifiers) // { capitalize: true }

[Vue入門] PropsとdefineProps 

このページは、すでにコンポーネントの基礎を読んでいることを前提にしています。初めてコンポーネントに触れる方は、まずこちらをお読みください。

プロパティの宣言

Vue のコンポーネントでは、明示的な props (プロパティ) の宣言が必要です。これにより Vue は、外部からコンポーネントに渡された props を fallthrough 属性 (次のセクションで説明します) として扱うべきかを知ることができます。

プロパティは、以下のように props オプションを使って宣言します:

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

//<script setup>を使わないやり方↓
export default {
  props: ['foo'],
  created() {
    // props は `this` 上で公開されます。
    console.log(this.foo)
  }
}

プロパティの宣言には、文字列の配列に加え、オブジェクト構文を用いることもできます:

// in <script setup>
defineProps({
  title: String,
  likes: Number
})
export default {
  props: {
    title: String,
    likes: Number
  }
}

オブジェクト宣言の構文に含める各プロパティについて、キーにはプロパティの名前、値には目的の型のコンストラクター関数を指定します。

これは自分のコンポーネントを文書化するのに役立ちます。また、誤った型を渡した時にブラウザーのコンソールに警告が表示されるようになり、コンポーネントを利用する他の開発者のためにもなります。このページの後半では、プロパティのバリデーションについて詳しく説明します。

コンポーネントのプロパティの型付けも合わせて参照してください。

プロパティ渡しの詳細

プロパティ名での大文字・小文字の使い分け

長いプロパティ名は、camelCase (キャメルケース) で宣言します。そうすると、プロパティのキーとして使うときに引用符を使わなくて済みます。camelCase は JavaScript の有効な識別子であるため、以下のようにテンプレート式で直接参照することができます:

defineProps({
  greetingMessage: String
})

//<script setup>を使わないやり方↓
export default {
  props: {
    greetingMessage: String
  }
}
<span>{{ greetingMessage }}</span>

技術的には、子コンポーネントにプロパティを渡すときにも camelCase を用いることができます (ただし DOM テンプレート内を除く)。しかし、常に kebab-case (ケバブケース) を用いて HTML の属性に揃える、以下のような表記が慣例となっています:

<MyComponent greeting-message="hello" />

コンポーネントのタグには、可能な限り PascalCase を用いることが推奨されます。これは Vue コンポーネントとネイティブ要素の区別が付き、テンプレートの可読性が高まるためです。しかし、プロパティを渡すときに camelCase を用いることには、それほど実用的なメリットがありません。そのため、Vue では各言語の規約に従うことが推奨されます。

静的なプロパティと動的なプロパティ

ここまでで、静的な値として渡すプロパティを見てきました。例:

<BlogPost title="My journey with Vue" />

v-bind またはそのショートカットである : を使って、プロパティを動的に割り当てる例も見てきました。例:

<!-- 変数の値を動的に代入 -->
<BlogPost :title="post.title" />

<!-- 複雑な式の値を動的に代入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />

いろいろな種類の値を渡す

上の 2 つは、たまたま文字列の値を渡す例ですが、プロパティには どんな 種類の値も渡すことができます。

数値

<!-- `42` は静的な値ですが、これが文字列ではなく JavaScript の        -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost :likes="42" />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :likes="post.likes" />

ブール値

<!-- 値なしでプロパティを指定すると、暗黙で `true` を指定したことになります。 -->
<BlogPost is-published />

<!-- `false` は静的な値ですが、これが文字列ではなく JavaScript の     -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost :is-published="false" />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :is-published="post.isPublished" />

配列

<!-- 静的な配列でも、これが文字列ではなく JavaScript の         -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost :comment-ids="[234, 266, 273]" />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :comment-ids="post.commentIds" />

オブジェクト

<!-- 静的なオブジェクトでも、これが文字列ではなく JavaScript の -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
 />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :author="post.author" />

オブジェクトを利用した複数のプロパティのバインディング

オブジェクトに含まれるすべてのプロパティを props として渡したい場合には、引数なしの v-bind を使用します (:プロパティ名 の代わりに v-bind)。例えば、以下のような post オブジェクトがあるとします:

const post = {
  id: 1,
  title: 'My Journey with Vue'
}

//<script setup>を使わないやり方↓
export default {
  data() {
    return {
      post: {
        id: 1,
        title: 'My Journey with Vue'
      }
    }
  }
}

このとき、テンプレートでは以下を用いることができます:

<BlogPost v-bind="post" />

これは以下の表記と等価です:

<BlogPost :id="post.id" :title="post.title" />

一方向のデータフロー

すべてのプロパティでは、子のプロパティと親のプロパティとの間に一方向バインディングが形成されます。親のプロパティが更新されたときには子にも流れますが、その逆はありません。これにより、親の状態が誤って子コンポーネントによって変更されてアプリのデータフローが把握しにくくなる、といった事態が防がれます。

さらに、親コンポーネントが更新されるたびに、子コンポーネント内のすべてのプロパティは最新の値に更新されます。そのため、子コンポーネント内でプロパティの変更を試みてはいけません。もし試みると、Vue がコンソールで警告を発します:

const props = defineProps(['foo'])

// ❌ warning, props are readonly!
props.foo = 'bar'


//<script setup>を使わないやり方↓
export default {
  props: ['foo'],
  created() {
    // ❌ 警告、プロパティは読み取り専用です!
    this.foo = 'bar'
  }
}

通常、プロパティを変更したい状況には以下の 2 つがあります:

  • プロパティは初期値を渡すために用いて、それ以降、子コンポーネントではローカルのデータプロパティとして利用したい。 この場合、以下のようにローカルのデータプロパティを定義して、その初期値にプロパティを使用するのが最も適切です:export default { props: ['initialCounter'], data() { return { // this.initialCounter は counter の初期値を指定するためだけに // 使われ、今後発生するプロパティの更新からは切り離されます。 counter: this.initialCounter } } }
const props = defineProps(['initialCounter'])

// counter only uses props.initialCounter as the initial value;
// it is disconnected from future prop updates.
const counter = ref(props.initialCounter)
  • プロパティを、変換が必要なそのままの値として渡したい。 この場合、以下のような算出プロパティを定義して、その中でプロパティの値を利用するのが最も適切です:export default { props: ['size'], computed: { // プロパティが変更されると自動的に更新される算出プロパティ normalizedSize() { return this.size.trim().toLowerCase() } } }
const props = defineProps(['size'])

// computed property that auto-updates when the prop changes
const normalizedSize = computed(() => props.size.trim().toLowerCase())

オブジェクト/配列のプロップを変更する

オブジェクトや配列をプロパティとして渡した場合、子コンポーネントがプロパティのバインディングを変更することはできませんが、オブジェクトや配列のネストされたプロパティを変更することは可能です。これは、JavaScript ではオブジェクトや配列が参照渡しであり、Vue がそのような変更を防ぐのにかかるコストが現実的でないためです。

このような変更の主な欠点は、親コンポーネントにとって明瞭でない方法で子コンポーネントが親の状態に影響を与えることを許してしまい、後からデータの流れを見極めるのが難しくなる可能性があることです。親と子を密に結合させる設計でない限り、ベストプラクティスとしてはそのような変更を避けるべきです。ほとんどの場合、子コンポーネントはイベントを発行して、変更を親コンポーネントに実行してもらう必要があります。

プロパティのバリデーション

先ほど見た型のように、コンポーネントではプロパティに対する要件を指定することができます。要件が合わないと、Vue がブラウザーの JavaScript コンソールで警告を発します。他の人に使ってもらうことを想定したコンポーネントを開発する場合、これはとても便利です。

プロパティのバリデーションを指定するには、文字列の配列の代わりに props オプションを用いて、バリデーションの要件を持たせたオブジェクトを指定します。例:

defineProps({
  // Basic type check
  //  (`null` and `undefined` values will allow any type)
  propA: Number,
  // Multiple possible types
  propB: [String, Number],
  // Required string
  propC: {
    type: String,
    required: true
  },
  // Number with a default value
  propD: {
    type: Number,
    default: 100
  },
  // Object with a default value
  propE: {
    type: Object,
    // Object or array defaults must be returned from
    // a factory function. The function receives the raw
    // props received by the component as the argument.
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // Custom validator function
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // Function with a default value
  propG: {
    type: Function,
    // Unlike object or array default, this is not a factory function - this is a function to serve as a default value
    default() {
      return 'Default function'
    }
  }
})


//<script setup>を使わないやり方↓
export default {
  props: {
    // 基本的な型チェック
    // (`null` 値と `undefined` 値は、任意の型を許可します)
    propA: Number,
    // 複数の型の可能性
    propB: [String, Number],
    // 必須の文字列
    propC: {
      type: String,
      required: true
    },
    // デフォルト値を持つ数値
    propD: {
      type: Number,
      default: 100
    },
    // デフォルト値を持つオブジェクト
    propE: {
      type: Object,
      // オブジェクトと配列のデフォルトは、ファクトリー関数を使って
      // 返す必要があります。ファクトリー関数は、コンポーネントが
      // 受け取った生の各プロパティを引数として受け取ります。
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // カスタムのバリデーター関数
    propF: {
      validator(value) {
        // 値が以下の文字列のいずれかに一致する必要がある
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // デフォルト値を持つ関数
    propG: {
      type: Function,
      // オブジェクトや配列のデフォルトと異なり、これはファクトリー関数ではなく、デフォルト値として機能する関数です
      default() {
        return 'Default function'
      }
    }
  }
}

その他の詳細:

  • required: true が指定されていないすべてのプロパティは、デフォルトでオプションです。
  • Boolean 以外のオプションのプロパティは、値が指定されないと undefined 値になります。
  • Boolean のプロパティは、値が指定されないと false に変換されます。望みの動作を得るためには、default の値を指定する必要があります。
  • default の値を指定すると、プロパティの値が undefined に解決される時、それが使用されます。プロパティが指定されなかった場合と、明示的に undefined 値が渡された場合も、これに含まれます。

プロパティのバリデーションに失敗すると、Vue がコンソールに警告を出します (開発ビルドを使用する場合)。

注意

プロパティのバリデーションは、コンポーネントのインスタンスが生成されるに実行されます。そのため、default や validator 関数の中ではインスタンスのプロパティ (例えば datacomputed など) が使用できないことに注意してください。

実行時の型チェック

type には、以下のネイティブコンストラクターを指定することができます:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

加えて、type にはカスタムのクラスやコンストラクター関数を指定することもできます。その場合、instanceof チェックによってアサーションが行われます。例えば、次のクラスがあったとします:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

これをプロパティの型として用いるとします:

defineProps({
  author: Person
})

//<script setup>を使わないやり方↓
export default {
  props: {
    author: Person
  }
}

こうすると、author というプロパティの値が new Person で作成されたものであることをバリデーションで確かめることができます。

ブール値の型変換

Boolean 型のプロパティは、ネイティブのブール値の属性が振る舞う様子を模倣するために、特殊な型変換の規則を持っています。次のような宣言を含む <MyComponent> があるとします:

export default {
  props: {
    disabled: Boolean
  }
}

このコンポーネントは、次のように使用することができます:

<!-- :disabled="true" を渡すのと同等 -->
<MyComponent disabled />

<!-- :disabled="false" を渡すのと同等 -->
<MyComponent />

また、以下のように複数の型を受け付けるようにプロパティを宣言した場合:

export default {
  props: {
    disabled: [Boolean, Number]
  }
}

Boolean の型変換の規則は、型が登場する順序に関係なく適用されます。

[Vue入門]コンポーネントの登録

このページは、すでにコンポーネントの基礎を読んでいることを前提にしています。初めてコンポーネント学ぶ方は、まずそちらをお読みください。

Vue のコンポーネントをテンプレートで使用する時は、それがどこで実装されているかを Vue に知らせるため、「登録」を行う必要があります。コンポーネントの登録方法には、グローバルとローカルの 2 つがあります。

グローバル登録

開発中の Vue アプリケーションでグローバルにコンポーネントを利用できるようにするには、以下に示す app.component() メソッドを使用します:

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // the registered name
  'MyComponent',
  // the implementation
  {
    /* ... */
  }
)

SFC を使用する場合は、インポートした .vue ファイルを登録します:

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

app.component() メソッドはチェーンにすることができます:

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

グローバル登録したコンポーネントは、アプリケーション内の任意のコンポーネントのテンプレートで使用することができます:

<!-- これはアプリ内のどのコンポーネントでも動作します -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

これは、サブコンポーネントにも漏れなく適用されます。そのため、上の 3 つのコンポーネントはすべて 各コンポーネント内でも互いに 使える、ということになります。

ローカル登録

グローバル登録は便利な反面、以下に示すいくつかの欠点があります:

  1. グローバル登録では、未使用のコンポーネントを削除してくれるビルドシステムの処理 (いわゆる「ツリーシェイク」) が阻害されます。グローバル登録したコンポーネントは、最後までアプリのどこにも用いなかった場合でも、最終的なバンドルには含まれてしまいます。
  2. グローバル登録では、大規模なアプリケーションでの依存関係の分かりやすさが低下します。グローバル登録では、子コンポーネントを使っている親コンポーネントから、子コンポーネントの実装部分を探し出すことが難しくなります。きわめて多くのグローバル変数が使われている状況と同じように、これは長期的な保守性に影響を与える可能性があります。

ローカルでの登録を利用すると、登録したコンポーネントを使えるスコープが現在のコンポーネントのみに限定されます。これによって依存関係が分かりやすくなり、ツリーシェイクが働きやすくなります。

ローカル登録は、以下のように components オプションを使って行います:

でSFCを使用する場合<script setup>、インポートされたコンポーネントは登録なしでローカルで使用できます。

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

<script setup>を使わない場合は下記のように登録する必要があります。

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  },
  setup() {
    // ...
  }
}

オブジェクト内のプロパティごとcomponentsに、キーはコンポーネントの登録名になり、値にはコンポーネントの実装が含まれます。上記の例は、ES2015プロパティの省略形を使用しており、次と同等です。

export default {
  components: {
    ComponentA: ComponentA
  }
  // ...
}

ただし、 ローカル登録されたコンポーネントが子孫のコンポーネントでも利用できるようにはならないことに注意してください。上の場合、ComponentA は現在のコンポーネントのみで利用可能になり、その子や子孫のコンポーネントで利用可能になるわけではありません。

<script setup>を使用してコンポーネントをインポートした場合は、登録が必要ありません。インポートした時点でテンプレートでの使用が可能になります。

コンポーネント名での大文字・小文字の使い方

このガイドでは、コンポーネントを登録する際に PascalCase の名前を用いています。これは次の理由によります:

  1. PascalCase の名前は JavaScript の識別子として有効です。そのため、JavaScript でコンポーネントをインポートしたり登録したりするのが容易になります。また、IDE のオートコンプリートも働きやすくなります。
  2. テンプレートで <PascalCase /> を用いると、これがネイティブの HTML 要素ではなく、Vue のコンポーネントであることがより明確になります。また、Vue コンポーネントとカスタムの要素 (Web Components) を区別することも可能になります。

このスタイルは、SFC や文字列テンプレートを合わせて使う時に推奨されるスタイルです。ただし、DOM テンプレート解析の注意点 で説明しているように、DOM テンプレート内では PascalCase のタグが使えません。

幸いなことに、Vue は PascalCase で登録したコンポーネントから kebab-case 形式のタグへの解決をサポートしています。これにより、MyComponent として登録したコンポーネントは、<MyComponent> と <my-component> のどちらを使ってもテンプレート内で参照できます。そのため、テンプレートの出どころに関わらず、JavaScript のコンポーネント登録のコードには同じものを用いることができます。