widget

webclient grammar version xindi.1.1

  1. Widgets
    1. Configuration
      1. Text, number, list and stategroup
      2. Binding
      3. Widgets
      4. Views and windows
    2. Expressions
      1. Context in expressions
    3. Let declarations
    4. Variables
    5. Navigation

Widgets


Widgets are used to initialize and compose controls and bind data to them using the client bindings. Widgets in turn are used by views which manage the binding to the application model.

A widget can be seen as two parts. First its configuration, and second its implementation. The widget configuration describes an interface for others that make use of widgets (i.e. views).

The implementation part of a widget determines what gets rendered using one or more controls. The implementation binds data from the client state or the widget configuration to the controls. Binding in this case means that the controls are updated whenever the source data changes. Selecting controls and binding data is done by using expressions.

'binding context': stategroup (
	'none' { [ static ] }
	'select' {
		'binding': [ binding ] reference
		'switch client binding context': stategroup (
			'yes' { [ on ]
				'constrained on containing binding': stategroup (
					'yes' {
						'type path': component 'client binding type path'
						'instance binding': reference
					}
					'no' { [ unconstrained ]
						'instance binding': reference
					}
				)
			}
			'no' { }
		)
	}
)
'widget': component 'widget configuration node'
'optional context definition': component 'optional context definition'
'expression': component 'expression'

Configuration


The configuration part of a widget consist of attributes of the types: widget, window, view, binding, number, text, list and stategroup.

Some attributes are optional if a default is set. For example text attributes can have a default set.

'language': text default: "English"

Text, number, list and stategroup

Attributes of this type are used to configure the behaviour and state of the widget. These values can be updated using expressions when invoked from an event.

Binding

A binding attribute references a binding type in the client bindings. It can be used to access data from the client state. When an attribute of this type is used in a view, it typically needs some additional model information like property name.

Widgets

A widget attribute references another widget definition. They can be used in expressions to bind to a markup attribute of a control or to invoke in an expression at the top level of the widget implementation.

Views and windows

Views are shown in a window. A window is a fixed place in the user interface where zero or more views can be rendered. A webclient project needs at least one window and one view to properly function.

For widgets, view attributes are used to open new content. E.g. opening a new view when the user clicks on a link or a row in a table.

'widget configuration node' { [ {, } ]
	'attributes': dictionary {
		'switch client binding context': stategroup (
			'yes' { [ on ]
				'constrained on containing binding': stategroup (
					'yes' { }
					'no' { [ unconstrained ]
						'instance binding': reference
					}
				)
				'type path': component 'client binding type path'
			}
			'no' { }
		)
		'type': [ : ] stategroup (
			'widget' { [ widget ] }
			'window' { [ window ]
				'node': component 'widget configuration node'
			}
			'view' { [ view ] }
			'configuration' {
				'type': stategroup (
					'number' { [ number ] dynamic-order
						'persistence': component 'configuration attribute persistence'
						'has default': stategroup (
							'yes' { [ default: ]
								'value': integer
							}
							'no' { }
						)
					}
					'text' { [ text ] dynamic-order
						'has default': stategroup (
							'yes' { [ default: ]
								'type': stategroup (
									'static' {
										'value': text
									}
									'unique id' { [ unique-id ] }
								)
							}
							'no' { }
						)
						'persistence': component 'configuration attribute persistence'
					}
					'list' { [ list ]
						'node': component 'widget configuration node'
					}
					'state group' { [ stategroup ] dynamic-order
						'has default': stategroup (
							'yes' { [ default: ]
								'state': reference
							}
							'no' { }
						)
						'persistence': component 'configuration attribute persistence'
						'states': [ (, ) ] dictionary {
							'state default': component 'optional state HACK'
							'node': component 'widget configuration node'
						}
					}
				)
			}
			'binding' { [ binding ]
				'constrained on containing binding': stategroup (
					'yes' {
						'type path': component 'client binding type path'
						'instance binding': reference
					}
					'no' { [ unconstrained ]
						'instance binding': reference
					}
				)
				'node': component 'widget configuration node'
			}
		)
	}
}
'configuration attribute persistence' {
	'persist': stategroup (
		'yes' { [ @persist ]
			'per session': stategroup (
				'yes' { [ session ] }
				'no' { }
			)
			'per entry': stategroup (
				'yes' { [ entry ] }
				'no' { }
			)
		}
		'no' { }
	)
}

