Open18

VueUse 覚書

koyama shigehitokoyama shigehito

Vue.jsのCompositionAPIの学習も兼ねて、VueUseのガイドを読みつつ実装を学習できたらよいなと思います。

VueUseはAnthony Fuさんを中心に開発されているCompositionAPIに基づくユーティリティ関数のライブラリ。
https://vueuse.org/
Vue2とVue3で使える(Vue Demiで実現している)

インストール

NPM

$ npm i @vueuse/core
# or
$ yarn add @vueuse/core

CDN

<script src="https://unpkg.com/@vueuse/shared"></script>
<script src="https://unpkg.com/@vueuse/core"></script>

使い方

必要な機能を@vueuse/coreからインポートして使用する

import { useMouse } from '@vueuse/core'
koyama shigehitokoyama shigehito

VueUseのほとんどの関数はrefsオブジェクトを返す

const mouse = useMouse()
console.log(mouse.x.value)

分割代入

const { x, y } = useMouse()
console.log(x.value)

refsreactive()でアンラップして使用できる

const mouse = reactive(useMouse())
// valueなしで呼び出し可能
console.log(mouse.x)
koyama shigehitokoyama shigehito

Event Filters

  • throttleFilter
    処理を一定間隔で間引く
// 例:localStorageへの書き込み制御
import { throttleFilter, useLocalStorage } from '@vueuse/core'

const storage = useLocalStorage(
  'my-key',
  { foo: 'bar' },
  { eventFilter: throttleFilter(1000) }
);
  • debounceFilter
    指定時間内に何度発生しても最後の1回だけ実行する
// 例:マウスポインターの位置情報取得
import { debounceFilter, useMouse } from '@vueuse/core'

const { x, y } = useMouse({ eventFilter: debounceFilter(100) })
  • pauseableFilter
    イベントの一時停止、再開
// 例:デバイスのモーションコントロールの制御
import { pauseableFilter, useDeviceMotion } from '@vueuse/core'

const motionControl = pauseableFilter()
const motion = useDeviceMotion({ eventFilter: motionControl.eventFilter })
// 一時停止
motionControl.pause() 
// 再開
motionControl.resume()
koyama shigehitokoyama shigehito

コンポーネントスタイル

@vueuse/componentsパッケージを使用するとコンポーネントスタイルで使用できる

インストール

$ npm i @vueuse/core @vueuse/components

<script setup>
import { OnClickOutside } from '@vueuse/components'

function close () {
  // 処理
}
</script>

<template>
  <OnClickOutside @trigger="close">
    <div>Click Outside of Me</div>
  </OnClickOutside>
</template>

v-slotで戻り値にアクセスできる

<UseMouse v-slot="{ x, y }">
  x: {{ x }}
  y: {{ y }}
</UseMouse>
koyama shigehitokoyama shigehito

useActiveElement

アクティブな要素を動的に取得

<script lang="ts">
import { defineComponent } from 'vue';
import { useActiveElement } from '@vueuse/core';

export default defineComponent({
  setup() {
    const activeElemet = useActiveElement();
    const active = computed(() => activeElemet.value?.dataset?.id || 'null');

    return {
      active,
    };
  },
});
</script>

<template>
  <h2 data-id="title">useActiveElement</h2>
  <label>
    猫:<input type="radio" name="animal" value="cat" data-id="cat" />
  </label>
  <label>
    犬:<input type="radio" name="animal" value="dog" data-id="dog" />
  </label>
  <div>選択中:{{ active }}</div>
</template>
koyama shigehitokoyama shigehito

useBreakpoints

ブレークポイントを動的に判定
ブレークポイントの値はパラメータで渡してあげる
別途関数でbreakpointsTailwindbreakpointsBootstrapV5などが用意されていて、importして利用できる
https://github.com/vueuse/vueuse/blob/main/packages/core/useBreakpoints/breakpoints.ts

<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useBreakpoints } from '@vueuse/core';
export default defineComponent({
  setup() {
    const breakpoints = useBreakpoints({
      tablet: 640,
      laptop: 1024,
      desktop: 1280,
    });

    const mobile = breakpoints.smaller('tablet');
    const tablet = breakpoints.between('tablet', 'laptop');
    const laptop = breakpoints.between('laptop', 'desktop');
    const desktop = breakpoints.greater('desktop');

    return {
      mobile,
      tablet,
      laptop,
      desktop,
    };
  },
});
</script>

<template>
  <h2>useBreakpoints</h2>
  <div v-if="mobile" class="p-[12px] bg-red-500 text-white">mobile</div>
  <div v-else-if="tablet" class="p-[12px] bg-green-500 text-white">tablet</div>
  <div v-else-if="laptop" class="p-[12px] bg-blue-500 text-white">laptop</div>
  <div v-else-if="desktop" class="p-[12px] bg-purple-500 text-white">
    desktop
  </div>
