vue-writable-object.ts
edited
1import { computed, readonly, type Ref, ref, toRef } from 'vue';
2
3import isEqual from 'dequal';
4
5const clone = <T>(value: T): T => {
6 if (value !== null && typeof value === 'object') {
7 // nested reactivity means that structuredClone() isn't possible, the next
8 // closest thing is to stringify the object into JSON and parse it back.
9 return JSON.parse(JSON.stringify(value)) as T;
10 }
11
12 return value;
13};
14
15const writableObject = <T extends Record<string, unknown>, K extends keyof T>(
16 upstream: Ref<T>,
17 keys: K[]
18) => {
19 const scratch = ref(
20 ((): T => {
21 const ro = readonly(upstream);
22 const cloned: Record<string, unknown> = {};
23
24 for (const key in upstream.value) {
25 if (keys.includes(key as any)) {
26 cloned[key] = clone(upstream.value[key]);
27 } else {
28 cloned[key] = toRef(() => ro.value[key]);
29 }
30 }
31
32 return cloned as T;
33 })()
34 ) as Ref<T>;
35
36 return {
37 scratch: scratch as Readonly<Ref<T>>,
38 hasChanges: computed(() => {
39 return !isEqual(upstream.value, scratch.value);
40 }),
41 resetChanges: () => {
42 for (const key in upstream.value) {
43 scratch.value[key] = clone(upstream.value[key]);
44 }
45 },
46 };
47};