A portable DOM for the web and Node.js.

tagr, portable DOM manipulation


The DOM is a messy place. Tagr is a concise interface to the DOM: work with only elements and strings, and manipulate their events, styles, and properties. Tagr is not a "rapid" web development library, but instead a solid foundation for manipulating HTML with a consistent API on which other tools can be built.

Tagr's biggest advantage is portability: Like a <canvas> context, tagr creates HTML context where all styles and events are relative and sandboxed. Documents can be built procedurally, or built with JSON or HTML. You can even construct entire widgets on the server and send them to the client, serializing them as HTML to take advantage of fast parsing times.

Tagr is a small library, minified (15kb) and gzipped (5kb). Supports IE6+, Firefox 3.5+, Chrome, Safari, and Opera.

  • Sizzle — Including this adds support for complex CSS3 queries using the Sizzle engine.
  • selection.js — Including this enables the Selection API.
  • store.js by marcuswestin — Easy client-side storage.

Elements are instantiated by tagr.create(), and text are simple strings.

> var div = tagr.create('div.alert');
> div.push('Hello world!')
> div.toHTML()

<div class="alert">Hello world!</div>

You can manipulate or index children like an array:

> div[0]

"Hello world!"

> div.splice(0, 1, 'Such ', tagr.create('b', {}, 'bold'), ' moves!')
> div.toJSON()

['div', {}, 'Such ', ['b', {}, 'bold'], ' moves!']

Attach elements to a document by creating a context, from an existing element or writing one directly:

> tagr.getContext(document.body)
> tagr.writeContext('div#contents', {}, 'Great for inline widgets!')

