Overview
Elix components are written in functional-reactive style using ReactiveMixin. A core concept in that model is that a component should have a read-only state member that embodies all of the component's current state information.
This state pattern is used in many UI frameworks, including React, Vue, and others. In those frameworks, the state object is typically represented by a plain JavaScript object, and updated via a setState
method.
Elix generally follows this model, but the extensive factoring of component UI logic into mixins has implications for the representation of state. For example, it is common for one mixin to manage a state member a
that has some relationship with a state member b
which is managed by another mixin. When the second mixin calls setState
to update b
, the first mixin may need to reflect that update by updating a
. This needs to be done in such a way that the two mixins are otherwise independent of each other.
To handle these scenarios, Elix represents a component's state as an instance of the State
class. Instances of State
know how to participate in the construction of a new state that can satisfy the relationships between state members.
Responding to state changes
To offer a concrete example, SingleSelectionMixin manages a state member called selectedIndex, and the independentContentItemsMixin manages a separate but related state member called items. We would like to guarantee that the selectedIndex
value should always be a valid array index into the items
array (or the special value -1, which indicates no selection).
To maintain this invariant, if ContentItemsMixin
updates the items
array, SingleSelectionMixin
needs to check that selectedIndex
is still a valid array index. If not, it will want to adjust selectedIndex
to fall within the new bounds of the items
array. Exactly how it will do that depends on several factors, but a simplistic answer is that, if selectedIndex
is now too large, it will be clipped to the new size of the array.
SingleSelectionMixin
accomplishes this by registering a change handler that will run whenever items
changes via setState
. When SingleSelectionMixin
is asked for the component's defaultState, it invokes onChange on the state being constructed to register a change handler. A much-simplified version of the code looks like this:
function SingleSelectionMixin(Base) {
return class SingleSelection extends Base {
get [internal.defaultState]() {
const result = super[internal.defaultState];
// Ask to be notified when items change.
result.onChange('items', state => {
const { items, selectedIndex } = state;
const length = items.length;
// Force index within bounds of -1 (no selection)
// to array length - 1.
const boundedIndex = Math.max(Math.min(selectedIndex, length-1), -1);
return {
selectedIndex: boundedIndex
};
});
return result;
}
};
}
The change handler above will run whenever code invokes setState
to make a change to the items
property. The handler inspects the state's current selectedIndex
and the length of the items
array, then forces the index to fit with the array's bounds. The handler returns a new object containing the final selectedIndex
value. That value may or may not be different than the current selectedIndex
; if it is different, the updated selectedIndex
is applied to the state, and any change handlers defined elsewhere that trigger on selectedIndex
will run.
Naturally, it's possible to create situations in which multiple state change handlers update values in such a way as to trigger an infinite loop. However, the Elix project has found that, at the scale of Elix's web components, such situations can easily be avoided with proper care.
API
copy With Changes(changes) method
Return a new copy of this state that includes the indicated changes,
invoking any registered onChange
handlers that depend on the changed
state members.
There is no need to invoke this method yourself. ReactiveMixin will take care of doing that when you invoke [internal.setState]](ReactiveMixin[internal.setState]).
Parameters:
- changes:
object
– the changes to apply to the state
Returns: object
- the new `state`, and a `changed` flag indicating
whether there were any substantive changes
on Change(dependencies, callback) method
Ask the State
object to invoke the specified callback
when any of the
state members listed in the dependencies
array change.
The callback
should be a function that accepts:
- A
state
parameter indicating the current state. - A
changed
parameter. This will be a set of flags that indicate which specified state members have changed since the last time the callback was run. If the handler doesn't care about which specific members have changed, this parameter can be omitted.
The callback should return null
if it finds the current state acceptable.
If the callback wants to make changes to the state, it returns an object
representing the changes that should be applied to the state. The callback
does not need to check to see whether the changes actually need to be
applied to the state; the State
object itself will avoid applying
unnecessary changes.
The common place to invoke onChange
is when an element's defaultState
is being constructed.
Parameters:
- dependencies:
Array.
– the name(s) of the state fields that should trigger the callback if they are changed|string - callback:
function
– the function to run when any of the dependencies changes