Quick Start

A-DOM is alpha software. This documentation is a work in progress, but it is comprehensive.

Create a project boilerplate using A-DOM's built-in backend router

npx adom-js create my-app

A-DOM is extremely flexible and can be used with any backend. Use the following command to create the same boilerplate but using Express instead

npx adom-js create my-app --express

And for the smallest possible boilerplate, use this command

npx adom-js create my-app --lean

Syntax

A-DOM syntax is extremely terse but it is not whitespace sensitive. It's a much less shift-y version of HTML. You simply drop the angle brackets, and put children between square brackets. Tags without children as well as void tags are ended with an empty set of square brackets.

form method='POST' action='/user' [
  input name='first' []
  input name='last' []
  input type='submit' value='submit' []
]

Textnodes go inside strings.

a href='/' [ 'home' ]

If a textnode is the only child, the brackets may be omitted.

a href='/' 'home'

Remember, there is no whitespace sensitivity, so the following is equally valid.

a href='/'
  'home'

A-DOM supports class shorthand, as seen in other templating languages.

a.btn href='/' 'home'

Of course, you can use the regular class attribute.

a class='btn' href='/' 'home'

A-DOM supports double-quoted string, single-quoted strings, and backtick strings. All strings may have interpolations.

html [
  head []
  body [
    p `
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec fermentum arcu.
    Aliquam vel ullamcorper ipsum. Nullam euismod nisl vel a tristique odio luctus.
    Integer dapibus nec odio at lacinia. Pellentesque euismod id odio nec hendrerit.
    `
  ]
]

Backtick strings give you total whitespace control when using them with whitespace sensitive tags like code or pre

html [
  head []
  body [
    code `
    const foo = require('foo')

    foo()
    `
  ]
]

This prints the following block

const foo = require('foo')

foo()

What's happening here is that the first line is being ignored because it contains only a newline. And the second backtick, the one on the bottom, is controlling how much whitespace is to the left of the code. Look at the next example where the bottom backtick is moved over two spaces

html [
  head []
  body [
    code `
    const foo = require('foo')

    foo()
  `
  ]
]

This produces the following block

  const foo = require('foo')

  foo()

Document

An A-DOM document is complete and doesn't require some sort HTML bootloader file. The following is a complete program.

html [
  head []
  body [
    h1 "Hello from A-DOM"
  ]
]

Data

What follows is not Javascript. It is purely static A-DOM data declaration. This is one of the key differences between A-DOM and the rest. A-DOM's opinion is that the document data, (what React calls "state"), belongs to the markup, and not the Javascript. A-DOM sees Javascript as an extension that allows for interactivity.

let name = 'Matt'

html [
  head []
  body [
    h1 "Hello, {{name}}"
  ]
]

A-DOM supports strings, numbers, arrays, objects, boolean, and null, just like JSON.

let colors = [
  'red',
  'blue',
  'green'
]

let person = {
  name: 'Matt',
  age: 33
}

html [
  head []
  body [
    h1 "Hello, {{person.name}}"
  ]
]

A-DOM supports the following operators

+ - / * % ?:
let count = 4 * (2 + 3)

let many = count > 10 ? true : false

Arrays and objects can be accessed using expressions, as you might expect

let person = {
  name: 'Matt',
  age: 33
}

let age = person.age
let name = person['nam' + 'e']

Expressions can be interpolated into strings using double curly braces.

let count = 4 * (2 + 3)

html [
  head []
  body [
    p '{{ count > 10 ? "many" : "not many" }}'
  ]
]

Expressions can be passed into attributes using a single set of curly braces.

let link = '/'
let text = 'home'

html [
  head []
  body [
    a href={link} '{{text}}'
  ]
]

Once an A-DOM variable is declared it cannot be reassigned, except using Javascript (more on that later). The following is not valid.

let name = 'Matt'

name = 'Bob' // this is illegal

html [
  head []
  body [
    h1 'Hello {{name}}'
  ]
]

For data transformations, A-DOM has the pipe operator |

let name = 'matt' | toupper

let nums = 1 | repeat 10 // creates an array of 10 1s

let length = nums | length // 10

html [
  head []
  body [
    h1 'Hello {{name}}' // Hello MATT
  ]
]

