Vus.jsのまとめ

  1. 基本構文
    1. 単一コンポーネントファイル
  2. データ表示 ref
  3. 計算結果を表示 (Computed Properties)
  4. オブジェクトをまとめてリアクティブ (reactive)
  5. ディレクティブ (v- で始まる属性)
  6. v-bind 属性にデータバインド
    1. 属性値のない属性へのバインド (disabled, readonly etc)
    2. 属性をテンプレート変数として指定
    3. 複数属性にまとめてバインディングする方法
    4. style 属性へのバインディング
    5. class 属性へのバインディング
  7. v-on イベントへのディレクティブ
    1. イベント
    2. メソッドの引数 イベントオブジェクトを受け取る
    3. メソッドの引数 イベントオブジェクト以外を引数とするイベントハンドラ
    4. メソッドの引数 イベントオブジェクトとその他の引数を併用する
    5. v-on の修飾子(Modifiers)
      1. prevent 修飾子
      2. クリックイベントとキーイベントの修飾子
        1. キーイベントの修飾子
        2. click イベントの修飾子(v-on:click)
        3. システム修飾子(v-on:click.shift, v-on:keydown.alt.enter)
        4. ecact 修飾子
  8. v-model 双方向データバインディング
    1. 文字列入力以外のコントロールでの v-mobel
    2. v-model の修飾子
  9. その他のデータバインディングのディレクティブ
    1. v-html HTML文字列をそのまま表示する
    2. v-pre 静的コンテンツとして表示させる
    3. v-once データバインドを一度だけ行う
    4. v-cloak レンダリング終了までマスタッシュ構文を非表示にする
  10. v-if 条件分岐のディレクティブ
    1. 基本
    2. v-else-if と v-else
    3. templateタグ
  11. v-show 表示/非表示
  12. v-for ループのディレクティブ
    1. 配列のループ
    2. 連想配列のループ
      1. Key 値の工夫
    3. Map のループ
    4. 連想配列を利用していない通常のオブジェクトのループ
    5. オブジェクトの配列のループ
    6. Mapとオブジェクトの組み合わせのループ
    7. カウンタ変数を利用したループ
    8. リスト操作
      1. ループ対象データの絞り込み
      2. 配列のデータ操作
        1. Vue内でラップされた状態で用意されている配列操作メソッド
      3. Map データ操作
      4. オブジェクト内のデータ変更
      5.  リストデータ内のオブジェクトの変更
  13. ウォッチャー
    1. watchEffect() リアクティブの変化を監視する
    2. watch() 監視対象を明示する
  14. ライフサイクルフック
  15. 子コンポーネントの利用
    1. スタイルブロックを独立させる
  16. 親から子へのコンポーネント間通信
    1. Props(プロップス)親からデータをもらう
    2. Props(プロップス)親のテンプレート変数をPropsに渡す
    3. v-forとPropsとの組み合わせ
    4. Propsの応用
    5. Propsのデフォルト値
  17. 子から親へのコンポーネント通信
    1. 子から親への通信はイベント処理
    2. 親コンポーネントにデータを渡す
      1. 子コンポーネント
      2. 親コンポーネント
    3. v-model による子から親への通信

基本構文

単一コンポーネントファイル

<script setup lang="ts">
  // スクリプトブロック
  // JavaScript (TypeScript) コードを記述する
</script>

<template>
  <!-- テンプレートブロック -->
  <!-- HTMLタブを記述する -->
</template>

<style>
  /* スタイルブロック */
  /* CSSコードを記述する */
</style>

データ表示 ref

用語内容
リアクティブ変数の値の変化に連動して表示内容が自動的に変化すること
リアクティブシステムリアクティブを表現する仕組みのこと
マスタッシュ構文スクリプトブロックで用意した変数を表示するための構文
{{ 変数名 }}
テンプレート変数テンプレートブロック利用する変数
const 変数名 = ref(値)
変数名.value = 新しい値
<script setup lang="ts">
import { ref } from 'vue' // ref()関数インポート

// 現在時刻文字列でテンプレート変数を作成
const now = new Date()
const nowStr = now.toLocaleTimeString()
const timeStrRef = ref(nowStr)

// 現在時刻文字列テンプレート変数を現在時刻で更新
function changeTime(): void {
  const newTime = new Date()
  const newTimeStr = newTime.toLocaleTimeString()
  timeStrRef.value = newTimeStr  // テンプレート変数のvalueプロパティ値を変更
}

// 1秒ごとに changeTime()を実行
setInterval(changeTime, 1000)
</script>

<template>
  <p>現在時刻: {{ timeStrRef }}</p>  <!-- テンプレート変数表示マスタッシュ構文 -->
</template>

計算結果を表示 (Computed Properties)

const 変数名 = computed(
  (): 計算結果のデータ型 => {
    計算処理
    return 計算結果
  }
}
<script setup lang="ts">
import { ref, computed } from 'vue'  // ref(), computed() 関数をインポート

// 半径テンプレート変数
const radiusInit = Math.round(Math.random() * 10)
const radius = ref(radiusInit)

// 円周率テンプレート変数
const PI = ref(3.14)

// 円の面積の算出プロパティ
const area = computed((): number => {
  return radius.value * radius.value * PI.value
})

// 1秒ごとに半径テンプレート変数を更新
setInterval((): void => {
  radius.value = Math.round(Math.random() * 10)
}, 1000)
</script>

<template>
  <!-- area が算出プロパティ -->
  <p>半径{{ radius }}の円の面積を円周率{{ PI }}で計算すると、{{ area }}</p>
</template>

オブジェクトをまとめてリアクティブ (reactive)

<script setup lang="ts">
import { reactive, computed } from 'vue'

// リアクティブなテンプレート変数をまとめて用意
const data = reactive({
  PI: 3.14,
  radius: Math.round(Math.random() * 10),
})

// 円の面積の算出プロパティを用意
const area = computed((): number => {
  return data.radius * data.radius * data.PI
})

// 半径のテンプレート半数に新しい乱数を1秒ごとに格納
setInterval((): void => {
  data.radius = Math.round(Math.random() * 10)
}, 1000)
</script>

<template>
  <p>半径{{ data.radius }}の円の面積を円周率{{ data.PI }}で計算すると、{{ area }}</p>
</template>

ディレクティブ (v- で始まる属性)

ディレクティブ役割
v-bindデータバインディング
v-onイベント処理
v-model双方向データバインディング
v-htmlHTML文字列表示
v-pre静的コンテンツ表示
v-onceデータバインディングを初回のみに限定
v-cloakマスタッシュ構文非表示
v-if条件分岐
v-show表示/非表示
v-forループ処理

v-bind 属性にデータバインド

v-bind:△△="〇〇"
要素の△△属性の値として、テンプレート変数〇〇の値を設定

// 省略形(可読性の始点から省略しない方が良い)
:△△="〇〇"
<script setup lang="ts">
import { ref } from 'vue'

const url = ref('https://vuejs.org/')
</script>

<template>
  <p><a v-bind:href="url" target="_blank">Vue.jsのサイト</a></p>
  <p><a :href="url" target="_blank">Vue.jsのサイト(省略系)</a></p>
  <p><a v-bind:href="url + 'guide/introduction.html'" target="_blank">Vue.jsガイドのページ</a></p>
</template>

属性値のない属性へのバインド (disabled, readonly etc)

<script setup lang="ts">
import { ref } from 'vue'

const isSendButtonDisabled = ref(true)  // true/false の boolean型データを用意
</script>

<template>
  <!-- true/false の boolean型を使用 -->
  <p><button type="button" v-bind:disabled="isSendButtonDisabled">送信</button></p>
</template>

属性をテンプレート変数として指定

<script setup lang="ts">
import { ref } from 'vue'

const widthOrHeight = ref('height')    // 属性名をテンプレート変数で用意
const widthOrHeightValue = ref(100)
</script>

<template>
  <!-- height属性をテンプレート変数として指定 (動的引数)-->
  <p><img alt="VueLogo" src="./assets/logo.svg" v-bind:[widthOrHeight]="widthOrHeightValue" /></p>
</template>

複数属性にまとめてバインディングする方法

<script setup lang="ts">
import { ref } from 'vue'

const imgAttributes = ref({    // テンプレート変数をオブジェクトにする
  src: '/images/logo.svg',     // 属性名: 値
  alt: 'Vueのロゴ',
  width: 75,
  height: 75,
})
</script>

<template>
  <p><img v-bind:="imgAttributes" /></p>
  <p><img v-bind="imgAttributes" title="ロゴです!" /></p>
  <p><img v-bind="imgAttributes" alt="ロゴです!" /></p>   <!-- alt は後にある記述が優先 -->
</template>

style 属性へのバインディング

v-bind:style="{スタイルプロパティ: 値, ・・・}"
<script setup lang="ts">
import { ref, computed } from 'vue'

const msg = ref('こんにちは!世界')

// 単体プロパティのテンプレート変数
const msgTextRed = ref('red')
const msgTextColor = ref('white')
const msgBgColor = ref('black')

// スタイルの組み合わせをオブジェクトリテラルで指定したテンプレート変数
const msgStyles = ref({ color: 'white', backgroundColor: 'black' })
const msgStyles2 = ref({ fontSize: '24pt' })
const msgStyles3 = ref({ color: 'pink', fontSize: '24pt' })

// 算出プロパティ
const textSize = computed((): string => {
  const size = Math.round(Math.random() * 25) + 10
  return `${size}pt`
})
</script>

<template>
  <!-- テンプレート変数を記述 msgTextRed -->
  <p v-bind:style="{ color: msgTextRed }">1:{{ msg }}</p>

  <!-- リテラルを記述 'pink' -->
  <p v-bind:style="{ color: 'pink' }">2:{{ msg }}</p>

  <!-- 算出プロパティを記述 textSize -->
  <p v-bind:style="{ fontSize: textSize }">3:{{ msg }}</p>

  <!-- style の値を複数指定 -->
  <p v-bind:style="{ color: msgTextColor, backgroundColor: msgBgColor }">4:{{ msg }}</p>

  <!-- スタイルプロパティをケバブ記法で記述 'background-color' -->
  <p v-bind:style="{ color: msgTextColor, 'background-color': msgBgColor }">5:{{ msg }}</p>

  <!-- スタイルの組み合わせをオブジェクトリテラルで指定 -->
  <p v-bind:style="msgStyles">6:{{ msg }}</p>

  <!-- 複数のオブジェクトリテラルを配列で指定 -->
  <p v-bind:style="[msgStyles, msgStyles2]">7:{{ msg }}</p>
  <p v-bind:style="[msgStyles, msgStyles3]">8:{{ msg }}</p>
  <p v-bind:style="[msgStyles3, msgStyles]">9:{{ msg }}</p>
</template>

class 属性へのバインディング

v-bind:class="{スタイルクラス名: true/false, ・・・}"
<script setup lang="ts">
import { ref, computed } from 'vue'

const msg = ref('こんにちは!世界')

// クラススタイルの適用の有無をテンプレート変数で指定
const isTextColorRed = ref(true)
const isBgColorBlue = ref(false)

// オブジェクトリテラルをテンプレート変数で指定
const styles = ref({ textColorRed: false, bgColorBlue: true })

// スタイルクラスを適用するかどうかを示す true/false値を算出プロパティで設定
const computedStyles = computed((): { textColorRed: boolean; bgColorBlue: boolean } => {
  // textColorRed用
  const randText = Math.round(Math.random())
  let textColorFlg = true
  if (randText == 0) {
    textColorFlg = false
  }
  // bgColorBlue用
  const randBg = Math.round(Math.random())
  let bgColorFlg = true
  if (randBg == 0) {
    bgColorFlg = false
  }
  return {
    textColorRed: textColorFlg,
    bgColorBlue: bgColorFlg,
  }
})
</script>

<template>
  <!-- スタイルクラス適用を true/false で指定 -->
  <p v-bind:class="{ textColorRed: true, bgColorBlue: true }">{{ msg }}</p>

  <!-- true/false の値をテンプレート変数として用意
  <p v-bind:class="{ textColorRed: isTextColorRed, bgColorBlue: isBgColorBlue }">{{ msg }}</p>

  <!-- スタイルクラス名はキャメル記法。ケバブ記法の場合はシングルクォーテーションで囲む -->
  <p v-bind:class="{ textColorPink: true }">{{ msg }}</p>
  <p v-bind:class="{ 'text-color-pink': true }">{{ msg }}</p>

  <!-- v-bind:class と 通常の class属性を併用
  <p class="textSize24" v-bind:class="{ textColorRed: isTextColorRed, bgColorBlue: isBgColorBlue }">
    {{ msg }}
  </p>

  <!-- オブジェクトリテラルをテンプレート変数で指定 -->
  <p class="textSize24" v-bind:class="styles">{{ msg }}</p>

  <!-- true/false値を算出プロパティで指定 -->
  <p v-bind:class="computedStyles">{{ msg }}</p>
</template>

<style>
/* class属性へバインディングされるスタイルクラス */
.textColorRed {
  color: red;
}
.text-color-pink {
  color: pink;
}
.bgColorBlue {
  background-color: blue;
}
.textSize24 {
  font-size: 24px;
}
</style>

v-on イベントへのディレクティブ

v-on:イベント名="イベント発生時に実行するメソッド名"
const メソッド名 = (): void => {
  処理内容
}
<button @click="onButtonClick">クリック!</button>
<script setup lang="ts">
import { ref } from 'vue'

const randValue = ref('まだです')

// イベント定義
const onButtonClick = (): void => {
  const rand = Math.round(Math.random() * 10)
  randValue.value = String(rand)
}
</script>

<template>
  <section>
    <button v-on:click="onButtonClick">クリック!</button>
    <p>クリックの結果: {{ randValue }}</p>
  </section>
</template>

イベント

分類イベント名発生タイミング
フォーカスblurフォーカスを失った時
focusフォーカスを受けた時
focusinフォーカスを受けた時(親要素でもイベント検出可能)
focusoutフォーカスを失った時(親要素でもイベント検出可能)
マウスclickクリックしたとき
contextmenuコンテキストメニューが表示されるとき
mousedownマウスのボタンが押された時
mouseenterマウスポインタが要素に入った時(自要素のみ)
mouseleaveマウスポインタが要素を出た時(自要素のみ)
mousemoveマウスポインタが移動したとき
mouseoutマウスポインタが要素から出た時(子要素も含む)
mouseoverマウスポインタが要素に入った時(子要素も含む)
mouseupマウスボタンが放された時
wheelマウスホイールが放された時
入力changeドロップダウンなどで入力内容が変更されたとき
compositionendIMEを使って入力を終了したとき
compositionstartIMEを使って入力を開始したとき
compositionupdateIMEを使って入力中
input入力内容が更新されたとき
keydownキーが押された時
keypressキーを押して文字が入力されたとき
keyupキーが離された時
selectテキストが選択された時
その他resize要素サイズが変更されたとき
scrollスクロールされたとき

メソッドの引数 イベントオブジェクトを受け取る

<script setup lang="ts">
import { ref } from 'vue'

const mousePointerX = ref(0)
const mousePointerY = ref(0)

// メソッドの引数としてイベントオブジェクトを受け取る
const onImgMousemove = (event: MouseEvent): void => {
  mousePointerX.value = event.offsetX
  mousePointerY.value = event.offsetY
}
</script>

<template>
  <section>
    <!-- イベントオブジェクトのみの場合は引数は不要(自動で渡される) -->
    <img src="./assets/logo.svg" alt="Vueのロゴ" width="200" v-on:mousemove="onImgMousemove" />
    <p>ポインタの位置: x={{ mousePointerX }}; y={{ mousePointerY }}</p>
  </section>
</template>

メソッドの引数 イベントオブジェクト以外を引数とするイベントハンドラ

<script setup lang="ts">
import { ref } from 'vue'

const pBgColor = ref('white')

// メソッドの引数として背景色文字列を受け取る
const onPClick = (bgColor: string): void => {
  pBgColor.value = bgColor
}
</script>

<template>
  <!-- イベントオブジェクトではなく任意の引数 ('red') を渡す
  <p v-on:click="onPClick('red')" v-bind:style="{ backgroundColor: pBgColor }">
    ここをクリックすると背景色が変わります。
  </p>
</template>

メソッドの引数 イベントオブジェクトとその他の引数を併用する

<script setup lang="ts">
import { ref } from 'vue'

const pMsg = ref('イベント前(ここをクリック!)')
const pBgColorEvent = ref('white')

// メソッドの引数として bgColor と event の2つの引数を受け取る
const onPClickWithEvent = (bgColor: string, event: MouseEvent): void => {
  pBgColorEvent.value = bgColor
  pMsg.value = event.timeStamp.toString()
}
</script>

<template>
  <!-- bgColor と event の2つの引数を渡す -->
  <p v-on:click="onPClickWithEvent('green', $event)"
     v-bind:style="{ backgroundColor: pBgColorEvent }">
    {{ pMsg }}
  </p>
</template>

v-on の修飾子(Modifiers)

DOMイベントフェーズ(Event Phase:イベント伝達)
  1. キャプチャーフェーズ(Capture Phase)
    イベントがルート要素からターゲット要素まで下りていく
  2. ターゲットフェーズ(Target Phase)
    イベントがターゲット要素に到達する
  3. バブリングフェーズ(Bubbling Phase)
    イベントがルート要素までさかのぼる
    DOMイベントハンドラの実行は通常バブリングフェーズでおこる
修飾子 内容
stopバブリングをキャンセルして、バブリングフェーズで親要素に設定されたイベントハンドラの実行を阻止する。
(event.stopPropagation()に同じ)
captureキャプチャフェーズで該当のイベントハンドラを実行し、代わりにバブリングフェーズではイベントハンドラを実行しないように設定する。
selfその要素自体がイベントの発生源のときのみイベントハンドラを実行するように設定する。
preventイベントが規定の処理をキャンセルする。
(event.preventDefault()に同じ)
passive該当イベントのデフォルトの挙動が即実行される。
.passive を scroll イベントに適用すると、スクロール処理が即座に実行されるようになり、モバイル環境でのスクロールのカクツキを軽減できる。
onceイベントの実行を1回のみとする

prevent 修飾子

<script setup lang="ts">
import { ref } from 'vue'

