0.5.10 • Published 7 years ago

@mojule/tree-factory v0.5.10

Weekly downloads
2
License
MIT
Repository
github
Last release
7 years ago

tree-factory

Generates a tree API over whatever tree-like backing data you may have.

If you don't have a specific tree structure that you'd like to use and just want to stick data into a tree and get the benefits of tree-factory's common API without having to write adapters, plugins etc. consider using the tree package instead

The API returned by the factory supports the following methods, documented more extensively later in this document:

const api = {
  // adapter
  isNode, isValue, createRawNode, getChildren, getValue, setValue, remove, add,

  // miscellaneous
  get, getRoot, map, clone,

  // traversal
  ancestors, childAt, closest, descendants, firstChild, getParent, lastChild,
  nextSibling, previousSibling, siblings, walk, walkUp,

  // query
  accepts, atPath, contains, find, findAll, getMeta, getPath, hasChild,
  hasChildren, index, isEmpty, meta, nodeType, setMeta, slug,

  // manipulation
  append, empty, insertAfter, insertAt, insertBefore, prepend, prune,
  removeAt, replaceChild, unwrap, value, wrap
}

Usage

npm install @mojule/tree-factory

const TreeFactory = require( '@mojule/tree-factory' )

// see below for information on how adapters work and how to create them
const adapter = require( './path/to/your/adapter' )
const plugins = require( './path/to/your/plugins' )

const MyTree = TreeFactory( adapter, plugins )

const root = MyTree( 'Root' )
const child = MyTree( 'Child' )

root.add( child )

About

It takes an adapter, which tells the factory how to use your backing data as a tree, then adds a bunch of commonly used functions over the top, for doing things like querying the data, traversing the data and manipulating the data, then any custom functions you may want to provide. Your custom functions can also extend or override the default behaviours. The ability to map from one type of tree to another is also provided.

The common functions are modelled on the DOM/jQuery, but they are suitable for working with any tree-like structure, not just DOM elements - for example, all of the following can be treated as trees:

  • Various markup languages, HTML, XML etc.
  • Concrete implementations of HTML, eg DOM and virtual DOM nodes
  • Objects without circular references, eg JSON-compatible objects
  • Natural language texts
  • The directory structure of a file system
  • Code (as parse trees, ASTs etc)
  • Decision trees, like some flowcharts

Basic terminology

Nodes in our trees have two important properties - their value, and their children.

The value is any information that you need to know about a node.

The children is a list of nodes that descend from the current node.

We call the underlying representation of a node in your backing data a raw node

We use node to refer to a object that wraps your raw data in useful functions, such as getting or setting the node's value, adding new children etc.

value

For very simple tree data, a string may suffice as the value, as in this biology tree example

A more complex value, like for HTML nodes, might be an object something like this:

const value = {
  nodeName: 'div',
  nodeType: 'element',
  attributes: {
    id: 'myDiv'
  }
}

children

Regardless of how the children are actually stored in your raw data, the API expects to always work with an array. This is covered further in the Adapter section below.

raw node

A raw node will vary according to your backing structure - let's use the case described above of a biology tree, with the value as a string

A raw node described as an object might look like this:

{
  value: 'Animalia',
  children: [
    {
      value: 'Chordate',
      children: [] // omitted for brevity
    },
    {
      value: 'Arthropoda',
      children: [] // omitted for brevity
    }
  ]
}

A raw node described as an array might look like this:

[
  'Animalia',
  [
    'Chordate'
  ],
  [
    'Arthropoda'
  ]
]

A raw node in text format might look like this:

Animalia
  Chordate
  Arthropoda

node

Finally, a node as created by the API would be used like this:

const animalia = Tree( 'Animalia' )
const chordate = Tree( 'Chordate' )
const arthropoda = Tree( 'Arthropoda' )

animalia.add( chordate )
animalia.add( arthropoda )

// Animalia
console.log( animalia.getValue() )

/*
Chordate
Arthropoda
*/
animalia.getChildren().forEach( child => console.log( child.getValue() ) )

Adapters

An adapter is a function that takes the node API and the current node's state (the raw node), and returns a bag of functions, which is an object where each key is the name of the function and the property is the function itself:

const adapter = ( api, state ) => {
  // implement functions here

  return {
    $isNode, $isValue, $createRawNode, getChildren, getValue, setValue, remove, add
  }
}

For examples of adapters, please see the test fixtures, which include raw tree data for the biology example represented as both arrays and objects, and the adapters for working with either format

The $prefix means that the function is static and will be available on your tree API, not just node instances.

If you want a better understanding of why this pattern is used (it is used by tree-factory for both adapters and plugins), please read the documentation for api-factory, on which tree-factory is built.

Adapter functions

Adapters must implement the following functions:

$isNode, $isValue, $createRawNode, getChildren, getValue, setValue, remove, add

The signatures for these are as follows, using similar syntax to typescript or rtype.

RawNode refers to the raw underlying node data

$isNode( rawNode:Any ) => isNode:Boolean

$isValue( value:Any ) => isValue:Boolean

$createRawNode( value:Any ) => rawNode:RawNode

