tracked-instance
Version:
Build large forms and track all changes
219 lines (156 loc) • 11.5 kB
Markdown
<a href="https://www.npmjs.com/package/tracked-instance"><img src="https://img.shields.io/npm/v/tracked-instance.svg?sanitize=true" alt="Version"></a>
> Track form changes in Vue 3 and send only modified fields to the backend — no more diffing payloads by hand.
```js
const {data, changedData, isDirty} = useTrackedInstance({name: 'Jack', age: 30})
data.value.name = 'John'
changedData.value // { name: 'John' } ← only what changed
isDirty.value // true
data.value.name = 'Jack' // revert
changedData.value // undefined ← back to clean
isDirty.value // false
```
```bash
npm i tracked-instance
```
Supports **Vue 3** only.
---
Track changes to a single object, primitive, or array.
```js
import {useTrackedInstance} from 'tracked-instance'
const {data, changedData, isDirty, loadData, reset} = useTrackedInstance({
name: 'Jack',
isActive: false,
})
```
**Mutate `data.value` directly** — `changedData` and `isDirty` update automatically:
```js
data.value.name = 'John'
isDirty.value // true
changedData.value // { name: 'John' }
// Revert to original value → field disappears from changedData
data.value.name = 'Jack'
isDirty.value // false
changedData.value // undefined
```
**`reset()`** — revert all changes back to the last loaded baseline:
```js
data.value.name = 'John'
reset()
data.value // { name: 'Jack', isActive: false }
```
**`loadData(newData)`** — replace data without marking anything dirty (use after a successful save):
```js
loadData({name: 'Joe', isActive: true})
isDirty.value // false ← Joe is now the new baseline
```
Works with primitives and arrays too:
```js
useTrackedInstance(false)
useTrackedInstance([1, 2, 3])
```
### Custom equality with `equals`
By default values are compared with `===`. Override this for edge cases — for example when a UI component writes `null`
but the backend sends `""`:
```js
const {data, isDirty} = useTrackedInstance(
{comment: null},
{equals: (a, b) => (a ?? '') === (b ?? '')}
)
data.value.comment = '' // treated as equal to null
isDirty.value // false
data.value.comment = 'hi'
isDirty.value // true
```
---
Track an array of items — add, remove, modify, and reset the whole list.
```js
import {useCollection} from 'tracked-instance'
const {items, isDirty, add, remove, loadData, reset} = useCollection()
loadData([{name: 'Jack'}, {name: 'John'}, {name: 'Joe'}])
```
Each item in `items` is a `CollectionItem` with its own `TrackedInstance`:
```js
items.value[0].instance.data.value.name = 'Stepan'
isDirty.value // true
```
**`add(item, index?)`** — add a new item (marked `isNew: true`):
```js
const newItem = add({name: 'Taras'})
// newItem.isNew.value === true
// newItem.isRemoved.value === false
add({name: 'Taras'}, 0) // insert at position 0
```
**`remove(index, isHardRemove?)`** — soft-delete by default, hard-delete with `true`:
```js
remove(0) // soft remove: isRemoved = true, item stays in array
remove(0, true) // hard remove: spliced out immediately
```
Soft-removed items can be restored with `reset()` or by setting `isRemoved.value = false` manually.
**`reset()`** — removes new items, restores soft-removed ones, reverts all changes:
```js
reset()
```
### Item meta
Attach computed or reactive metadata to each item via a factory function:
```js
const {add, items} = useCollection(instance => ({
isValidName: computed(() => instance.data.value.name.length > 0)
}))
add({name: ''})
items.value[0].meta.isValidName.value // false
```
The same `options` (including `equals`) are forwarded to every `TrackedInstance` in the collection:
```js
const {items} = useCollection(
() => undefined,
{equals: (a, b) => (a ?? '') === (b ?? '')}
)
```
---
```typescript
useTrackedInstance<Data>(initialData ? : Data, options ? : TrackedInstanceOptions)
:
TrackedInstance<Data>
```
| Option | Type | Description |
|----------|---------------------------------------|------------------------------------------------------------|
| `equals` | `(a: unknown, b: unknown) => boolean` | Custom equality for primitive leaf values. Replaces `===`. |
| Return | Type | Description |
|---------------------|--------------------------|-------------------------------------------------------------|
| `data` | `Ref<Data>` | Reactive reference to current data. Mutate directly. |
| `changedData` | `ComputedRef<DeepPartial<Data>>` | Only modified fields. `undefined` when nothing has changed. |
| `isDirty` | `ComputedRef<boolean>` | `true` when any field differs from the original. |
| `loadData(newData)` | `void` | Replace data and clear dirty state (new baseline). |
| `reset()` | `void` | Revert all changes back to the last `loadData()` baseline. |
### useCollection(options?)
```typescript
useCollection<Item, Meta>(options?: CollectionOptions<Item, Meta>): Collection<Item, Meta>
```
| Option | Type | Description |
|------------------|--------------------------------------------|------------------------------------------------------------|
| `equals` | `(a: unknown, b: unknown) => boolean` | Custom equality for primitive leaf values. Replaces `===`. |
| `createItemMeta` | `(instance: TrackedInstance<Item>) => Meta` | Factory for per-item metadata, called on loadData/add. |
| Return | Type | Description |
|--------------------------------|-------------------------|------------------------------------------------------------------|
| `items` | `Ref<CollectionItem[]>` | Reactive array of collection items. |
| `isDirty` | `ComputedRef<boolean>` | `true` if any item is dirty, new, or soft-removed. |
| `add(item, index?)` | `CollectionItem` | Add a new item. Appended to end by default. |
| `remove(index, isHardRemove?)` | `void` | Soft-remove by default. Pass `true` to splice from array. |
| `loadData(items)` | `void` | Replace all items and clear dirty state. |
| `reset()` | `void` | Remove new items, restore soft-removed, reset all instance data. |
```typescript
interface CollectionItem<Item, Meta = undefined> {
instance: TrackedInstance<Item> // tracked instance for this item
isNew: Ref<boolean> // true for items added via add()
isRemoved: Ref<boolean> // true after soft remove
meta: Meta // custom metadata from createItemMeta()
remove(isHardRemove?: boolean): void // shortcut to remove self
}
```