baton core

This article describes the core of batonjs, the baton function and its surroundings.

baton

Launches batonjs. Once launched, batonjs manages a single page state.

Example
const withState = baton({count:0}, show, document.getElementById('root'))
Parameters
name type default description
state any mandatory Initial page state; you cannot specify null or undefined.
show function mandatory A function to convert page state to UI declaration. See below.
baseEl DOM element document.documentElement* In the UI declaration, the CSS selector is used to extract DOM elements, and this is the DOM element that is used as the starting point for the extraction. If omitted, it will be the entire HTML document.
* You cannot omit this if you run batonjs on Node.js.
Return Value
name type description
withState function It takes a function that updates the page state, calls that function, and reflects the resulting page state on the page. See below.

Page state and its update by withState

The page state is represented by a single javascript value. The value can be anything, but null and undefined are not allowed. Use withState to change the page state. batonjs does not change the page state at all.

In most cases, withState is used in event handlers. The flow of the process is as follows: the user interacts with the UI, an event occurs, and the page state is updated using withState in the handler.

The callback function passed to withState updates the page state and returns it.
At this time, to update the page state, you must return a different value (in the sense of ===) than the original value. batonjs uses === to determine if the page state has changed or not. This means that you cannot use an assignment (destructive update) like state.count++.
Also, if the returned value is null or undefined, batonjs determines that the page state has not changed.

If batonjs is launched multiple times within a single page, withState is a different value for each launch.
You can get withState as the return value of the function baton that invokes batonjs.

Example
myButton.addEventListener(ev => {
  withState(state => ({count: state.count + 1}))
})
Parameters
name type default description
update function mandatory A function that takes a page state and returns a new, updated page state. If this function returns null or undefined, it is assumed that the page state has not changed.
Return Value

none

show and UI declaration

batonjs calls the show function when the page state is updated.
The show function is a user-defined function that translates the page state into a UI declaration.
You pass the show function to batonjs as a bootstrap parameter.

A UI declaration is a single javascript value that expresses the state of the UI. It primarily expresses which UI components change in response to changes in page state and how.
There are two basic syntaxes for UI declarations.

Element declaration
This is a declaration of a DOM element. The left-hand side is a CSS selector, and the right-hand side is a block (object) that groups together other declarations. The implication is that the declarations on the right-hand side should be applied to the DOM elements extracted by the CSS selector on the left-hand side.
Note that batonjs uses querySelectorAll internally, but the specification of this is slightly different from that of CSS.
I mentioned earlier that "the show function returns the UI declaration," but this was actually not strictly true. Strictly speaking, "the show function returns the right-hand side of the element declaration." What is the left-hand side, then, is the DOM element that is passed as the third argument to the baton function.
Property declaration
Declaration of properties or attributes of a DOM element. The left-hand side is the property name and the right-hand side is the value of it.
You can write any property or attribute of any DOM element, even a data-* attribute such as data-my-attr. However, if you write a read-only property, an error will occur later when batonjs tries to update it.

In the example below, lines 2-4 are the element declaration and line 3 is a property declaration; the block starting at line 1 is the right-hand side of the element declaration.

Example
const show = (state) => ({
  "#counter": {
    innerText: state.count
  }
})
const withState = baton(state, show, document.body)
Parameters
name type default description
state any mandatory Latest page state.
Return Value
name type description
uiDeclaration object UI declaration (Strictly, the right-hand side of Element declaration)
Discrimination between element and property declarations

batonjs determines the type of declaration only by its left-hand side. The left-hand side of an element declaration is the CSS selector and that of a property declaration is the property name.
So how is it determined if the left-hand side is option? The answer is a property declaration.
batonjs determines that a left-hand side is a property declaration if it consists of a sequence of alphanumeric characters and hyphens/underscores. Therefore, option consisting only of alphanumeric characters is determined to be a property declaration.

Conversely, what if you want to write a UI declaration for an HTML option element? If you simply write "option" as a CSS selector, it will be interpreted as a property declaration. So you need to write a CSS selector that has the same meaning but is not interpreted as a property declaration.
CSS selectors that meet this requirement include: "option:not(.phantom-class)", "option:defined", "*|option", "option "

The author recommends using the third one with namespace ("*|option"). I like this one because it is short, has no unnecessary words, and is easy to identify as a CSS selector.

Update monitoring of property

You can have a callback function called when a DOM element property value changes; in batonjs we call this update monitoring.
Update monitoring is written in the UI declaration. The left-hand side is the property name to be monitored prefixed with "&" , and the right-hand side is the callback function.

const handleChecked = (element, propertyName, newValue, oldValue, cleanup) => {...}
const show = (state) => ({
  checked: state.checkboxChecked, 
  "&checked": handleChecked
})
Parameters
name type default description
element DOM Element mandatory DOM element with changed properties
propertyName string mandatory Name of the property that was changed
newValue any mandatory Property value after change
oldValue any mandatory Property value before change
cleanup function|null mandatory The cleanup process is passed as a function if batonjs needs the cleanup process after the callback is executed. If you receive a non-null value, call it after you have finished using the target DOM element. cleanup function has no parameters or return value.
Return Value

none

You may have thought. "I changed the page state myself and reflected it in the UI myself. And now you're going to tell me what you've changed? What's the value in that?"
Actually, there is great value in this. It is the splitting of the program code.

The update monitoring can be used to trigger animations or external library components, such as bootstrap, when property value changes.
These processes are as obtrusive as a side road from the perspective of managing page status.
The purpose of update monitoring is to separate these side processes from the management of page state.

Strong update monitoring of property

The update monitoring described in the previous chapter is actually just a typical story. By typical, we mean the pattern of "the page state changes, the UI declaration changes accordingly, the property values of the DOM elements are reflected, and you are notified of the changes."
There are two other situations in batonjs where the property values of DOM elements change.

The first is when batonjs is launched.
Once batonjs is launched, a UI declaration is created based on the initial page state, which is then reflected on the already existing HTML page.
At this time, if there is a difference between the properties derived from the existing HTML and the properties derived from the UI declaration, the property values will be updated.

The second is when batonjs creates a DOM element.
The creation of DOM elements is discussed below, but batonjs may create DOM elements based on templates.
At this time, if there is a difference between a property derived from the template and a property derived from the UI declaration, the property value will be changed.

The two cases above are somewhat special in the sense that the property value changes even though the page state or UI declaration has not changed; it is the first time batonjs encounters that DOM element, so it may not follow batonjs' rules.

batonjs allows for update monitoring in these situations.
To do this, use "&&" instead of "&" when declaring update monitoring in the UI declaration.
Update monitoring with "&" only monitors changes in property values that result from changes in page state. On the other hand, update monitoring with "&&" also monitors property value changes that are not directly related to changes in the page state.
In batonjs, update monitoring with "&&" is called "strong update monitoring".

The author believes that there are not many situations where you would use strong update monitoring. However, it may be useful, for example, in situations where you want to start an animation as the page loads.
The example below instructs the popup to launch with animation as the page loads.

"#startup-popup": {
  "class-is-open": true, 
  "&&class-is-open": myTransition
}

Other features of UI declaration

The UI declaration has various features that make batonjs more useful. This section introduces those features.

Event handler

You can set event handlers in UI declarations instead of using addEventListener. The property name should be prefixed with "on" before the event type, such as onclick, onchange, etc.
Despite its property name, batonjs internally uses addEventListener and removeEventListener.

"#button": {
  onclick: countUp
}

Remark
If you write a function expression, such as function () {...} in the UI declaration, a function object will be created each time the show function is called. This is inefficient in most cases, so it is better to define event handler functions outside the show function as much as possible.

Nesting UI declarations