toupper

Makes a string uppercase

let name = 'hello' | toupper

tolower

Makes a string lowercase

let name = 'HELLO' | tolower

length

Returns the length of the input

let len = x | length

repeat n

Repeats the input N times. All data types are supported, and will be repeated. Even large complex objects.

let nums = 1 | repeat 10 // 10 1s
let x = {} | repeat 100 // 100 empty objects

map x

Maps an array to an expression. The expression has 2 implicit arguments: _a, and _b which represent the value and the index respectively. This idea was borrowed from the Lobster programming language.

let people = [{
  name: 'Frodo'
}, {
  name: 'Sam'
}]
let names = people | map _a.name
// ['Frodo', 'Sam']

Here is an example of map being used in conjunction with repeat

let nums = 1 | repeat 5 | map _a + _b
// [1, 2, 3, 4, 5]

The equivalent expression in javascript would look like this

let nums = (new Array(5)).fill(1).map((_a, _b) => _a +_b);

filter x

Filters an array against an expression that returns true or false. The expression has 2 implicit arguments: _a, and _b which represent the value and the index respectively

let nums = 0 | repeat 10 | map _a + _b | filter _b % 2 == 0
// [0, 2, 4, 6, 8]

split x

Splits a string at a given delimeter into an array

let greet = 'helloxworld' | split 'x'
// ['hello', 'world']

includes x

Checks whether or not an item is included in an array

let has4 = [1, 2, 3, 4, 5] | includes 4
// true

indexof x

Retrieves the index of an item in an array. Returns -1 if not found

let pos = [1, 2, 3, 4, 5] | indexof 4
// 3

reverse

Reverses a string or array

let nums = [1, 2, 3] | reverse
// [3, 2, 1]
let hello = 'hello' | reverse
// olleh

tostring

Converts any data type to a string, including arrays and objects

let person = { name: "matt" } | tostring 
// '{"name":"matt"}'

let num = 10 | tostring 
// '10'

todata

Converts a string of data into useable data

let person = '{"name":"matt"}' | todata
let number = '10' | todata
let thetruth = 'true' | todata

html [
  head[]
  body[
    h1 'Hello {{person.name}}'
  ]
]

replace x y

Replaces the first instance of X in a string with Y

let str = 'hello world' | replace 'd' 'd!'
// 'hello world!'

join x

Joins an array into a string using a delimeter

let x = ['hello', 'world'] | join ' '
// 'hello world'

keys

Create an array from the keys of an object

let person = {
  name: 'Frodo',
  age: 80
}
let keys = person | keys
// ['name', 'age']

values

Create an array from the values of an object

let person = {
  name: 'Frodo',
  age: 80
}
let vals = person | values
// ['Frodo', 80]

trim

Trim the whitespace off the end of a string

let str = '  Hello world   ' | trim
// 'Hello world'

slice x y

Create a slice from an array or string

let str = 'Hello world' | slice 0 5
// 'Hello'

rand x

Create a random floating point value between 0 and x

let val = 100 | rand 
// 85.312

MATH

ceil floor sin cos tan sqrt

A-DOM supports the following math pipes

let v0 = 1 | sin
let v1 = 1 | cos 
let v2 = 1 | tan

let v3 = 100 | sqrt
let v4 = 3.5 | ceil
let v5 = 4.5 | floor

let v6 = 100 | rand | floor
// In javascript, this would be equivalent to
//  Math.floor(Math.random() * 100)

Control Flow

In A-DOM, there are 3 control flow keywords: if else each

If statements evaluate a single expression. Brackets are used for control flow just like tag children.

let num = 10

html [
  head []
  body [
    if (num > 10) [
      p 'Number is greater than 10'
    ] else if (num > 5) [
      p 'Number is greater than 5'
    ] else [
      p 'Number is less than or equal to 5'
    ]
  ]
]

Each statements iterate over arrays and objects.

let num = [1, 2, 3, 4, 5]

html [
  head []
  body [
    // the i is optional, and is the index of the item
    each (n, i in nums) [
      p 'value: {{n}}'
      p 'index: {{i}}'
    ]
  ]
]

With objects, the first and second variables of the each statement are the key and value respectively

