Closed10

Vue v3 Composition APIを試してみる

chart-sample

以前作った、Vue-chartをComposition APIを使用したもので作ろうと思います。

一旦ここまで動くようになりました。
Vue-chartの埋め込みで少し苦戦中。
こちら(Vue composition APIでchart.jsを使いグラフを作成(vue-chart.jsは使わない))を参照するに、vue-chartは使わない方が良さそうだったので、chart.jsで実装することに。

以下ソースコード。
ソースコードの話はまた後ほどやろうと思っています。

Home.vue
<template>
  <div>
    <h1>棒グラフと線グラフ</h1>
    <Tabs
      @switch-tab="switchTab"
    ></Tabs>
    <div class="chart-box">
      <Chart
        :chartData="chartData"
        :chartType="chartType"
      ></Chart>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, Ref } from 'vue'
import Chart from '@/components/Chart.vue'
import Tabs from '@/components/Tabs.vue'
import { data, TData } from '@/data/data'

export default defineComponent({
  name: 'Home',
  components: {
    Chart,
    Tabs
  },
  setup () {
    const chartType: Ref = ref(1)
    const test = 'test' as string
    const chartData = data as TData

    return {
      chartType,
      test,
      chartData,
      switchTab: (id: number) => {
        chartType.value = id
      }
    }
  }
})
</script>

<style>
h1 {
  text-align: center;
}

.chart-box {
  width: 80%;
  max-width: 800px;
  margin: 0 auto;
}
</style>
Tabs.vue
<template>
  <div class='tabs'>
    <Tab
      v-for='tab in tabs'
      :label='tab.label'
      :key='tab.id'
      :id='tab.id'
      @select-tab='onClick'
      :active='tab.active'
    ></Tab>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, onMounted } from 'vue'
import Tab from './atoms/Tab.vue'

type TTab = {
  id: number;
  label: string;
  active: boolean;
}

type TTabs = {
  tab1: TTab;
  tab2: TTab;
  tab3: TTab;
}

export default defineComponent({
  name: 'tabs',
  components: {
    Tab
  },
  setup (props, context) {
    // Tabの設定
    const tabs = reactive<TTabs>({
      tab1: {
        id: 1,
        label: 'TAB1',
        active: true
      },
      tab2: {
        id: 2,
        label: 'TAB2',
        active: false
      },
      tab3: {
        id: 3,
        label: 'TAB3',
        active: false
      }
    })
    const tabsArray: Array = Object.entries(tabs)

    onMounted(() => {
      // 最初のactiveを1つ目のタブに設定
      tabsArray.forEach((tab, index: number) => {
        tab[1].active = false
        if (index === 0) tab[1].active = true
      })
    })

    return {
      tabs,
      tabsArray,
      onClick: (propsId: number) => {
        const id = propsId
        tabsArray.map(tab => {
          tab[1].id === id ? tab[1].active = true : tab[1].active = false
        })
        context.emit('switch-tab', propsId)
      }
    }
  }
})
</script>

<style scoped>
.tabs {
  width: 500px;
  margin: 0 auto;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-bottom: 30px;
}
</style>
Tab.vue
<template>
  <span
      class="tab"
      :class="{ 'active': active }"
      :id="'tab-' + id"
      @click="onClick"
  >
    {{ label }}
  </span>
</template>

<script lang="ts">
import { defineComponent, SetupContext } from 'vue'

export default defineComponent({
  name: 'tab',
  props: {
    label: {
      type: String,
      required: true
    },
    id: {
      type: Number,
      required: true
    },
    active: {
      type: Boolean,
      required: true
    }
  },
  setup (props, context: SetupContext) {
    return {
      onClick: () => {
        context.emit('select-tab', props.id)
      }
    }
  }
})
</script>

<style scoped>
  span {
    cursor: pointer;
    padding: 10px;
    border: 1px solid rgba(191, 191, 191, 1);
  }

  span:hover {
    opacity: .5;
    transition: .3s;
  }

  .active {
    background: rgba(191, 191, 191, 1);
  }
</style>

流れとしては
それぞれのタブにidを割り振り、クリックされたidをトップレベルまで渡し、Chart.vueに渡しています。
渡されたidと、実際に描画するデータのidとを照らし合わせ、一致するデータでチャートを作成しています。
reactiveでリアクティブにしたデータなので、変更があればキャッチして、変更したデータを適応しているという感じになります。

Vue3のwatchは感覚的に、ReactでのhooksのuseEffectや、useCallbackの第二引数のような感じ。
リアクティブなオブジェクトに変更があった事を感知することによって発火するイベント。

