🕌

Material Design IconsをVue (Composition API+script setup+TypeScript)で使う

2023/02/12に公開

Pictogrammers - Open-source iconography for designers and developersの公式に書かれている方法は、TypeScriptの型が活かせません。

型が活かせるように、componentを書き直しました。

公式のやり方

まずは、公式。

公式で紹介されているVueでの利用方法は下記です。

ab-testing - Material Design Icons - Pictogrammers

(参考情報: webフォントは非推奨とのことですWebfont Alternatives - Docs - Pictogrammers

<template>
  <svg-icon type="mdi" :path="path"></svg-icon>
</template>

<script>
import SvgIcon from '@jamescoyle/vue-icon';
import { mdiAbTesting } from '@mdi/js';

export default {
  name: "my-component",
  components: {
    SvgIcon
  },
  data() {
    return {
       path: mdiAbTesting,
    }
  }
}
</script>

SVGのpath文字列のの情報を@mdi/jsからimportし、@jamescoyle/vue-iconのSvgIconコンポーネントで表示するというものです。

素直で良いのですが'@jamescoyle/vue-icon'に型がないのが唯一気になります。

該当componentを定義しているファイルは以下です。

https://github.com/Pictogrammers/vue-icon/blob/master/lib/svg-icon.vue

上記componentをTypeScript (特にComposition API + script setup + TypeScript)で書き直すチャレンジをしました。

Composition API + script setup + TypeScript化

早速成果物です。

https://gist.github.com/junara/f583a7053249ce535a81196bcb588d62

本componentは、Vue 2.7および Vue 3.2以降であれば動くと思います。

コードを以下に転記します。。

<template>
  <svg
    :width="sizeValue"
    :height="sizeValue"
    :viewBox="viewboxValue"
  >
    <path :d="path" />
  </svg>
</template>

<script setup lang="ts">
// https://github.com/Pictogrammers/vue-icon/blob/master/lib/svg-icon.vue
import { computed } from 'vue';

interface Props {
  type?: 'mdi' | 'simple-icons' | 'default';
  path: string;
  size?: string | number;
  viewbox?: string;
  flip?: 'horizontal' | 'vertical' | 'both' | 'none';
  rotate?: number;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'default',
  rotate: 0,
});

const types = new Map([
  ['mdi', { size: 24, viewbox: '0 0 24 24' }],
  ['simple-icons', { size: 24, viewbox: '0 0 24 24' }],
  ['default', { size: 0, viewbox: '0 0 0 0' }],
]);

const defaults = computed(() => {
  const t = types.get(props.type);
  if (!t) {
    throw new Error(`Unknown type ${props.type}`);
  } else {
    return t;
  }
});

const rotateValue = computed(() => {
  return isNaN(props.rotate) ? props.rotate : props.rotate + 'deg';
});

const scaleHorizontalValue = computed(() => {
  return props.flip && ['both', 'horizontal'].includes(props.flip) ? '-1' : '1';
});

const scaleVerticalValue = computed(() => {
  return props.flip && ['both', 'vertical'].includes(props.flip) ? '-1' : '1';
});

const sizeValue = computed(() => {
  return props.size || defaults.value.size;
});

const viewboxValue = computed(() => {
  return props.viewbox || defaults.value.viewbox;
});
</script>

<style scoped lang="css">
svg {
  transform: rotate(v-bind(rotateValue)) scale(v-bind(scaleHorizontalValue), v-bind(scaleVerticalValue));
}
path {
  fill: currentColor;
}
</style>

使い方は、'@jamescoyle/vue-icon'とほぼ同じ。

たとえば、上記を src/components/SvgIcon.vueに保存した場合はimport文をいかに書き換えるだけです。

<script>
// import SvgIcon from '@jamescoyle/vue-icon';
import SvgIcon from 'src/components/SvgIcon.vue';

// 省略
</script>

書き直した箇所の解説

数点書き直した箇所を解説します。

Vue 2.7およびVue3.2以降で使える、CSSのv-bindでstylesを代替しました。

SFC CSS 機能 | Vue.js

CSS内のv-bind(rotateValue) で JavaScript rotateValueをbindしています。

svg {
  transform: rotate(v-bind(rotateValue)) scale(v-bind(scaleHorizontalValue), v-bind(scaleVerticalValue));
}

v-bind代替に伴い、svgタグ内で存在していたstyles attributeは削除しました。

  <svg
    :width="sizeValue"
    :height="sizeValue"
    :viewBox="viewboxValue"
  >

v-bindのおかげで直感的なコードにできました。

interface Props {
  type?: 'mdi' | 'simple-icons' | 'default';
  path: string;
  size?: string | number;
  viewbox?: string;
  flip?: 'horizontal' | 'vertical' | 'both' | 'none';
  rotate?: number;
}

flipのvalidatorをTypeScriptのUnion型でシンプルに記述できました。

const types = new Map([
  ['mdi', { size: 24, viewbox: '0 0 24 24' }],
  ['simple-icons', { size: 24, viewbox: '0 0 24 24' }],
  ['default', { size: 0, viewbox: '0 0 0 0' }],
]);

ObjectをMapにしました。Objectのままでも良いです。
keyが文字列であることが明示的な方が読みやすいかとおもって、Mapにしました。

以上。

所感

Composition API + script setup + TypeScript 最高。

書いていて楽しい。

参考

Discussion