let person = {
  name: 'Frodo',
  age: 80,
  location: 'Shire'
}

html [
  head []
  body [
    each (k, v in person) [
      p '{{k}}: {{v}}'
    ]
  ]
]

Components

You create components in A-DOM with the tag keyword. Data that is local to a component must be declared within the component

// a component with a single button
tag MyButton [
  let text = 'click'
  button.btn '{{text}}'
]

html [
  head[]
  body[
    // use like any tag
    MyButton[]
  ]
] 

Components do not need to have a root element, and can simply be a list of elements

tag MyButtonList [
  let text = 'click'

  button.btn '{{text}}'
  button.btn '{{text}}'
  button.btn '{{text}}'
]

html [
  head[]
  body[
    MyButtonList[]
  ]
] 

Components can be moved to separate files at your discretion. You are free to have single file components or multi-file components. Components in separate files can be imported using the import keyword and exported using the export keyword

// buttons.adom
export tag BlueButton [
  let text = 'click'
  button.btn-blue '{{text}}'
]

export tag GreenButton [
  let text = 'click'
  button.btn-green '{{text}}'
]

// index.adom
// all exports are visible to this file
import 'buttons.adom'

html [
  head[]
  body[
    BlueButton[]
    GreenButton[]
  ]
] 

While simple, this import strategy may cause a name collision. To handle this, an imported module may be namespaced using the 'as' keyword

// index.adom
import 'buttons.adom' as btn

html [
  head[]
  body[
    btn::BlueButton[]
    btn::GreenButton[]
  ]
] 

Component props can be accessed using the props keyword. Props are passed to components as regular attributes

tag MyButton [
  button.btn '{{props.text}}'
]

html [
  head []
  body [
    MyButton text='click' []
  ]
]

Components can have children, and are displayed using the yield keyword

tag MySection [
  div.section-div [
    yield
  ]
]

html [
  head []
  body [
    MySection [
      p [
        'I am a child of .section-div'
      ]
    ]
  ]
]

Using yield is how common page layouts are achieved in A-DOM

// layout.adom
export tag Layout [
  html lang='en' [
    head [
      title '{{props.title}}'
    ]
    body [
      yield
    ]
  ]
]

// index.adom
import 'layout.adom'

Layout title='My Blog' [
  main [
    h1 "Welcome!"
  ]
]

Javascript

A-DOM documents can be made interactive by adding Javascript. This requires no additional setup. All Javascript in A-DOM goes between sets of dashes ---, or in event listener strings. The on: directive is used to attach event listeners to elements

tag Counter [
  // remember, this is not Javascript
  let count = 0
  
  // this part is Javascript
  // Javascript in A-DOM has the power to update template data directly
  ---
  function increment() {
    count++
  }
  ---

  // any valid browser event can follow the on: directive
  // the string following 'on:click' is also javascript
  button on:click='increment()' 'click count: {{count}}'
]

html [
  head []
  body [
    Counter[]
  ]
]

From within Javascript, there are 3 functions and 1 object to know about: $on $emit $sync and $e. And that's all. The Javascript experience in A-DOM doesn't require complex concepts and implicit contexts to learn about. It is simply contextualized properly so that you have access to your A-DOM data and the ability to update the document.

$sync()

This is the most important function call in your Javascript. This call updates the UI when your data changes. This call is analgous to setState in React, except that it's synchronous and takes no arguments. $sync() does NOT need to be called after event listeners, because it is called automatically.

// in this example, a counter is incremented every second
let count = 0
---
setInterval(() => {
  count++
  // sync needs to be called because this was not triggered by an event listener
  $sync()
}, 1000)
---
html [
  head []
  body [
    h1 "Count: {{count}}"
  ]
]

$sync() does not need to be called in the following example because increment() was called in an event listener.

tag Counter [
  let count = 0
  ---
  function increment() {
    count++
  }
  ---
  button on:click='increment()' 'click count: {{count}}'
]

html [
  head[]
  body[
    Counter[]
  ]
]

$e

This represents the event object within an event listener. There is nothing else to know about it. It is only available from within the event listener string

let name = 'Matt'

---
function setInput(e) {
  name = e.target.value
}
---

html [
  head []
  body [
    input on:input='setInput($e)' []
    h1 'Hello {{name}}'
  ]
]

