Skip to content

laserware/dominator

Repository files navigation

Dominator

Better ergonomics for working with the DOM and TypeScript.

See documentation if you want to get right to the API. For a high-level overview of the library, keep reading!

Examples

Let's face it, working with the DOM API kind of sucks (especially with TypeScript). This library aims to make it a little less painful.

The following examples compare the use of DOM APIs with this library.

Creating Elements

Instead of creating an Element with the DOM APIs:

const div = document.createElement("div");
div.id = "example";
div.ariaLabel = "Hello!";

You can use the createElement function:

import { createElement } from "@laserware/dominator";

const div = createElement("div", { id: "example", ariaLabel: "Hello!" });

Finding Elements

Finding a single element or multiple elements in the DOM isn't that bad.

Assuming you have this HTML:

<div role="grid" aria-colcount="6">
  <div role="rowgroup">
    <div role="row">
      <div role="columnheader" aria-colindex="1">First name</div>
      <div role="columnheader" aria-colindex="2">Last name</div>
      <div role="columnheader" aria-colindex="5">City</div>
      <div role="columnheader" aria-colindex="6">Zip</div>
    </div>
  </div>
  <div role="rowgroup">
    <div role="row">
      <div role="gridcell" aria-colindex="1">Debra</div>
      <div role="gridcell" aria-colindex="2">Burks</div>
      <div role="gridcell" aria-colindex="5">New York</div>
      <div role="gridcell" aria-colindex="6">14127</div>
    </div>
  </div></div>

Here's a couple of ways you could find element(s) using the DOM APIs:

// Single element:
const firstHeaderRow = document.querySelector(`[role="row"] [role="columnheader"]:first-child`);

// Multiple elements in document:
const allGridCells = document.querySelectorAll(`[role="gridcell"]`);

// Finding all children in parent element:
const grid = document.querySelector("grid");
const gridRows = grid.querySelectorAll(`[role="row"]`);

But there are some issues. For one, document.querySelectorAll returns a NodeList, which are annoying to work with.

You can use findElement and findAllElements instead:

import { findElement, findAllElements } from "@laserware/dominator";

const firstHeaderColumn = findElement(
  `[role="row"] [role="columnheader"]:first-child`,
);

// Returns the grid cell elements as an array:
const allGridCells = findAllElements<"div">(`[role="gridcell"]`);

// You can use string selectors for the target and parent to get children:
const gridRows = findAllElements(`[role="row"]`, "grid");

Setting Attributes

Setting attributes with the DOM APIs has some drawbacks:

  1. You need to call setAttribute for each attribute you want to set.
  2. You have to stringify each value before setting it.
const div = document.createElement("div");

div.setAttribute("role", "gridcell");

// Need to convert boolean to a string:
div.setAttribute("aria-disabled", "true");

// And do the same for numbers:
div.setAttribute("aria-colindex", "1");

You can use setAttribute or setAttributes instead. Both functions return the element:

import { 
  createElement,
  setAttribute,
  setAttributes, 
} from "@laserware/dominator";

let div = createElement("div");

// Set one attribute:
div = setAttribute(div, "role", "gridcell");

// Set multiple attributes:
div = setAttributes(div, {
  // You can just use a boolean, no need to stringify:
  "aria-disabled": true,
  // Same goes for numbers:
  "aria-colindex": 1,
});

Removing Attributes

Removing attributes using the DOM APIs requires that you remove each attribute individually.

Assuming you have this HTML:

<div id="example" role="gridcell" aria-disabled="true">Example</div>

Instead of using the element.removeAttribute API:

const div = document.getElementById("example");

div.removeAttribute("role");
div.removeAttribute("aria-disabled");

You can use removeAttribute or removeAttributes. Both functions return the element:

import {
  findElement,
  removeAttribute,
  removeAttributes,
} from "@laserware/dominator";

let div = findElement("#example")!;

div = removeAttribute(div, "role");

div = removeAttributes(div, ["aria-disabled", "aria-colindex"]);

Checking for Attributes

If you want to check for the existence of attributes using the DOM API, you'd use element.hasAttribute.

If you want to check if a value matches, you're stuck with the annoyances of getAttribute. So you're back to dealing with strings.

Assuming you have this HTML:

<div id="example" role="gridcell" aria-disabled="true" aria-colindex="1">Example</div>

Here's how you would check for attributes with the DOM APIs:

const div = document.getElementById("example");

const hasRole = div.hasAttribute("role");

const index = 1;
const isCol = div.getAttribute("aria-colindex") === index.toString();

You can use hasAttribute, hasAllAttributes, and hasSomeAttributes instead:

import { 
  findElement,
  hasAttribute,
  hasAllAttributes,
  hasSomeAttributes,
} from "@laserware/dominator";

const div = findElement("#example");

const hasRole = hasAttribute(div, "role");

// Check if any of the attributes are present:
const someArePresent = hasSomeAttributes(div, ["aria-colindex"]);

// Check if any of the attributes names and values match:
const someMatchValues = hasSomeAttributes(div, { "aria-colindex": 1 });

// Check if all of the attributes match, you can use `null` to
// check for the _existence_ of an attribute only:
const allMatch = hasAllAttributes(div, { 
  "aria-colindex": 1, 
  "aria-disabled": null,
});

Selecting Elements with Attributes

Building a CSS selector to find something based on attributes requires a lot of manual labor.

Assuming you have this HTML:

<div role="grid" aria-colcount="6">
  <div role="rowgroup">
    <div role="row">
      <div role="columnheader" aria-colindex="1">First name</div>
      <div role="columnheader" aria-colindex="2">Last name</div>
      <div role="columnheader" aria-colindex="5">City</div>
      <div role="columnheader" aria-colindex="6">Zip</div>
    </div>
  </div>
  <div role="rowgroup">
    <div role="row">
      <div role="gridcell" aria-colindex="1">Debra</div>
      <div role="gridcell" aria-colindex="2">Burks</div>
      <div role="gridcell" aria-colindex="5">New York</div>
      <div role="gridcell" aria-colindex="6">14127</div>
    </div>
  </div></div>

You'll need to write the selectors yourself:

const firstHeaderColumn = findElement(`[role="row"] [role="columnheader"]`);

const secondGridCell = findElement(`[role="gridcell"][aria-colindex="2"]`);

You can use selectAttribute and selectAttributes instead:

import { 
  findElement,
  selectAttribute,
  selectAttributes,
} from "@laserware/dominator";

const firstHeaderSelector = [
  selectAttribute("role", "row"),
  selectAttribute("role", "columnheader"),
].join(" ");

const firstHeaderColumn = findElement(firstHeaderSelector);

const secondGridCellSelector = selectAttributes({
  role: "gridcell",
  // Note that we're using a number:
  "aria-colindex": 2,
});

const secondGridCell = findElement(secondGridCellSelector);

You can do more than work with attributes. You can also set, get, remove, and select dataset entries, CSS variables, and styles.

About

Better ergonomics for working with the DOM and TypeScript.

Topics

Resources

License

Stars

Watchers

Forks