</template>
koyama shigehitokoyama shigehito

onClickOutside

要素の外側のクリックをリッスンする。
モーダルやドロップダウンメニューなどで、外側をクリックした際のロジックの実装用途など

onClickOutside(target, handler, options)

opstions
"click", "mousedown", "mouseup"など
https://vueuse.org/core/onClickOutside/#type-declarations

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { onClickOutside } from '@vueuse/core';

export default defineComponent({
  setup() {
    const modal = ref(false);
    const modalRef = ref(null);
    onClickOutside(modalRef, () => {
      modal.value = false;
    });

    return {
      modal,
      modalRef,
    };
  },
});
</script>

<template>
  <button
    class="
      absolute
      top-6
      left-6
      px-[12px]
      py-[6px]
      bg-green-500
      text-green-50
      rounded-md
      shadow-md
    "
    @click="modal = true"
  >
    Open Modal
  </button>

  <div
    v-if="modal"
    ref="modalRef"
    class="
      fixed
      top-[50%]
      left-[50%]
      translate-x-[-50%] translate-y-[-50%]
      z-10
    "
  >
    <div
      class="
        px-[120px]
        py-[60px]
        bg-yellow-200
        p-[24px]
        text-yellow-800 text-center
        rounded-md
        shadow-md
      "
    >
      <p>click outside</p>
    </div>
  </div>
</template>
koyama shigehitokoyama shigehito

useDark

リアクティブにダークモードを制御
localStorage / sessionStorageを利用した永続性にも対応

const isDark = useDark()

isDark = trueでデフォルトでhtmlタグにdarkクラスが適用される
オプションでselctorattributeを変更できる
https://vueuse.org/core/useDark/#configuration

<script lang="ts">
import { defineComponent } from 'vue';
import { useDark, useToggle } from '@vueuse/core';

export default defineComponent({
  setup() {
    const isDark = useDark();
    const toggleDark = useToggle(isDark);
    return {
      isDark,
      toggleDark,
    };
  },
});
</script>

<template>
  <button
    class="
      m-[24px]
      p-[12px]
      bg-gray-600
      text-gray-50
      dark:bg-gray-200 dark:text-gray-800
      rounded-md
      shadow-sm
    "
    @click="toggleDark()"
  >
    <span v-show="isDark" class="material-icons text-red-600 align-middle"
      >light_mode</span
    >
    <span v-show="!isDark" class="material-icons text-yellow-400 align-middle"
      >dark_mode</span
    >
    <span class="ml-2 font-bold align-middle">{{
      isDark ? 'Dark' : 'Light'
    }}</span>
  </button>
</template>
koyama shigehitokoyama shigehito

useCssVar

CSS変数の操作

const el = ref(null)
const color = useCssVar('--color', el)

操作対象の要素をrefで参照して、useCSSVarの第一引数に変数名、第二引数に対象要素を指定して使用する

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useCssVar } from '@vueuse/core';

export default defineComponent({
  setup() {
    const el = ref(null);
    const bgColor = useCssVar('--bgColor', el);

    const switchBgColor = () => {
      if (bgColor.value === '#F9D1D8') {
        bgColor.value = '#D0EAE9';
      } else {
        bgColor.value = '#F9D1D8';
      }
    };
    return {
      switchBgColor,
      el,
    };
  },
});
</script>

<template>
  <div
    ref="el"
    style="--bgColor: #d0eae9; background-color: var(--bgColor)"
    class="flex justify-center items-center h-[120px]"
  >
    <div>
      <button
        class="
          px-[24px]
          py-[12px]
          bg-indigo-500
          text-indigo-100
          rounded-md
          shadow-md
          hover:opacity-80
        "
        @click="switchBgColor"
      >
        change
      </button>
    </div>
  </div>
</template>
koyama shigehitokoyama shigehito

useMediaQuery

メディアクエリの値を動的に取得
useBreakpointsなどでも内部的に使用されている

<script lang="ts">
import { useMediaQuery } from '@vueuse/core';
import { defineComponent } from 'vue';

export default defineComponent({
  setup() {
    const isLargeScreen = useMediaQuery('(min-width: 1024px)');
    const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');

    return {
      isLargeScreen,
      prefersDark,
    };
  },
});
</script>

<template>
  <div class="p-[24px] bg-gray-200">
    <div>width > 1024px:{{ isLargeScreen }}</div>
    <div>dark mode:{{ prefersDark }}</div>
  </div>
</template>
koyama shigehitokoyama shigehito

useElementVisibility

要素がビューポート内にあるか動的に判定
ターゲット要素をref参照してuseElementVisibility(参照ターゲット)で真偽値を取得

