Functional Layout

Functional layout is a layout concept inspired by spreadsheets where the value of a cell can reference the value of another cell. For example “WidgetA.left” can be set to the expression “WidgetB.right + 10” which places WidgetA 10 pixels to the right of WidgetB.

The concept is somewhat similar to Vue.js computed properties.

Live demo is here: https://codepen.io/samhepworth/project/full/AONPBY/

The live demo is a “proof of concept” it is not intended for any real-world usage. The source code for the demo is optimized, but it can be optimized a lot more. However, more optimization will make the source code more difficult to read. Capabilities of the demo include:

  • The code is fast and it is not difficult to make it even faster.
  • Only when a value change is it written to the DOM elements. This makes the code very fast.
  • All DOM updates happen in requestAnimationFrame callbacks.
  • There are some nice operators to select widgets in the expression language.
  • Expressions are compiled to javascript functions making expression evaluation very fast.
  • Expression references are looked up before expressions are evaluated. This enables expressions to be evaluated in a topologically sorted order. Browser stack size is not a limitation. Recursive functions have been replaced with iterative functions where needed.
  • If a value is not used directly or indirectly to update a property on a DOM element, then this value is not evaluated unless is it read directly. Not evaluating “unused” values provide a major performance boost.
  • The code is open source and distributed with a MIT license.

Limitations of the demo include:

  • Adding and removing widgets results in all references to be re-evaluated. This can be fixed, but it will make the source code more complicated.
  • Hidden widgets are not handled. One way to handle hidden widgets is to maintain a list of the child widgets that are not hidden, and basically ignore all the hidden widgets most of the time.
  • Properties are not inserted into the generated HTML when widgets are rendered. All properties are applied after creating the DOM elements. Rendering selected properties when the HTML is generated is a simple addition.
  • Values cannot have properties (composite values) - a value must be a property of a widget. It might be an advantage to rewrite the code so that widgets are a special kind of values, and that values can have properties.