You can create elements with the method tagr.create(), or parse a JSON tree or HTML with tagr.parse() to return a TagrElement.

  • tagr.create([tag[, properties[, children...]]) returns TagrElement — Create a new TagrElement with the given tag and properties, and optionally children.
  • tagr.parse(json) returns TagrElement — Parses a JSON structure or HTML string into a TagrElement.

Your entry point to the DOM is a Tagr "context". The DOM inside a context is mirrored by Tagr and must only be modified with Tagr commands. You can either load an existing element and its children, or write one into the DOM directly:

  • tagr.getContext(node) returns TagrElement — Get a Tagr context for the given DOM node.
  • tagr.writeContext(tag[, properties[, children...]]) returns TagrElementdocument.write a TagrElement and immediately return it. Useful for self-contained scripts.

And the obligatory DOMContentLoaded polyfill:

  • tagr.ready(callback) — Trigger the given callback once the DOM is (or has already) finished loading.

A Tagr element object can be created using tagr.create() or parsing JSON or HTML using tagr.parse(). It has the following read-only properties:

  • el.tag
  • el.id
  • el.parent

Tagr includes a classList property, similar to the DOM property, which lets you add or remove classes easily.

  • el.classList.contains(key)
  • el.classList.add(key)
  • el.classList.remove(key)
  • el.classList.toggle(key)

Tagr supports modifying built-in DOM properties using .get(key) and .set(key, value) methods, similar to the Backbone.js API.

  • el.get(key)
  • el.set(key, value | properties) returns el
  • el.remove(key)
  • el.call(key, arguments...)
  • el.properties() returns map

When a property is set on an element, a change:[property name] event is emitted on the element. This includes when the user modifies an input field, so .on('change:value', function () { ... }) works as advertised for inputs and textareas!

Only properties and attributes that are unique to an element are exposed. For instance, you can modify the href of a link or the currentTime of a video, but parentNode and other DOM manipulation methods are not exposed. In this way, you can leverage the HTML JavaScript API without inadvertantly modifying the DOM.

Custom properties are allowed on elements, and provide a powerful way to create widgets:

> ctx.query('.progress').on('change:level', function () { /* set progress bar level */ })
> var prog = tagr.create('.progress', {level: 50})
> prog.set('level', 75) // etc

Custom properties are kept separate from built-in HTML attributes, and are serialized as data-* attributes to HTML and JSON. See Serialization below.

You can manipulate an element's styles and individual style rules simply:

  • el.style(key, value | styles) returns el
  • el.addRule(key, value)
  • el.removeRule(key)
  • el.rules() returns map

Tagr elements support the familiar EventEmitter API from Node.js. All listeners are attached to the DOM element themselves:

  • el.on(type, callback) returns el
  • el.once(type, callback) returns el
  • el.addListener(type, callback) returns el
  • el.removeListener(type)
  • el.listeners([type])
  • el.removeAllListeners()
  • el.emit(type, args...)
  • el.setMaxListeners(count)

Note that Tagr does not have a mechanism to propagate an event to itself and its ancestors. (Though maybe it should.)

Tagr elements act like arrays, and inherit the array prototype. You can iterate over them by index, though array access is read-only:

  • el.length
  • el[0...length]

Tagr elements implement array methods to manipulate and all elements and text in-place. You can pass in other tagr elements or raw strings as children. Whenever an element is attached to a parent, an attached event is fired on the element, and vice versa for detached. This can be used to dynamically enable behaviors for elements.

These methods manipulate an element's children:

  • el.push(children...)
  • el.pop() returns object
  • el.unshift(children...)
  • el.shift() returns object
  • el.splice(index, deleteCount, insert...)
  • el.sort([compare])
  • el.reverse()

In addition, Tagr provides all ES5 array utility methods and supports slice, sort, reverse, indexOf, forEach, map, every, lastIndexOf, filter, some, reduce, and reduceRight. Note that arrays returned by these methods are not instances of TagrElement.

Tagr also provides its own chainable methods to manipulate an element's children in-place:

  • el.insert(index, children...) returns el — Inserts the children at the given index. The index can be negative; -1 inserts at the end of the array.
  • el.remove(index[, count]) returns el — Removes children at the given index. The index can be negative; -1 removes from the end of the array.
  • el.children(_children...) returns el — Overwrites all children with the given list.

As well as methods to manipulate a child itself:

  • el.insertSelf(parent, index) returns el
  • el.removeSelf() returns el
  • el.indexOfSelf() returns el

Since text nodes are represented in Tagr as regular strings, Tagr elements provide methods to split or splice text directly:

  • el.spliceText(index, spliceIndex, deleteCount, insertString)
  • el.splitText(index, splitIndex)

A TagrQuery can be generated relative to any Tagr element:

  • el.query(selector) returns TagrQuery

See the API for TagrQuery below. As an additional shorthand on elements, you can directly request the first or an array of elements matching a query selector.

  • el.find(selector) returns TagrElement — Shorthand for el.select(selector).findAll().
  • el.findAll(selector) returns [TagrElement] — Shorthand for el.select(selector).findAll().

A TagrElement can be serialized to valid JSON or HTML. (The preferred serialization format is JSON.) Either format can be reconstructed as a tree using tagr.parse().

  • el.toJSON() returns Object — Returns a JSONML formatted tree.
  • el.toHTML() returns String

By default, properties are not persisted as JSON or HTML unless they are built-in element attributes (like value, src, rowspan, etc.) You can define that custom attributes be persisted or HTML elements should not be persisted by specifying them manually:

  • el.persist(name)
  • el.stopPersisting(name)

If a custom property is persisted, it is serialized to JSON and set as a data attribute (prefixed with data-). These attributes are properly read by tagr.parse() and the data- prefix is removed.

Tagr is for web apps, so a few convenience functions are included to get you started:

  • el.useWhitespace(toggle = true) — Sets that whitespace should be significant. Since you aren't dealing with a markup language, you can have your non-breaking cake and eat it, too.
  • el.setSelectable(toggle = true) — Sets that element text should not be selectable by the cursor.

A Tagr query encapsulates a base element and a selector to match elements below it. A query can be created using TagrElement::query(), and the query contains a reference to the base element:

  • query.base — The Element context for this TagrQuery.

Many of the same actions you can perform on a TagrElement can be done for a dynamic query. You can listen for and emit events, or create styles and rules:

> ctx.query('.tooltip').on('mouseover', function () { /* display tooltip */ })
> ctx.query('.btn').style({'background': 'blue'})

Additionally, at any point you can return a static array of elements which match the query at the time it was invoked:

  • query.find() returns TagrElement
  • query.findAll() returns [TagrElement]

(This is a work in progress.)

  • tagr.view.getBox(element[, relativeTo]) returns Rectangle — Get the bounding box of the element, with properties width, height, top, right, bottom, left. If an element is provided for relativeTo, these coordinates are relative to this other element.
  • tagr.view.getStyle(element, key) — Gets the computed CSS value for key of element.

The selection API is available when selection.js is included. All elements are TagrElements:

  • tagr.view.selection.has(window)
  • tagr.view.selection.getOrigin(window)
  • tagr.view.selection.getFocus(window)
  • tagr.view.selection.getStart(window)
  • tagr.view.selection.getEnd(window)
  • tagr.view.selection.set(window, origin, focus)