JSFiddle - React, Tailwind, and code Playground
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.16/vue.min.js"></script>
<div id="app">
<fieldset>
<legend>CONTROLS</legend>
<div>
<input v-model="obj.a">
obj.a
</div>
<div>
<input v-model="obj.b.c.d">
obj.b.c.d
</div>
<div>
<button @click="toggleNestedProp">toggle</button>
obj.b.c.e: {{ obj.b.c.e ?? '__no_value__' }}
<div>
<div>
<input v-model.number="obj.f" type="number">
obj.f
<ul>
<li v-for="(n, i) in obj.fValues">
<input v-model="obj.fValues[i]">
obj.fValues[{{ i }}]
</li>
</ul>
</div>
</fieldset>
<fieldset>
<legend>OBJ</legend>
<pre>{{ obj }}</pre>
</fieldset>
<fieldset>
<legend>LOG</legend>
<button @click="changelog = []">clear</button>
<table>
<thead>
<tr>
<th>path</th>
<th>type</th>
<th>new</th>
<th>old</th>
</tr>
</thead>
<tbody>
<tr v-for="row in changelog">
<td v-for="n in row">{{ n ?? '__no_value__' }}</td>
</tr>
</tbody>
<table>
</fieldset>
</div>
CSS
* {
font-family: monospace;
}
td {
border: 1px solid silver;
padding: 5px;
}
JavaScript
function diff(a, b, path = '') {
if (!(a instanceof Object) || a.constructor !== b?.constructor) {
return a === b ? [] : [ [ path, 'changed', a, b ] ];
}
const keysA = new Set(Object.keys(a));
const keysB = new Set(Object.keys(b));
const addKey = k => path ? `${path}.${k}` : k;
return [
...Array.from(keysA.difference(keysB), k => [ addKey(k), 'added', a[k], , ]),
...Array.from(keysB.difference(keysA), k => [ addKey(k), 'deleted', , b[k] ]),
...Array.from(keysA.intersection(keysB), k => diff(a[k], b[k], addKey(k))).flat(),
];
}
new Vue({
el: '#app',
data: () => ({
obj: {
a: 69,
b: {
c: {
d: 'hello, world!!',
},
},
f: 0,
fValues: [],
},
changelog: [],
}),
computed: {
objCopy() {
return structuredClone(this.obj);
},
},
watch: {
objCopy(newVal, oldVal) {
this.changelog.unshift(...diff(newVal, oldVal));
},
'obj.f'(val) {
val = this.obj.f = Math.max(0, val | 0);
this.obj.fValues.push(...Array.from(
{ length: Math.max(0, val - this.obj.fValues.length) },
() => Math.random() * 10 | 0
));
this.obj.fValues.length = Math.min(val, this.obj.fValues.length);
},
},
methods: {
toggleNestedProp() {
if (this.obj.b.c.e) {
this.$delete(this.obj.b.c, 'e');
} else {
this.$set(this.obj.b.c, 'e', 187);
}
},
},
});