You can nest UI declarations.
In such a case, if we were to call the outer declaration "parent" and the inner "child," the child CSS selector would be used starting from the DOM element extracted by the parent CSS selector.

".parent": {
  ".child": {...}, 
  "data-index": ...
}

In the second line of the example above, the same DOM element is extracted as the CSS selector .parent .child.
Also note that nested element declarations (.child) and property declarations (data-index) can be written side by side.

Referencing DOM elements in UI declarations

The right-hand side of the element declaration (the part of the block corresponding to the CSS selector on the left-hand side) can be a function.
If this is a function, the function will be called with each DOM element extracted by the CSS selector as an argument.

{
  ".option": (element, index) => ({
    selected: state.selection === element.value
  })
}
class-* property and style-* property

Property declarations have a convenient notation for class and style attributes.

If the property name starts with "class-", batonjs interprets this as specifying a single CSS class. The value of the property is a boolean value.
For example, the declaration "class-is-open": true is the same as classList.add("is-open") in the sense. Conversely,"class-is-open": false would be classList.remove("is-open").

Similarly, if the property name starts with "style-", batonjs interprets this as a single CSS style specification. The value of the property is a string.
For example, the declaration "style-font-size": "16px" is the same in the sense as font-size: 16px; in the style attribute.

Remark

  • Be careful not to set a numeric value for the style-* property. Bad example: "style-font-size": 16. This could cause malfunctions.
  • The behavior when class-* and class are used together in a UI declaration is unknown. The same is true when style-* and style are used together. Do not use them in such a way. It is acceptable to use the class and style attributes in HTML together with the class-* and style-* properties in the UI declaration.
Activating CSS transitions

You can use the update monitoring mechanism to trigger CSS transitions. See the CSS Transitions page for details.

{
  "class-is-open": state.isAccordionOpen, 
  "&class-is-open": cssTransition("transition", "size")
}
Adding and Removing DOM Elements

batonjs can also add and remove DOM elements when explicitly instructed. To do so, you tell batonjs which DOM elements are to be managed as children.
From here on, we will refer to the DOM elements to be added and removed as "child" and their parent DOM elements as "parent".

Instruct the parent element to add and remove child elements by defining the batonChildKeys property and batonChildTemplate property in its UI declaration.
batonChildKeys is an array of child element keys, indicating the existence and order of the child elements. batonChildTemplate is a template element used to create new child elements.
The key is a string that identifies the child, must be immutable, and must not have duplicates among siblings. batonjs stores each key in the data-baton-key attribute of the child element.

{
  batonChildKeys: state.todos.map(todo => todo.id), 
  batonChildTemplate: document.getElementById('todo-template')
}

batonjs recognizes the correct order of the child elements from the key array and adds, removes, and rearranges them so that the children are aligned accordingly in the DOM as well.

Remark
If the child is pre-described in HTML, be sure to write the data-baton-key attribute on it.

Supplement
You may also specify a function for batonChildTemplate. In that case, the function should take the key and index as parameters and return the child DOM element.

"mounted" property and update monitoring of lifecycle

The previous chapter described the addition and removal of DOM elements. Using the update monitoring mechanism, we can monitor these lifecycle changes in child elements.
To do so, set strong update monitoring on the mounted property of the child element.
In the example below, CSS transitions are triggered when child elements are added or removed.

{
  "&&mounted": cssTransition("transition", "size")
}

The mounted property is true when the child element is added to the DOM tree and comes under batonjs control, and false when it leaves the control and just before it is removed from the DOM tree.

Be careful with the mounted property and the condition of the child element, as there can be a timing where the mounted property is false and not under batonjs' control, but still present in the DOM tree. This half state is necessary to perform CSS transitions on removal.

Update monitoring of the lifecycle using the mounted property can be used not only for directly added and removed children elements, but also for their descendants.

Remark
The mounted property is a special property that is only valid within batonjs. Since it does not actually exist, you cannot access that property. The only thing you can do is detect updates.