When building applications, you often need to manage and compare objects to determine what has changed between different states of the same data. This is where the compareObjects function comes in handy. The compareObjects function takes two instances of an object, typically representing different states of the same data and returns a detailed array of changes. Each entry in this array represents a specific property that has been added, removed, or modified, including both its old and new values.

The logic behind compareObjects is designed to handle various scenarios, including nested objects and complex data structures. It works by categorizing differences into three main types: added, removed and changed by looping through the properties of both objects, the function detects which properties have been added or removed and identifies any changes to existing properties. Additionally, it handles recursive comparison for nested objects, ensuring that even deeply nested changes are captured accurately. The function also provides an optional ignoreList parameter, allowing developers to exclude specific properties from the comparison, offering flexibility in how changes are tracked and reported.

Overall, compareObjects simplifies the process of tracking state changes, making it easier to implement features like undo/redo functionality, change tracking, and state synchronization across components.

Code

/**
 * This composable offers objects functionality to vue components.
 */
export const useObjects = () => {
  type Diff = {
    property: string,
    action: 'added' | 'removed' | 'changed',
    oldValue: unknown,
    newValue: unknown
  }

  /**
   * Takes in two objects (ideally with the same props) and compares them returns an array of objects each object
   * has 4 props which are affected prop name, old value, new value, action type (added/removed/changed).
   * This function is meant to compare the current stat of an object with an older copy of the same object.
   * pass objects with different props on your own responsibility.
   *
   * The function logic goes as follows
   * 1. two objects are taken as input prams
   * 2. we have 3 main categories of action here changed|added|removed
   * 3. the changed is the intersection of these objects, added is the Right JOIN, removed is Left Join.
   * 4. we loop over the currentObj(right), compare to the originalObj, add changes|added to diffs array
   * 5. we loop over the originalObj(left), compare to the currentObj, add changes|removed to diffs array
   * 6. filter the array for the ignoreList and return the diffs.
   */
  const compareObjects = (
    originalObj: object,
    currentObj: object,
    ignoreList: string[] = []
  ): Array<Diff> => {
    if (!originalObj) {
      return []
    }

    const diffs = new Array<Diff>()

    for (const [key, value] of Object.entries(originalObj)) {
      const currentValue = Object.getOwnPropertyDescriptor(currentObj, key)?.value
      if (!currentObj.hasOwnProperty(key)) {
        if (typeof value === 'object' && value !== null) {
          diffs.push(...compareObjects(value, {}).map(diff => ({
            property: `${key}.${diff.property}`,
            action: 'removed',
            oldValue: diff.oldValue,
            newValue: undefined
          })))
        } else {
          diffs.push({ property: key, action: 'removed', oldValue: value, newValue: undefined })
        }
      } else if (typeof value !== typeof currentValue) {
        diffs.push({ property: key, action: 'changed', oldValue: value, newValue: currentValue })
      } else if (
        typeof value === 'object' && value !== null &&
        typeof currentValue === 'object' && currentValue !== null
      ) {
        diffs.push(...compareObjects(value, currentValue).map(diff => ({
          property: `${key}.${diff.property}`,
          action: diff.action,
          oldValue: diff.oldValue,
          newValue: diff.newValue
        })))
      } else if (currentValue !== value) {
        diffs.push({ property: key, action: 'changed', oldValue: value, newValue: currentValue })
      }
    }

    // we loop over the currentObj and compare to the current to see if anything got removed
    for (const [key, value] of Object.entries(currentObj)) {
      if (!originalObj.hasOwnProperty(key)) {
        if (typeof value === 'object' && value !== null) {
          // If the added property is an object, add its nested properties to the diff array
          diffs.push(...compareObjects({}, value).map(diff => ({
            property: `${key}.${diff.property}`,
            action: 'added',
            oldValue: undefined,
            newValue: diff.newValue
          })))
        } else {
          // Otherwise, just add the added property to the diff array
          diffs.push({ property: key, action: 'added', oldValue: undefined, newValue: value })
        }
      }
    }

    return diffs.filter(diff => !ignoreList.includes(diff.property))
  }

  return { compareObjects }
}