とくにここが参考になりました。

リアクティブの状態でpropsを扱う場合は、computedにする必要がある。
参考箇所

参考:
Vue Composition API の watch & watchEffect についてまとめてみた

Vue.js の Composition API における親子コンポーネント間のデータ受け渡し

Sample Tab

一旦できました。
ただ、少しバグが残っているので直さないといけないところはありますが、挙動的にはリプレイスができたかなと思います。
気になっていたバグは修正できました。
自分の実装は結構強引にやった気もするので、こうした方がいいとかあると教えてもらえると嬉しいです!

Chart.vueに部分作成
Home.vueにも追記してあります。

Chart.vue
<template>
  <div>
    <canvas ref="canvasRef" />
  </div>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, watch, computed, reactive } from 'vue'
import { Chart } from 'chart.js'
import { options } from '@/config/config'
import { TChartData } from '@/data/data'

export default defineComponent({
  name: 'chart',
  props: {
    chartData: {
      type: TChartData,
      required: true
    },
    chartType: {
      type: Number,
      required: true
    }
  },
  setup (props) {
    // チャートに表示させるデータをまとめたオブジェクト
    const data = {
      dataSets: props.chartData,
      dataOptions: options
    }

    // 初期表示
    onMounted(() => {
      createCharts()
    })

    // computedせずに再代入するとreactiveではなくなってしまう。
    const chartType = computed(() => props.chartType)
    // chartTypeが変更されたかどうかを監視
    // 変更されたらChartを変更
    watch(chartType, () => {
      createCharts()
    })

    // canvasに対してref設定
    const canvasRef = ref<HTMLCanvasElement | null>(null)

    function getSelectedData () {
      const dataArray = Object.entries(data.dataSets)
      const newData = Object.entries(dataArray[0][1])
      const selectedArray = []
      newData.map(item => {
        if (chartType.value === Number(item[0])) {
          selectedArray.push(item[1])
        }
      })
      return selectedArray
    }

    const obj = {}

    function createCharts () {
      const data = getSelectedData()[0]
      if (canvasRef.value === null) return
      const canvas = canvasRef.value.getContext('2d')
      if (canvas === null) return
      // すでにチャートが作られていたら更新処理をする
      if (obj.chart instanceof Chart) {
        obj.chart.data.datasets[0] = data.datasets
        obj.chart.update()
      } else {
        // 初期表示の時だけチャートを生成
        const c = new Chart(canvas, {
          type: 'bar',
          data: {
            labels: data.labels,
            datasets: [{
              label: data.datasets.label,
              data: data.datasets.data
            }]
          }
        })
        obj.chart = c
      }
    }

    return {
      data,
      canvasRef
    }
  }
})
</script>
<style></style>

コメントに書いてあることで大体の説明は終わってるのですが
追記しておきます。

new Chart()しているところで、updateの記述がないとタブの切り替えを行った時に、一つ前に作ったチャートが残っていて、チャートをホバーすると複数のチャートに切り替わってしまう問題があったので
一度オブジェクトを保管しておき、Chart.jsの機能のupdateで切り替えることでインスタンスを使い回すようにしないといけませんでした。

ここが少しハマりポイントでした。

使用しているチャートのデータは以下

data.ts
export const data = {
  chartData: {
    1: {
      labels: ['January', 'February', 'March', 'April', 'May', 'June'],
      datasets: {
          label: 'Sample Data1',
          data: [10, 20, 30, 40, 50, 30]
       }
    },
    2: {
      labels: ['January', 'February', 'March', 'April', 'May', 'June'],
      datasets: {
        label: 'Sample Data2',
        data: [30, 10, 40, 50, 20, 30]
      }
    },
    3: {
      labels: ['January', 'February', 'March', 'April', 'May', 'June'],
      datasets: {
        label: 'Sample Data3',
        data: [20, 50, 10, 40, 50, 20]
      }
    }
  }
}

export type TData = {
  chartData: TChartData;
}

export type TChartData = {
  1: TDataItem;
  2: TDataItem;
  3: TDataItem;
}

type TDataItem = {
  labels: string[];
  datasets: object;
}

とても初歩的なところではありますが、
refでpropsを渡す時は、ref自体を渡さないといけない。
ref.value などでpropsに渡していて、propsの値が変更されずに困りました。

親コンポーネントから子コンポーネントのメソッドを実行する

このスクラップは4ヶ月前にクローズされました
ログインするとコメントできます