ジェネリック型 Vue コンポーネント#
- セットアップでのコンポーネントの定義
- ジェネリックプロップ
- SFC をベースコンポーネントとして使用
- SFC コンポーネントからプロップタイプを抽出
- ジェネリックスロット
- ジェネリックコンポーネントイベント
BaseGenericComponent.vue
<template>
<pre>
<slot :current-value="value" :old-value="oldValue">
{{ value }}
</slot>
</pre>
</template>
<script setup lang="ts">
import { watch, defineProps, ref, PropType } from 'vue';
const props = defineProps({
value: {
type: null as PropType<unknown>,
required: true,
},
});
const oldValue = ref<unknown>();
watch(
() => props.value,
(_, oldVal) => {
oldValue.value = oldVal;
}
);
</script>
types.ts
export type ExtractComponentProps<TComponent> =
TComponent extends new () => {
$props: infer P;
}
? P
: never;
useGenericComponent.ts
import { defineComponent, h, VNode } from 'vue';
import BaseGenericComponent from './BaseGenericComponent.vue';
import { ExtractComponentProps } from './types';
// `onChanged` イベントを省略して上書きできるようにします。これは `value` プロップと同じケースです。
// Vue 3 ではイベントとプロップは同じように扱われますが、イベントには `on` プレフィックスがあります。
// したがって、イベントをプロップに変換したい場合は、その規則に従うことができます(`changed` => `onChanged`)
type NonGenericProps = Omit<ExtractComponentProps<typeof BaseGenericComponent>, 'value' | 'onChanged'>
interface GenericProps<TValue> extends NonGenericProps {
value: TValue;
}
interface GenericSlotProps<TValue> {
currentValue: TValue;
oldValue: TValue;
}
export function useGenericComponent<TValue = unknown>() {
// 2 番目の引数からスロットオブジェクトを取得することを忘れないでください
const wrapper = defineComponent((props: GenericProps<TValue>, { slots }) => {
// `setup` で関数を返すことは、これがレンダリング関数であることを意味します
return () => {
// スロットとイベントハンドラを通過させます
return h(BaseGenericComponent, props, slots);
};
});
// 既存のコンポーネント型情報を失わないようにラッパーをそのままキャストします
return wrapper as typeof wrapper & {
// ラッパー型を拡張して、スロット型情報を `$slots` オブジェクトに追加するコンストラクター型を追加します
new (): {
// `$slots` と同じトリックで、そのコンポーネントの emit 情報を上書きします
$emit: {
(e: 'changed', value: TValue): void;
};
$slots: {
// 各関数はスロットに対応し、その引数は利用可能なスロットプロップです
// これはデフォルトのスロット定義で、`GenericSlotProps` プロパティをスロットプロップとして提供します。
// `VNode` の配列を返す必要があります
default: (arg: GenericSlotProps<TValue>) => VNode[];
};
};
};
}
SFC からプロップタイプを抽出する方法 (ここで画像を見るにはダークモードをオンにする必要があります)
これがどのように機能するかの例です:
<script lang="ts" setup>
import { useGenericComponent } from './genericComponent';
const StringComponent = useGenericComponent<string>();
interface User {
id: number;
name: string;
}
const ObjectComponent = useGenericComponent<User>();
function onChange(value: User) {
console.log(value);
}
</script>
<template>
<!-- 🛑 これはタイプエラーのため、Volar で警告が表示されるはずです -->
<StringComponent
:value="str"
v-slot="{ currentValue, oldValue }"
@changed="onChange"
>
<div>current: {{ currentValue }}</div>
<div>old: {{ oldValue }}</div>
</StringComponent>
<ObjectComponent
:value="userObj"
v-slot="{ currentValue, oldValue }"
@changed="onChange"
>
<div>current: {{ currentValue }}</div>
<div>old: {{ oldValue }}</div>
</ObjectComponent>
</template>
実用的なジェネリックコンポーネント#
この SelectInput
コンポーネントは次のことを行うべきです:
- オプションのリストを受け入れる。
- 選択された値を emit し、
v-model
API をサポートする(modeValue
プロップを受け入れ、update:modelValue
イベントを emit する)。
浮動メニュー内の各オプションの UI をカスタマイズするためにitem
スロットを公開する。
選択されたオプションの UI をカスタマイズするためにselected
スロットを公開する。
https://stackblitz.com/edit/vitejs-vite-wemb2u?embed=1
コンポーネントのノイズを減らすためのコンポジション API#
setup
関数内でコンポーネントをその場で構築し、テンプレートで使用できます。- ジェネリックコンポーネントとして機能するコンポーネントラッパーを作成できます。
これにより、いくつかの方法で助けになります: - 一部のプロップを事前定義する
- 明示的な動作でより明確な API を公開する
- 一部の複雑さを隠す
features/modal.ts
import { ref, defineComponent, h } from 'vue';
import ModalDialog from '@/components/ModalDialog.vue';
export function useModalDialog<TData = unknown>(
onConfirmProp: (data: TData) => void
) {
// コンテキストデータ
const data = ref<TData>();
function onClose() {
data.value = undefined;
}
function show(value: TData) {
data.value = value;
}
function hide() {
data.value = undefined;
}
const DialogComponent = defineComponent({
inheritAttrs: false,
setup(_, { slots, emit }) {
function onConfirmed() {
if (data.value !== undefined) {
onConfirmProp(data);
}
}
return () =>
h(
ModalDialog,
{
onClose,
onConfirmed,
visible: data.value !== undefined,
},
{
default: () => slots.default?.({ data: data.value }),
}
);
},
});
return {
DialogComponent:
DialogComponent as typeof DialogComponent & {
// ラッパー型を拡張して、スロット型情報を `$slots` オブジェクトに追加するコンストラクター型を追加します
new (): {
$emit: {
(e: 'confirmed', data: TData): void;
};
$slots: {
default: (arg: { data: TData }) => VNode[];
};
};
},
show,
hide
};
}
使用例#
<script setup lang="ts">
import { ref } from 'vue';
import { useModalDialog } from '@/features/modal';
interface Item {
id: number;
name: string;
}
const items: Item[] = ref([
//...
]);
const {
show: onDeleteClick,
DialogComponent: DeleteItemDialog,
hide,
} = useModalDialog<Item>((item) => {
const idx = items.value.findIndex((i) => i.id === item.id);
items.value.splice(idx, 1);
console.log('Deleted', item);
hide();
});
</script>
<template>
<ul>
<li
v-for="item in items"
:key="item.id"
@click="onDeleteClick(item)"
>
{{ item.name }}
</li>
</ul>
<DeleteItemDialog
v-slot="{ data: itemToDelete }"
>
{{ itemToDelete.name }} を削除してもよろしいですか?
</DeleteItemDialog>
</template>
“アイテムをクリックすると、そのためのダイアログが開き、アクションが確認されると、アイテムを削除できるように通知します”。
多くのコンポーネントがアクセスして使用できる「共有」モーダルダイアログを作成することができます。おそらく provide/inject
API を使用して:
// 親ページまたはコンポーネント内
const { DialogComponent, ...modalApi } = useModalDialog();
// モーダル API を子コンポーネントに提供します
provide('modal', modalApi);
// 子コンポーネントのどこかで:
const modal = inject('modal');
modal.show(data);
参考文献: