From 1a0d006d7afb442a58691dd51f73dd79e4c331d2 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 23 Apr 2024 21:25:19 +0000 Subject: [PATCH] refactor: moved useMultiselect --- src/components/DropdownSelect.vue | 2 +- src/components/TabNav.vue | 2 +- .../__tests__/useMultiselect.spec.ts | 65 +++++++++++++++++++ .../useMultiselect.ts | 30 +++++++-- 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 src/composables/__tests__/useMultiselect.spec.ts rename src/{stories/Composables => composables}/useMultiselect.ts (75%) diff --git a/src/components/DropdownSelect.vue b/src/components/DropdownSelect.vue index c6388aa..9164c06 100644 --- a/src/components/DropdownSelect.vue +++ b/src/components/DropdownSelect.vue @@ -9,7 +9,7 @@ import { filterOptions, orderOptionsBySelectedFirst, } from "@/helpers/normaliseOptions"; -import { useMultiselect } from "@/stories/Composables/useMultiselect"; +import { useMultiselect } from "@/composables/useMultiselect"; import type { selectOption } from "@/types/listItem"; import { type PropType, computed, onMounted, ref, watch } from "vue"; diff --git a/src/components/TabNav.vue b/src/components/TabNav.vue index 74e1d3f..bf6c714 100644 --- a/src/components/TabNav.vue +++ b/src/components/TabNav.vue @@ -2,7 +2,7 @@ import type { PropType } from "vue"; import BaseButton from "./BaseButton.vue"; import type { selectOption } from "@/types/listItem"; -import { useMultiselect } from "@/stories/Composables/useMultiselect"; +import { useMultiselect } from "@/composables/useMultiselect"; const emit = defineEmits([ /** The page the user has navigated to, either by clicking directly on a page or by using the previous and next buttons */ diff --git a/src/composables/__tests__/useMultiselect.spec.ts b/src/composables/__tests__/useMultiselect.spec.ts new file mode 100644 index 0000000..e3fb3cc --- /dev/null +++ b/src/composables/__tests__/useMultiselect.spec.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from "vitest"; +import { + type requiredEmits, + type requiredProps, + useMultiselect, +} from "../useMultiselect"; + +// Setup the spy on emit, so we can check if it was called +const emit = vi.spyOn( + { + emit: () => {}, + }, + "emit" +) as unknown as requiredEmits; + +describe("useMultiselect", () => { + // Mock props and emit function + const props = { + options: [ + { id: "1", render: "Option 1", disabled: false }, + { id: "2", render: "Option 2", disabled: true }, + { id: "3", render: "Option 3", disabled: false }, + ], + displayKey: "render", + multiple: true, + modelValue: ["1"], + modelKey: "id", + } as requiredProps; + + it("should compute normalisedOptions correctly", async () => { + const { normalisedOptions } = useMultiselect(props, emit); + expect(normalisedOptions.value).toEqual([ + { id: "1", render: "Option 1", disabled: false }, + { id: "2", render: "Option 2", disabled: true }, + { id: "3", render: "Option 3", disabled: false }, + ]); + }); + + it("should compute selecteableOptions correctly", async () => { + const { selecteableOptions } = useMultiselect(props, emit); + expect(selecteableOptions.value).toEqual([ + { id: "1", render: "Option 1", disabled: false }, + { id: "3", render: "Option 3", disabled: false }, + ]); + }); + + it("should return correct label for an option", async () => { + const { getLabel } = useMultiselect(props, emit); + expect(getLabel({ id: "1", render: "Option 1", disabled: false })).toBe( + "Option 1" + ); + }); + + it("should update modelValue correctly when a value is selected", async () => { + const { updateModelValue } = useMultiselect(props, emit); + updateModelValue("2", true); + expect(emit).toHaveBeenCalledWith("update:modelValue", ["1", "2"]); + }); + + it("should update modelValue correctly when a value is unselected", async () => { + const { updateModelValue } = useMultiselect(props, emit); + updateModelValue("1", false); + expect(emit).toHaveBeenCalledWith("update:modelValue", []); + }); +}); diff --git a/src/stories/Composables/useMultiselect.ts b/src/composables/useMultiselect.ts similarity index 75% rename from src/stories/Composables/useMultiselect.ts rename to src/composables/useMultiselect.ts index 7133bc7..a67abd6 100644 --- a/src/stories/Composables/useMultiselect.ts +++ b/src/composables/useMultiselect.ts @@ -2,7 +2,7 @@ import { normaliseOptions } from "@/helpers/normaliseOptions"; import type { normalisedOptionObject, selectOption } from "@/types/listItem"; import { computed, toRaw } from "vue"; -type requiredProps = { +export type requiredProps = { options: selectOption[]; displayKey: "id" | "render"; multiple: boolean; @@ -10,7 +10,10 @@ type requiredProps = { modelKey: "id" | "render"; }; -type requiredEmits = (evt: "update:modelValue", args_0: string[]) => void; +export type requiredEmits = ( + evt: "update:modelValue", + args_0: string[] +) => void; // A composable for a multiselect component export function useMultiselect(props: requiredProps, emit: requiredEmits) { @@ -36,9 +39,14 @@ export function useMultiselect(props: requiredProps, emit: requiredEmits) { * * @param newValue * @param valueSelected If the value should be marked as selected or not in the mode value. Of you want the user to be toggle the value on/off with multiple clicks on the same value, set this to false + * @param existingValueIndex the value index to update - this is useful if you want to update a specific value in the array * @returns */ - const updateModelValue = (newValue: string | null, valueSelected = true) => { + const updateModelValue = ( + newValue: string | null, + valueSelected = true, + existingValueIndex = false as number | false + ) => { if (!newValue) { return; } @@ -48,10 +56,18 @@ export function useMultiselect(props: requiredProps, emit: requiredEmits) { return; } - // Always emit the full array of selected page IDs - const newPages = props.modelValue.includes(newValue) - ? props.modelValue.filter((id) => id !== newValue) - : [...props.modelValue, newValue]; + let newPages: string[] = []; + + // If we are updating a specific value in the array + if (existingValueIndex !== false) { + newPages = [...props.modelValue]; + newPages[existingValueIndex] = newValue; + } else { + // Always emit the full array of selected page IDs + newPages = props.modelValue.includes(newValue) + ? props.modelValue.filter((id) => id !== newValue) + : [...props.modelValue, newValue]; + } emit("update:modelValue", newPages); };