art-object-tree-factory

Create Declarative frameworks that output tree structures with properties (e.g. ArtReact)

Usage no npm install needed!

<script type="module">
  import artObjectTreeFactory from 'https://cdn.skypack.dev/art-object-tree-factory';
</script>

README

Art.ObjectTreeFactory

simple, elegant and fast declarative tree generation

Fast, easy way to create declarative APIs for data-structures which consist of per-node-properties and per-node-ordered-children.

The Object Tree Factory

An object-tree-factory (OTF) is powerful tool for creating declarative programmatic structures or DSLs. OTFs are useful because they accept simplify a complex set of descriptive inputs into a simple set of outputs:

an arbitrary list of plain-objects, a function which accepts an arbitrary

<function>: objectTreeFactory = ->
  IN:
    Arguments are compacted and flattened
    The resulting list of arguments can be any combination of:
      plainObjects for props (merged in the order they appear)
      other objects which become the 'children'

  OUT:
    object-tree-node generated by the nodeFactory

Example ArtHtmlFactory

Object-tree-factories excel when used in a reduced-syntax language such as CaffeineScript (or CoffeeScript). For example, you can express HTML much more compactly:

# CaffeineScript
import &ArtHtmlFactory

console.log
  Html
    Head
      Meta name: "viewport"                               content: "user-scalable=no, width=device-width, initial-scale=1.0, viewport-fit=cover"
      Meta name: "apple-mobile-web-app-capable"           content: "yes"
      Meta name: "apple-mobile-web-app-status-bar-style"  content: "black"
      Meta name: "format-detection"                       content: "telephone=no"

    Body
      H1 "Art.ObjectTreeFactory"
      P "simple, elegant, fast declarative tree generation library"

Outputs:

<html>
  <head>
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, viewport-fit=cover">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
  </head>
  <body>
    <h1>Art.ObjectTreeFactory</h1>
    <p>simple, elegant, fast declarative tree generation library</p>
  </body>
</html>

Even in JavaScript, object-tree-factories are an effective way to express HTML programmatically:

import {Html, Head, Meta, Body, H1, P} from "art-html-factory"

console.log(
  Html(
    Head(
      Meta({
        name:     "viewport",
        content:  "user-scalable=no, width=device-width, initial-scale=1.0, viewport-fit=cover"
      }),
      Meta({ name: "apple-mobile-web-app-capable",          content: "yes"          }),
      Meta({ name: "apple-mobile-web-app-status-bar-style", content: "black"        }),
      Meta({ name: "format-detection",                      content: "telephone=no" })
    ),

    Body(
      H1("Art.ObjectTreeFactory"),
      P("simple, elegant, fast declarative tree generation library")
    )
  )
);

Example: React

You can easily create all your React factories:

# CaffeineScript
import &ArtStandardLib, &ArtObjectTreeFactory, &React

createObjectTreeFactories
  :Div :Link
  (nodeName, props, children) -> createElement nodeName, props, children...

Use:

# CaffeineScript
# React Function Component in
(props) ->
  Div
    "Everything you need "
    Link "is here" src: "http://wikipedia.org"

API

createObjectTreeFactory

Create one object-tree-factory.

createObjectTreeFactory = (...inputs) => objectTreeFactory

Note: createObjectTreeFactory's inputs can appear in any order:

  • nodeFactory:

    (props <Object>, children <Array>) => anything
    
  • nodeClass:

    class Foo {
      constructor(props <Object>, children <Array>) {}
    }
    
  • options: <Object>

    • inspectedName: string for introspection: Factory.getName() == inspectedName

    • class: alternative way to pass the nodeClass

    • bind: <String or Array<String>> list of method-names to bind from nodeClass onto the factory (note: nodeClass must be set)

    • mergePropsInto: (intoProps <Object>, fromProps <Object>) -> custom function to merge fromProps into intoProps. This function is only called if there are two or more props-objects. If so, it will be called for every props object passed into the factory. However, since it won't be called if there is exactly one props object, it should not be relied upon to preprocess props. Note: it's safe to mutate intoProps, but NOT safe to mutate fromProps.

    • preprocessElement: (inputElement, Factory) -> inputElement called on every input element, whether props, null, undefined or any other value. Note: you can return any value you wish, but don't mutate inputElement.

Example:

class TreeNode
  constructor: (@props, @children) ->
  toObjects: ->
    TreeNode: {}
      @props
      children: @children && array child in @children
        child.toObjects?() || child

Node = createObjectTreeFactory TreeNode

commonProps = color: "black"

a = Node
  commonProps
  height: "100"
  width: "200"

  "Does this work for you?"

  Node commonProps, source: "images/piglet.png"

  "This works for me!"

  Node "Ka-blam!"

b = TreeNode:
  props:
    color:  :black
    height: :100
    width:  :200

  children: []
    "Does this work for you?"

    TreeNode:
      props:
        color:  :black
        source: :images/piglet.png

      children: undefined

    "This works for me!"

    TreeNode: props: undefined, children: [] "Ka-blam!"

JSON.stringify(a.toObjects()) === JSON.stringify(b)

createObjectTreeFactories

Create many object-tree-factories:

createObjectTreeFactories = (...inputs) => factoryMap

note: createObjectTreeFactories is itself an object-tree-factory! That means you can pass in arguments in any order. Props (options) are merged down. Strings are parsed into words and concatenated.

IN:

  • options <Object>: passed directly to createObjectTreeFactory as options
  • nodeTypeNames <Strings> strings are parsed into words and concatenated
  • nodeFactory <(nodeTypeName, props, children) -> node> called each time a factory is used to construct the node
  • nodeFactoryFactory <(nodeTypeName) -> (props, children) -> node> called once for each nodeTypeName to generate a factory for that node-type

Required: You must have exactly one nodeFactory or one nodeFactoryFactory.

OUT:

  • factories <Object> is a map from nodeTypeNames (upperCamelCased) to factories returned from createObjectTreeFactory

Example:

# An example class
class TagNode
  constructor: (@tag, @props, @children) ->

  # Output an HTML string
  toString: (indent = '')->
    "<#{@tag}"
    + if @props
      ' ' +
        array v, k in @props
          "#{k}='#{v}'"
        .join ' '
    else ''
    + ">"
    + if @children
      indent2 = indent + '  '
      "\n" + indent2 +
        array child in @children
          child.toString indent2
        .join "\n#{indent2}"
      + "\n"
    else ''
    + "#{indent}</#{@tag}>"

##############################
 Create Object Tree Factories
##############################
{Html, Head, Body, Div, P, B} = createObjectTreeFactories
  :html :head :body :div :p :b
  TagNode

##########################
 Create and output a Tree
##########################
console.log Html Head Body
  Div
    class: "row"
    Div
      class: "col"
      P
        "This is truly "
        B "fantastic"
        "!"

    Div
      class: "col"
      P "What do you think?"

The output:

<html>
  <head>
    <body>
      <div class='row'>
        <div class='col'>
          <p>
            This is truly
            <b>
              fantastic
            </b>
            !
          </p>
        </div>
        <div class='col'>
          <p>
            What do you think?
          </p>
        </div>
      </div>
    </body>
  </head>
</html>