const msg = ref('未送信')

const onFormSubmit = (): void => {
  msg.value = '送信されました。'
}
</script>

<template>
  <!-- フォームバリデーション:入力値が正しいか検証する仕組み -->
  <!-- フォームバリデーションを有効にするにはfromタグで囲みボタンのtypeをsubmitにする -->
  <!-- たたしv-on:submitだけだとメソッド実行後に本来のサブミットイベント(button type="submit") -->
  <!-- も実行されて from action="#"が呼び出されてしまう。 -->
  <!-- この本来のイベントを .prevent 修飾子でキャンセルしている。 -->
  <form action="#" v-on:submit.prevent="onFormSubmit">
    <input type="text" required />
    <button type="submit">送信</button>
  </form>
  <p>{{ msg }}</p>
</template>

クリックイベントとキーイベントの修飾子

<script setup lang="ts">
import { ref } from 'vue'

const msg = ref('まだです!')

const onEnterKey = (): void => {
  msg.value = 'エンターキーが押下されました。'
}

const OnRightButtonClick = (): void => {
  msg.value = 'ボタンが右クリックされました。'
}

const onShiftclick = (): void => {
  msg.value = 'シフトを押しながらクリックされました。'
}
</script>

<template>
  <p>{{ msg }}</p>
  <input type="text" v-on:keydown.enter="onEnterKey" /><br />
  <button v-on:click.right="OnRightButtonClick">右クリック</button><br />
  <button v-on:click.shift="onShiftclick">シフトを押しながらクリック</button><br />
</template>
キーイベントの修飾子
修飾子対応キー
アルファベット・数字abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
enter[Enter]
tab[Tab]
delete[Delete]または[Backspace]
esc[Esc]
space[Space]
up[↑]
down[↓]
left[←]
rignt[→]
click イベントの修飾子(v-on:click)
修飾子対応ボタン
left左ボタン
right右ボタン
middle中ボタン
システム修飾子(v-on:click.shift, v-on:keydown.alt.enter)
修飾子対応キー
ctrl[Ctrl]
alt[Alt]
shift[Shift]
metamacOSでは[]、Windowsでは[]
ecact 修飾子

.enter 修飾子は[Enter]キーを単独で押下したときだけでなく[Shift]+[Enter]キーなど他のキーとの組み合わせでもイベントが発生してしまう。
[Enter]キーが単独で押されたときのみに限定したい場合は .exact 修飾子を併用する。

v-on:keydown.enter.exact="onEnterKey"

v-model 双方向データバインディング

<script setup lang="ts">
import { ref } from 'vue'

const inputName2Way = ref('双方向')

const onInputName2Way = (event: Event): void => {
  const element = event.target as HTMLInputElement
  inputName2Way.value = element.value
}
</script>

<template>
  <section>
    <input type="text" v-bind:value="inputName2Way" v-on:input="onInputName2Way" />
    <p>{{ inputName2Way }}</p>
  </section>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const inputNameModel = ref('双方向')

// v-modelの値としてテンプレート変数を
// 指定しておけばイベントハンドラがなくても
// テンプレート変数は更新される。

</script>

<template>
  <section>
    <input type="text" v-model="inputNameModel" />
    <p>{{ inputNameModel }}</p>
  </section>
</template>

この方法は、type 属性が email や number、data など、文字列入力に準ずるものであれば同じように利用できる。

文字列入力以外のコントロールでの v-mobel

<script setup lang="ts">
import { ref } from 'vue'

const inputTextarea = ref('テキストエリアへの入力文字。\n改行も加えてみる。')  // (1) テキストエリア
const memberType = ref(1)           // (2) ラジオボタン
const memberTypeSelect = ref(1)     // (3) ドロップダウンリスト
const isAgreed = ref(false)         // (4) 単一チェックボックス(true/false)
const isAgreed01 = ref(0)           // (5) 単一チェックボックス(値でデータ管理)
const selectedOS = ref([])          // (6) 複数選択チェックボックス(配列)
const selectedOSSelect = ref([])    // (7) 複数選択セレクトボックス(配列)
</script>

<template>
  <!-- (1) テキストエリア ・マッシュ構文不要 -->
  <textarea v-model="inputTextarea"></textarea>
  <p>{{ inputTextarea }}</p>
  <br />

  <!-- (2) ラジオボタン ・属性値として同一のテンプレート変数を指定 -->
  <section>
    <label><input type="radio" name="memberType" value="1" v-model="memberType" />通常会員</label>
    <label><input type="radio" name="memberType" value="2" v-model="memberType" />特別会員</label>
    <label><input type="radio" name="memberType" value="3" v-model="memberType" />優良会員</label>
    <br />
    <p>選択されたラジオボタン: {{mimberType}}</p>  <!-- value の値 -->
  </section>
  <br />

  <!-- (3) ドロップダウンリスト ・selectタグにv-modelディレクティブを記述 -->
  <section>
    <select v-model="memberTypeSelect">
      <option value="1">通常会員</option>
      <option value="2">特別会員</option>
      <option value="3">優良会員</option>
    </select>
    <br />
    <p>選択されたリスト: {{ memberTypeSelect }}</p>  <!-- value の値 -->
  </section>
  <br />

  <!-- (4) 単一チェックボックス(true/false) -->
  <section>
    <label><input type="checkbox" v-model="isAgreed" />同意する</label>
    <p>同意の結果: {{ isAgreed }}</p>  <!-- true/false -->
  </section>

  <!-- (5) 単一チェックボックス(値でデータ管理) -->
  <section>
    <label>
      <input type="checkbox" v-model="isAgreed01" true-value="1" false-value="0" />同意する
    </label>
    <p>同意の結果: {{ isAgreed01 }}</p>  <!-- true-value 又は false-value の値 -->
  </section>
  <br />

  <!-- (6) 複数選択チェックボックス ・同一テンプレート変数を指定すると配列として扱える -->
  <section>
    <label><input type="checkbox" v-model="selectedOS" value="1" />macOS</label>
    <label><input type="checkbox" v-model="selectedOS" value="2" />Windows</label>
    <label><input type="checkbox" v-model="selectedOS" value="3" />Linux</label>
    <label><input type="checkbox" v-model="selectedOS" value="4" />iOS</label>
    <label><input type="checkbox" v-model="selectedOS" value="5" />Android</label>
    <p>選択されたOS: {{ selectedOS }}</p>  <!-- value の値の配列 例)["1", "3", "5"] -->
  </section>

  <!-- (7) 複数選択セレクトボックス ・配列の形でデータを扱う -->
  <section>
    <select v-model="selectedOSSelect" multiple>
      <option value="1">macOS</option>
      <option value="3">Windows</option>
      <option value="4">Linux</option>
      <option value="5">iOS</option>
      <option value="6">Android</option>
    </select>
    <p>選択されたOS: {{ selectedOSSelect }}</p> <!-- value の値の配列 例)["1", "3", "5"] -->
  </section>
</template>

v-model の修飾子

修飾子内容
lazyinput の代わりに change イベントで双方向データバインドを行う。
type=”text” の input タグへの双方向データバインディングは1文字入力/消去するたびにデータバインディングが行われ、マスタッシュ構文による表示部分でも即座に値が反映される。
これを一通り入力を終えて、入力コントロールからフォーカスが離れるのを待ってからデータバインディングを行うようにするのが .lazy 修飾子
number入力値を数値として扱う。
input type=”number” のように数値を入力するコントロールに有効。
このようなコントロールでは数値で入力しても入力値は原則文字列(string型)データとなるが TypeScript側では数値は number型の変数に格納する必要がある。
そこで、文字列として入力されて数値データを number型に変換したうえで nunber型のテンプレート変数に格納してくれるのが .number 修飾子。
trim入力値の前後の余分な空白を取り除く。
前後の空白が取り除かれた上でデータバインドが行われる。

その他のデータバインディングのディレクティブ

v-html HTML文字列をそのまま表示する

<script setup lang="ts">
import { ref } from 'vue'

const htmlStr = ref('<a href="https://vue.js.org//">Vue.jsのTOPページ</a>')
</script>

<template>
  <section>{{ htmlStr }}</section>       <!-- HTMLエスケープされる -->
  <section v-html="htmlStr"></section>   <!-- そのまま表示される -->
</template>

v-pre 静的コンテンツとして表示させる

<script setup lang="ts">
</script>
<template>
  <section v-pre>
    <p v-on:click="showHello">{{ hello! }}</p>  <!-- そのまま「{{ hello! }}」と表示される
  </section>
</template>

v-once データバインドを一度だけ行う

<script setup lang="ts">
import { ref } from 'vue'

const price = ref(1000)
</script>

<template>
  <section>
    <input type="number" v-model="price" />円<br />
    <p>金額は{{ price }}円です。</p>         <!-- 入力のたびに値が反映される -->
    <p v-once>金額は{{ price }}円です。</p>  <!-- 2回目以降の入力値は反映されない。初期値のまま -->
  </section>
</template>

v-cloak レンダリング終了までマスタッシュ構文を非表示にする

<script setup lang="ts">
import { ref } from 'vue'

const hello = ref('こんにちは!')
</script>

<template>
  <!-- v-cloak ディレクティブは最初は属性としてそのままレンダリングされるが -->
  <!-- マスタッシュ構文のレンダリングが終了したタイミングで Vue によって取り除かれる -->
  <p v-cloak>{{ hello }}</p>
</template>

<style>
/* v-cloakが存在する間はこの設定が有効なのでタグそのものが非表示となる */
[v-cloak] {
  display: none;
}
</style>

v-if 条件分岐のディレクティブ

基本

<script setup lang="ts">
import { computed, ref } from 'vue'

const number = ref(80)

// 算出プロパティ
const showOrNot = computed((): boolean => {
  let showOrNot = false
  const rand = Math.round(Math.random() * 100)  // 乱数発生
  if (rand >= 50) {  // 乱数が50以上ならtrue
    showOrNot = true
  }
  return showOrNot
})
</script>

<template>
  <!-- v-ifは条件に合致したタグがレンダリングされる -->
  <!-- number は常に80なのでレンダリングされる -->
  <p v-if="number >= 50">条件に合致したので表示①</p>

  <!-- 条件部分に式を記述できる。しかし可読性の観点から算出プロパティにしたほうが良い -->
  <p v-if="Math.round(Math.random() * 100) >= 50">条件に合致したので表示②</p>

  <!-- 複雑な条件式を算出プロパティとして記述 -->
  <p v-if="showOrNot">条件に合致したので表示③</p>
</template>

v-else-if と v-else

<script setup lang="ts">
import { computed } from 'vue'

const randomNumber = computed((): number => {
  return Math.round(Math.random() * 100)
})
</script>

<template>
  <p>
    点数は{{ randomNumber }}点で

    <!-- 一連の条件分岐ディレクティブは連続して配置する。別のタグは入れられない。-->
    <span v-if="randomNumber >= 80">優です</span>
    <span v-else-if="randomNumber >= 70">良です</span>
    <span v-else-if="randomNumber >= 60">可です</span>
    <span v-else>不可です</span>
  </p>
</template>

templateタグ

<!-- 複数タグをレンダリングするか判定する場合、まとめるタグが必要 -->

<!-- 条件に合致すると<div>タグ自体もレンダリングされる -->
<!-- ディレクティブのためのタグなので意味がなく無駄に階層が深くなる -->
<div v-if="showOrNot">
  <img alt="vue logo" src="./assets/logo.svg">
  <p>ロゴを表示</p>
</div>

<!-- templateタグは条件が合致して内部コードがレンダリングされる際にはレンダリングされない -->
<template v-if="showOrNot">
  <img alt="vue logo" src="./assets/logo.svg">
  <p>ロゴを表示</p>
</template>

v-show 表示/非表示

<script setup lang="ts">
import { computed } from 'vue'

const showOrNot = computed((): boolean => {
  let showOrNot = false
  const rand = Math.round(Math.random() * 100)
  if (rand >= 50) {
    showOrNot = true
  }
  return showOrNot
})
</script>

<template>
  <section>
    v-ifを利用
    <p v-if="showOrNot">条件に合致したので表示</p>
  </section>
  <section>
    v-showを利用
    <p v-show="showOrNot">条件に合致したので表示</p>
  </section>
</template>
<section> 
  v-ifを利用
  <p>条件に合致したので表示</p>
</section>

<section>
  v-showを利用
  <p>条件に合致したので表示</p>
</section>
<section>
  v-ifを利用
  <!--v-if-->
</section>

<section>
  v-showを利用
  <p style="display: none;">条件に合致したので表示</p>
</section>
タグレンダリング適した処理
v-show常にレンダリングされる表示/非表示の切り替えコストが低いので、頻繁に切り替えるような処理に向く
v-if条件に合致しない場合レンダリングされない合致しない場合レンダリングコストがかからないので、表示/非表示があらかじめ決まっていて切り替えが不要な場合に向く

v-for ループのディレクティブ

配列のループ

v-for="各要素を格納する変数 in ループ対象"
v-for="(各要素を格納する変数, インデックス値を格納する変数) in ループ対象"
<script setup lang="ts">
import { ref } from 'vue'

const cocktailListInit: string[] = ['ホワイトレディ', 'ブルーハワイ', 'ニューヨーク']
const cocktailList = ref(cocktailListInit)
</script>

<template>
  <ul>
    <!-- 基本的なループ -->
    <!-- v-bind:key は必須。各要素をVue本体から識別するためのキーなので一意となる文字列が的する。-->
    <li v-for="cocktailName in cocktailList" v-bind:key="cocktailName">{{ cocktailName }}</li>
  </ul>
  <ul>
    <!-- インデックスを利用 -->
    <!-- インデックス値は v-bind:keyには的さない -->
    <li v-for="(cocktailName, index) in cocktailList" v-bind:key="cocktailName">
      {{ cocktailName }}(インデックス{{ index }})
    </li>
  </ul>
</template>

連想配列のループ

v-for="(各要素の値を格納する変数, 各要素のキーを格納する変数) in ループ対象"
v-for="(各要素の値を格納する変数, 各要素のキーを格納する変数, インデックス値を格納する変数) in ループ対象"
<script setup lang="ts">
import { ref } from 'vue'

<!-- 連想配列 -->
<!-- { [key: number]: string } はTypeScriptのキーと値のデータ型を指定 -->
const cocktailListInit: { [key: number]: string } = {
  2345: 'ホワイトレディ',
  4412: 'ブルーハワイ',
  6792: 'ニューヨーク',
}

const cocktailList = ref(cocktailListInit)
</script>

<template>
  <ul>
    <!-- 連想配列をループ -->
    <li v-for="(cocktailName, id) in cocktailList" v-bind:key="'cocktailList' + id">
      IDが{{ id }}のカクテルは{{ cocktailName }}
    </li>
  </ul>
  <ul>
    <!-- 連想配列でインデックス値を利用 -->
    <li v-for="(cocktailName, id, index) in cocktailList" v-bind:key="'cocktailListWithIdx' + id">
      {{ index + 1 }}: IDが{{ id }}のカクテルは{{ cocktailName }}
    </li>
  </ul>
</template>

Key 値の工夫

連想配列のキーは各データを区別可能であるため v-bind:key にもってこいである。
しかし、同じデータを同一コンポーネント内で複数回ループ表示される場合は、v-bind:key が一意になるようにIDの前に文字列を付与するなど工夫する必要がある。
サンプルコードではループごとに、IDの前に「cocktailList」と「cocktailListWithIdx」を付けている。

v-bind:key="'cocktailList' + id"

v-bind:key="'cocktailListWithIdx' + id"

Map のループ

v-for="[各要素のキーを格納する変数, 各要素の値を格納する変数] in ループ対象"
<script setup lang="ts">
import { ref } from 'vue'

<!-- Map 定義 -->
const cocktailListInit = new Map<number, string>()
cocktailListInit.set(2345, 'ホワイトレディ')
cocktailListInit.set(4412, 'ブルーハワイ')
cocktailListInit.set(6792, 'ニューヨーク')
const cocktailList = ref(cocktailListInit)
</script>

<template>
  <ul>
    <!-- エイリアス部分はブラケット[]で囲む -->
    <!-- 括弧内の変数は連想配列と逆で左側にキー、右側に値が格納される -->
    <li v-for="[id, cocktailName] in cocktailList" v-bind:key="id">
      IDが{{ id }}のカクテルは{{ cocktailName }}
    </li>
  </ul>
</template>

連想配列を利用していない通常のオブジェクトのループ

<script setup lang="ts">
import { ref } from 'vue'

// ひとつのカクテルを表すオブジェクトリテラル
const whiteLadyInit: {
  id: number
  name: string
  price: number
  recipe: string
} = {
  id: 2345,
  name: 'ホワイトレディ',
  price: 1200,
  recipe: 'ジン30ml+コワントロー15ml+レモン果汁+15ml',
}

// オブジェクトリテラルをテンプレート変数へ
const whiteLady = ref(whiteLadyInit)
</script>

<template>
  <dl>
    <!-- オブジェクトのループは連想配列と同じ -->
    <!-- 複数のタグを繰り返すには templateタグを使う。ここでは<dt>と<dd>をまとめている -->
    <template v-for="(value, key) in whiteLady" v-bind:key="key">
      <dt>{{ key }}</dt>
      <dd>{{ value }}</dd>
    </template>
  </dl>
</template>

オブジェクトの配列のループ

<script setup lang="ts">
import { ref } from 'vue'

// 配列の要素がオブジェクト
const cocktailDataListInit: Cocktail[] = [
  { id: 2345, name: 'ホワイトレディ', price: 1200 },
  { id: 4412, name: 'ブルーハワイ', price: 1500 },
  { id: 6792, name: 'ニューヨーク', price: 1100 },
  { id: 8429, name: 'マティーニ', price: 1500 },
]
const cocktailDataList = ref(cocktailDataListInit)

//オブジェクトの型定義を再利用したい場合は 
// TypeScript ではインターフェースとして型定義を別に定義することができる
interface Cocktail {
  id: number
  name: string
  price: number
}
</script>

<template>
  <ul>
    <li v-for="cocktailItem in cocktailDataList" v-bind:key="cocktailItem.id">
      {{ cocktailItem.name }}の値段は{{ cocktailItem.price }}円
    </li>
  </ul>
</template>

Mapとオブジェクトの組み合わせのループ

<script setup lang="ts">
import { ref } from 'vue'

// オブジェクトを要素として持つMap
// キーはナンバー型、値はCocktail型
const cocktailDataListInit = new Map<number, Cocktail>()
cocktailDataListInit.set(2345, { id: 2345, name: 'ホワイトレディ', price: 1200 })
cocktailDataListInit.set(4412, { id: 4412, name: 'ブルーハワイ', price: 1500 })
cocktailDataListInit.set(6792, { id: 6792, name: 'ニューヨーク', price: 1100 })
cocktailDataListInit.set(8429, { id: 8429, name: 'マティーニ', price: 1500 })

const cocktailDataList = ref(cocktailDataListInit)

// オブジェクトのインターフェース
interface Cocktail {
  id: number
  name: string
  price: number
}
</script>

<template>
  <ul>
    <!-- Mapを利用するのでエイリアスは[] -->
    <li v-for="[id, cocktailItem] in cocktailDataList" v-bind:key="id">
      {{ cocktailItem.name }}の値段は{{ cocktailItem.price }}円
    </li>
  </ul>
</template>

カウンタ変数を利用したループ

v-for="カウンタ変数 in 終端の値"
<script setup lang="ts">
</script>
<template>
  <ul>
    <!-- inの右側には終端の値のみを記述 -->
    <!-- 開始時の値は 1 で固定されている -->
    <li v-for="r in 5" v-bind:key="r">半径{{ r }}の円の円周:{{ 2 * r * 3.14 }}</li>
  </ul>
</template>

リスト操作

ループ対象データの絞り込み

<script setup lang="ts">
import { ref, computed } from 'vue'

// 配列の要素がオブジェクト
const cocktailDataListInit: Cocktail[] = [
  { id: 2345, name: 'ホワイトレディ', price: 1200 },
  { id: 4412, name: 'ブルーハワイ', price: 1500 },
  { id: 6792, name: 'ニューヨーク', price: 1100 },
  { id: 8429, name: 'マティーニ', price: 1500 },
]
const cocktailDataList = ref(cocktailDataListInit)

// データを絞り込む算出プロパティ
const cocktail1500 = computed((): Cocktail[] => {

  // 配列のfilter()メソッドを使って新たな配列を生成
  const newList = cocktailDataList.value.filter(

    // filter()メソッドの絞り込み条件関数
    // 引数は配列の各要素であるcocktailオブジェクト
    (cocktailItem: Cocktail): boolean => {
      // 値段が1500かどうかの結果を戻り値とする
      return cocktailItem.price == 1500
    },
  )
  
  // 絞り込まれた新たな配列を返す
  return newList
})

// オブジェクトのインターフェース
interface Cocktail {
  id: number
  name: string
  price: number
}
</script>

<template>
  <!-- そのまま表示 -->
  <section>
    全てのカクテルリスト
    <ul>
      <li v-for="cocktailItem in cocktailDataList" v-bind:key="cocktailItem.id">
        {{ cocktailItem.name }}の値段は{{ cocktailItem.price }}円
      </li>
    </ul>
  </section>

  <!-- データを絞り込んで表示 -->
  <section>
    値段が1500円のカクテルリスト
    <ul>
      <!-- 算出プロパティのテンプレート変数をループ対象とする -->
      <li v-for="cocktailItem in cocktail1500" v-bind:key="'coctail1500' + cocktailItem.id">
        {{ cocktailItem.name }}の値段は{{ cocktailItem.price }}円
      </li>
    </ul>
  </section>
</template>
配列の filter() メソッド

配列オブジェクトの filter() メソッドは JavaScript/TypeScript の標準メソッドであり配列内の条件に合致した要素で構成された新たな配列を生成する。
このメソッドの引数には、条件に合致するかどうか判別する関数を渡す。

const cocktail1500 = computed((): Cocktail[] => {  // Coctail型の配列を返す
  const newList = cocktailDataList.value.filter(   // filter()
    (cocktailItem: Cocktail): boolean => {         // 条件判別関数(アロー式)
      return cocktailItem.price == 1500            // 要素の条件評価。true/false値を返す
    },
  )
  return newList
})

配列のデータ操作

<script setup lang="ts">
import { ref } from 'vue'

// 配列データ
const cocktailListInit: string[] = ['ホワイトレディ', 'ブルーハワイ', 'ニューヨーク']
const cocktailList = ref(cocktailListInit)

// (1) 配列そのものをごっそり入れ替え
const changeCocktailList = (): void => {
  cocktailList.value = ['バラライカ', 'XYZ', 'マンハッタン']
}

// (2) 配列の末尾に要素を追加
const addCocktailList = (): void => {
  cocktailList.value.push('ブルームーン')
}

// (3) 配列の末尾要素を削除
const deleteFromCocktailList = (): void => {
  cocktailList.value.pop()
}
</script>

<template>

  <!-- 配列データ要素を表示 -->
  <!-- 下記のボタンで配列を操作するだけで表示が変わる -->
  <ul>
    <li v-for="(cocktailName, index) in cocktailList" v-bind:key="cocktailName">
      {{ cocktailName }}(インデックス{{ index }})
    </li>
  </ul>

  <!-- (1) 配列そのものをごっそり入れ替え -->
  <p>
    CocktailListを
    <button v-on:click="changeCocktailList">変更</button>
  </p>

  <!-- (2) 配列の末尾に要素を追加 -->
  <p>
    CocktailListの末尾に「ブルームーン」を
    <button v-on:click="addCocktailList">追加</button>
  </p>

  <!-- (3) 配列の末尾要素を削除 -->
  <p>
    CocktailListから末尾の要素を
    <button v-on:click="deleteFromCocktailList">削除</button>
  </p>
</template>
Vue内でラップされた状態で用意されている配列操作メソッド
メソッド内容
push(element)element を末尾に追加
pop()末尾の要素を削除
shift()先頭の要素を削除
unshift(element)elemnt を先頭に追加
splice(start[, count[, e1[, e2, …]]])start 番目から count 個の要素を e1, e2, …, で置き換え
sort()ソート
reverse()順番反転

上記以外のメソッドを利用する場合は、新たな配列を生成してごっそり入れ替えた方が良い。

Map データ操作

<script setup lang="ts">
import { ref } from 'vue'

// Map データ
const cocktailListInit = new Map<number, string>()
cocktailListInit.set(2345, 'ホワイトレディ')
cocktailListInit.set(4412, 'ブルーハワイ')
cocktailListInit.set(6792, 'ニューヨーク')

// Map テンプレート変数
const cocktailList = ref(cocktailListInit)

// (1) v-on:clickのメソッド。Map要素をごっそり入れ替え
const changeCocktailList = (): void => {
  cocktailList.value.clear()
  cocktailList.value.set(3416, 'バラライカ')
  cocktailList.value.set(5517, 'XYZ')
  cocktailList.value.set(7415, 'マンハッタン')
}

// (2) Mapの末尾に要素を追加
const addCocktailList = (): void => {
  cocktailList.value.set(8894, 'ブルームーン')
}

// (3) Mapから指定の要素を削除
const deleteFromCocktailList = (): void => {
  cocktailList.value.delete(5517)
}
</script>

<template>

  <!-- Mapの要素を表示 -->
  <ul>
    <li v-for="[id, cocktailName] in cocktailList" v-bind:key="id">
      IDが{{ id }}のカクテルは{{ cocktailName }}
    </li>
  </ul>

  <!-- (1) Map要素をごっそり入れ替え -->
  <p>
    CocktailListを
    <button v-on:click="changeCocktailList">変更</button>
  </p>

 <!-- (2) Mapの末尾に要素を追加 -->
  <p>
    CocktailListに「ブルームーン」を
    <button v-on:click="addCocktailList">追加</button>
  </p>

  <!-- (3) Mapから指定の要素を削除 -->
  <p>
    CocktailListから5517のXYZを
    <button v-on:click="deleteFromCocktailList">削除</button>
  </p>
</template>

オブジェクト内のデータ変更

<script setup lang="ts">
import { ref } from 'vue'

// ひとつのカクテルを表すオブジェクトリテラル
const whiteLadyInit: {
  id: number
  name: string
  price: number
  recipe: string
} = {
  id: 2345,
  name: 'ホワイトレディ',
  price: 1200,
  recipe: 'ジン30ml+コワントロー15ml+レモン果汁+15ml',
}

// オブジェクトのテンプレート変数
const whiteLady = ref(whiteLadyInit)

// オブジェクト内データ変更メソッド
const changeWhiteLadyPrice = (): void => {
  <!-- テンプレート変数内のカクテルデータを表すオブジェクトの価格を変更 -->
  whiteLady.value.price = 1500
}
</script>

<template>
  <!-- オブジェクトデータ表示 -->
  <dl>
    <template v-for="(value, key) in whiteLady" v-bind:key="key">
      <dt>{{ key }}</dt>
      <dd>{{ value }}</dd>
    </template>
  </dl>

  <!-- クリックイベント -->
  <p>
    価格を1500円に
    <button v-on:click="changeWhiteLadyPrice">変更</button>
  </p>
</template>

 リストデータ内のオブジェクトの変更

