Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential Regression: emits null right after emitting the selected value in 2.6.8 #419

Open
malik-steinert opened this issue Jul 2, 2024 · 0 comments

Comments

@malik-steinert
Copy link

Version

  • Vue version: 3

Description

After upgrading from 2.6.7 to 2.6.8 the component started to emit a null value right after emitting the selected value, essentially deleting the selection. Fixing the version number to 2.6.7 solves the issue. Here is how we are currently using the component in our nuxt project.

Wrapper component:

<script lang="ts" setup>
/**
 * A wrapper around the Vueform Multiselect component, with some customizations.
 *
 * The wrapper is not (yet) 100% api compatible with the upstream component,
 * so you can extend the api as you see fit anytime.
 *
 * @see https://github.com/vueform/multiselect
 */
import type { Component } from "nuxt/schema";
import Multiselect from "@vueform/multiselect";

defineOptions({
  inheritAttrs: false,
});

export interface Props {
  mode?: "single" | "multiple" | "tags";
  options: Array<any> | Function;
  required?: boolean;
  searchable?: boolean;
  valueProp?: string;
  label?: string;
  placeholder?: string;
  disabled?: boolean;
  limit?: number;
  clearOnSearch?: boolean;
  closeOnSelect?: boolean;
  clearOnBlur?: boolean;
  delay?: number;
  filterResults?: boolean;
  resolveOnLoad?: boolean;
  infinite?: boolean;
  loading?: boolean;
  id?: string;
  canClear?: boolean;
  searchFilter?: Function | undefined;
  noOptionsText?: string;
  noResultsText?: string;
  classes?: Object;
}

export interface Emits {
  (event: "search-change", query: string, select$: Component): void;
}

const props = withDefaults(defineProps<Props>(), {
  mode: "single",
  options: () => [],
  required: false,
  searchable: true,
  valueProp: "value",
  label: "label",
  placeholder: "",
  disabled: false,
  limit: 0,
  clearOnSearch: false,
  closeOnSelect: true,
  clearOnBlur: true,
  delay: 0,
  filterResults: true,
  resolveOnLoad: true,
  infinite: false,
  loading: false,
  id: "multiselect",
  canClear: true,
  searchFilter: undefined,
  noOptionsText: "The list is empty",
  noResultsText: "No results found",
  classes: () => ({}),
});

const [modelValue] = defineModel<any>();

const emits = defineEmits<Emits>();

const classes = {
  container: "multiselect tw-multiselect",
  option: "multiselect-option tw-multiselect-option",
  ...props.classes,
};
</script>

<template>
  <slot name="label" />
  <Multiselect
    v-bind="$attrs"
    v-model="modelValue"
    :mode="mode"
    :options="options"
    :required="required"
    :searchable="searchable"
    :value-prop="valueProp"
    :label="label"
    :placeholder="placeholder"
    :disabled="disabled"
    :limit="limit"
    :clear-on-search="clearOnSearch"
    :close-on-select="closeOnSelect"
    :clear-on-blur="clearOnBlur"
    :delay="delay"
    :filter-results="filterResults"
    :resolve-on-load="resolveOnLoad"
    :infinite="infinite"
    :loading="loading"
    :id="id"
    :can-clear="canClear"
    :search-filter="searchFilter"
    :no-options-text="noOptionsText"
    :no-results-text="noResultsText"
    :classes="classes"
    @search-change="emits('search-change', $event, $el)"
  >
    <template v-if="$slots.placeholder" #placeholder>
      <slot name="placeholder" />
    </template>
    <template v-if="$slots.afterlist" #afterlist>
      <slot name="afterlist" />
    </template>
    <template v-if="$slots.beforelist" #beforelist>
      <slot name="beforelist" />
    </template>
    <template v-if="$slots.multiplelabel" #multiplelabel="{ values }">
      <slot name="multiplelabel" v-bind="{ values }" />
    </template>
    <template v-if="$slots.nooptions" #nooptions>
      <slot name="nooptions" />
    </template>
    <template v-if="$slots.noresults" #noresults>
      <slot name="noresults" />
    </template>
    <template v-if="$slots.grouplabel" #grouplabel="{ group, isPointed, isSelected }">
      <slot name="grouplabel" v-bind="{ group, isPointed, isSelected }" />
    </template>
    <template v-if="$slots.option" #option="{ option, isPointed, isSelected, search }">
      <slot name="option" v-bind="{ option, isPointed, isSelected, search }" />
    </template>
    <template v-if="$slots.singlelabel" #singlelabel="{ value }">
      <slot name="singlelabel" v-bind="{ value }" />
    </template>
    <template v-if="$slots.tag" #tag="{ option, handleTagRemove, disabled }">
      <slot name="tag" v-bind="{ option, handleTagRemove, disabled }" />
    </template>
    <template v-if="$slots.caret" #caret="{ handleCaretClick, isOpen }">
      <slot name="caret" v-bind="{ handleCaretClick, isOpen }" />
    </template>
    <template v-if="$slots.clear" #clear="{ clear }">
      <slot name="clear" v-bind="{ clear }" />
    </template>
    <template v-if="$slots.spinner" #spinner>
      <slot name="spinner" />
    </template>
    <template v-if="$slots.infinite" #infinite>
      <slot name="infinite" />
    </template>
  </Multiselect>