Ideas:

  • Re-evaluate references based “lowest level” (nearest root) widget observation. No need to evaluate all references when widgets are added/removed.
  • Implement something like shadow DOM for widget children that are “private” to the widget.
    • a.private.b (widget b is a private child of a, ^b == a)
  • Implement widgets as values that returns reference to self when read.
  • New operators for accessing properties vs children:
    • a'b.c (property c of child b of property a)
    • 'a'b.c (property c of child b of child a)
    • 'a (child a not property a)
    • a (property a)
    • .a (property a)
    • a..b (simple property b of property a)
    • .a..b (simple property b of property a)
    • 'a..b (simple property b of child a)
    • ..a (simple property a)
    • a..b'c (error - when a new value is assigned to a.b then this is not discovered.)
    • a..b.c (error)
    • < > ^ (sibling widget, parent widget)
    • '* (all children)
    • '*'* (all children of all children)
    • a\sa (property “a a”)
    • .\1a\sa (property “1a a”)
    • a'“hello world”.c (child with special name)
    • a.“hello world”.c (property with special name)
    • '“hello world”.c (child with special name)
    • .“hello world”.c “property with special name)
    • ^.a (property a on parent)
    • ^'a (child a on parent)
    • ^a (property on parent)
    • <.a (property a on previous child)
    • <'a (child a on previous child)
    • <a (property on previous child)
    • (old) a (assume property, then child)
    • (old) a.b (assume a is child, b is property)
    • (old) a.b.c (assume a, b is child, c is property)
    • (old) a.b..c (c simple property of value b of widget a)
    • (old) a.b..c..d (d simple property of simple property c of value b of widget a)
    • (old) a.b..c.d (error?)
    • (old) @a (child a - never property a)
    • (old) a@b (child b of widget a - not property b of child a)
    • (old) a|b.c (property c of property b of widget a - not property c of widget b of widget a)
    • (old) |b (property b - never widget b)
    • (old) |”a-1“ (property with special name)
    • (old) @“a-1” (child with special name”)
  • Arrays
    • [b] (value with index b of values)
    • .[b] (value with index b)
    • '[b] (child with index b)
    • a[b] (value with index b of property a)
    • .a[b] (value with index b of property a)
    • 'a[b] (value with index b of child a)
    • length (number of values)
    • push(b) (push b on values)
    • pop() (pull value from values)
    • a.length (number of values of a)
    • a.push(b) (push b on values of a)
    • a.pop() (pull value from values of a)
    • children (number of children)
    • a.children (number of children of property a)
    • 'a.children (number of children of child a)
    • 'a'[b] (child with index b of child a)
    • 'a'[b]'[c] (child with index c of child with index b of child a)
    • array(a, b, c) (create array [a, b, c])
    • {x: a, y: b, z: c}..a (create object and get simple property a)
  • References
    • ref(a) (reference to property or child a)
    • val(a) (value of reference a - when a changes a new lookup must be performed)
    • lookup(“a'b”) (dynamic lookup)
    • lookup(“<”).b (property b of dynamic lookup)
    • lookup(“<”)'b (child b of dynamic lookup)
    • store(a, ref(b)) (store value a in property b - at some interval)
  • States
    • Transition from state a to state b.
      • A state override selected property expressions when the state is active.
        • a.c = ~state = “a” ? 1 : ~state = “b” ? 2 : ~state = “c” ? 3 : 4

Directed Acyclic Graph (DAG)

A directed acyclic graph (DAG) is a graph where there are no cycles and the edges between vertices can only go in one direction (otherwise there is a cycle). In this case a value is a vertice in the graph and a refernce to another value is an edge.

ExpressionReferences
a = b + ca reference b
a reference c
b = db reference d
c = dc reference d

Values are computed using depth-first recursive evaluation. This can lead to a deep call stack, so deep that it exceeds the browsers javascript stack depth maximum. To prevent this values are evaluated in a topologically sorted order when the call stack gets deep.

Delayed evaluation

When a value is set to a constant (simple value) or an expression a few things happen - as few a possible. When a value is set to an expression evaluation of the expression is delayed until the value is ready or the browser is ready to update the DOM. This saves time.

After evaluating an expression the new value is compared to the old value. Only if the new and old values differ is the DOM updated. This also saves time.

Widgets & Values

Values are stored as widget properties. Here is an example:

// Create widget "a" and make it a child og the DOM element with id "view". This is then a root widget.
var a = Widget("view", "a")
 
// Create value "ax" on widget "a"
Value(a, "ax");             
 
// Create value "ay" on widget "a". This value is written to the DOM element styles as fontWeight using the unit "px".
Value(a, "ay", "style:fontWeight:px"); 
 
// Create value "az" on widget "a"
Value(a, "az"); 
 
// Set "ax" to "10+20" that evaluates to "30"
a.ax = "10+20"; 
 
// Set "ay" to "ax+10" that evaluates to "40"
a.ay = "ax+10"; 
 
// Set "az" to the text "This is not an expression"
a.az = Simple("This is not an expression"); 
 
// Set "az" to the text "This is an expression"
a.az = '"This is not an expression"'; 

Expressions

A value can be a constant (a simple value) or an expression. Expressions may contain the following operators.

OperatorDescription
"text"A string. Enter a quote as \“ and escape as \\.
"This is a text"
"This is a \"text\" with qoutes and \\escape".
number.fractionA number with an optional fraction.
10
10.20
nameWidget or value with a given name. If a value is not found on the current widget, it is checked if there exists a global value with the given name.
left : Value named left.
a.left : Value left on child widget a
/Root widget.
/left : Value left of widget root
/a.left : Value left of child widget a of widget root.
//Named root widget.
//a.left : Value left of root widget a
//a.b.left : Value left of child widget b of root widget a.
^Parent widget.
^left : Value left of parent widget.
^a.left : Value left of sibling widget a.
^*.width : Width of this and all sibling widgets.
<Previous widget.
<left : Left value on previous widget.
^<left : Left value on previous widget of parent widget.
>Next widget.
>left : Left value on next widget.
^>left : Left value on next widget of parent widget.
~Named widget or value on any parent.
~a : Value named a on any parent.
~a.left : Left value of widget a on any parent.
#Named widget or value (must have same root as current widget).
#a : Any value named a.
#a.left : Left value of any widget (with same root as current) named a.
#a.<left : Left value on widget previous to any widget name a
##Named widget or value with any root.
##a : A value named with any root.
##a.left : Value left on widget a with any root.
###Widget with id.
###w101.left : Left value of widget with id w101.
###w101^.left : Left value of parent of widget with id W101.
*All children.
*.left : Left value of all children widgets.
^*.left : Left value of this and all sibling widgets.
.Widget property.
a.left : Left value of child widget a.
^a.left left value of sibling widget a.
firstFirst widget.
first.left : Left value of first child.
^first.left : Left value of first sibling.
lastLast widget.
last.left : Left value of last child.
^last.left : Left value of last sibling.
+ -Sign
-10 * +2
%Percentage
10 * %50 : Equals 10 * (50/100)
+ -Addition and subtraction.
left + width : Left value of this widget plus width value of this widget.
* /Multiplication and division.
left * width / 2 : Left value of this widget multiplied by width value of this widget divided by 2.
( )Grouping expressions.
(2 + 2) * (1 + 3) = 4 * 4 = 16
> < == = <= >= !=Compare values.
left < a : True if value left is less than value a
&& & || |Logic
a > 10 & a < 20: True if (a > 10) and (a < 20)
function(arguments)Function call.
max(2, 3, 4): Call function max with 3 arguments.
a ? b : cIf a then b else c.
a < b ? “a is less than b” : “b is less than or equal to a”

Default Global Values

The following global values are predefined:

GlobalDescription
undefinedA ready only value which equals undefined.
max(…)Return max of arguments. Arguments can be arrays.
sum(…)Return sum of arguments. Arguments can be arrays.
nan(a)Return 0 if isNaN(a) else return a.
nan(a, b)Return b if isNan(a) else return a.
nan(a, b, c)Return c if isNan(a) else return b.

Default Widget Values

A widget has a number of predefined values:

Widget ValuesDescription
indexIndex of widget.
nan(<index, -1) + 1
countCount of children.
nan(last.index + 1, 0)
totalCountTotoal count of children.
count + sum(*.totalCount)
xThis value is changed by moving the widget left/right.
0
yThis value is changed by moving the widget up/down.
0
wThis value is changed by sizing the widget left/right.
0
hThis value is changed by sizing the widget up/down.
0
widthWidth of widget.
w
heightHeight of widget.
h
leftLeft position of widget.
x
right-width
topTop position of widget.
y
bottom-height
rightRight position of widget.
left+width
x
bottomBottom position of widget.
top+height
y
clientLeftGlobal left position of widget.
nan(^clientLeft, ^clientLeft + ^border + ^padding, 0)+left
clienTopGlobal top position of widget.
nan(^clientTop, ^clientTop + ^border + ^padding, 0) + top
clientRightGlobal right position of widget.
max(*.clientRight, clientLeft + width)
clientBottomGlobal bottom position of widget.
max(*.clientBottom, clientTop + height)
overflowWidget.style.overflow
undefined
borderWidget border. Left, top, right and bottom border is same value.
undefined
borderColorBorder color.
undefined
borderStyleBorder style.
solid
paddingWidget padding. Left, top, right and bottom padding is same value.
0
marginWidget margin.
0
innerWidthWidth excluding padding and border
width - border*2 - padding*2
innerHeightHeight excluding padding and border
height - border*2 - padding*2
outerWidthWidth including margin
width + margin*2
outerHeightHeight including margin
height + margin*2
colorstyle.color
undefined
backgroundColorstyle.backgroundColor
undefined
htmlWidget html. HTML for widget children is not part of this.
Widget FunctionsDescription
bounds(left, top, width, height)Set left, top, width and height. If an argument is undefined the value is not set.
xywh(x, y, w, h)Set x, y, w, h. If an argument is undefined the value is not set.