Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Vue Composition APIのトリック

ジェネリック型 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 からプロップタイプを抽出する方法 (ここで画像を見るにはダークモードをオンにする必要があります)
image

これがどのように機能するかの例です:

<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);

参考文献:

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。