Skip to content

Commit

Permalink
revert(defineModel): reuse useVModel composable for generics where …
Browse files Browse the repository at this point in the history
…the type is `T | T[]` (#998)
  • Loading branch information
mlmoravek authored Jul 4, 2024
1 parent b322188 commit c00abf5
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 26 deletions.
9 changes: 3 additions & 6 deletions packages/oruga/src/components/autocomplete/Autocomplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,7 @@ const props = defineProps({
default: undefined,
},
/** The value of the inner input, use v-model:input to make it two-way binding */
input: {
type: String,
default: "",
},
input: { type: String, default: "" },
/**
* Options / suggestions
* @type string[]|object[]
Expand Down Expand Up @@ -694,9 +691,9 @@ function handleBlur(event: Event): void {
}
/** emit input change event */
function onInput(value: string): void {
function onInput(value: string | number): void {
if (props.keepFirst && !selectedOption.value) hoverFirstOption();
emits("input", value);
emits("input", String(value));
checkHtml5Validity();
}
Expand Down
14 changes: 8 additions & 6 deletions packages/oruga/src/components/dropdown/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
useMatchMedia,
useEventListener,
useClickOutside,
useVModel,
} from "@/composables";
import type { DropdownComponent } from "./types";
Expand Down Expand Up @@ -156,7 +157,7 @@ const props = defineProps({
*/
ariaRole: {
type: String,
default: getOption("dropdown.ariaRole", "list"),
default: () => getOption("dropdown.ariaRole", "list"),
validator: (value: string) =>
["list", "listbox", "menu", "dialog"].indexOf(value) > -1,
},
Expand Down Expand Up @@ -275,7 +276,8 @@ const emits = defineEmits<{
}>();
/** The selected item value */
const vmodel = defineModel<T | T[]>({ default: undefined });
// const vmodel = defineModel<T | T[]>({ default: undefined });
const vmodel = useVModel<T | T[]>();
/** The active state of the dropdown, use v-model:active to make it two-way binding */
const isActive = defineModel<boolean>("active", { default: false });
Expand Down Expand Up @@ -468,21 +470,21 @@ function selectItem(value: T): void {
if (vmodel.value && Array.isArray(vmodel.value)) {
if (vmodel.value.indexOf(value) === -1) {
// add a value
vmodel.value = [...vmodel.value, value] as T;
vmodel.value = [...vmodel.value, value];
} else {
// remove a value
vmodel.value = vmodel.value.filter((val) => val !== value) as T;
vmodel.value = vmodel.value.filter((val) => val !== value);
}
} else {
// init new value array
vmodel.value = [value] as T;
vmodel.value = [value];
}
// emit change after vmodel has changed
nextTick(() => emits("change", vmodel.value));
} else {
if (vmodel.value !== value) {
// update a single value
vmodel.value = value as T;
vmodel.value = value;
// emit change after vmodel has changed
nextTick(() => emits("change", vmodel.value));
}
Expand Down
23 changes: 14 additions & 9 deletions packages/oruga/src/components/input/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
lang="ts"
generic="
IsNumber extends boolean,
ModelValue extends IsNumber extends true ? number : string
T extends IsNumber extends true ? number : string
">
import {
ref,
Expand All @@ -19,7 +19,12 @@ import OIcon from "../icon/Icon.vue";
import { getOption } from "@/utils/config";
import { uuid } from "@/utils/helpers";
import { defineClasses, useDebounce, useInputHandler } from "@/composables";
import {
defineClasses,
useDebounce,
useInputHandler,
useVModel,
} from "@/composables";
import { injectField } from "../field/fieldInjection";
Expand All @@ -43,7 +48,7 @@ const props = defineProps({
* @type string | number
*/
modelValue: {
type: [Number, String] as unknown as PropType<ModelValue>,
type: [Number, String] as unknown as PropType<T>,
default: undefined,
},
/** @type boolean */
Expand Down Expand Up @@ -226,13 +231,13 @@ const emits = defineEmits<{
* modelValue prop two-way binding
* @param value {string | number} updated modelValue prop
*/
(e: "update:modelValue", value: ModelValue): void;
(e: "update:modelValue", value: T): void;
/**
* on input change event
* @param value {string | number} input value
* @param event {Event} native event
*/
(e: "input", value: ModelValue, event: Event): void;
(e: "input", value: T, event: Event): void;
/**
* on input focus event
* @param event {Event} native event
Expand Down Expand Up @@ -283,7 +288,8 @@ const {
// inject parent field component if used inside one
const { parentField, statusVariant, statusVariantIcon } = injectField();
const vmodel = defineModel<ModelValue>({ default: undefined });
// const vmodel = defineModel<T>({ default: undefined });
const vmodel = useVModel<T>();
// if id is given set as `for` property on o-field wrapper
if (props.id) parentField?.value?.setInputId(props.id);
Expand Down Expand Up @@ -350,7 +356,7 @@ watch(
function onInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
const input = (props.number ? Number(value) : String(value)) as ModelValue;
const input = (props.number ? Number(value) : String(value)) as T;
emits("input", input, event);
}
Expand Down Expand Up @@ -386,8 +392,7 @@ function iconClick(event: Event): void {
function rightIconClick(event: Event): void {
if (props.passwordReveal) togglePasswordVisibility();
else if (props.clearable)
vmodel.value = (props.number ? 0 : "") as ModelValue;
else if (props.clearable) vmodel.value = (props.number ? 0 : "") as T;
if (props.iconRightClickable) {
emits("icon-right-click", event);
nextTick(() => setFocus());
Expand Down
2 changes: 1 addition & 1 deletion packages/oruga/src/components/select/examples/grouped.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from "vue";
const selected = ref([]);
const selected = ref<string[]>([]);
</script>

<template>
Expand Down
5 changes: 3 additions & 2 deletions packages/oruga/src/components/switch/Switch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { computed, ref, type PropType } from "vue";
import { getOption } from "@/utils/config";
import { defineClasses, useInputHandler } from "@/composables";
import { defineClasses, useInputHandler, useVModel } from "@/composables";
import type { ComponentClass } from "@/types";
Expand Down Expand Up @@ -205,7 +205,8 @@ const { onBlur, onFocus, onInvalid, setFocus } = useInputHandler(
props,
);
const vmodel = defineModel<T>({ default: undefined });
// const vmodel = defineModel<T>({ default: undefined });
const vmodel = useVModel<T>();
const isChecked = computed(
() =>
Expand Down
9 changes: 8 additions & 1 deletion packages/oruga/src/components/switch/tests/switch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ describe("OSwitch tests", () => {
const trueValue = { a: "a", b: "b" };
const falseValue = { y: "y", x: "X" };
const wrapper = mount<typeof OSwitch<object>>(OSwitch, {
props: { modelValue: falseValue, trueValue, falseValue },
props: {
modelValue: falseValue,
trueValue,
falseValue,
"onUpdate:modelValue": (e) =>
wrapper.setProps({ modelValue: e }),
},
});

const input = wrapper.find("input");
Expand All @@ -144,6 +150,7 @@ describe("OSwitch tests", () => {
await input.setValue(false);
emits = wrapper.emitted("update:modelValue");
expect(emits).toHaveLength(2);
expect(emits[0]).toContainEqual(trueValue);
expect(emits[1]).toContainEqual(falseValue);
expect(wrapper.vm.value).toEqual(falseValue);
});
Expand Down
4 changes: 3 additions & 1 deletion packages/oruga/src/components/taginput/Taginput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
defineClasses,
getActiveClasses,
useInputHandler,
useVModel,
} from "@/composables";
import type { ComponentClass } from "@/types";
Expand Down Expand Up @@ -298,7 +299,8 @@ const emits = defineEmits<{
const autocompleteRef = ref<ComponentInstance<typeof OAutocomplete<T>>>();
const items = defineModel<T[]>({ default: () => [] });
// const items = defineModel<T[]>({ default: () => [] });
const items = useVModel<T[]>();
// use form input functionalities
const { setFocus, onFocus, onBlur, onInvalid } = useInputHandler(
Expand Down
1 change: 1 addition & 0 deletions packages/oruga/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from "./useProgrammatic";
export * from "./useClickOutside";
export * from "./useScrollingParent";
export * from "./useObjectMap";
export * from "./useVModel";
64 changes: 64 additions & 0 deletions packages/oruga/src/composables/useVModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ref, watch, nextTick, getCurrentInstance, type Ref } from "vue";

/**
* Adaption of {@link https://vueuse.org/core/useVModel/} options.
*/
export interface PropBindingOptions {
/**
* Attempting to check for changes of properties in a deeply nested object or array.
* Apply only when `passive` option is set to `true`
*
* @default false
*/
deep?: boolean;
}

/**
* @deprecated can be replaced by `defineModel` with vue 3.5
* Use two-way model binding in script setup syntax.
* Adaption of {@link https://vueuse.org/core/useVModel/}.
* @param name Property name
* @param options Extended usage options
* @returns Ref<T>
*/
export function useVModel<T>(
name: string = "modelValue",
options?: PropBindingOptions,
): Ref<T> {
// getting a hold of the internal instance in setup()
const vm = getCurrentInstance();
if (!vm)
throw new Error(
"useVModel must be called within a component setup function.",
);

/** reactive two-way binding model */
const proxy = ref<T>(vm.proxy.$props[name!]) as Ref<T>;

let isUpdating = false;

watch(
() => vm.proxy.$props[name!],
(value) => {
if (!isUpdating) {
isUpdating = true;
proxy.value = value;
nextTick(() => (isUpdating = false));
}
},
);

watch(
proxy,
(value) => {
if (
(!isUpdating && value !== vm.proxy.$props[name!]) ||
options?.deep
)
vm.emit(`update:${name}`, value);
},
{ deep: options?.deep },
);

return proxy;
}

0 comments on commit c00abf5

Please sign in to comment.