Skip to content

Instantly share code, notes, and snippets.

@bodograumann
Created June 29, 2021 11:04
Show Gist options
  • Save bodograumann/522b88d6d62c6935efacb2eee83b81c8 to your computer and use it in GitHub Desktop.
Save bodograumann/522b88d6d62c6935efacb2eee83b81c8 to your computer and use it in GitHub Desktop.
Vue 3 composable: Internal model state
import { ref, toRaw, nextTick } from "vue";
import type { Ref } from "vue";
import useModelState from "../useModelState";
describe("useModelState", () => {
const initialValue = [0];
const updatedValue = [1];
const validator = (value?: Array<unknown>) =>
value !== undefined && value.length === 1;
const invalidValue = [0, 1];
expect(updatedValue).not.toBe(initialValue);
let emit: () => void;
let source: Ref<Array<unknown>>;
beforeEach(() => {
source = ref(initialValue);
emit = jest.fn();
});
it("accepts an initial value", () => {
const { model } = useModelState(emit, initialValue);
expect(toRaw(model.value)).toBe(initialValue);
});
it("accepts an initial ref", () => {
const { model } = useModelState(emit, source);
expect(toRaw(model.value)).toBe(initialValue);
expect(model).not.toBe(source);
});
it("tracks changes to the inital ref", async () => {
const { model } = useModelState(emit, source);
source.value = updatedValue;
await nextTick();
expect(toRaw(model.value)).toBe(updatedValue);
});
it("emits new values", () => {
const { model } = useModelState(emit);
model.value = updatedValue;
expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith("update:modelValue", updatedValue);
});
it("emits valid new values", () => {
const { model } = useModelState(emit, undefined, validator);
model.value = updatedValue;
expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith("update:modelValue", updatedValue);
});
it("does not emit invalid new values", () => {
const { model } = useModelState(emit, undefined, validator);
model.value = invalidValue;
expect(toRaw(model.value)).toBe(invalidValue);
expect(emit).toHaveBeenCalledTimes(0);
});
it("can reset to the source value", async () => {
const { model, reset } = useModelState(emit, source, validator);
source.value = updatedValue;
await nextTick();
model.value = invalidValue;
expect(toRaw(model.value)).toBe(invalidValue);
reset();
expect(toRaw(model.value)).toBe(updatedValue);
});
});
import { ref, computed, watch, isRef, unref } from "vue";
import type { Ref } from "vue";
/**
* Keep an internal copy of the model state
*
* Note: You still need to register the “update:modelValue” event for your
* component, like all other events in Vue 3.
*
* This allows a component to work, without an external model being connected.
* When a validator is defined, only valid values are emitted. To restore the
* internal model value to the outside state, even when the outside state
* didn’t change, call `reset`.
*/
export default function useModelState<T>(
emit: (eventName: "update:modelValue", value?: T) => unknown,
source?: T | Ref<T>,
validator?: (value: T | undefined) => boolean
) {
const internalValue = ref() as Ref<T | undefined>;
function reset() {
internalValue.value = unref(source) as T | undefined;
}
if (isRef(source)) {
watch(source, reset, { immediate: true });
} else {
internalValue.value = source;
}
return {
model: computed({
get: () => internalValue.value,
set: (value?: T) => {
internalValue.value = value;
if (!validator || validator(value)) {
emit("update:modelValue", value);
}
},
}),
reset,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment