Reactive DOM

Neomacs maintain reactive DOMs based on lwcells. This enables observers and computed attributes to update in real-time depending on DOM content.

Nodes

This section documents the low-level node classes making up Neomacs's reactive DOM. Note that the interface here is low-level in the sense that text-node's are being exposed. The majority of Neomacs API hides text-node's as an implementation detail and the DOM conceptually consists of element's and character's.

Class: node inherits (standard-object)
Slot: parent
Slot: next-sibling
Slot: previous-sibling
Slot: host
Class: text-node inherits (node)
Slot: text
Class: element inherits (node)
Slot: first-child
Slot: last-child
Slot: tag-name
Slot: attributes
Slot: invisible-p
Function: element-p (object)
Function: text-node-p (object)
Function: tag-name-p (node tag-name)
Test if NODE is an element with TAG-NAME.
Standard generic function: clone-node (node &optional deep)
Clone NODE.
If DEEP is non-nil, recursively clone all descendant. DEEP defaults to T.
Function: child-nodes (node)
Return immediate child nodes of NODE as a list.
Function: children (node)
Return immediate child elements of NODE as a list.
Function: text-content (node)
Function: get-elements-by-class-name (node class)
Find all descendant elements of NODE with CLASS.

Traversing DOM

Function: do-dom (function node)
Call FUNCTION on every descendant of NODE in post-order. This includes elements and text-nodes. Returns NODE.
Function: do-elements (function node)
Like do-dom, but only call FUNCTION on elements. Returns NODE.
Function: next-node (node)
Next DOM node in pre-order traversal.
Function: previous-node (node)
Previous DOM node in pre-order traversal.

Attributes

In Neomacs, every attribute is backed by a cell. Other cells can depend on attribute value, and attribute values themselves can be computed and updated from other cell values. The following functions get and set attribute values or functions for compute them.

Function, setf-able: attribute (element name)
Function: set-attribute-function (element attribute function)
Set a computed attribute. Make ATTRIBUTE of ELEMENT computed by calling FUNCTION with ELEMENT as a single argument.

Attributes can have any name. Those named by strings are kept in sync with renderer-side attributes with the same names, i.e. changes in Lisp side are pushed to renderer (but not the other way). Those with non-string names (typically symbols) instead have no counterpart on renderer side.

Attribute related utility functions:

Function: add-class (element class)
Add CSS CLASS to ELEMENT.
Function: remove-class (element class)
Remove CSS CLASS from ELEMENT.
Function: class-p (node class &rest more-classes)
Test if NODE is an element of one of CSS CLASS or MORE-CLASSES.

Special Attributes

Some attribute name has special meaning:

Symbol read-only:
If value is non-nil, the immediate inner level of this element is read-only. No child elements or characters can be added or deleted from this element. The effect is not transitive, i.e. edits can still be made inside child elements.
Symbol keymap:
If value is non-nil, it should be a keymap and will be added to the buffer's active keymaps when buffer focus is inside this element. It takes precedence over any keymap attribute of outer elements, and over any mode keymaps. See looking up key bindings for more details about key binding look up process.

Low-level DOM edits

This section documents low-level primitives for modifying Lisp-side DOM. They are used to implement programmer-facing editing operations, see Editing primitives.

Function: insert-before (parent new-node reference)
Insert NEW-NODE under PARENT before REFERENCE.
If REFERENCE is nil, insert NEW-NODE as last child of PARENT. Returns NEW-NODE.
Function: append-child (parent new-node)
Insert NEW-NODE as last child of PARENT.
Returns NEW-NODE.
Function: append-children (parent children)
Insert CHILDREN as last children of PARENT.
Returns CHILDREN.
Function: remove-node (node)
Remove NODE from DOM tree.

Notes on DOM consistency

It is imperative that Lisp-side and renderer-side DOM are consistent. Otherwise, editing operations may behave erroneously and inconsistency can be propagated and amplified. All belts are off in such case (which can be recovered only by re-opening the buffer, or in some cases revert-buffer).

Normally, all programmer-facing editing operations (Editing primitives) maintain consistency, under the assumption that the requested editing operations only ever result in valid HTML. If attempts are made to create invalid HTML, the renderer will usually perform "fix-up"s to restore the DOM to valid HTML, but currently Neomacs has no way to know about these. Note that creating invalid HTML then fix them in with-post-command (Editing hooks) is not acceptable. Examples of operations that might shoot you in the foot:

If you are using Low-level DOM edits, or debugging implementation of editing primitives, take care to enforce the following consistency rules:

  • Lisp-side and renderer-side DOM must have exactly the same structure, i.e. element's and text-node's must 1-1 correspond. Parent, child and sibling (order included) relations must be exactly the same.
  • Length of corresponding text nodes must be exactly the same.
  • It is acceptable that corresponding elements have different tag-names.
  • It is acceptable that corresponding text nodes have different characters at the same offset.
  • It is acceptable that corresponding elements have different attribute values for an attribute named by a string on Lisp-side, but keep in mind that changes from Lisp-side will be pushed to renderer and overwrite its values.
  • It is totally ok that renderer-side elements have some attributes not known to Lisp-side. This is useful for internal bookkeeping done by JavaScript code which Lisp doesn't need to know. Lisp-side has similar ability by using attributes not named by strings.