Expressions


Expressions are used to yield a value. At the top level of the widget implementation it is used to select a control. The most simple variant of an expression is.

control 'empty' { }

The above snippet initializes a new control called ‘empty’ and renders nothing.

Expressions are also used to bind data to control properties.

'text' = $ .'text'

Context in expressions

Widget expressions use a so called context to get their data from or to execute instructions.

At the start of the widget, the context is set to the binding context of the node and the root widget configuration. The binding context refers to a binding in the client bindings.

This is best explained using the following example.

binding 'node' {
	'text': binding 'text' {
		'empty label': text
	}
}

switch ::'text'.'is set' (
	|'no' as $ => control 'text' {
		'text' = $ . ::'empty label'
	}
	|'yes' as $'text value' => control 'text' {
		'text' = $'text value'.'text'
	}
)

The widget is configured to start at the node binding. It has one binding attribute called text. That attribute has one sub attribute empty label.

The widget expression starts with a switch to check if the text property is set or not. First the text attribute is selected using the double colon keyword (::). This does two things. First it set the widget configuration context to the node containing the empty label attribute. Second it switches the client binding context from node to text since it is a binding attribute.

Then the is set property is selected. The is set property is a member of the text binding from the client bindings. The two cases of the switch are handled. In the no case the context is assigned to the anonymous variable $. This does not change the configuration context, but does change the client binding context to 'text'.'is set'?'no'. The yes case defines a names scope $'text value'. It is used to get the text value from.

To summarize. There are three scopes:

Pro Tip! Only use implicit scope when the scope is used very locally. No more than a couple of lines away from where it was introduced. It becomes very hard to reason about otherwise.

Pro Tip! Use variable names when widgets get more complex and intertwine different scopes.