<script setup lang="ts">
import { ref, computed } from 'vue'

// オブジェクトを要素として持つMap
const cocktailDataListInit = new Map<number, Cocktail>()
cocktailDataListInit.set(2345, { id: 2345, name: 'ホワイトレディ', price: 1200 })
cocktailDataListInit.set(4412, { id: 4412, name: 'ブルーハワイ', price: 1500 })
cocktailDataListInit.set(6792, { id: 6792, name: 'ニューヨーク', price: 1100 })
cocktailDataListInit.set(8429, { id: 8429, name: 'マティーニ', price: 1500 })

// オブジェクトを要素として持つMapのテンプレート変数
const cocktailDataList = ref(cocktailDataListInit)

// データを絞り込む算出プロパティ
const cocktail1500 = computed(
  // 算出関数。戻り値の型はMap
  (): Map<number, Cocktail> => {
    // 絞り込んだ要素を格納する新しいMapを用意
    const newList = new Map<number, Cocktail>()

    // cocktailDataList内のMapをループ処理 (froEach()メソッド利用)
    cocktailDataList.value.forEach(
      // ループの各処理内容を表す関数
      (value: Cocktail, key: number): void => {
        // カクテルの価格が1500円ならnewListに登録
        if (value.price == 1500) {
          newList.set(key, value)
        }
      },
    )

    // 絞り込んだ新しいMapを戻り値とする
    return newList
  },
)

// 価格変更メソッド v-on:clickで呼び出される
const changreWhileLadyPriceInList = (): void => {
  // id「2345」のデータを取得
  const whileLady = cocktailDataList.value.get(2345) as Cocktail

  // 価格変更
  whileLady.price = 1500
}

// オブジェクトのインターフェース
interface Cocktail {
  id: number
  name: string
  price: number
}
</script>

<template>

  <!-- 全カクテルのリスト表示 -->
  <section>
    全カクテルのリスト
    <ul>
      <li v-for="[id, cocktailItem] in cocktailDataList" v-bind:key="id">
        {{ cocktailItem.name }}の値段は{{ cocktailItem.price }}円
      </li>
    </ul>
  </section>

  <!-- 絞り込まれたリストの表示 -->
  <section>
    値段が1500円のカクテルリスト
    <ul>
      <li v-for="[id, cocktailItem] in cocktail1500" v-bind:key="'cocktail1500' + id">
        {{ cocktailItem.name }}の値段は{{ cocktailItem.price }}円
      </li>
    </ul>
  </section>

  <!-- クリックイベント 価格変更メソッド呼び出し -->
  <p>
    CocktailDataList内のホワイトレディの価格を1500円に
    <button v-on:click="changreWhileLadyPriceInList">変更</button>
  </p>
</template>
型のアサーション
const whileLady = cocktailDataList.value.get(2345) as Cocktail

Mapのgetの戻り値は「undefined 又は値のデータ型」となっている。
TypeScriptでは複数のデータ型を組み合わせたデータ型をユニオン型と呼んでおり、型は「Cocktail | undefined」と記述できる。
undefinedの可能性を残したままアクセスしようとするとエラーとなる。
このエラーを避けるために「as」を使い強制的に Cocktail型に変換している。
このような「as」による型変換を型アサーションという。

ウォッチャー

watchEffect() リアクティブの変化を監視する

リアクティブ変数の値の変化に応じて特定の処理を実行

// 引数としてコールバック関数を受け取る。
// コールバック関数内部にはリアクティブ変数が変化したときに実行する処理を記述
// コールバック関数内で利用されているリアクティブ変数全てを監視している
watchEffect(
  (): void => {
    リアクティブ変数に応じて実行される処理
  }
}
<script setup lang="ts">
import { ref, watchEffect } from 'vue'

// カクテル番号のテンプレート変数を用意
const cocktailNo = ref(1)

// カクテル番号に対応するカクテル情報のテンプレート変数を用意
const priceMsg = ref('')

// ウォッチャーとして設定。
// コールバック関数内で利用されているリアクティブ変数をすべて監視している。
watchEffect((): void => {
  // リアクティブ変数が変化したときに実行する処理
  priceMsg.value = getCocktailInfo(cocktailNo.value)
})

// cocktailNoを1秒ごとに1~4の乱数を使って変更
setInterval((): void => {
  cocktailNo.value = Math.round(Math.random() * 3) + 1
}, 1000)

interface Cocktail {
  id: number
  name: string
  price: number
}

// カクテル番号に対応するカクテル情報を取得する関数
// リアクティブ変数が変化したときに呼び出される関数
function getCocktailInfo(cocktailNo: number): string {
  const cocktailDataListInit = new Map<number, Cocktail>()
  cocktailDataListInit.set(1, { id: 1, name: 'ホワイトレディ', price: 1200 })
  cocktailDataListInit.set(2, { id: 2, name: 'ブルーハワイ', price: 1500 })
  cocktailDataListInit.set(3, { id: 3, name: 'ニューヨーク', price: 1100 })
  cocktailDataListInit.set(4, { id: 4, name: 'マティーニ', price: 1500 })

  // カクテル番号に該当するカクテルデータを取得
  const cocktail = cocktailDataListInit.get(cocktailNo)

  // カクテル番号に該当する情報がない場合のメッセージを用意
  let msg = '該当カクテルはありません。'

  // カクテル番号に該当する情報があるなら
  if (cocktail != undefined) {
    // カクテル番号に該当するカクテルの名前と金額を表示する文字列を生成
    msg = `該当するカクテルは${cocktail.name}で、価格は${cocktail.price}円です。`
  }
  // 表示文字列をリターン
  return msg
}
</script>

<template>
  <p>現在のカクテル番号: {{ cocktailNo }}</p>
  <p>{{ priceMsg }}</p>
</template>

watch() 監視対象を明示する

watch(監視対象リアクティブ変数,
  (): vooid => {
    監視対象が変化した際に実行される処理
  }
}
// 第1引数を配列とすれば複数の変数を監視対象にできる
watch([監視対象リアクティブ変数1, 監視対象リアクティブ変数2, ・・・],
  (): void => {
    // 必要な処理
  }
}
// watch()は初回起動時にはコールバック関数を実行しない。
// 実行するには、第3引数に{immediate: true}オプションを指定。
watch(監視対象リアクティブ変数,
  (): void => {
    監視対象が変化した際に実行される処理
  },
  {immediate: true}
}
// コールバック関数の引数を指定する。
watch(監視対象リアクティブ変数,
  (newVal: データ型, oldVal: データ型): void => {  // コールバックの引数を指定
    監視対象が変化した際に実行される処理
  }
}
// コールバック関数の引数を配列で指定する。
watch([監視対象リアクティブ変数1, 監視対象リアクティブ変数2, ・・・],
  (newVal: データ型[], oldVal: データ型[]): void => {  // コールバックの引数を指定
    監視対象が変化した際に実行される処理
  }
}
<script setup lang="ts">
import { ref, watch } from 'vue'

// カクテル番号のテンプレート変数を用意
const cocktailNo = ref(1)

// カクテル番号に対応するカクテル情報のテンプレート変数を用意
const priceMsg = ref('')

// watchを設定
watch(
  cocktailNo,
  (newVal, oldVal): void => {
    // 表示用文字列を用意
    let msg = '前のカクテル:'
    msg += getCocktailInfo(oldVal)
    msg += '<BR>現在のカクテル:'
    msg += getCocktailInfo(newVal)
    // 表示文字列をpriceMsgに設定
    priceMsg.value = msg
  }
)

// cocktailNoを1秒ごとに1~4の乱数を使って変更
setInterval((): void => {
  cocktailNo.value = Math.round(Math.random() * 3) + 1
}, 1000)

interface Cocktail {
  id: number
  name: string
  price: number
}

// カクテル番号に対応するカクテル情報を取得する関数
function getCocktailInfo(cocktailNo: number): string {
  console.log('->', cocktailNo)
  const cocktailDataListInit = new Map<number, Cocktail>()
  cocktailDataListInit.set(1, { id: 1, name: 'ホワイトレディ', price: 1200 })
  cocktailDataListInit.set(2, { id: 2, name: 'ブルーハワイ', price: 1500 })
  cocktailDataListInit.set(3, { id: 3, name: 'ニューヨーク', price: 1100 })
  cocktailDataListInit.set(4, { id: 4, name: 'マティーニ', price: 1500 })

  // カクテル番号に該当するカクテルデータを取得
  const cocktail = cocktailDataListInit.get(cocktailNo)

  // カクテル番号に該当する情報がない場合のメッセージを用意
  let msg = '該当カクテルはありません。'

  // カクテル番号に該当する情報があるなら
  if (cocktail != undefined) {
    // カクテル番号に該当するカクテルの名前と金額を表示する文字列を生成
    msg = `該当するカクテルは${cocktail.name}で、価格は${cocktail.price}円です。`
  }
  // 表示文字列をリターン
  return msg
}
</script>

<template>
  <p>現在のカクテル番号: {{ cocktailNo }}</p>
  <!-- p>{{ priceMsg }}</p -->
  <p v-html="priceMsg"></p>
</template>

ライフサイクルフック