</template>

<style lang="scss">
...
</style>

Actual usage

<script lang="ts" setup>
import { groupAreasByZip, isAreaGroupedByZip } from "~/models/Location";
import type { LocationArea, LocationRegion } from "~/models/Location";

const [modelValue] = defineModel<number>({ type: Number, required: true });

const props = defineProps({
  city: String,
});

const limit = ref(12);

const { $api } = useNuxtApp();
const fetchLocations = async (query: string) => {
  return $api.housing.getLocations({
    params: {
      query,
      country: "de",
      limit: limit.value,
    },
  });
};

const formatLocations = (locations: LocationRegion[]) => {
  return locations
    .map((region) => region.cities.map((city) => groupAreasByZip(city.areas.map((area) => ({ ...area, city })) as LocationArea[])))
    .flat(2)
    .map((o) => (isAreaGroupedByZip(o) ? o.areas : [o]))
    .flat();
};

const getOptions = async (query: string) => {
  return fetchLocations(query ?? props.city ?? "").then(({ data }: { data: LocationRegion[] }) => (data && data.length ? formatLocations(data) : []));
};

const asyncOptions = async (query: string) => {
  const data = await getOptions(query);
  return data;
};
</script>

<template>
  <tw-form-multiselect
    id="select-geo-id"
    v-model="modelValue"
    :options="async (query: string) => { return await asyncOptions(query) }"
    :valueProp="'id'"
    :classes="{ option: '' }"
    :limit="limit"
    :delay="500"
    :can-clear="false"
    :resolve-on-load="true"
    :filter-results="false"
    :placeholder="$t('registration.home.location_placeholder')"
    searchable
    required
    infinite
  >
    <template #label>
      <label class="label" for="select-geo-id">{{ $t("registration.home.place") }}</label>
    </template>

    <template #option="{ option }">
      <label class="radio" :for="`area-${option.id}`">
        <input v-model="modelValue" type="radio" :id="`area-${option.id}`" name="area" :value="option.id" hidden />
        <div :class="['radio-button', { active: option.id === modelValue }]">
          <tw-icon-check class="icon-check" width="1rem" />
        </div>
        <div class="area-name">
          <span class="city-name">{{ option.city?.name }}</span>
          <span class="delimiter">-</span>
          <span class="zip">{{ option.zip }}</span>
          <span v-if="option.name">({{ option.name }})</span>
        </div>
      </label>
    </template>

    <template #singlelabel="{ value }">
      <div class="area-name single-label">
        <span class="city-name">{{ value.city?.name }}</span>
        <span class="delimiter">-</span>
        <span class="zip">{{ value.zip }}</span>
        <span v-if="value.name">({{ value.name }})</span>
      </div>
    </template>

    <template #nooptions>
      <div class="nothing-found">
        {{ $t("nothing-found") }}
      </div>
    </template>

    <template #noresults>
      <div class="nothing-found">
        {{ $t("nothing-found") }}
      </div>
    </template>

    <template #spinner>
      <div class="spinner">
        <tw-ui-spinner class="loading" />
      </div>
    </template>
  </tw-form-multiselect>
</template>

<style lang="scss" scoped>
...
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant