{"id":16548162,"url":"https://github.com/dgraham/query-selectors","last_synced_at":"2025-03-04T11:45:34.293Z","repository":{"id":57332350,"uuid":"102216700","full_name":"dgraham/query-selectors","owner":"dgraham","description":"A querySelector function returning an Option type rather than null.","archived":false,"fork":false,"pushed_at":"2020-08-31T01:37:47.000Z","size":152,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2023-11-20T15:48:36.476Z","etag":null,"topics":["maybe","optional"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"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/dgraham.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":"2017-09-02T18:45:53.000Z","updated_at":"2021-02-23T20:45:24.000Z","dependencies_parsed_at":"2022-09-15T12:31:27.074Z","dependency_job_id":null,"html_url":"https://github.com/dgraham/query-selectors","commit_stats":null,"previous_names":[],"tags_count":7,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgraham%2Fquery-selectors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgraham%2Fquery-selectors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgraham%2Fquery-selectors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgraham%2Fquery-selectors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dgraham","download_url":"https://codeload.github.com/dgraham/query-selectors/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241844281,"owners_count":20029611,"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":["maybe","optional"],"created_at":"2024-10-11T19:25:11.078Z","updated_at":"2025-03-04T11:45:34.251Z","avatar_url":"https://github.com/dgraham.png","language":"JavaScript","readme":"# Optional querySelector\n\nA querySelector function returning an [Option][] type rather than null.\n\n[Option]: https://github.com/dgraham/option-type\n\n## Usage\n\n```js\nimport {querySelector} from 'query-selectors'\n\n// Our favorite robot.\nconst url = 'https://github.com/hubot.png'\n\n// This querySelector returns an Option\u003cT\u003e, never null.\nconst avatar = querySelector(document, '.avatar', HTMLImageElement)\n\n// Test and unwrap the Option\u003cHTMLImageElement\u003e.\nif (avatar.isSome()) {\n  avatar.unwrap().src = url\n}\n\n// Or operate on the element only if it was found as a Some\u003cHTMLImageElement\u003e.\navatar.match({\n  Some: (el) =\u003e el.src = url,\n  None: () =\u003e console.log('not found')\n})\n```\n\n## Standard element queries\n\nFinding an individual element in the document tree and operating on it can\nlead to null pointer exceptions.\n\n```js\nconst el = document.querySelector('.expected-element')\n// el may be null!\nel.classList.add('selected')\nel.setAttribute('title', 'hello')\n```\n\nA safer alternative is to guard against null values with a conditional statement.\n\n```js\nconst el = document.querySelector('.expected-element')\nif (el) {\n  el.classList.add('selected')\n  el.setAttribute('title', 'hello')\n}\n```\n\nBecause `querySelector` is so frequently used in web applications, and it's\ntedious to guard every element query with null checks, these tests are most\noften omitted and replaced with the hope that the element will exist on the page.\n\n## Flow\n\nAdding [Flow][] type annotations improves the reliability of our code at compile time\nby forcing these null checks. When checked by Flow, the above example raises an error.\n\n[Flow]: https://flow.org\n\n```js\nconst el = document.querySelector('.expected-element')\nel.classList.add('selected')\n// Error: property `classList` Property cannot be accessed on possibly null value\n```\n\nSo we'll have to test every query for a possible null result to satisfy the\ntype system.\n\nAdditionally, the `document.querySelector()` function returns an `HTMLElement`\nbase class, which means we cannot access subclass properties with a type cast.\n\nIf we want to access a method or attribute defined on an element subclass,\nlike HTMLImageElement's `src` or HTMLInputElement's `value`, Flow requires\nanother conditional test to assert that the element is actually\nan `\u003cimg\u003e` or `\u003cinput\u003e`.\n\n```js\nconst el = document.querySelector('.expected-element')\nif (el) {\n  el.value = 'hello'\n  // Error: property `value` Property not found in HTMLElement\n}\n```\n\nAdding an `instanceof` test passes the Flow type checker.\n\n```js\nconst el = document.querySelector('.expected-element')\nif (el instanceof HTMLInputElement) {\n  el.value = 'hello'\n}\n```\n\nThe combination of null tests and subclass type refinements feels like we're\nworking against the type system, rather than with it, and like there's a\nmissing abstraction that could simplify things.\n\nOne solution is to combine Flow's type system with an option type.\n\n## Option type\n\nSeveral languages replace the concept of a null value with an [option type][].\nAn `Option` is either a `Some` or a `None`, but either way, it's an object\nwith methods and never null.\n\n[option type]: https://en.wikipedia.org/wiki/Option_type\n\n```js\nimport {Some} from 'option-type'\nconst option = Some(42)\noption.isSome() // =\u003e true\noption.isNone() // =\u003e false\noption.unwrap() === 42 // =\u003e true\n```\n\nAn `Option` is just a wrapper for a value with some interesting methods on it.\nAnd when applied to selector queries, it prevents a `null` value from ever\nentering our code.\n\n```js\nimport {querySelector} from 'query-selectors'\n\nconst option = querySelector(document, '.expected-element')\n\nif (option.isSome()) {\n  const el = option.unwrap()\n  el.classList.add('selected')\n  el.setAttribute('title', 'hello')\n}\n\nif (option.isNone()) {\n  console.log('element not found')\n}\n```\n\nIn this example, we still have conditional tests for the presence and absence\nof the element in the tree, simply trading null tests for options.\n\nWhere `Option` gets more compelling is the ability to chain function calls onto\nit. The functions evaluate only if the value is `Some`. When a `None`\nappears, the function chain stops naturally without requiring any conditionals.\n\n```js\nimport {querySelector} from 'query-selectors'\nquerySelector(document, '.expected-element')\n  .andThen(addClass('selected')\n  .andThen(setAttribute('title', 'hello')\n```\n\n## Element type filters\n\nReturning to the `instanceof` type refinements required in the `\u003cimg\u003e` and\n`\u003cinput\u003e` example, what we really want is a set of two filter criteria when\nquerying the document tree:\n\n1. The selectors to find the element, as usual\n2. An additional type filter to assert the element is an `\u003cinput\u003e`\n\nThis is where the optional third argument to `querySelector` can be used.\n\n```js\nconst option = querySelector(document, '.expected-element', HTMLInputElement)\n\nif (option.isSome())\n  option.unwrap().value = 'hello'\n}\n```\n\nBoth criteria must pass for a `Some` to be returned. The element must exist in\nthe page with the `expected-element` class name and it must be an\nHTMLInputElement so that we can address the `value` property.\n\n## Query functions\n\nThis library has two sets of functions: those that query the element tree and\nthose that operate on the returned `Option\u003cElement\u003e` values.\n\n- `query(context, selector, klass)`\n- `querySelector(context, selector, klass)`\n- `querySelectorAll(context, selector, klass)`\n- `closest(element, selector, klass)`\n\nThe `query` function is an easy way to begin using the library. It returns an\n`Element`, rather than an `Option\u003cElement\u003e`, throwing an exception if it's\nnot found.\n\nThis can be used in place of the [`invariant`](https://github.com/zertosh/invariant)\nassertion function commonly used in Flow projects.\n\n```js\nimport {query} from 'query-selectors'\nimport invariant from 'invariant'\n\nconst el = query(document, '.selected', HTMLImageElement)\n\n// is equivalent to\n\nconst el = document.querySelector('.selected')\ninvariant(el instanceof HTMLImageElement)\n```\n\n## DOM functions\n\nOnce we have an `Option\u003cElement\u003e` from a query, these functions can be\nchained onto it with a call to `andThen`.\n\n```js\nquerySelector(document, '.selected')\n  .andThen(removeClass('selected', 'active'))\n  .andThen(addClass('hidden', 'inactive'))\n  .andthen(nextSibling('p'))\n  .andThen(after('A text node'))\n```\n\n### Traversal\n\nTraverse up and down the tree from a root element.\n\n- `find(selectors, klass)`\n- `parent(selectors, klass)`\n- `prev(selectors, klass)`\n- `next(selectors, klass)`\n\n### Mutation\n\nAdd, remove, and change nodes in the tree.\n\n- `addClass(names)`\n- `removeClass(names)`\n- `toggleClass(name, force)`\n- `getAttribute(name)`\n- `setAttribute(name, value)`\n- `getValue()`\n- `setValue(value)`\n- `setText(value)`\n- `append(nodes)`\n- `prepend(nodes)`\n- `after(nodes)`\n- `before(nodes)`\n- `replaceWith(nodes)`\n- `remove()`\n- `namedItem(name, klass)`\n\n## Better with match syntax\n\nThe option type isn't without its drawbacks. After we query the tree and\nhave an `Option`, we need to extract its value with a `match` function to\noperate on it.\n\n```js\nquerySelector(document, '.expected-element').match({\n  Some: (el) =\u003e {\n    el.innerHTML = '\u003cp\u003ehello there\u003c/p\u003e'\n    el.classList.toggle('selected')\n  },\n  None: () =\u003e {\n    console.log('not found')\n  }\n})\n```\n\nThis uses two function closures which is slower to create and invoke than an\n`if` or `switch` statement. It also precludes using early `return` statements\nbecause they're wrapped in their own functions.\n\nLanguages with a native option type pair it with pattern matching—a control\nflow construct, like `if` and `switch`, but with the ability to destructure\na value into its constituent parts.\n\nThe [Pattern Matching Syntax][match] proposal for JavaScript would make working\nwith option types even better.\n\n[match]: https://github.com/tc39/proposal-pattern-matching\n\nThe `match` function invocation above would become something like this.\n\n```js\nmatch querySelector(document, '.expected-element') {\n  Some(el): {\n    el.innerHTML = '\u003cp\u003ehello there\u003c/p\u003e'\n    el.classList.toggle('selected')\n  },\n  None: {\n    console.log('not found')\n  }\n})\n```\n\nThe proposed `match` keyword avoids the closures and function calls\nrequired today.\n\n## Alternatives\n\nThe [Optional Chaining Operator][op] proposal is another way to improve null\nchecks. It's an addition to JavaScript's syntax that would alleviate the need\nfor many of the conditional tests resulting from `document.querySelector()`.\n\n```js\nconst el = document.querySelector('.expected-element')\nel?.classList.add('selected')\nel?.setAttribute('title', 'hello')\n```\n\n[op]: https://github.com/tc39/proposal-optional-chaining\n\n## Development\n\n```\nnpm install\nnpm test\n```\n\n## License\n\nDistributed under the MIT license. See LICENSE for details.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgraham%2Fquery-selectors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdgraham%2Fquery-selectors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgraham%2Fquery-selectors/lists"}