getChildren() => childNodes:[RawNode]

getValue() => value:Any

setValue( value:Any ) => value:Any

remove( rawChild:RawNode ) => removedChild:RawNode

add( rawChild:RawNode, reference?:RawNode ) => addedChild:RawNode

Notes

$isNode, $isValue and $createRawNode are treated as static functions, that is, they shouldn't depend on the state passed to your adapter

add called with a single argument should add the new child to the end of the child node list, but if called with a second reference argument, should insert the new child before the reference node.

Adapter wrapper

Once you provide your adapter, it will be wrapped in such a way that other functions in the API that work with it can pass it wrapped API nodes and get the same back - this abstracts away the underlying raw implementation to make implementing the various functions much safer and easier.

It also wraps remove to allow calling remove on a node with no child argument, which instead removes the node from it's parent's children - it will throw if you try and do this with the root node, as it doesn't have a parent.

Common functions

These categories are a bit arbitrary - while some functions clearly fit into one or the other, others were placed where they are by gut feel alone.

const common = {
  // miscellaneous
  get, getRoot, map, clone,

  // traversal
  ancestors, childAt, closest, descendants, firstChild, getParent, hasAncestor,
  lastChild, nextSibling, previousSibling, siblings, walk, walkUp,

  // query
  accepts, atPath, contains, find, findAll, getMeta, getPath, hasChild,
  hasChildren, index, isEmpty, meta, nodeType, setMeta, slug,

  // manipulation
  append, empty, insertAfter, insertAt, insertBefore, prepend, prune,
  removeAt, replaceChild, unwrap, value, wrap
}

Miscellaneous

get

Gets the underlying raw node

const rawNode = node.get()

getRoot

Gets the node at the root of the tree

const root = node.getRoot()

map

Maps the tree from one type of tree to another - takes an optional value mapper if the value expected by the target tree differs from that of the source tree

const Tree1 = require( './path/to/implementation' )
const Tree2 = require( './path/to/implementation' )

const tree1RawData = require( './path/to/data.json' )

// our first imaginary tree's value type is just a string, our second imaginary
// tree expects an object with the string in a 'text' property
const mapper = value => ({ text: value })

const tree1Root = Tree1( tree1RawData )

const tree2Root = tree1Root.map( Tree2, mapper )

clone

Creates a true clone of the node and its children - true in the sense that the underlying value of the cloned node will not === the original node, changes to either will not affect the other etc.

Only use if your value is JSON serializable! Anything else will throw, as it works behind the scenes by roundtripping JSON.stringify/JSON.parse

const cloned = node.clone()

Traversal

ancestors

Returns an array of the node's parent, the parent's parent etc. all the way to the root. If called on the root node, returns an empty array

const ancestors = node.ancestors()

childAt

Returns the child at the specified index

const second = node.childAt( 2 )

closest

Finds the closest ancestor matching the predicate, or undefined if no node is found

const divAncestor = node.closest( n => n.nodeName() === 'div' )

descendants

Returns an array of all descendant nodes, or an empty array if the node has no children

const descendants = node.descendants()

firstChild

Gets the first child of the node, or undefined if the node has no children

const first = node.firstChild()

getParent

Gets the parent of the current node, or undefined if the node is the root

const parent = node.getParent()

hasAncestor

Returns true if the given node is an ancestor of the current node, or false if not

const has = node.hasAncestor( otherNode )

lastChild

Gets the last child of the node, or undefined if the node has no children

const last = node.lastChild()

nextSibling

Gets the node in the parent's children after the current node, or undefined if the current node is the last node

const next = node.nextSibling()

previousSibling

Gets the node in the parent's children before the current node, or undefined if the current node is the last node

const previous = node.previousSibling()

siblings

Gets all of the parent's children excluding the current node

const siblings = node.siblings()

walk

Does a depth first traversal from the current node, calling the supplied callback for each node including the start node. If your callback returns a truthy value at any point the walk will be terminated.

The callback is passed the current node, its parent, and the current depth in the tree (where the root is 0)

node.walk( ( current, parent, depth ) => {
  console.log( current.getValue() )

  return depth > 5
})

walkUp

Walks up the tree from the current node, then to its parent, then the parent's parent etc until it reaches the root, or until your callback returns a truthy value

node.walkUp( current => {
  const value = current.getValue()

  console.log( value )

  return value.nodeName === 'body'
})

Query

accepts

Returns a boolean indicating whether the current node will accept the given node as a child

Also used by the wrapper around add to throw an error if an unacceptable child is added

The default implementation just returns the result of isEmpty, which in the default implementation always returns false

This is provided as a stub that you can override with a plugin, using knowledge about your specific tree, eg text nodes in the DOM can't accept children, UL nodes can only accept LI nodes etc.

if( !node.accepts( child ) ){
  console.log( 'Drat' )
}

atPath

Returns the node in the tree at the specified path. The path is parsed by splitting on the provided separator if provided, or the default separator of '/' if not provided, then walking down from the root and finding the child with a matching slug (see slug below)

const target1 = root.atPath( '/0/4/1/1/0' )

const target2 = root.atPath( '.0.4.1.1.0', '.' )