コンポーネントを解析、表示、非表示する間に生じるさまざまなコンポーネントの状態の遷移をライフサイクルと言い、それぞれの状態に応じて処理を行うライフサイクルフックという関数が用意されている。

  1. Vueアプリケーションが起動する
  2. Vueアプリケーションの初期化処理が行われる
    この段階でスクリプトブロック内のコードが実行され、テンプレート変数や算出プロパティ、メソッドなどが準備される
  3. コンポーネントの解析処理が行われる
    これによりテンプレートブロック内のコードが解析され、タグ構成(DOM構造)が決定される
  4. 決定したDOM構造がレンダリングされる
  5. レンダリング処理が完了すると、表示状態になる
    この状態で初めて画面が見えるようになる
    この状態を公式ドキュメントでは Mounted と表現している
  6. リアクティブシステムにより再レンダリングが必要になった際に行われる処理
    再レンダリングが終わると再びMounted状態に戻る
  7. 表示が不要になったコンポーネントに非表示処理を行う
  8. 非表示処理が完了すると、コンポーネントは非表示状態となる
    この状態を公式ドキュメントでは Unmounted と表現している
onライフサイクルフック名(
  () => {
    行いたい処理
  }
)
<script setup lang="ts">
import {
  ref,
  computed,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onRenderTracked,
  onRenderTriggered,
} from 'vue'

// 外部モジュールで定義されたインターフェースなどのデータ型定義そのもののインポートは、
// import type を利用する。
import type { DebuggerEvent } from 'vue'

// リアクティブ変数
const heightInit = Math.round(Math.random() * 10)
const widthInit = Math.round(Math.random() * 10)
const height = ref(heightInit) // 縦
const width = ref(widthInit)   // 横

// 面積計算、算出プロパティ
const area = computed((): number => {
  return height.value * width.value
})

// ボタンのイベントハンドラ
// 縦・横の値をランダムに変更
const change = (): void => {
  height.value = Math.round(Math.random() * 10)
  width.value = Math.round(Math.random() * 10)
}

//------------------
// ライフサイクルフック
//------------------
// レンダリング処理の前
onBeforeMount((): void => {
  console.log(`beforeMount called: ${height.value} * ${width.value}`)
})

// レンダリング処理終了
onMounted((): void => {
  console.log(`mounted called: ${height.value} * ${width.value}`)
})

// 再レンダリング処理の前
onBeforeUpdate((): void => {
  console.log(`befpreUpdate called: ${height.value} * ${width.value}`)
})

// 再レンダリング処理の後
onUpdated((): void => {
  console.log(`updated called: ${height.value} * ${width.value}`)
})

// デバッグ用のライフサイクルフック
// リアクティブデータに初回アクセスのタイミングで呼び出される
// 引数で DebuggerEventが渡される
onRenderTracked((event: DebuggerEvent): void => {
  console.log(`renderTracked called: ${height.value} * ${width.value}`)
  console.log(event)
})

// デバッグ用のライフサイクルフック
// リアクティブデータにアクセスするたびに呼び出される
// 引数で DebuggerEventが渡される
onRenderTriggered((event: DebuggerEvent): void => {
  console.log(`renderTriggered called: ${height.value} * ${width.value}`)
  console.log(event)
})
</script>

<template>
  <p>縦が{{ height }}、横が{{ width }}の長方形の面積は{{ area }}</p>
  <button v-on:click="change">値を変更</button>
</template>
ライフサイクルフック名呼び出しのタイミング
beforeCreate起動開始直後、Vue アプリケーションの初期化処理前
createdVue アプリケーションの初期化処理後
beforeMountコンポーネントの解析処理後、決定した DOM をレンダリングする直前
mountedDOM のレンダリングが完了し、表示状態になった時点。Mounted状態になった時点。
beforeUpdateリアクティブデータが変更され、DOM の再レンダリング処理を行う前
updatedDOM の再レンダリングが完了した時点
beforeUnmountコンポーネントの DOM の非表示処理を開始する直前
unmountedコンポーネントの DOM の非表示処理が完了した時点。Unmountedな状態になった時点
errorCaptured配下のコンポーネントを含めてエラーを検知した時点
renderTrackedリアクティブ変数に初めてアクセスが発生した時点
renderTriggeredリアクティブ変数が変更されたのを検知して、その変数へのアクセスがあった時点
activatedコンポーネントが待機状態ではなくなった時点
deactivatedコンポーネントが待機状態になった時点

子コンポーネントの利用

<script setup lang="ts">
// 子コンポーネントの.vueファイルをインポート
import OneSection from './components/OneSection.vue'
</script>

<template>
  <h1>コンポーネント基礎</h1>
  <section>
    <h2>コンポーネント1個</h2>

    <!-- テンプレートブロックにタグを記述 -->
    <OneSection />

  </section>
  <section>
    <h2>コンポーネントが複数</h2>

    <!-- テンプレートブロックにタグを記述 -->
    <OneSection />
    <OneSection />
    <OneSection />

  </section>
</template>

<style>
section {
  border: blue 1px solid;
  margin: 10px;
}
</style>
<template>
  <section class="box">
    <h4>ひとつのコンポーネント</h4>
    <p>コンポーネントとは、・・・</p>
  </section>
</template>

<style>
.box {
  border: green 1px dashed;
  margin: 10px;
}
</style>

スタイルブロックを独立させる

<script setup lang="ts">
import WithMode from './components/WithModel.vue'
</script>

<template>
  <h1>コンポーネントの独立性</h1>
  <section>
    <h2>v-modelを含むコンポーネント</h2>
    <WithMode />
    <WithMode />
  </section>
</template>

