{"id":13447693,"url":"https://github.com/josh/selector-observer","last_synced_at":"2025-03-22T01:31:13.427Z","repository":{"id":25763866,"uuid":"16013375","full_name":"josh/selector-observer","owner":"josh","description":"Allows you to monitor DOM elements that match a CSS selector","archived":true,"fork":false,"pushed_at":"2020-11-17T11:39:02.000Z","size":205,"stargazers_count":218,"open_issues_count":0,"forks_count":14,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-19T20:16:10.749Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/josh.png","metadata":{"files":{"readme":"README.md","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":"2014-01-17T22:19:51.000Z","updated_at":"2024-12-03T19:09:10.000Z","dependencies_parsed_at":"2022-08-26T07:40:40.903Z","dependency_job_id":null,"html_url":"https://github.com/josh/selector-observer","commit_stats":null,"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josh%2Fselector-observer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josh%2Fselector-observer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josh%2Fselector-observer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josh%2Fselector-observer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/josh","download_url":"https://codeload.github.com/josh/selector-observer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244893430,"owners_count":20527588,"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","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":[],"created_at":"2024-07-31T05:01:24.542Z","updated_at":"2025-03-22T01:31:13.124Z","avatar_url":"https://github.com/josh.png","language":"JavaScript","readme":"# selector-observer\n\nselector-observer allows you to monitor DOM elements that match a CSS selector. Rather than imperatively querying the DOM, register an observer for a CSS selector and trigger behavior whenever those elements appear on the page. Typical uses include: registering event handlers and initializing a component or plugin.\n\n## Why\n\nIt is a common jQuery practice to initialize code inside a jQuery [ready] function.\n\n[ready]: http://api.jquery.com/ready/\n\n\u003e The .ready() method offers a way to run JavaScript code as soon as the page's Document Object Model (DOM) becomes safe to manipulate.\n\n```javascript\n$(document).ready(function() {\n  $('p.hello').text('The DOM is now loaded and can be manipulated.')\n})\n```\n\nThe issue with this approach is that this initialization code is run only once per page load. Modern web pages change over time after the initial load. This example depends on a `\u003cp class=\"hello\"\u003e` being rendered on the server and being on the initial page load. However, if it is inserted later via AJAX or dynamically through some other JS, it will not be found.\n\nInstead of imperatively querying the DOM, we could declare the selectors we needed to operate on.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nobserve('p.hello', {\n  add(el) {\n    $(el).text('The DOM is now loaded and can be manipulated.')\n  }\n})\n```\n\nUsing the `observe` helper, any matching element matching `p.hello` will trigger the `add` hook running our initialization code.\n\n## Usage\n\nTwo types of APIs are provided: a functional singleton API and a class-based API that allows you to change the scope of observation.\n\nThe `observe` function will install an observer on the current `document`.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nobserve('.foo', {\n  add(el) {\n    console.log(el, 'added to', document)\n  },\n  remove(el) {\n    console.log(el, 'removed from', document)\n  }\n})\n```\n\nAlternatively, the class-based `SelectorObserver` allows you to configure the root element. This API is similar to [`MutationObserver`][mutation].\n\n[mutation]: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver\n\n```javascript\nimport SelectorObserver from 'selector-observer'\n\nconst rootElement = document.getElementById('root')\nconst observer = new SelectorObserver(rootElement)\n\nobserver.observe('.foo', {\n  add(el) {\n    console.log(el, 'added to', rootElement)\n  },\n  remove(el) {\n    console.log(el, 'removed from', rootElement)\n  }\n})\n```\n\n## Use Cases\n\n### Event Handlers\n\n`selector-observer` can help automatically install event handlers on any matching elements and ensure a cleanup stage ran. This is often necessary instead of using an event delegation technique if the event does not bubble.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nfunction handleMouseEnter(event) {\n  event.currentTarget.classList.add('active')\n}\n\nfunction handleMouseLeave(event) {\n  event.currentTarget.classList.remove('active')\n}\n\nobserve('.dropzone', {\n  add(el) {\n    el.addEventListener('mouseenter', handleMouseEnter)\n    el.addEventListener('mouseleave', handleMouseLeave)\n  },\n  remove(el) {\n    el.removeEventListener('mouseenter', handleMouseEnter)\n    el.removeEventListener('mouseleave', handleMouseLeave)\n  }\n})\n```\n\n### Initialize third-party component or plugin\n\nMany third-party component or plugin libraries require a manual initialization step to be installed on a given element. The add and remove hooks can be used if the plugin provides a cleanup hook. Often these libraries omit that kind of API too. To work around this, `observe` provides an `initialize` hook that only runs once per a given element.\n\nThis example initializes the `tippy` tooltip library on any `btn` buttons on the page.\n\n```javascript\nimport {observe} from 'selector-observer'\nimport tippy from 'tippy'\n\nobserve('.btn', {\n  initialize(el) {\n    tippy(el)\n  }\n})\n```\n\n### Event Delegation vs. Direct Binding\n\nThere are two established patterns for attaching event handlers to elements: direct binding and event delegation.\n\nDirect binding means calling `addEventListener` directly on an element instance. The downside of this is you need to first find the element to attach the event handler. Often this is done on page load. However, this misses elements that may be added dynamically after the page loads.\n\nEvent delegation is a technique where event handlers are registered to a CSS selector and are matched against all triggered events. The advantage is that it matches elements that are dynamically added and removed from the page. There's also less performance overhead to registering event handlers as you do not need to query the DOM upfront.\n\nHowever, there are cases where event delegation does not work, or there may be a significant performance overhead to doing so. `selector-observer` can be used with direct binding to discover elements on page load and those dynamically added later.\n\nHere's an example of using the [delegated-events](https://github.com/dgraham/delegated-events) library to install a click handler\n\n```javascript\nimport {on} from 'delegated-events'\n\nfunction handleClick(event) {\n  console.log('clicked', event.currentTarget)\n}\n\non('click', '.foo', handleClick)\n```\n\nSimilarly using `selectors-observer` using direct binding.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nfunction handleClick(event) {\n  console.log('clicked', event.currentTarget)\n}\n\nobserve('.foo', {\n  add(el) {\n    el.addEventListener('click', handleClick)\n  },\n  remove(el) {\n    el.removeEventListener('click', handleClick)\n  }\n})\n```\n\nBoth accomplish similar tasks. However, in this example, using `delegated-events` would be preferred as there's little upfront overhead to installing the click handler. Each `selector-observer` registration adds a little bit of startup overhead.\n\nHowever, not all events types will work with `delegated-events`. For the event to work, it must bubble. Specific event types do not bubble like `mouseenter` and `mouseleave`. In this case, use `selector-observer` and direct bind.\n\n## Advanced Usage\n\n## constructor matching\n\nWhen using the [flow] type checker, elements passed to add and remove hooks will be typed as `Element`. CSS selectors could match any HTML element and even SVG elements.\n\n[flow]: https://flow.org/\n\n```javascript\nobserve('.any-element', {\n  add(el /*: Element */) {},\n  remove(el /*: Element */) {}\n})\n```\n\nIf you know the target element is a specific HTML element like a `\u003cform\u003e`, you can specify the constructor of the element to inform the type checker.\n\n```javascript\nobserve('.some-form', {\n  constructor: HTMLFormElement,\n  add(form /*: HTMLFormElement */) {},\n  remove(form /*: HTMLFormElement */) {}\n})\n```\n\n## initialize hook\n\nWhile defining `add` and `remove` hooks is preferred, a third `initialize` hook exists to enable use cases where plugins or components do not provide proper teardown APIs. Where `add` will run multiple times per `Element` instance, `initialize` will only run once.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nobserve('.foo', {\n  initialize(el) {\n    console.log('initialize')\n  },\n  add(el) {\n    console.log('add')\n  },\n  remove(el) {\n    console.log('remove')\n  }\n})\n\nconst el = document.createElement('div')\ndocument.body.appendChild(el) // log: initialize, add\ndocument.body.removeChild(el) // log: remove\ndocument.body.appendChild(el) // log: add\n```\n\n## State via initialize closure\n\nThe `initialize` hooks allow for a special return value of `{add: Function, remove: Function}` to dynamically set hooks for the given element instance. This enables private state to be captured in the initialize closure and shared between add and remove hooks.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nobserve('.foo', {\n  initialize(el) {\n    let counter = 0\n    return {\n      add() {\n        counter++\n      },\n      remove() {\n        counter--\n      }\n    }\n  }\n})\n```\n\nIt is also useful for defining per element event handlers that can access private state inside the initialization closure.\n\n```javascript\nimport {observe} from 'selector-observer'\n\nobserve('.foo', {\n  initialize(el) {\n    let isMouseOver = false\n\n    function handleMouseEnter() {\n      isMouseOver = true\n    }\n\n    function handleMouseLeave() {\n      isMouseOver = false\n    }\n\n    return {\n      add() {\n        el.addEventListener('mouseenter', handleMouseEnter)\n        el.addEventListener('mouseleave', handleMouseLeave)\n      },\n      remove() {\n        el.removeEventListener('mouseenter', handleMouseEnter)\n        el.removeEventListener('mouseleave', handleMouseLeave)\n      }\n    }\n  }\n})\n```\n\n## Observables\n\nRxJS—and other compatible observable libraries—that return a subscription object can automatically be disposed when elements are removed from the page.\n\n```js\nimport {observe} from 'selector-observer'\nimport {fromEvent} from 'rxjs'\n\nobserve('button', {\n  subscribe: button =\u003e fromEvent(button, 'click').subscribe(() =\u003e console.log('Clicked!'))\n})\n```\n\n## See Also\n\n- [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)\n- [rkusa/selector-observer](https://github.com/rkusa/selector-observer)\n- [rafaelw/mutation-summary](https://github.com/rafaelw/mutation-summary)\n- [csuwildcat/SelectorListener](https://github.com/csuwildcat/SelectorListener)\n- [Live Query](https://github.com/hazzik/livequery)\n","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosh%2Fselector-observer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjosh%2Fselector-observer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosh%2Fselector-observer/lists"}