'expression' {
	'type': stategroup (
		'control' {
			'control binding': component 'control binding'
		}
		'node switch' { [ switch ]
			'context': component 'context selection'
			'from': stategroup (
				'collection' {
					'collection': [ . ] reference
					'key': [ [, ] ] component 'scalar'
				}
				'list' {
					'list': [ . :: ] reference
					'index': [ [, ] ] component 'scalar'
				}
			)
			'cases': [ (, ) ] group {
				'variable assignment': [ | node ] component 'variable assignment'
				'node': [ => ] component 'one or more expression'
				'none': [ | none => ] component 'one or more expression'
			}
		}
		'switch' { [ switch ]
			'context': component 'context selection'
			'type': stategroup (
				'configuration' {
					'state group': [ . :: ] reference
					'states': [ (, ) ] dictionary { [ | ]
						'variable assignment': component 'variable assignment'
						'next': [ => ] component 'one or more expression'
					}
				}
				'binding' {
					'property': [ . ] reference
					'states': [ (, ) ] dictionary { [ | ]
						'variable assignment': component 'variable assignment'
						'next': [ => ] component 'one or more expression'
					}
				}
			)
		}
		'match' { [ match ]
			'type': stategroup (
				'text' { [ text ]
					'left expression': component 'scalar'
					'right expression': [ == ] component 'scalar'
				}
				'number' { [ number ]
					'left expression': component 'scalar'
					'right expression': [ == ] component 'scalar'
				}
			)
			'cases': [ (, ) ] group {
				'true': [ | true => ] component 'one or more expression'
				'false': [ | false => ] component 'one or more expression'
			}
		}
		'walk' { [ walk ]
			'context': component 'context selection'
			'type': stategroup (
				'configuration' {
					'list': [ . :: ] reference
				}
				'widget binding' {
					'collection property': [ . ] reference
					'sort': stategroup (
						'yes' { [ sort by ]
							'by': [ (, ) ] component 'expression'
						}
						'no' { }
					)
				}
			)
			'variable': component 'variable'
			'entry expression': [ => ] component 'one or more expression'
		}
		'create entry' {
			'for': stategroup (
				'state' {
					'state': reference
				}
				'control node' { }
			)
			'node': component 'widget implementation node'
		}
		'none' { [ none ] }
		'instruction' {
			'instruction': component 'instruction selection'
		}
		'sort collection' {
			'context': component 'context selection'
			'direction': stategroup (
				'ascending' { [ ascending ] }
				'descending' { [ descending ] }
			)
		}
		'scalar' {
			'value': component 'scalar'
		}
	)
}
'expression list' {
	'optional context definition': component 'optional context definition'
	'expression': component 'expression'
	'has next': stategroup (
		'yes' {
			'next': [ , ] component 'expression list'
		}
		'no' { }
	)
}
'one or more expression' {
	'multiplicity': stategroup (
		'singular' {
			'expression': component 'expression'
		}
		'plural' {
			'expression': [ [, ] ] component 'expression list'
		}
	)
}
'widget implementation attribute' {
	'expression': [ = ] component 'one or more expression'
}
'widget implementation node' { [ {, } ]
	'attributes': dictionary {
		'attribute': component 'widget implementation attribute'
	}
}
'control binding' {
	'binding type': stategroup (
		'let declaration' {
			'path': component 'context ancestor path'
			'declaration': [ @ ] reference
		}
		'static' {
			'control': [ control ] reference
			'node binding': component 'widget implementation node'
		}
		'window' {
			'window': [ window ] reference
			'variable': [, => ] component 'variable'
			'control binding': component 'control binding'
		}
		'widget' {
			'context': component 'context selection'
			'widget': [ widget ] reference
		}
		'client binding widget' {
			'context': [ binding widget ] component 'context selection'
		}
	)
}
'scalar' {
	'binding type': stategroup (
		'static text' {
			'text': text
		}
		'phrase' {
			'phrase': reference
		}
		'configuration' {
			'context': component 'context selection'
			'type': stategroup (
				'text' { [ . ] }
				'number' { [ # ] }
			)
			'attribute': [ :: ] reference
		}
		'event' {
			'type': stategroup (
				'text' { [ argument ] }
				'number' { [ # argument ] }
			)
		}
		'binding' {
			'context': component 'context selection'
			'type': stategroup (
				'key' { [ .key ] }
				'property' {
					'type': stategroup (
						'text' { [ . ] }
						'reference' { [ > ] }
						'number' { [ # ] }
					)
					'property': reference
				}
			)
		}
		'static number' {
			'value': integer
		}
		'unary expression' {
			'type': stategroup (
				'absolute value' { [ abs ] }
				'sign inversion' { [ - ] }
			)
			'value': [ (, ) ] component 'scalar'
		}
		'list expression' {
			'operation': stategroup (
				'sum' { [ sum ] }
				'minimum' { [ min ] }
				'maximum' { [ max ] }
				'product' { [ product ] }
			)
			'value': [ (, ) ] component 'one or more expression'
		}
		'current time' { [ current time ]
			'throttle': stategroup (
				'yes' {
					'interval': [ interval: ] integer
				}
				'no' { }
			)
		}
		'list index' {
			'context': [, index ] component 'context selection'
		}
	)
	'transform': stategroup (
		'no' { }
		'yes' { [ transform ]
			'transformer': reference
		}
	)
	'format': stategroup (
		'no' { }
		'yes' { [ format ]
			'formatter': reference
		}
	)
}
'instruction selection' {
	'context': component 'context selection'
	'configuration attribute type': stategroup (
		'binding' {
			'instruction': [ >> ] reference
			'has arguments': stategroup (
				'no' { }
				'yes' {
					'first argument': [ (, ) ] component 'instruction argument'
				}
			)
		}
		'configuration' {
			'instruction': stategroup (
				'set state' { [ set state ]
					'state group': reference
					'state': [ : ] reference
					'node': component 'instruction argument configuration node'
				}
				'set number' { [ set number ]
					'number': reference
					'argument': [ = ] component 'number argument'
				}
				'set text' { [ set text ]
					'text': reference
					'argument': [ = ] component 'text argument'
				}
			)
		}
	)
}
'instruction argument' {
	'type': stategroup (
		'number' { [ number ]
			'argument': component 'number argument'
		}
		'text' { [ text ]
			'argument': component 'text argument'
		}
		'file' { [ file ] }
		'view' { [ view ]
			'context': component 'context selection'
			'view configuration': reference
		}
	)
	'next argument': stategroup (
		'no' { }
		'exists' { [ , ]
			'argument': component 'instruction argument'
		}
	)
}
'number argument' {
	'expression': component 'expression'
}
'text argument' {
	'expression': component 'expression'
}
'instruction argument configuration node' { [ (, ) ]
	'attributes': dictionary {
		'type': stategroup (
			'configuration' {
				'type': [ : ] stategroup (
					'number' { [ number ]
						'argument': [ = ] component 'number argument'
					}
					'text' { [ text ]
						'argument': [ = ] component 'text argument'
					}
					'state group' { [ state ]
						'state': reference
						'node': component 'instruction argument configuration node'
					}
				)
			}
		)
	}
}

Let declarations

'let declarations' {
	'let declarations': dictionary { [ let ]
		'expression': [ : ] component 'expression'
	}
}

Variables

'variable' {
	'context': component 'context selection'
	'identifier': [ as $ ] stategroup (
		'anonymous' { }
		'named' {
			'named values': dictionary {
				'has successor': stategroup = node-switch successor (
					| node = 'yes' { }
					| none = 'no'
				)
			}
			'name': reference = first
		}
	)
	'let declarations': component 'let declarations'
	'more variables': stategroup (
		'yes' { [ , ]
			'variable': component 'variable'
		}
		'no' { }
	)
}
'optional context definition' {
	'define context': stategroup (
		'yes' {
			'variable': [ define context, => ] component 'variable'
		}
		'no' { }
	)
}
'variable assignment' {
	'assign': stategroup (
		'yes' {
			'variable': component 'variable'
		}
		'no' { }
	)
}
'client binding type path' {
	'has steps': stategroup (
		'no' { }
		'yes' {
			'type': stategroup (
				'state' {
					'state group': [ . ] reference
					'state': [ ? ] reference
				}
				'collection' {
					'collection': [ . ] reference
				}
				'binding' {
					'instance binding': reference
				}
			)
			'tail': component 'client binding type path'
		}
	)
}
'context ancestor path' {
	'has step': stategroup (
		'no' {
			'context': stategroup (
				'implicit' { }
				'anonymous' { [ $ ] }
				'named' {
					'variable name': [ $ ] reference
				}
			)
		}
		'yes' { [ ^ ]
			'tail': component 'context ancestor path'
		}
		'root' { [ root ] }
	)
}
'context selection' {
	'change context to': stategroup (
		'engine state' { [ engine ]
			'engine state binding': reference
		}
		'other context' {
			'ancestor path': component 'context ancestor path'
			'path': component 'context selection path'
		}
	)
}
'context selection path' {
	'has steps': stategroup (
		'no' { }
		'yes' {
			'type': stategroup (
				'unconstrained configuration attribute' {
					'configuration attribute': [ unconstrained :: ] reference
				}
				'constrained configuration attribute' {
					'configuration attribute': [ :: ] reference
				}
				'client binding' {
					'binding': [ / ] reference
				}
			)
			'tail': component 'context selection path'
		}
	)
}
'optional state HACK' { }