$on(event, callback)

This function call allows you to listen for lifecycle events within components. The current list of events is: mount, unmount, change, render

tag MyComponent [
  ---
  $on('mount', () => {
    console.log('mounted')
  })

  $on('unmount', () => {
    console.log('unmounted')
  })

  $on('change', (oldProps) => {
    console.log('props are different than last time')
  })

  $on('prerender', () => {
    console.log('right before rendering')
  })

  $on('render', () => {
    console.log('finished rendering')
  })
  ---
]

html [
  head []
  body [
    MyComponent[]
  ]
]

$emit(event, data)

This function call allows components to emit events that parent components can listen for

tag Child [
  ---
  function myClick(e) {
    // emit a synthetic click event, and pass the object along
    $emit('myclick', e)
  }
  ---
  button on:click='myClick($e)' 'click'
]

tag Parent [
  // You simply use the on: directive on a component to listen for synthetic events
  Child on:myclick='alert($e)' []
]

html [
  head []
  body [
    Parent[]
  ]
]

Usage

adom.compile()

Fundamentally, A-DOM is a library that takes in an A-DOM source tree and outputs a single HTML file or string.

const adom = require('adom-js');

adom.compile({
  input: 'src/index.adom',
  output: 'public/index.html'
});

No other step is required for a fully reactive, bundled, minified application.

This function may also be used to serve dynamic content in real time.

const express = require('express');
const adom = require('adom-js');

const app = express();

app.get('/', async (req, res) => {
  const html = await adom.compile({
    input: 'src/index.adom'
  });
  res.end(html);
});

app.listen(3838);

In order for the above example to have production quality speed, you must enable caching. And if desired, Javascript minification can be enabled as well

const html = await adom.compile({
  input: 'src/index.adom',
  cache: true,
  minify: true
});

The last thing to know about A-DOM's compile function is document data.

const html = await adom.compile({
  input: 'src/about.adom',
  data: {
    pageTitle: 'About me'
  }
});

In your A-DOM document pageTitle can be found on the global 'data' object

// index.adom
html [
  head []
  body [
    h1 '{{data.pageTitle}}'
  ]
]

adom.app()

As mentioned above, A-DOM ships with a built-in backend router. This router is exposed through the 'app' function, which returns a Node.js HTTP request handler

const http = require('http');
const adom = require('adom-js');

const app = adom.app({
  publicDir: './public',
  cache: true,
  minify: true
})

app.get('/', {
  input: 'src/index.adom',
});

app.get('/', {
  input: 'src/about.adom',
  data: {
    pageTitle: 'About Me'
  }
});

http.createServer(app).listen(5000);

A-DOM's router is also completely compatible with Express middleware

const http = require('http');
const adom = require('adom-js');

const app = adom.app({
  publicDir: './public',
  cache: true,
  minify: true
})

app.get('/', (req, res, next) => {
  console.log('middleware')
  next();
});

app.get('/', {
  input: 'src/home.adom',
});

http.createServer(app).listen(5000);

req.params

A-DOM supports URL paramters

app.get('/blog/:post_id', {
  input: 'src/blog.adom',
  // when using adom.app(), data can either be an object or an async function that returns an object
  data: async (req) => {
    return await getPost(req.params.post_id)
  }
});

Wildcards are also supported

// app.route() will route to any request method
app.route('*', {
  input: 'index.adom'
});

Wildcards can be used with named parameters

// path will be the remainder of the URL
app.route('/page/:path*', 'index.adom');

req.query

If there are query string parameters present in the URL, they will be parsed and available

// GET /blog?post_id=123
app.get('/blog', {
  input: 'src/blog.adom',
  data: async (req) => {
    return await getPost(req.query.post_id)
  }
});

req.body

If there is a JSON encoded, or query string encoded request body, it will be parsed and available

app.post('/login', (req, res) => {
  const { user, pass } = req.body;
  // use credentials
});

adom.serve()

The last function is for when you have minimal backend needs, or possibly no backend needs and you need a simple dev server

const adom = require('adom-js');

adom.serve({
  publicDir: './public',
  port: 3838,
  routes: {
    '/': {
      input: 'index.adom'
    }
  }
});