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!

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.

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 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 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 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"]);

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,
});

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.