<style>
section {
  border: blue 1px solid;
  margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'

const name = ref('名無し')
</script>

<template>
  <section>
    <p>{{ name }}さんですね!</p>
    <input type="text" v-model="name" />
  </section>
</template>

<style scoped>
section {
  border: green 1px dashed;
  margin: 10px;
}
</style>

親から子へのコンポーネント間通信

Props(プロップス)親からデータをもらう

interface Props {
  各Prop名: データ型
   :
}
defineProps<Props>()
<子コンポーネント名
  各Prop名="このPropsに渡す値"
    :
/>
<script setup lang="ts">
import OneInfo from './components/OneInfo.vue'
</script>

<template>
  <h1>Props基礎</h1>
  <section>
    <h2>属性に直接記述</h2>

    <!-- 親からPropsにデータを渡す -->
    <OneInfo 
     title="Propsの利用" 
     content="子コンポーネントにデータを渡すにはPropsを利用する。" />

  </section>
</template>

<style>
section {
  border: blue 1px solid;
  margin: 10px;
}
</style>
<script setup lang="ts">

// 此処のPropを記述したインターフェース定義
interface Props {
  title: string
  content: string
}

// defineProps() 関数を実行
defineProps<Props>()

</script>

<template>
  <section class="box">
    <h4>{{ title }}</h4>
    <p>{{ content }}</p>
  </section>
</template>

<style scoped>
.box {
  border: green 1px dashed;
  margin: 10px;
}
</style>

Props(プロップス)親のテンプレート変数をPropsに渡す

<子コンポーネント名
  v-bind:Prop名="テンプレート変数名"
   :
/>
<script setup lang="ts">
import { ref } from 'vue'
import OneInfo from './components/OneInfo.vue'

// テンプレート変数定義
const propsTitle = ref('発生した乱数')
const rand = Math.round(Math.random() * 100)
const propsContent = ref(rand)

</script>

<template>
  <h1>Props基礎</h1>
  <section>
    <h2>テンプレート変数を利用</h2>
    
    <!-- 親からPropsにテンプレート変数を渡す -->
    <OneInfo 
     v-bind:title="propsTitle" 
     v-bind:content="propsContent" />
  </section>
</template>

<style>
section {
  border: blue 1px solid;
  margin: 10px;
}
</style>
<script setup lang="ts">

// 此処のPropを記述したインターフェース定義
interface Props {
  title: string
  content: number
}

// defineProps() 関数を実行
defineProps<Props>()

</script>

<template>
  <section class="box">
    <h4>{{ title }}</h4>
    <p>{{ content }}</p>
  </section>
</template>

v-forとPropsとの組み合わせ

<script setup lang="ts">
import { ref } from 'vue'
import OneInfo from './components/OneInfo.vue'

// 表示用のリストデータを用意
const weatherListInit = new Map<number, Weather>()
weatherListInit.set(1, { id: 1, title: '今日の天気', content: '今日は一日中、晴れでしょう。' })
weatherListInit.set(2, { id: 2, title: '明日の天気', content: '明日は一日中、雨れでしょう。' })
weatherListInit.set(3, { id: 3, title: '明後日の天気', content: '明後日は一日中、雪でしょう。' })
const weatherList = ref(weatherListInit)

interface Weather {
  id: number
  title: string
  content: string
}
</script>

<template>
  <h1>Props基礎</h1>
  <section>
    <h2>ループでコンポーネントを生成</h2>

    <!-- ループするタグはOneInfo -->
    <OneInfo
      v-for="[id, weather] in weatherList"
      v-bind:key="id"
      v-bind:title="weather.title"
      v-bind:content="weather.content"
    />
  </section>
</template>
<script setup lang="ts">

// 此処のPropを記述したインターフェース定義
interface Props {
  title: string
  content: string
}

// defineProps() 関数を実行
defineProps<Props>()

</script>

<template>
  <section class="box">
    <h4>{{ title }}</h4>
    <p>{{ content }}</p>
  </section>
</template>

<style scoped>
.box {
  border: green 1px dashed;
  margin: 10px;
}
</style>

Propsの応用

<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'

// 会員リストデータを用意
const memberListInit = new Map<number, Member>()
memberListInit.set(33456, { id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',})
memberListInit.set(47783, { id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,})
const memberList = ref(memberListInit)

// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => {
  let total = 0
  for (const member of memberList.value.values()) {
    total += member.points
  }
  return total
})

// 会員情報インターフェース
interface Member {
  id: number
  name: string
  email: string
  points: number
  note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>

<template>
  <section>
    <h1>会員リスト</h1>
    <p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
    <!-- OneMemberをv-forでループ-->
    <OneMember
      v-for="[id, member] in memberList"
      v-bind:key="id"
      v-bind:id="id"
      v-bind:name="member.name"
      v-bind:email="member.email"
      v-bind:points="member.points"
      v-bind:note="member.note"
    />
  </section>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'

// Propsインターフェースの定義
interface Props {
  id: number
  name: string
  email: string
  points: number
  note?: string // ?はオプションパラメーター(引数省略可能)
}

// Propsオブジェクトの設定
// 関数の戻り値をpropsで受け取っている
// これにより個々のデータが取り出せるようになる。
const props = defineProps<Props>()

// このコンポーネント内で利用するポイント数のテンプレート変数
const localPoints = ref(props.points)

// Propsのnoteを加工する算出プロパティ
const localNote = computed((): string => {
  let localNote = props.note
  if (localNote == undefined) {
    localNote = '--'
  }
  return localNote
})

// [ポイント加算]ボタンをクリックしたときのメソッド
// 各会員のポイントが増加
// App.vueの全会員の保有ポイント合計は変化しない
const pointUp = (): void => {
  localPoints.value++
}
</script>

<template>
  <section class="box">
    <h4>{{ name }}さんの情報</h4>
    <dl>
      <dt>ID</dt>
      <dd>{{ id }}</dd>
      <dt>メールアドレス</dt>
      <dd>{{ email }}</dd>
      <dt>保有ポイント</dt>
      <dd>{{ localPoints }}</dd>
      <dt>備考</dt>
      <dd>{{ localNote }}</dd>
    </dl>
    <button v-on:click="pointUp">ポイント加算</button>
  </section>
</template>

<style>
.box {
  border: green 1px solid;
  margin: 10px;
}
</style>
  • Propsの値は子コンポーネントでは直接変更できない。

Propsのデフォルト値

withDefaults(
  defineProps<Props>(),
  {
    非必須Prop名: デフォルト値,
     :
  }
)
<script setup lang="ts">
  :
// Propsオブジェクトの設定
// ディフォルト値設定なし
const props = defineProps<Props>()




// Propsのnoteを加工する算出プロパティ
const localNote = computed((): string => {                   // (4)
  let localNote = props.note                                 // (5)
  if (localNote == undefined) {
    localNote = '--'
  }
  return localNote
})
  :
</script>

<template>
  :
      <!-- 算出プロパティでデフォルト処理 -->
      <dd>{{ localNote }}</dd>
  :
</template>
<script setup lang="ts">
  :
// Propsオブジェクトの設定
// ディフォルト値を設定
const props = withDefaults(
  defineProps<Props>(),
  {note: "--"}
)

// 算出プロパティは不要






 
 :
</script>

<template>
  :
      <!-- テンプレート変数をそのまま表示 -->
      <dd>{{ note }}</dd>
  :
</template>

子から親へのコンポーネント通信

子から親への通信はイベント処理

interface Emits {
  (event: "イベント名"): void
  :
}
const emit = defineEmits<Emits>()

このサンプルは子コンポーいないネントから親コンポーネントのメソッドの実行のみでデータは渡していない。

<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'

// テンプレート変数
const randInit = Math.round(Math.random() * 10)
const rand = ref(randInit)

// 子コンポーネントから通知を受けた際に
// 実行する処理メソッド
const onCreateNewRand = (): void => {
  rand.value = Math.round(Math.random() * 10)
}
</script>

<template>
  <section>
    <p>親コンポーネントで乱数を表示: {{ rand }}</p>
    
    <!-- 子コンポーネントタグ -->
    <!-- v-on:イベント名を表す任意の文字列
            ="通知を受けた際に実行するメソッド -->
    <OneSection 
     v-bind:rand="rand" 
     v-on:createNewRand="onCreateNewRand" 
    />
  </section>
</template>
<script setup lang="ts">

interface Props {
  rand: number
}

// Emitのインタフェース定義
// (引数名ここではevent:'型としてイベント名')
interface Emits {
  (event: 'createNewRand'): void
}

defineProps<Props>()

// Emitsインタフェースをジェネリクスとして
// 指定しながらdefineEmits()関数を実行
const emit = defineEmits<Emits>()

// ボンタンクリックイベントのメソッド
// emit()を実行
const onNewRandButtonClick = (): void => {
  emit('createNewRand')
}
</script>

<template>
  <section class="box">
    <p>子コンポーネントで乱数を表示: {{ rand }}</p>
    <button v-on:click="onNewRandButtonClick">
      新たな乱数を発生
    </button>
  </section>
</template>

<style scoped>
.box {
  border: green 1px solid;
  margin: 10px;
}
</style>
  1. 子コンポーネントのボタンクリック
    1. v-on:clickのイベント onNewRandButtonClick()が実行される
    2. onNewRandButtonClick()の中でemit()が実行される
  2. emit()の引数 createNewRand に合致する親コンポーネントのv-on:createNewRandを探す
  3. 親コンポーネントのv-on:createNewRandのメソッドonCreateNewRand()が実行される
  4. onCreateNewRand()でコンポーネント変数randが変更される

このようにEmitを利用することで、子コンポーネントから親コンポーネントのメソッドが実行できる。

親コンポーネントにデータを渡す

<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'

// 会員リストデータを用意
const memberListInit = new Map<number, Member>()
memberListInit.set(33456, {id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',})
memberListInit.set(47783, {id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,})
const memberList = ref(memberListInit)

// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => {
  let total = 0
  for (const member of memberList.value.values()) {
    total += member.points
  }
  return total
})

// Emit により実行されるメソッド
// 引数idを受け取る
const onIncrementPoint = (id: number): void => {
  // 処理関数の id に該当する会員情報オブジェクトを取得
  const member = memberList.value.get(id)

  // 会員情報オブジェクトが存在するならポイントインクリメント
  if (member != undefined) {
    member.points++
  }
}

// 会員情報インターフェース
interface Member {
  id: number
  name: string
  email: string
  points: number
  note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>

<template>
  <section>
    <h1>会員リスト</h1>
    <p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
    <!-- OneMemberをv-forでループ-->
    <OneMember
      v-for="[id, member] in memberList"
      v-bind:key="id"
      v-bind:id="id"
      v-bind:name="member.name"
      v-bind:email="member.email"
      v-bind:points="member.points"
      v-bind:note="member.note"
      v-on:incrementPoint="onIncrementPoint"
    />
  </section>
</template>
<script setup lang="ts">

// Propsインターフェースの定義
interface Props {
  id: number
  name: string
  email: string
  points: number
  note?: string // ?はオプションパラメーター(引数省略可能)
}

// Emit インターフェースの定義
interface Emits {
  // event引数に加え第2引数でidを定義
  (event: 'incrementPoint', id: number): void
}

// Propsオブジェクトの設定
const props = withDefaults(
  defineProps<Props>(),
  {note: "--"}
)

// Emit の設定
const emit = defineEmits<Emits>()

// [ポイント加算]ボタンをクリックしたときのメソッド
const pointUp = (): void => {
  emit('incrementPoint', props.id)
}
</script>

<template>
  <section class="box">
    <h4>{{ name }}さんの情報</h4>
    <dl>
      <dt>ID</dt>
      <dd>{{ id }}</dd>
      <dt>メールアドレス</dt>
      <dd>{{ email }}</dd>
      <dt>保有ポイント</dt>
      <dd>{{ points }}</dd>
      <dt>備考</dt>
      <dd>{{ note }}</dd>
    </dl>
    <button v-on:click="pointUp">ポイント加算</button>
  </section>
</template>

<style>
.box {
  border: green 1px solid;
  margin: 10px;
}
</style>

子コンポーネント

Emitインターフェース定義で、event引数に加えて第2引数でidを定義

// Emit インターフェースの定義
interface Emits {
  // event引数に加え第2引数でidを定義
  (event: 'incrementPoint', id: number): void
}

親コンポーネント

Emitにより実行されるメソッドで引数idを受け取る

// Emit により実行されるメソッド
// 引数idを受け取る
const onIncrementPoint = (id: number): void => {
  // 処理関数の id に該当する会員情報オブジェクトを取得
  const member = memberList.value.get(id)

  // 会員情報オブジェクトが存在するならポイントインクリメント
  if (member != undefined) {
    member.points++
  }
}

v-model による子から親への通信

コメント

タイトルとURLをコピーしました