contains

Returns a boolean indicating whether the node or any of its descendants matches the predicate

const hasText = node.contains( n => n.nodeType() === 'text' )

find

Starting from and including the current node and walking down its descendants, find the first node that matches the predicate

const text = node.find( n => n.nodeType() === 'text' )

findAll

Starting from and including the current node and walking down its descendants, find all nodes that matches the predicate and return them in an array

const textNodes = node.findAll( n => n.nodeType() === 'text' )

getMeta

Get previously stored metadata about a node - see setMeta. Metadata is persisted for the lifetime of the node API, but not stored anywhere in the raw data. Useful for things like displaying a tree in the browser where the user can expand or collapse nodes visually.

if( node.getMeta( 'collapsed' ) ){
  console.log( '' )
}

getPath

Get a string representing the path to the current node - the path is generated by walking down to the node from the root and joining each node's slug with the passed in seperator, or '/' if no seperator is defined

const path1 = node.getPath()
const path2 = node.getPath( '.' )

hasChild

Returns a boolean indicating if the current node has a child matching the predicate

if( node.hasChild( n => n.getValue() === 'Dang' ) ){
  console.log( 'Drat' )
}

hasChildren

Returns a boolean indicating whether the current node has any children, eg the length of its children is > 0

Not the same as whether or not a node can have children - see isEmpty

if( !node.hasChildren() ){
  console.log( 'So sad' )
}

index

Returns the index of the node within its parent's child list

const index = node.index()

isEmpty

Returns a boolean indicating whether a node is not allowed to have children.

The default implementation always returns false, all nodes can have children.

Intended to be overridden by implementations that have leaf-only nodes, like a text node in HTML for example.

Named to align with the definition of empty nodes in HTML

if( node.isEmpty() ){
  console.log( 'Too bad' )
}

meta

Convenience function over getMeta and setMeta that does several things depending on the shape of its arguments.

Get all metadata associated with this node - note that this is the same object used behind the scenes, so manipulating the returned value will affect the underlying metadata

const meta = node.meta()

Set all metadata associated with this node - node that this uses Object.assign under the hood, so any properties omitted in the object passed to meta will retain their current values. Returns all metadata associated with the node.

const newMeta = node.meta( { collapsed: true } )

Get the metadata for a named property, same as getMeta:

const isCollapsed = node.meta( 'collapsed' )

Set the metadata for a named property, same as setMeta:

node.meta( 'collapsed', true )

nodeType

Returns a string representing the type of the node. The default implementation always returns 'node', intended as a stub that can be overridden by implementations

console.log( node.nodeType() )

setMeta

Sets some metadata for a node - see getMeta for more information on metadata.

node.setMeta( 'collapsed', true )

slug

Returns a string that identifies a node uniquely amongst its siblings. By default it returns the node's index within its parent, or if called on the root, and empty string.

const slug = node.slug()

Manipulation

append

An alias for add, but won't accept a reference argument. Adds the child to the end of the node's child list.

node.append( child )

empty

Removes all of a node's children

node.empty()

insertAfter

Insert the child after the reference node in the node's children

node.insertAfter( child, reference )

insertAt

Inserts the child at the specified index within the node's children

node.insertAt( child, 1 )

insertBefore

An alias for add. Inserts the child into the node's children before the reference node.

node.insertBefore( child, reference )

prepend

Adds the child to the beginning of the node's child list

node.prepend( child )

prune

Searches downwards from and including the current node for all nodes that match the predicate, and removes them from the tree

node.prune( n => n.getValue() === 'delet this' )

removeAt

Removes the child at the specified index

node.removeAt( 4 )

replaceChild

Replaces the old child with a new one, at the same position in the child list

node.replaceChild( child, old )

unwrap

Removes the current node, while moving all of its children to the position in the parent's child list previously occupied by the current node

node.unwrap()

value

Convenience method over getValue and setValue. If called with no arguments, the same as getValue. If called with an argument, the same as setValue

const value = node.value()

value.text = 'Hi'

node.value( value )

wrap

Adds the current node to the wrapper node, then replaces the current node in its parent's child list with the wrapper node

node.wrap( wrapper )

Tree

The function returned by the tree-factory.

Takes either a raw node or a value, and returns a node API, with the wrapped node set as the root of the tree.

If you add one of these wrapped nodes to another tree, they'll be demoted from root nodes to ordinary nodes.

const root = Tree( 'Root' )
const child = Tree( 'Child' )

root.add( child )

Also has the static methods defined in the adapter attached:

console.log( Tree.isValue( 'Root' ) )
console.log( Tree.isNode( [ 'Root' ] ) )
0.5.10

7 years ago

0.5.8

7 years ago

0.5.7

7 years ago

0.5.6

7 years ago

0.5.5

7 years ago

0.5.3

7 years ago

0.5.2

7 years ago

0.5.1

7 years ago

0.4.1

7 years ago

0.3.5

7 years ago

0.3.4

7 years ago

0.3.3

7 years ago

0.3.1

7 years ago

0.3.0

7 years ago

0.2.2

7 years ago

0.2.1

7 years ago

0.2.0

7 years ago