diff --git a/docs/reference/core/collection/index.md b/docs/reference/core/collection/index.md index 36add6ab2..1a3942527 100644 --- a/docs/reference/core/collection/index.md +++ b/docs/reference/core/collection/index.md @@ -188,7 +188,7 @@ Enables or disables field tracking for the collection. See [Field-Level Reactivi The Collection class is equipped with a set of events that provide insights into the state and changes within the collection. These events, emitted by the class, can be crucial for implementing reactive behaviors and persistence management. Here is an overview of the events: * `added`: Triggered when a new item is added to the collection. The event handler receives the added item as an argument. -* `changed`: Fired when an existing item in the collection undergoes modification. The event handler is passed the modified item. +* `changed`: Fired when an existing item in the collection undergoes modification. The event handler is passed both the modified item and the item before modification. * `removed`: Signaled when an item is removed or deleted from the collection. The event handler receives the removed item. * `validate`: Emitted when an item should be validated. The event handler receives the item as an argument. Validate the item inside of the event handler and throw an error if the item is invalid. This will prevent the item from being inserted or updated. diff --git a/docs/reference/core/cursor/index.md b/docs/reference/core/cursor/index.md index 550054211..114c15a14 100644 --- a/docs/reference/core/cursor/index.md +++ b/docs/reference/core/cursor/index.md @@ -80,7 +80,7 @@ This method allows observation of changes in the cursor items. It uses callbacks * `callbacks`: An object of Callback functions for different observation events. * `added(item: T)`gets called when a new item was added to the cursor * `addedBefore(item: T, before: T)`gets called when a new item was added to the cursor and also indicates the position of the new item - * `changed(item: T)`gets called when an item in the cursor was changed + * `changed(item: T, before: T)`gets called when an item in the cursor was changed, passing the state after and before the change * `movedBefore(item: T, before: T)`gets called when an item moved its position in the cursor * `removed(item: T)`gets called when an item was removed from the cursor * `skipInitial`: A boolean to decide whether to skip the initial observation event. diff --git a/packages/base/core/CHANGELOG.md b/packages/base/core/CHANGELOG.md index 9b004c2c5..e5896ccd3 100644 --- a/packages/base/core/CHANGELOG.md +++ b/packages/base/core/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Added the previous state (before the update) as an argument to `'changed'` event handlers. This provides additional information to handlers, enabling e.g. history functionality. + ## [1.8.1] - 2026-03-17 ### Fixed diff --git a/packages/base/core/__tests__/Collection.spec.ts b/packages/base/core/__tests__/Collection.spec.ts index ba80e805d..f09410961 100644 --- a/packages/base/core/__tests__/Collection.spec.ts +++ b/packages/base/core/__tests__/Collection.spec.ts @@ -174,7 +174,7 @@ describe('Collection', () => { collection.updateOne({ id: '1' }, { $set: { name: 'Jane' } }) - expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } }) + expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } }, { id: '1', name: 'John' }) }) it('should not throw an error if no item matches the selector', () => { @@ -272,8 +272,8 @@ describe('Collection', () => { collection.updateMany({ name: 'John' }, { $set: { name: 'Jane' } }) expect(eventHandler).toHaveBeenCalledTimes(2) - expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } }) - expect(eventHandler).toHaveBeenCalledWith({ id: '3', name: 'Jane' }, { $set: { name: 'Jane' } }) + expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } }, { id: '1', name: 'John' }) + expect(eventHandler).toHaveBeenCalledWith({ id: '3', name: 'Jane' }, { $set: { name: 'Jane' } }, { id: '3', name: 'John' }) }) it('should throw an error if trying to update the item id to a value that already exists', () => { @@ -312,7 +312,7 @@ describe('Collection', () => { collection.replaceOne({ id: '1' }, { name: 'Jack' }) - expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jack' }, { name: 'Jack' }) + expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jack' }, { name: 'Jack' }, { id: '1', name: 'John' }) }) it('should not throw an error if no item matches the selector', () => { diff --git a/packages/base/core/__tests__/Cursor.spec.ts b/packages/base/core/__tests__/Cursor.spec.ts index 8bd56ce40..a973ff9ad 100644 --- a/packages/base/core/__tests__/Cursor.spec.ts +++ b/packages/base/core/__tests__/Cursor.spec.ts @@ -186,7 +186,10 @@ describe('Cursor', () => { await wait() // Wait for all operations to finish expect(callbacks.added).not.toHaveBeenCalled() expect(callbacks.addedBefore).not.toHaveBeenCalled() - expect(callbacks.changed).toHaveBeenCalledWith(expect.objectContaining({ id: 1, name: 'item1_modified' })) + expect(callbacks.changed).toHaveBeenCalledWith( + expect.objectContaining({ id: 1, name: 'item1_modified' }), + expect.objectContaining({ id: 1, name: 'Item 1' }), + ) expect(callbacks.movedBefore).not.toHaveBeenCalled() expect(callbacks.removed).not.toHaveBeenCalled() }) @@ -289,7 +292,10 @@ describe('Cursor', () => { await wait() // Wait for all operations to finish expect(callbacks.added).not.toHaveBeenCalled() expect(callbacks.addedBefore).not.toHaveBeenCalled() - expect(callbacks.changed).toHaveBeenCalledWith(expect.objectContaining({ id: 2, name: 'Item 30' })) + expect(callbacks.changed).toHaveBeenCalledWith( + expect.objectContaining({ id: 2, name: 'Item 30' }), + expect.objectContaining({ id: 2, name: 'Item 2' }), + ) expect(callbacks.movedBefore).toHaveBeenCalledWith( expect.objectContaining({ id: 2, name: 'Item 30' }), null, @@ -357,7 +363,10 @@ describe('Cursor', () => { await new Promise((resolve) => { setTimeout(resolve, 0) }) - expect(callbacks.changed).toHaveBeenCalledWith(expect.objectContaining({ id: 1, name: 'item1_modified' })) + expect(callbacks.changed).toHaveBeenCalledWith( + expect.objectContaining({ id: 1, name: 'item1_modified' }), + expect.objectContaining({ id: 1, name: 'Item 1' }), + ) col.updateOne({ id: 1 }, { $set: { count: 42 } }) // Move existing item await new Promise((resolve) => { diff --git a/packages/base/core/src/Collection/Observer.ts b/packages/base/core/src/Collection/Observer.ts index a7b161bc2..1154d63f0 100644 --- a/packages/base/core/src/Collection/Observer.ts +++ b/packages/base/core/src/Collection/Observer.ts @@ -3,7 +3,7 @@ import uniqueBy from '../utils/uniqueBy' type AddedCallback = (item: T) => void type AddedBeforeCallback = (item: T, before: T) => void -type ChangedCallback = (item: T) => void +type ChangedCallback = (item: T, before: T) => void type ChangedFieldCallback = ( item: T, field: Field, @@ -122,7 +122,7 @@ export default class Observer { if (newItem) { if (this.hasCallbacks(['changed', 'changedField']) // If the item exists but has changed, call 'changed' callback && !isEqual(newItem.item, oldItem)) { - this.call('changed', newItem.item) + this.call('changed', newItem.item, oldItem) if (this.hasCallbacks(['changedField'])) { // check for changed fields and call 'changedField' callback diff --git a/packages/base/core/src/Collection/index.ts b/packages/base/core/src/Collection/index.ts index 8e19c335b..db129dd54 100644 --- a/packages/base/core/src/Collection/index.ts +++ b/packages/base/core/src/Collection/index.ts @@ -40,7 +40,7 @@ export interface CollectionOptions, I, E extends BaseItem interface CollectionEvents { 'added': (item: T) => void, - 'changed': (item: T, modifier: Modifier) => void, + 'changed': (itemAfter: T, modifier: Modifier, itemBefore: T) => void, 'removed': (item: T) => void, 'persistence.init': () => void, @@ -839,7 +839,7 @@ export default class Collection< this.emit('validate', modifiedItem) this.memory().splice(index, 1, modifiedItem) this.rebuildIndices() - this.emit('changed', modifiedItem, restModifier) + this.emit('changed', modifiedItem, restModifier, item) } this.emit('updateOne', selector, modifier) this.executeInDebugMode(callstack => this.emit('_debug.updateOne', callstack, selector, modifier)) @@ -900,8 +900,8 @@ export default class Collection< this.memory().splice(index, 1, item) }) this.rebuildIndices() - changes.forEach(({ item }) => { - this.emit('changed', item, restModifier) + changes.forEach(({ item: changedItem }, changeIndex) => { + this.emit('changed', changedItem, restModifier, items[changeIndex]) }) this.emit('updateMany', selector, modifier) this.executeInDebugMode(callstack => this.emit('_debug.updateMany', callstack, selector, modifier)) @@ -944,7 +944,7 @@ export default class Collection< this.emit('validate', modifiedItem) this.memory().splice(index, 1, modifiedItem) this.rebuildIndices() - this.emit('changed', modifiedItem, replacement as Modifier) + this.emit('changed', modifiedItem, replacement as Modifier, item) } this.emit('replaceOne', selector, replacement) this.executeInDebugMode(callstack => this.emit('_debug.replaceOne', callstack, selector, replacement))