カテゴリー
コンピューター

Vue.js 3 で Select2 をラップした SFC その4 。さらにカスタマイズする

やったこと

  • Select2 の config をなんでも受け取れるようにする。
  • props の変更検知は 1 つの watch にまとめられるのではないか?
  • Bootstrap 4 テーマの時、バリデーションエラーのスタイルを適用できるようにする。

Select2 ラッパー SFC とそれを利用するコンポーネントのコード全体

カスタマイズした Select2 ラッパー SFC の laravel/resources/js/components/BaseSelect2.vue です。

<template>
  <div :class="classValue">
    <select ref="root">
      <slot />
    </select>
  </div>
</template>

<script>
import _ from "lodash";
import "select2";
import { ref, onMounted, onUnmounted, watch } from "vue";
import $ from "jquery";

export default {
  name: "BaseSelect2",
  props: {
    classValue: {
      type: String,
      require: false,
      default: null,
    },
    config: {
      type: Object,
      require: false,
      default: () => {},
    },
    options: {
      type: Array,
      require: true,
      default: () => {
        [];
      },
    },
    modelValue: {
      type: String,
      require: false,
      default: null,
    },
  },
  emits: ["update:modelValue"],
  setup(props, { emit }) {
    const root = ref(null);

    const config = {
      data: props.options,
      theme: "bootstrap",
      ...props.config,
    };

    watch(
      () => props.modelValue,
      (modelValue) => {
        // update modelValue
        $(root.value).val(modelValue).trigger("change");
      }
    );
    watch(
      () => props.options,
      (options) => {
        // update options
        $(root.value)
          .empty()
          .select2({ ...config, data: options });
      }
    );
    watch(
      () => _.cloneDeep(props),
      (props, prevProps) => {
        console.log("watch props", props, prevProps);
        //$(root.value)
        //  .empty()
        //  .select2({ ...config, data: props.options, ...props.config })
        //  .val(props.modelValue)
        //  .trigger("change");
      },
      { deep: true }
    );

    onMounted(() => {
      $(root.value)
        // init select2
        .select2(config)
        .val(props.modelValue)
        .trigger("change")
        // emit event on change.
        .on("change", (event) => {
          emit("update:modelValue", event.target.value);
        });
    });

    onUnmounted(() => {
      $(root.value).off().select2("destroy");
    });

    return { root };
  },
};
</script>

Select2 ラッパー SFC を利用するコンポーネントの laravel/resources/js/views/SampleSelect2.vue です。

<template>
  <div class="container">
    <h1>Select2 ラッパー SFC の利用</h1>
    <p>Selected: {{ selected }}</p>
    <base-select-2
      v-model="selected"
      :config="config"
      :options="options"
      :class-value="'is-invalid'"
    >
      <option disabled value="0">Select one</option>
    </base-select-2>
  </div>
</template>

<script>
import { ref } from "vue";
import BaseSelect2 from "../components/BaseSelect2.vue";

//function matchCustom(params, data) {
const matchCustom = (params, data) => {
  // If there are no search terms, return all of the data
  //if ($.trim(params.term) === "") {
  if (!params.term || params.term.trim() === "") {
    return data;
  }

  // Do not display the item if there is no 'text' property
  if (typeof data.text === "undefined") {
    return null;
  }

  // `params.term` should be the term that is used for searching
  // `data.text` is the text that is displayed for the data object
  if (data.text.indexOf(params.term) > -1) {
    //var modifiedData = $.extend({}, data, true);
    let modifiedData = { ...data };
    modifiedData.text += " (matched)";

    // You can return modified objects from here
    // This includes matching the `children` how you want in nested data sets
    return modifiedData;
  }

  // 今回追加部分
  const hitted = [data.hiragana, data.alphabet]
    .filter((element) => element)
    .some((element) => {
      return element.indexOf(params.term) > -1;
    });
  if (hitted) {
    return data;
  }

  // Return `null` if the term should not be displayed
  return null;
};

export default {
  name: "SampleSelect2",
  components: { BaseSelect2 },
  setup() {
    const selected = ref("2");
    const options = [
      {
        id: "1",
        text: "北海道",
        hiragana: "ほっかいどう",
        alphabet: "hokkaido",
      },
      {
        id: "2",
        text: "青森県",
        hiragana: "あおもりけん",
        alphabet: "aomoriken",
      },
      { id: "3", text: "岩手県", hiragana: "いわてけん", alphabet: "iwateken" },
      {
        id: "4",
        text: "宮城県",
        hiragana: "みやぎけん",
        alphabet: "miyagiken",
      },
      { id: "5", text: "秋田県", hiragana: "あきたけん", alphabet: "akitaken" },
    ];

    const config = { matcher: matchCustom };

    return {
      config,
      options,
      selected,
    };
  },
};
</script>

Select2 の config をなんでも受け取れるようにする。

setup 内で config の変数を定義し、定義する時に props.config を展開するようにしてみました。

    const config = {
      data: props.options,
      theme: "bootstrap",
      ...props.config,
    };

onMounted 時はこの setup 内定義の config を使い、 watch 時などで Select2 の config の内容を変えたいときは、この setup 内定義の config を展開したものに新しい porps.config を展開して上書きする、という流れとなります。

    watch(
      () => props.options,
      (options) => {
        // update options
        $(root.value)
          .empty()
          .select2({ ...config, data: options });
      }
    );

props の変更検知は 1 つの watch にまとめられるのではないか?

今までは props.modelValue や props.options を個別に監視していました。これを、 props 全体を watch するようにすればコードの変更が容易で見通しもよくなるのではないか ? (ただし 1 つの watch に全ての props の変更に対応する処理を書く必要があるので、コードは複雑になってしまいそう )というアイデアです。

を参考に、次のようにすれば props を監視できることまで、突き止めました。

import _ from "lodash";

... 略 ...

    watch(
      () => _.cloneDeep(props),
      (props, prevProps) => {
        console.log("watch props", props, prevProps);
        //$(root.value)
        //  .empty()
        //  .select2({ ...config, data: props.options, ...props.config })
        //  .val(props.modelValue)
        //  .trigger("change");
      },
      { deep: true }
    );

Bootstrap 4 テーマの時、バリデーションエラーのスタイルを適用できるようにする。

LaravelでSelect2とBootstrap4を使って、バリデーションエラー時に赤枠で囲む – Qiita のページが参考になりました ! ありがとうとございます。

修正内容は簡単で、 Select2 を適用する <select><div> で囲み、この <div>class="is-invali" が適用できるようにするだけです。

ただ、props で class という名前は使えませんでした。おそらく予約語なのだと思います。次のエラーとなりました。ですので、 classValue という名前にしました。

$ npm run lint

... 略 ...

/var/www/html/laravel/resources/js/components/BaseSelect2.vue
  2:21  error  Parsing error: Unexpected end of expression  vue/no-parsing-error

✖ 1 problem (1 error, 0 warnings)

コードとしては次のようになりました。

Select2 ラッパー SFC の laravel/resources/js/components/BaseSelect2.vue です。

  <div :class="classValue">
    <select ref="root">
      <slot />
    </select>
  </div>

Select2 ラッパー SFC を利用するコンポーネントの laravel/resources/js/views/SampleSelect2.vue です。 :class-value="'is-invalid'" とベタガキしていますけれども、本来であれば変数を当てての利用が主となる想定です。

    <base-select-2
      v-model="selected"
      :config="config"
      :options="options"
      :class-value="'is-invalid'"
    >

おわりに

今までのシリーズです。

今後も少しずつ変わるかもしれません。

以上です。

コメントを残す