You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+77-11Lines changed: 77 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -176,7 +176,7 @@ Without a declared identity, arrays are diffed position-by-position. Deleting th
176
176
177
177
### Identity-keyed: `x-key`
178
178
179
-
Declare `x-key` on an array schema and patchwork matches elements across `base` and `draft` by that field. One element deleted produces one `remove` op, regardless of what follows it.
179
+
Declare `x-key` on an array schema and patchwork matches elements across `base` and `draft` by that field. One element deleted produces one `remove` op, regardless of what follows it. Field changes on a matched element produce one `replace` op at the element level, with a `changes` array of the individual field-level diffs.
`x-key` nests: arrays inside arrays can each declare their own key. The engine resolves the right field at each depth automatically via path-pattern matching.
221
+
`x-key` nests: arrays inside arrays can each declare their own key. By default, field changes in a nested keyed array bubble up and mark the parent element as modified (its `changes` will include them). Pass `cascade: false` to `diff()` to contain changes within their own identity boundary — a nested change will not mark the parent as modified.
212
222
213
-
The `identity` field on `DiffOp` carries the matched key value directly, so consumers don't need schema knowledge to identify what was addedor removed.
223
+
The `identity` field on `DiffOp` carries the matched key value, so consumers don't need schema knowledge to identify what was added, removed, or changed.
214
224
215
225
For a one-off without a schema:
216
226
217
227
```ts
218
228
engine.diff('$.regions', { key: 'id' });
219
229
```
220
230
231
+
### Ordered arrays: `x-ordered`
232
+
233
+
Add `x-ordered: true` alongside `x-key` to declare that position is meaningful. When an element's index shifts because something was added or removed nearby, patchwork surfaces that as a `move` op — a displacement — rather than hiding it.
`move` ops from identity-keyed arrays carry `identity` so you know which element was displaced. The `displacement` field on `replace` ops tells you how far an element moved when it was also modified.
249
+
250
+
To restore a displacement, pass the `move` op to `restore()` — it splices the element back to its base position.
251
+
221
252
### Set semantics: `x-key: '$self'`
222
253
223
254
For arrays of primitives that are semantically sets — tags, permission names, status flags — declare `x-key: '$self'`. The item itself is the identity. Reorders are invisible (sets have no order), duplicates collapse (sets have no duplicates), and a single add or remove produces a single op.
@@ -243,6 +274,34 @@ engine.diff();
243
274
244
275
Restricted to primitive items. For sets of objects, add a stable ID field and use `x-key: '<field>'`.
245
276
277
+
### Rendering full lists with `includeUnchanged`
278
+
279
+
By default `diff()` returns only changed elements. Pass `includeUnchanged: true` to include every element — changed or not — each labelled with its state. This lets you render a complete list with change highlighting from a single call, without merging the diff against the raw array yourself.
// plus { op: 'unchanged', ... } for every element that stayed the same
285
+
```
286
+
287
+
### Reverting a diff op
288
+
289
+
`restore(op)` takes any `DiffOp` produced by `diff()` and applies the inverse mutation to draft, pushing it onto the undo stack like any other operation. The diff must reflect the current draft state — if you mutate after diffing, re-diff before restoring.
engine.restore(removeOp); // re-inserts us-east at its original position
295
+
engine.undo(); // un-does the restore
296
+
```
297
+
298
+
| op | what `restore` does |
299
+
|---|---|
300
+
|`add`| deletes the element |
301
+
|`remove`| re-inserts it at its original position |
302
+
|`replace`| reverts the element to `oldValue`|
303
+
|`move`| splices it back to its base position |
304
+
246
305
## Scoped lenses
247
306
248
307
`getNodeEngine(path)` returns a `NodeEngine` — a lens onto a subtree. It owns no state; reads resolve through the parent on every access and writes forward to the parent with paths rewritten. **Both sides see the same physical state.**
@@ -335,7 +394,8 @@ See **[docs/angular.md](docs/angular.md)** for the full API, typed generics, cha
335
394
|`.getBase(path)`| Same as `get` but reads from base. |
336
395
|`.getValue(path)`| Strict single-match read from draft. Throws `Error` on multi-match; throws `undefined` on no-match. |
337
396
|`.getValueBase(path)`| Same as `getValue` but reads from base. |
338
-
|`.diff(path?, options?)`|`DiffOp[]` — structural diff between base and draft. |
397
+
|`.diff(path?, options?)`|`DiffOp[]` — structural diff between base and draft. `options.key` sets a one-off identity key; `options.includeUnchanged` includes unchanged elements; `options.cascade` (default `true`) controls whether nested identity-array changes bubble up to the parent. |
398
+
|`.restore(op)`| Invert a `DiffOp` from `diff()` and push it onto the undo stack. |
339
399
|`.undo()` / `.redo()`| Reverse / replay the last operation. |
340
400
|`.accept()`| Promote draft into base. Reversible. |
341
401
|`.decline()`| Reset draft from base. Reversible. |
@@ -364,17 +424,23 @@ See **[docs/angular.md](docs/angular.md)** for the full API, typed generics, cha
- `absolutePath` — present on ops from `NodeEngine.diff()`. Contains the full document path while `path` is relative to the child's `$`.
376
-
- `identity` — present on `add` / `remove` ops produced by identity-keyed array diffing. The matched key value (or the item itself for `$self`). Not present on field-level `replace` ops or index-zip ops.
439
+
- `identity` — the matched key value for identity-keyed array ops. Present on `add`, `remove`, `move`, and element-level `replace` ops. The item itself for `$self` arrays.
377
440
- `oldValue` — present on `replace` ops; the value that was there before.
441
+
- `displacement` — on element-level `replace` and `unchanged` ops from ordered arrays (`x-ordered:true`). Integer delta: `draftIndex − baseIndex`. Zero if position did not change.
442
+
- `changes` — on element-level `replace` ops. Flat list of field-level `DiffOp`s describing what changed inside the element. Paths are absolute document paths.
443
+
- `unchanged` op — only emitted when `diff()` is called with `includeUnchanged:true`.
0 commit comments