<script lang="ts">
import { ref } from 'vue';
import { useElementVisibility } from '@vueuse/core';

export default {
  setup() {
    const target = ref(null);
    const targetIsVisible = useElementVisibility(target);

    return {
      target,
      targetIsVisible,
    };
  },
};
</script>
<template>
  <div class="h-[1000px]">
    <div ref="target" class="bg-blue-300">
      <div class="p-[12px]">target</div>
    </div>
  </div>
  <div class="fixed bottom-0 left-0 p-[12px] bg-blue-200">
    visible:{{ targetIsVisible }}
  </div>
</template>
koyama shigehitokoyama shigehito

useIntersectionObserver

第1引数にターゲットとなる要素を
第2引数に判定結果をコールバック関数で
第3引数はオプションでルート要素:root、交差判定のオフセット値:rootMargin、ターゲットがどれくらい見えているかの閾値thresholdを指定
それぞれrefでリアクティブに

内部的にIntersection Observer APIを使用している

ウィンドウスクロール時のアニメーション等に利用?

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';

export default defineComponent({
  setup() {
    const root = ref(null);
    const target = ref(null);
    const isVisible = ref(false);
    useIntersectionObserver(
      target,
      ([{ isIntersecting }]) => {
        isVisible.value = isIntersecting;
      },
      { root }
    );
    const textColorClass = computed(() =>
      isVisible.value ? 'text-red-500' : 'text-blue-500'
    );
    const textVlue = computed(() => (isVisible.value ? '内' : '外'));

    return {
      root,
      target,
      textColorClass,
      textVlue,
    };
  },
});
</script>

<template>
  <div
    ref="root"
    class="mb-[24px] p-[12px] h-[200px] overflow-y-scroll bg-gray-100"
  >
    <h2 class="mb-[260px] font-bold text-[20px]">test intersection observer</h2>
    <div ref="target" class="mb-[260px] p-[6px] border-blue-500 border-[2px]">
      <p>Hello world!</p>
    </div>
  </div>
  <div class="text-center">
    ターゲット
    <span :class="textColorClass" class="font-bold text-[20px]">
      {{textVlue}}
    </span>
    です!
  </div>
</template>
koyama shigehitokoyama shigehito

useIntervalFn

インターバルを指定した処理の実行。
pause resume関数と状態判定用にisActiveを返す。

第一引数にコールバック関数
第二引数にインターバル
オプションでimmediate(即時実行するか、初期値はtrue)など

const { pause, resume, isActive } = useIntervalFn(() => {
  // コールバック関数
}, 1000, { immediate: true })
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useIntervalFn } from '@vueuse/core';

export default defineComponent({
  setup() {
    const backgroundColor = [
      'bg-red-100',
      'bg-purple-100',
      'bg-pink-100',
      'bg-green-100',
      'bg-blue-100',
      'bg-yellow-100',
    ];
    const backgroundColorClass = ref('bg-red-100');

    const { pause, resume, isActive } = useIntervalFn(() => {
      backgroundColorClass.value =
        backgroundColor[
          Math.round(Math.random() * (backgroundColor.length - 1))
        ];
    }, 1000);

    return {
      pause,
      resume,
      isActive,
      backgroundColorClass,
    };
  },
});
</script>

<template>
  <div
    :class="backgroundColorClass"
    class="flex justify-center items-center h-screen"
  >
    <div class="justify-center">
      <button
        v-if="isActive"
        class="py-[6px] px-[12px] bg-indigo-500 text-white rounded-md shadow-md"
        @click="pause"
      >
        停止
      </button>
      <button
        v-if="!isActive"
        class="py-[6px] px-[12px] bg-pink-500 text-white rounded-md shadow-md"
        @click="resume"
      >
        再開
      </button>
    </div>
  </div>
</template>
koyama shigehitokoyama shigehito

useClipboard

リアクティブに切り取り、コピー、貼り付けコマンドに応答する機能と、システムのクリップボードへの読み取り、書き込み操作を非同期に行う機能
内部的にクリップボードAPIを利用

copy:コピーイベント
isSupported:クリップボードへのアクセス許可(bool値)
text:クリップボードのテキスト(リアクティブなstring)
copied:コピーされたか(リアクティブなbool値、初期値では1.5sでリセットされる)

オプション
https://vueuse.org/core/useClipboard/#type-declarations

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useClipboard } from '@vueuse/core';

export default defineComponent({
  setup() {
    const input = ref('');
    const { text, isSupported, copy } = useClipboard();

    return {
      input,
      text,
      isSupported,
      copy,
    };
  },
});
</script>

