MVCS is a Web application framework similar to Model-View-Controller with the addition of centralized state in the form of State Variables (i.e., Model-View-Controller-State or MVCS), as shown below:
The Controller coordinates the Model and View with the State Variables. It activates the Model and View when it senses that a relevant state variable has changed. It’s also responsible for initializing the State Variables with the last saved state and saving them to persistent storage. See the Controller Overview for more detail.
The Controller is organized around Pages. Each Page object contains:
Some pages are invisible to the user and are used to maintain some internal State Variables and to participate in the Model computations. The Nav page is an example: It provides a standard navigation framework that will display the various pages based on user gestures. See the Page Overview for more detail.
State Variables (SV) are used for several things:
Some State Variables on each page are marked for saving and restoring (usually ones that represent user input). These form the restorable state of the app. The control automatically saves them to the device’s local storage and optionally will back that up to the cloud.
Each page state variable object has a name in the form page_name. If the SV is a numeric type with associated units, then the name is of the form page_name_units. All page state variables are accessible using the global namespace object, sv. Each key in the sv object refers to a state variable object. For example, a value can be set by the statement:
sv.adder_operand1.value = 1;
See the State Variables Overview for more detail.
Each page declares zero or more model computations. Each computation has a function to do the computation, a set of SVs that are the inputs to the computation, and a set of SVs that are the outputs of the computation. The Controller invokes the computation function whenever any of the input SVs indicates that it has changed. Prior to invocation, the output SVs are set to their default state. The model can also declare a set of SVs that need to be referenced by the computation but are not inputs (they don’t trigger the computation) or outputs (they should not be reset prior to the computation). The computation function is passed a restricted namespace of only the SVs declared as its inputs, outputs, and references. Using this namespace ensures that an error will occur if the function references an SV not recorded as an input or output.
Page computations may reference the SVs of other pages. The Controller uses the declared input dependencies between pages and tries to ensure that predecessor pages are computed before dependent pages. Cyclic dependencies are allowed, and the Controller will repetitively run all the active page computations until the SVs no longer change and the model stabilizes. An assertion failure is triggered if the model does not stabilize after a few tries.
The View manages a tree of active Components. The tree is created from the initial app setup, the active pages, and state variables. Each Component is responsible for the state and presentation of one semantic element. A Component is responsible for creating a set of DOM node trees that implement the Component’s semantics. Each component may have a set of child Components. The DOM nodes for these child Components may appear anywhere in the DOM trees created by the Component. This means that the form of the Component tree may be quite different than the form of the DOM tree, though children of a Component are restricted to be somewhere in the DOM tree owned by the parent. See the Component Overview for more detail..
When a DOM input changes, its Component implementation is responsible for updating any associated SV values. The Controller detects SV changes and invokes the appropriate computations in the model. When the Controller determines that the model has stabilized, it invokes the View to synchronize its output with the current values in the SVs.
A tree of Components is specified using Templates. Each Template object contains a tag, a props object, and a children array. The tag may be a String naming a Component or a reference to the Component’s class. The props object represents the XML properties of the Component, and the children array contains the Template objects for any child Components. See the Template Reference for more detail..
The easiest way to specify a Template tree is to use a tagged template literal, in a manner similar to JSX. The tag function is tml which stands for Template Markup Language. TML is an enhanced form of XML that takes advantage of tagged template literal capabilities. See the TML Reference for more detail..
For example, each page can specify its layout using an XML-like syntax that represents the layout using higher-level components, though adding HTML where necessary is allowed. Here’s an example:
tml`
<Page title=Adder>
<RowCard type=input cols=1>
<Cr> <Cl>Value 1</> <Cv>{{adder_operand1}}</> </Cr>
<Cr> <Cl>Value 2</> <Cv>{{adder_operand2}}</> </Cr>
<Cr ><Cl>Result ></> <Cv>{{adder_result}}</> </Cr>
</>
</>
`
Note that the template uses higher-level components that capture the meaning of each entity, not raw HTML. The {{…}} notation inserts an association with a State Variable. If the SV is a numeric or text input, the View will typically insert an <input> element and synchronize the SV’s value when the app or user changes it. If the SV is an output, the view will typically insert a <span> element, and when the SV value changes, it will update the contents of the span with the SV value formatted according to the SV’s specification. Other SV types can insert more sophisticated elements.
The tml tag function parses any elements it finds in the template literal into objects of the Template class that capture the tag, properties, and any children. The children are parsed as well to form an entire tree. If it finds the “{{stateVarName}}” notation, it converts that into a Template for a Component that implements the SV behavior. The tml function returns a string, a Template object, or an array of strings and/or Template objects.
TML has some other interesting features:
&entityName; will be converted into a string or Template as defined by the entityName in the characterEntity.js file. Entities can be any valid string or Template including SVG glyphs. This provides extensible custom font glyphs....${obj} within a tag element will turn all the enumerable keys in obj into equivalent properties.tml`<${Component}>`
This is useful when referencing non-public Components in the same module, or if you want to import a Component without adding it to the global tag string namespace.
The View makes it easy to add custom components, similar to React. Each custom component is a class that extends Component. A component is instantiated to represent each node in the component tree. Each instance can then create DOM nodes or reference other sub-components to implement the desired functionality. Here’s an example:
view.defineComponentTag('Card', class Card extends Component {
render () {
return (tml`
<div className=card}>
<div className=card_title>${this.props.title}</div>
<div className=card_panel>${this.children}</div>
</div>
`);
}
});
This is an implementation of the Card component we saw earlier. The render() method returns a Template, a String, a Number, a DOM node, another Component, or an array of these things. If they aren’t already, these are converted into DOM nodes and inserted into the parent node. Once instantiated in the DOM, the nodes are available using this.nodes. It’s OK if render() output results in more than one node.
render() can operate purely at the template level, as seen in the example, but it can also build individual child components, or individual DOM nodes. Here’s an equivalent implementation of Card, using a mix of nodes and components:
view.defineComponentTag('Card', class Card extends Component {
render () {
const panelCmpnt = this.buildChildComponent(tml`
<div className=card_panel>${this.children}</div>
`);
return (view.createNode(‘div’,
{className:’card’},
[
view.createNode(‘div’,
{className:’card_title’},
this.props.title
),
panelCmpnt.node
]
);
}
});
Note how the implementation places the node for the panel Component in the tree. Creating nodes might be preferable when the implementation needs to set up event listeners on particular nodes, or directly execute custom methods on sub-components. Note that the component tree doesn’t exactly mirror the DOM node tree. An implementation can have many layers of hand-built DOM nodes that contain a sub-component in the interior, though that sub-component will appear as the direct child of the parent component.
The call to view.defineComponentTag() is optional. It associates the tag string ‘Card” with the component. This makes the tag string global to the app (e.g., <Card>…</>). Alternatively, it’s OK to use a class reference in the tmlwhere a component should remain local to a module or be explicitly imported. For example:
tml`<${CardClass}/>`
By convention, custom components begin with capital letters. Unrecognized component strings that begin with lowercase letters are assumed to be HTML and are passed to the Html component that creates an equivalent DOM node.
sync() methodThe sync() method is called whenever the Controller or View thinks the Component needs to be synchronized to the current state. Usually, this is because an SV changed value, but it could also be for other reasons, like a component further up the tree fielding an event that required its subtree to be synced. The sync() method can optionally request that the component be discarded, rerendered, and remounted by calling this: remount().
The default sync() method in the Component superclass automatically calls the sync() methods of all the instance’s child components. If the subclass component implements sync() then it normally also calls super.sync() to sync its children, as well.
When a component element is created, the properties and children described in the template are passed via this.props object and the this.children array. Each component instance also starts with a copy of its parent’s contextProps object in this.contextProps. The instance can add and delete properties from its contextProps object to pass this data to all the instances’ child components. Properties may be evaluated during render() or sync() at the discretion of the component implementation.
The base module of the app is typically the app.js file. When that file loads it typically starts the MVCS framework by calling the controller ctl.startApp() function, Here’s an example:
ctl.startApp({title:’My App’, template: tml`<Nav ...${navProps} />`});
This sets the app title and builds the component tree from the template. Normally, the app specifies a Nav component which takes a navigation menu layout as one of its properties. Once the component tree is built, the DOM node(s) for the root component(s) is appended to the root DOM node. The root node is the only required HTML in the index.html file. Typically:
<body>
<div id="root"></div>
</body>
Then sync() is called on the root component(s) to synchronize the new view.
MVCS comes with several standard components. See Built-in Components for more detail.
Not all State Variables are used for application inputs and outputs. Some are used only to represent internal state and provide change notifications to the Model. State Variables that are used to represent visible elements must implement the component method that returns a component used to represent the SV and implement its user interface. Here are some pre-defined SV types:
textInput, textOutput: A text input or output field with an adjustable format.numInput, numOutput: A numeric input or output field with an adjustable format.unitInput, unitOutput: A numeric input or output field that supports multiple units (e.g., pounds and kilograms). Each unit is represented by its own separate sub-SV, and modifying one sub-SV will automatically send converted values to its siblings.selectorInput: An option selector. It can be presented as a dropdown list or a set of radio buttons. It can also be used as an output field containing just the selected text value.databaseListInput: A selector that presents a user-editable list. Each list item contains the state of an associated set of other SVs and is restored when a list element is selected.TML converts the {{name}} notation into:
<StateVar sVar=name />
The StateVar Component finds the named SV and instantiates its component in place.
Nav ComponentThe Nav Component provides standard page navigation and help. It currently provides two styles:
'tab''slide'The tab style is typically more useful on larger devices that have room for the tabs on the top or bottom of the screen. The slide style is typically more useful on small devices like phones where tabs won’t fit in the available width. The Nav component takes props that give the page layout and provides the table of contents for help.
The Nav Component is controlled by the nav page and its state variables. The nav_style SV selects the appropriate navigation style for a given device. The nav page also adjusts its view to layout nav elements and page content consistent with the selected style. See the Navigation System Reference for more detail.
The Page Component is the container element used by most pages to hold page content. It provides a scrollable container. Most pages also contain Card Components containing the inputs and computed values. Each Card has an optional header that contains a title and other elements. There can be multiple cards on each Page. Pages with more than two Cards can be organized using PageCol Components to control which Cards are associated with each column of Cards on wide devices.
A Card component provides a general container. A RowCard provides a container for rows each containing one or more columns of labels and values. The rows are defined by Cr Components that contain Cl and Cv Components for labels and variables. Here’s an example:
tml`<Page title="A Page">
<RowCard rowStyle=input>
<Cr><Cl>Value 1</Cl><Cv>{{aPage_value1}}</Cv></Cr>
<Cr><Cl>Value 2</Cl><Cv>{{aPage_value2}}</Cv></Cr>
</RowCard>
</Page>`
Each page’s render() method returns a template for the page at the time a page is opened. This means that the template is relatively static. The only dynamic change is provided by the state variable components. The When Component provides a means to re-layout sections of a page or the entire page contents. It’s a very powerful component. Here’s an example:
tml`<Page title="A Page">
<When hasChanged=nav_style>
${(io) => {
if (io.nav_style.isValue(‘tab’) {
return (tml`<LargeComponent>...</>`);
}
return (tml`<SmallComponent>...</>`);
}}
</When>
</Page>`
<When> uses the hasChanged property to specify a list of SV names that will cause it to rebuild its content when any SV on the list changes. It expects a single child that must be a function that returns a Template. The function is passed a namespace object that only contains the variables listed in hasChanged. This will result in a reference error if you forget to include something in the list. Of course, you can bypass this mechanism by using the global sv namespace.
<When> re-evaluates its contents whenever the SVs listed in hasChanged property changes and rebuilds the child Component tree and inserts the resulting node or nodes in the same position in its parent DOM node as the old tree. If the resulting template is empty, <When> inserts an invisible, empty <span> element in the parent as a marker to place future expansions.
From the page developer’s point of view, the two ways to change a page’s displayed information are by using state variables or <When> in a page’s template. State Variables are lightweight and self-contained. They usually only change the values stored in existing DOM elements without changing them. <When> is heavier weight; it completely rebuilds its child DOM elements when required. These two mechanisms are sufficient for relatively static pages, where <When> is only required occasionally or with content of moderate size.
Comparing this to React, using state variables requires no DOM differencing, but the cost is that the sync() method is usually called on the entire visible Component tree. <When> does not do differencing either but it does do a full rebuild of its children. An alternative implementation could use a custom Component to preserve the common DOM elements and child Components and only rebuild (or show/hide) exactly what’s required. These strategies give complete control of performance to the implementer.
Let’s start with an example of a simple adder. It has a selector that controls how many arguments are displayed. When any argument changes, it computes the sum:
const minArgs = 2;
const maxArgs = 6;
ctl.createPage({
pageId: 'adder',
title: 'Adder',
stateVarInfo: [
{id:'adder_numArgs', type:'selectorInput',
options:arrayGenerate(maxArgs - minArgs + 1, i => minArgs + i)),
},
{id:'adder_result', type:'numOutput', dflt:0, fmt:'6.2'},
].concat(arrayGenerate(maxArgs, i => (
{id:'adder_arg'+(i + 1), type:'numInput', dflt:0, fmt:'-6.2'}
))),
computeInfo: [
{
title: 'add',
inputs: ['adder_numArgs', {includePat:/^adder_arg\d+$/}],
outputs: ['adder_result'],
fn:(io) => {
let sum = 0;
for (let i = 1; i <= io.adder_numArgs.value; i++) {
sum += io[`adder_arg${i}`].value;
}
io.adder_result.value = sum;
},
},
],
// render the page
render () {
return (tml`
<Page>
<RowCard title="Input" type=input cols=1 className=argCard>
<Cr> <Cl>Number of arguments</> <Cv>{{adder_numArgs}}</> </Cr>
<When hasChanged=adder_numArgs>
${(io) => arrayGenerate(io.adder_numArgs.value, (i) => tml`
<Cr>
<Cl>Argument ${String(i + 1)}</>
<Cv>{{adder_arg${i + 1}}}</>
</Cr>
`)}
</When>
</RowCard>
<RowCard title="Output" type=output cols=1 className=rsltCard>
<Cr> <Cl>Sum</> <Cv>{{adder_result}}</> </Cr>
</RowCard>
</Page>
`);
},
});
The above code creates an Adder page that presents a number of inputs based on the adder_numArgs selector state variable. The computation section will add all the input SV values and sets the result in adder_result when any of adder_numArgs or the adder_argN SVs change. The template returned by the render() method uses the <When> Component to build the correct number of inputs based on the adder_numArgs value. Here’s a screenshot:
.
MVCS is designed to facilitate coordinating user input and output with the typical model-view-controller. State Variables are the primary mechanism for capturing, formatting, and detecting input and output changes. Whenever a State Variable changes, the MVC mechanism is automatically invoked, and the corresponding change to any State Variable is automatically synchronized with the View.