VueUse 覚書
Vue.jsのCompositionAPIの学習も兼ねて、VueUseのガイドを読みつつ実装を学習できたらよいなと思います。
VueUseはAnthony Fuさんを中心に開発されているCompositionAPIに基づくユーティリティ関数のライブラリ。Vue Demiで実現している)
Vue2とVue3で使える(インストール
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'
VueUseのほとんどの関数はrefs
オブジェクトを返す
const mouse = useMouse()
console.log(mouse.x.value)
分割代入
const { x, y } = useMouse()
console.log(x.value)
refs
をreactive()
でアンラップして使用できる
const mouse = reactive(useMouse())
// valueなしで呼び出し可能
console.log(mouse.x)
VueUseのドキュメントPWA対応されていた。早速インストール...
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()
コンポーネントスタイル
@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>
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>
useBreakpoints
ブレークポイントを動的に判定
ブレークポイントの値はパラメータで渡してあげる
別途関数でbreakpointsTailwind
やbreakpointsBootstrapV5
などが用意されていて、importして利用できる
<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>
onClickOutside
要素の外側のクリックをリッスンする。
モーダルやドロップダウンメニューなどで、外側をクリックした際のロジックの実装用途など
onClickOutside(target, handler, options)
opstions
は
"click", "mousedown", "mouseup"など
<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>
useDark
リアクティブにダークモードを制御
localStorage / sessionStorageを利用した永続性にも対応
const isDark = useDark()
isDark = true
でデフォルトでhtml
タグにdark
クラスが適用される
オプションでselctor
やattribute
を変更できる
<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>
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>
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>
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>
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>
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>
useClipboard
リアクティブに切り取り、コピー、貼り付けコマンドに応答する機能と、システムのクリップボードへの読み取り、書き込み操作を非同期に行う機能
内部的にクリップボードAPIを利用
copy
:コピーイベント
isSupported
:クリップボードへのアクセス許可(bool値)
text
:クリップボードのテキスト(リアクティブなstring)
copied
:コピーされたか(リアクティブなbool値、初期値では1.5sでリセットされる)
オプション
<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>
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>
useElementSize
HTML要素のサイズをリアクティブに取得
refで要素を参照してuseElementSize
の引数に渡す
ResizeObserverを内部的に使用しているuseResizeObsever
関数を使用している
<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>
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>