<template>
  <div v-if="isSupported">
    <input
      v-model="input"
      type="text"
      class="p-[4px] border-[2px] border-gray-[700] rounded-[4px]"
      placeholder="type something here"
    />
    <div>
      <button
        class="
          mt-[12px]
          px-[12px]
          py-[6px]
          rounded-[4px]
          font-bold
          text-gray-100
          bg-blue-500
          shadow-md
        "
        @click="copy(input)"
      >
        クリップボードにコピー
      </button>
    </div>
    <p class="mt-[12px]">
      コピー内容:
      <span class="font-bold text-blue-800">
        {{ text || 'none' }}
      </span>
    </p>
  </div>
  <p v-else>Your browser does not support Clipboard API</p>
</template>
koyama shigehitokoyama shigehito

useFavicon

ファビコンの操作
引数にソースの参照を渡して使用。参照の変更でファビコンも動的に変更される

useFavicon(favicon)

これは用途なんだろうと思ったけど、テーマ変更時にFaviconも変えたりするのかも

<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { useFavicon } from '@vueuse/core';

export default defineComponent({
  setup() {
    const isDark = ref(false);
    const favicon = computed(() => (isDark.value ? 'light.png' : 'dark.png'));

    useFavicon(favicon, {
      baseUrl: '../../public/',
      rel: 'icon',
    });

    return {
      isDark,
    };
  },
});
</script>

<template>
  <div class="mb-[12px]">
    ファビコン選択:
    <span v-if="isDark">dark</span>
    <span v-else>light</span>
  </div>
  <button
    class="
      mr-[12px]
      px-[12px]
      py-[6px]
      font-bold
      text-green-800
      bg-green-300
      rounded-md
      shadow-sm
      hover:opacity-80
    "
    @click="isDark = true"
  >
    dark
  </button>
  <button
    class="
      px-[12px]
      py-[6px]
      font-bold
      text-blue-800
      bg-blue-300
      rounded-md
      shadow-sm
      hover:opacity-80
    "
    @click="isDark = false"
  >
    light
  </button>
</template>
koyama shigehitokoyama shigehito

useElementSize

HTML要素のサイズをリアクティブに取得
refで要素を参照してuseElementSizeの引数に渡す

ResizeObserverを内部的に使用しているuseResizeObsever関数を使用している
https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
https://vueuse.org/core/useResizeObserver/

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { useElementSize } from '@vueuse/core';

export default defineComponent({
  setup() {
    const el = ref(null);
    const { width, height } = useElementSize(el);
    const roundedNumber = (val: number) => Math.round(val);
    const roundedWidth = computed(() => roundedNumber(width.value));
    const roundedHeight = computed(() => roundedNumber(height.value));

    return {
      el,
      roundedWidth,
      roundedHeight,
    };
  },
});
</script>

<template>
  <div>Height: {{ roundedHeight }} Width: {{ roundedWidth }}</div>
  <div ref="el" class="w-[30%] h-[30%] bg-blue-500"></div>
</template>
koyama shigehitokoyama shigehito

useTransition

useTransition(sorce, {
  // 以下オプション
  duration: '遷移時間(数値)',
  transition: 'イージング定義'
  delay: '遅延制御(数値)'
  onStarted: 'スタート時になにか処理(関数)'
  onFinished: '終了時になにか処理(関数)'
  disabled: '一時停止(bool値)'
}

sorceに遷移対象の数値を指定
transitionは3次ベジェ曲線でカスタマイズ、TransitionPresetsも利用できる

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { useTransition, TransitionPresets } from '@vueuse/core';
import { rand } from '@vueuse/shared';

export default defineComponent({
  setup() {
    const baseNumber = ref(0);
    const cubicBezierNumber = useTransition(baseNumber, {
      duration: 1000,
      transition: [0.75, 0, 0.25, 1],
    });

    const baseColor = ref([0, 0, 0]);
    const colorTransition = useTransition(baseColor, {
      duration: 500,
      transition: TransitionPresets.easeInCubic,
    });
    const color = computed(() => {
      const [r, g, b] = colorTransition.value;
      return `rgb(${r},${g},${b})`;
    });

    const toggle = () => {
      baseNumber.value = baseNumber.value === 100 ? 0 : 100;
      baseColor.value = [rand(0, 255), rand(0, 255), rand(0, 255)];
    };

    return { cubicBezierNumber, color, toggle };
  },
});
</script>

<template>
  <button
    class="
      mb-[12px]
      px-[12px]
      py-[4px]
      bg-blue-300
      rounded-[4px]
      hover:opacity-80
    "
    type="button"
    @click="toggle"
  >
    Transition
  </button>
  <div class="static w-[400px] px-[50px] bg-gray-100">
    <div
      class="relative w-[100px] h-[100px] -translate-x-1/2"
      :style="{ left: cubicBezierNumber + '%', backgroundColor: color }"
    ></div>
  </div>
</template>