{"id":15629713,"url":"https://github.com/anko/objerve","last_synced_at":"2025-10-13T22:09:32.415Z","repository":{"id":39898491,"uuid":"256588335","full_name":"anko/objerve","owner":"anko","description":"JS listenable objects","archived":false,"fork":false,"pushed_at":"2022-11-17T00:45:26.000Z","size":488,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-13T22:09:30.791Z","etag":null,"topics":["javascript","listener","object","property","reactive-programming","watch"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anko.png","metadata":{"files":{"readme":"readme.markdown","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-17T19:09:53.000Z","updated_at":"2022-05-23T00:14:57.000Z","dependencies_parsed_at":"2022-09-12T00:20:31.147Z","dependency_job_id":null,"html_url":"https://github.com/anko/objerve","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/anko/objerve","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Fobjerve","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Fobjerve/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Fobjerve/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Fobjerve/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anko","download_url":"https://codeload.github.com/anko/objerve/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anko%2Fobjerve/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279017152,"owners_count":26085983,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","listener","object","property","reactive-programming","watch"],"created_at":"2024-10-03T10:28:16.255Z","updated_at":"2025-10-13T22:09:32.393Z","avatar_url":"https://github.com/anko.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# objerve [![](https://img.shields.io/npm/v/objerve.svg?style=flat-square)](https://www.npmjs.com/package/objerve) [![](https://img.shields.io/travis/anko/objerve.svg?style=flat-square)](https://travis-ci.org/anko/objerve) [![](https://img.shields.io/coveralls/github/anko/objerve?style=flat-square)](https://coveralls.io/github/anko/objerve)\n\nDefine callbacks that get fired when given object properties change.\n\n![picture](https://user-images.githubusercontent.com/5231746/79608887-06a4df80-80f6-11ea-9476-b1e87e9efc38.png)\n\n## example\n\n\u003c!-- !test program node --\u003e\n\n\u003c!-- !test in first example --\u003e\n```js\nconst objerve = require('./main.js')\n\nconst obj = objerve()\n\nobjerve.addListener(obj, ['a', 'b'], (newValue, oldValue, action, path, obj) =\u003e {\n  console.log(`${action} ${path.join('.')}: `+\n    `${JSON.stringify(oldValue)} -\u003e ${JSON.stringify(newValue)}`)\n})\n\nobj.a = { b: 'hi' }\nobj.a.b = 'hello'\nobj.a = null\n```\n\n\u003c!-- !test out first example --\u003e\n\n\u003e ```\n\u003e create a.b: undefined -\u003e \"hi\"\n\u003e change a.b: \"hi\" -\u003e \"hello\"\n\u003e delete a.b: \"hello\" -\u003e undefined\n\u003e ```\n\n## features\n\n - Behaves exactly like an ordinary Object\n - Can listen to fixed paths or to all paths with a prefix, and use\n   [`objerve.each`](#objerveeach) inside paths to match all array indexes.\n - Putting objerve instances (or parts of them) inside each others' properties\n   fully works, and property changes are propagated between instances as you'd\n   expect if they are properties of each other (even with circular references)\n - Can tell apart `undefined` property value and property deletion, thanks to\n   the `action` argument passed to callbacks (shows `created` / `changed` /\n   `deleted`).\n - Calls your callbacks in [a nesting-respecting order](#call-order), so your\n   callbacks can setup and teardown state in the correct order (bottom-up\n   construction, top-down destruction)\n - Stores listeners in a prefix tree by target path, to speed up queries with\n   large objects and many listeners.\n\n## api\n\n### `objerve([obj])`\n\nWrap the given object (default `{}`) so it can be subscribed to.\n\nThe resulting object behaves like the object did before, but changes to its\npaths can be listened to with `objerve.addListener` or\n`objerve.addPrefixListener`.\n\n### `objerve.addListener(obj, path, callback)`\n\nThe path can contain `objerve.each`, which will match any Array index at that\nposition.\n\nWorks inside listener callbacks.  If inside a listener you add a new listener\nthat matches the same path, the new listener will also be called with this same\nchange.\n\n### `objerve.removeListener(obj, path, callback)`\n\nRemove the listener from the given path, so the callback is no longer called.\nThe path is useful to disambiguate in case the same callback function is being\nused as the listener for multiple paths.\n\nDoes nothing if it cannot find such a listener.\n\nWorks inside listener callbacks.  If you remove a listener for the same path\ninside a callback for that path, the removed listener won't be called for that\nchange either (unless it was already called before this one).\n\n### `objerve.addPrefixListener(obj, path, callback)`\n\nSame as `addListener`, but will be called for any property at all that has the\ngiven `path` as a prefix.  Pass `[]` for the path to be called for every change\nto any property.\n\n### `objerve.removePrefixListener(obj, path, callback)`\n\nSame as `removeListener`, but for prefix listeners.\n\n### `objerve.each`\n\nA special Symbol value that can be passed as part of a path to listen to.  It\nmatches any valid array index, i.e. `0`, `1`, `999999999` etc, so your listener\nis called for every element of an array being created, changed, or deleted.\n\n## how callbacks are called\n\nYour callback function is called immediately before the described change is\nactually applied to the object, with these arguments:\n\n - `newValue`\n - `oldValue`\n - `action`: One of the following strings:\n   - `'create'` if the property did not exist, and is being created\n   - `'change'` if the property exists, and its value is changing\n   - `'delete'` if the property exists, but it is being deleted\n\n   \u003cp\u003e\u003c/p\u003e\u003cdetails\u003e\u003csummary\u003eExample:  Using the \u003ccode\u003eaction\u003e\u003c/code\u003e argument to distinguish property deletion from being set to \u003ccode\u003eundefined\u003c/code\u003e\u003c/summary\u003e\n\n   \u003c!-- !test in using change argument --\u003e\n\n   ```js\n   const objerve = require('./main.js')\n   const obj = objerve()\n\n   objerve.addListener(obj, ['x'], (newValue, oldValue, action) =\u003e {\n     console.log(`${action} ${oldValue} -\u003e ${newValue}`)\n   })\n\n   obj.x = true\n   obj.x = undefined\n   delete obj.x\n   ```\n\n   \u003c!-- !test out using change argument --\u003e\n\n   \u003e ```\n   \u003e create undefined -\u003e true\n   \u003e change true -\u003e undefined\n   \u003e delete undefined -\u003e undefined\n   \u003e ```\n\n   Note how although both the `obj.x = undefined` and `delete obj.x` lines\n   triggered a callback with `newValue` `undefined`, their `action`s differed:\n   `'change'` and `'delete'`.\n\n   \u003c/details\u003e\n\n - `path`: An Array representing the property path through the object at which\n   this update happened.  Useful if you have a single callback function\n   listening to multiple paths.\n - `obj`: A reference to the object as it currently exists (just before the\n   described update is actually applied).\n - `updateId`: A number uniquely identifying the currently happening change.\n   All listeners that get called due to the same change (or caused by a\n   callback reacting to the same change) see the same identifier.\n\n   \u003cdetails\u003e\u003csummary\u003eExample: same update id when a listener itself triggers a change\u003c/summary\u003e\n\n   \u003c!-- !test in re-call --\u003e\n   ```js\n   const objerve = require('./main.js')\n   const obj = objerve()\n\n   // Listen to changes to 'obj.a'.  Reduce it by 1 unless it's 0.\n   objerve.addListener(obj, ['a'],\n     (val, previousVal, action, path, objRef, updateId) =\u003e {\n       console.log(`[${action}] ${previousVal} -\u003e ${val} (updateId ${updateId})`)\n       if (val \u003e 0) {\n         obj.a = val - 1\n       }\n     })\n   // Also create a listener listening to all properties on 'obj'.\n   objerve.addPrefixListener(obj, [],\n     (val, previousVal, action, path, objRef, updateId) =\u003e {\n       console.log(`prefix listener called (updateId ${updateId})`)\n     })\n\n   obj.a = 3\n   console.log(obj.a)\n   obj.a = 2\n   console.log(obj.a)\n   ```\n\n   Each time something is assigned to `obj.a`, the first listener gets called,\n   and assigns it 1 lower, until it's 0:\n\n   \u003c!-- !test out re-call --\u003e\n\n   \u003e ```\n   \u003e prefix listener called (updateId 0)\n   \u003e [create] undefined -\u003e 3 (updateId 0)\n   \u003e prefix listener called (updateId 0)\n   \u003e [create] undefined -\u003e 2 (updateId 0)\n   \u003e prefix listener called (updateId 0)\n   \u003e [create] undefined -\u003e 1 (updateId 0)\n   \u003e prefix listener called (updateId 0)\n   \u003e [create] undefined -\u003e 0 (updateId 0)\n   \u003e 0\n   \u003e prefix listener called (updateId 1)\n   \u003e [change] 0 -\u003e 2 (updateId 1)\n   \u003e prefix listener called (updateId 1)\n   \u003e [change] 0 -\u003e 1 (updateId 1)\n   \u003e prefix listener called (updateId 1)\n   \u003e [change] 0 -\u003e 0 (updateId 1)\n   \u003e 0\n   \u003e ```\n\n   Note that for each individual change (`obj.a = 3` and `obj.a = 2`), both\n   listeners were called multiple times, but during each change both were\n   called with the same `updateId`.\n\n   \u003c/details\u003e\n\nIf your callback wants to cancel the described change from happening, simply\nassign a value to the property being changed and it will take priority.\n\nNote that callbacks are always called *for every matching change*, even if\nchanges essentially invalidate previous ones by overwriting their values.  Some\nuse-cases (such as updating a UI in response to property changes) may only care\nabout the final results at the end of this event loop tick, so you may wish to\naccumulate the changes and defer your rendering with an API appropriate for\nyour use-case (such as [`setImmediate`][setImmediate],\n[`process.nextTick`][processNextTick], [`queueMicrotask`][queueMicrotask],\n[`requestAnimationFrame`][requestAnimationFrame], etc).\n\n\u003cdetails\u003e\u003csummary\u003eExample: Accumulating changes and deferring rendering using \u003ccode\u003eprocess.nextTick\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- !test in defer --\u003e\n\n```js\nconst objerve = require('./main.js')\nconst ArrayKeyedMap = require('array-keyed-map')\n\nconst obj = objerve()\nconst accumulatedChanges = new ArrayKeyedMap()\n\nconst render = () =\u003e {\n  // Put your expensive UI rendering code here\n  console.log(Array.from(accumulatedChanges.entries()))\n  accumulatedChanges.clear()\n}\n\nobjerve.addListener(obj, ['a'],\n  (newVal, oldVal, action, path) =\u003e {\n    if (accumulatedChanges.size === 0) process.nextTick(render)\n    if (!accumulatedChanges.has(path)) {\n      accumulatedChanges.set(path, {newVal, oldVal})\n    } else {\n      accumulatedChanges.get(path).newVal = newVal\n    }\n  })\n\n// Make a bunch of changes\nobj.a = 1\nobj.a = 2\nobj.a = 3\n```\n\nThe `render` function only gets called on next event loop tick tick, with the\ntotal accumulated change from `undefined` to `3`, and none of the intermediate\nstates between:\n\n\u003c!-- !test out defer --\u003e\n\n```\n[ [ [ 'a' ], { newVal: 3, oldVal: undefined } ] ]\n```\n\n\u003c/details\u003e\n\n## call order\n\nWhen one change triggers multiple callbacks, the order they are called depends\non whether the change is constructive or destructive:   If the property is\nbeing created or changed, callbacks are called in root→leaf order.  If the\nproperty is being deleted, callbacks are called in leaf→root order.\n\nBecause of this feature, your listeners can setup or teardown state (e.g.\nmanaging DOM elements) in response to creation or deletion, and sub-properties\ncan use that state (e.g. appending their own DOM elements to the parent's ones)\nwhile still being able to clean up the sub-properties' state gracefully and in\nthe right order even when a whole chain of properties is deleted all at once.\n\n\u003cdetails\u003e\u003csummary\u003eExample: Construction and destruction call order\u003c/summary\u003e\n\n\u003c!-- !test in call order --\u003e\n```js\nconst objerve = require('./main.js')\nconst obj = objerve()\n\nconst callback = (name) =\u003e {\n  return (val, previousVal, action) =\u003e {\n    console.log(`${action} ${name}`)\n  }\n}\n\nobjerve.addListener(obj, ['a'], callback('a'))\nobjerve.addListener(obj, ['a', 'b'], callback('a.b'))\nobjerve.addListener(obj, ['a', 'b', 'c'], callback('a.b.c'))\n\nobj.a = { b: { c: 'value' } }\ndelete obj.a\n```\n\n\u003c!-- !test out call order --\u003e\n\n\u003e ```\n\u003e create a\n\u003e create a.b\n\u003e create a.b.c\n\u003e delete a.b.c\n\u003e delete a.b\n\u003e delete a\n\u003e ```\n\u003c/details\u003e\n\nPrefix listeners and `objerve.each`-matching listeners are also considered\n\"parents\" of concrete property paths, so their listeners are called before the\nconcrete path's listeners on creation/change (prefix→each→concrete), and after\nthem on deletion (concrete→each→prefix).\n\n\u003cdetails\u003e\u003csummary\u003eExample: Construction and destruction call order, with prefix- and \u003ccode\u003eobjerve.each\u003c/code\u003e-listeners\u003c/summary\u003e\n\n\u003c!-- !test in tree each call order --\u003e\n```js\nconst objerve = require('./main.js')\nconst obj = objerve([])\n\nconst callback = (name) =\u003e {\n  return (val, previousVal, action) =\u003e console.log(`${action} ${name}`)\n}\n\n// Listen for property '0'\nobjerve.addListener(obj, [0], callback('concrete'))\n// Listen for any array index\nobjerve.addListener(obj, [objerve.each], callback('each'))\n// Listen for all properties\nobjerve.addPrefixListener(obj, [], callback('prefix'))\n\nobj[0] = true\ndelete obj[0]\n```\n\u003c!-- !test out tree each call order --\u003e\n\n\u003e ```\n\u003e create prefix\n\u003e create each\n\u003e create concrete\n\u003e delete concrete\n\u003e delete each\n\u003e delete prefix\n\u003e ```\n\u003c/details\u003e\n\nIf there are multiple listeners for a property that changes, the listeners are\ncalled in insertion order.\n\nOther than the above rules, the relative order in which any two paths'\ncallbacks are called may be arbitrary, so you shouldn't rely on it.\n\n## use-cases\n\n - Binding data to UI.\n - Testing.  Transparently adding logging to property changes is handy.\n - Reactive programming.\n\n## license\n\n[ISC](LICENSE); summary: use for anything, credit me, no warranty\n\n[setImmediate]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate\n[processNextTick]: https://nodejs.org/api/process.html#process_process_nexttick_callback_args\n[queueMicrotask]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/queueMicrotask\n[requestAnimationFrame]: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanko%2Fobjerve","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanko%2Fobjerve","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanko%2Fobjerve/lists"}