The controller orchestrates app startup, coordinates state variables, the Model, and the View, and manages persistent storage. The app communicates with the Controller using the ctl object, which contains the Controller's methods and properties.
When the app starts up, the initial app file (usually app.js, see App Startup) imports all the pages the app requires and calls ctl.startApp() with information like the app's title and version number, as well as a template for the initial View. This is usually a Component reference to the Nav page. For example:
tml`<Nav ...${navProps} />`
where navProps contains attributes for page layout and the initial page.
ctl.startApp() simply records the passed information, sets up the View's root template, and installs a window.onload handler. When the app is fully loaded, the handler first awaits storage.migrateLegacyLocalStorage() (a failure never prevents startup) and then initializes the Controller, which does the following:
init() method to do any setup after the app is fully loaded. This happens before the saved state is restored.view.init() to initialize the View and instantiate the DOM elements for the root template.ctl.doSync() to immediately synchronize the View with the current values of state variables.ctl.isInitialized to true.server.startSession() to let the server know a new session has started.When the Controller is initialized, it evaluates the computeInfo for every page. For each object in a computeInfo array, it does the following:
inputs, outputs, and referenced arrays.computeInfo that supply inputs to any object in this computeInfo. The list includes the precedents of the precedent pages. The list is roughly ordered so that precedents of precedent occur first, though circular dependencies are allowed.io state variable namespace to be passed to the computation function consisting of those specified by the inputs, outputs, and referenced arrays.When the Model is executed, the Controller goes through all pages, checking to see if any page's or page precedent's computations are out of date with respect to their inputs, using the previously computed input SvMonitors. If a page or its precedent pages have out-of-date computations, they are executed in order: the precedent page first, then the computInfo computations. This means that it's more efficient to arrange the computeInfo array so that computations that have outputs used as inputs to other computations appear earlier in the array.
When a computation in a computeInfo object is out of date, the Controller sets the state variables in the output array to their default state before calling the computation function. If the computation function terminates early due to an error, it does not have to worry about resetting outputs computed later in the function. This also prevents outputs from one invocation from accidentally remaining through the next invocation. If a state variable is referenced by a function but should neither trigger the computation when changed nor be reset to its default value, then it should be included in the referenced array so that it will appear the the io namespace.
The Controller repeatedly executes the Model until there is no computation that remains out of date. If the Controller is forced to do more than 10 passes (MAX_COMP_CYCLES), it will display an assertion failure.
The following details apply to every computation (they are implemented by the internal ModelComp class in modelCompute.js, which is not exported):
inputs, outputs, or referenced arrays are added to the same array automatically.inputs and outputs is treated only as an input and is not reset to its default.ctl.debug is true, exceptions in the computation function propagate so that a debugger can catch them. Otherwise, exceptions are caught and reported as assertion failures so that the app does not lock up.Model-type state variables (e.g. all state variables associated with visible inputs and outputs) call ctl.sync() to schedule a synchronization whenever the state variable is assigned a changed value (according to the state variable's change criteria). The actual synchronization happens later when the current thread of execution terminates. This allows many state variables to change in the thread while scheduling only one synchronization event. In addition, the Controller interposes on click and change events to ensure that synchronization is scheduled when a user interaction occurs.
When the synchronization event fires, it calls ctl.doSync(), which executes the Model as described above. When Model execution stabilizes, the Controller calls view.sync() to have the View synchronize the DOM state with the updated state variable values caused by Model execution.
The Controller maintains persistent storage for saved application state on the device and on the server. The storage system uses the DOM localStorage for saved data. The server-based storage is accessed using HTTP requests. See Server API and Reference.
When the controller completes a synchronization, it checks to see whether any saved page state has changed since the last save by calling each page's hasChanged() method. If any savable state has changed, it builds a saved state object by calling each page's getSavedState() method and stores it locally and, if online, to the server.
The server is not contacted during startup. It is first contacted when the first state is stored after startup. If the server then reports that another device has saved more recent data (a conflict), the Controller handles it as described in Transferring data between devices.
In general, only state variables with a save property of 'local' or 'global' will be saved in persistent storage. Both types are saved to local device storage and to the server. The difference between the two comes when values are restored. When state variables marked 'local' are restored, the data for that state variable in local storage always overrides the server data.
This is useful, for example, for recording that the user considers this device to be only used by themselves, and there is no need to display and confirm the terms of use every time the application starts.
The server requires some way of identifying users to associate with saved data. By default, the Controller creates a GUID (globally unique ID) using a timestamp the first time it starts up on a particular device. This becomes the user ID within the saved data and for the id argument of the server request. The server will keep backups using this ID as well.
If the user enters an email address and security word, the user ID used to save the data and record other server requests becomes a combination of those two items. The only purpose of the security word is to ensure that third parties that know a user's email address can't access the data without also knowing the security word. The security word functions like a password, but it does not require prior registration with the server, nor is it stored or transmitted securely.
The email address and security word combination can be used to reinstate the last saved data when opening the app on a new device. When a new combination is entered, the Controller checks whether the combination exists on the server. If the combination does not exist, it askes the user to confirm whether they would like to create new saved values for this combination in case they typed something wrong. If the combination already exists on the server, it asks the user whether they'd like to use the device or cloud data. If the former, the current device data will overwrite the cloud data. The user can request that the server email a list of all the security words associated with that email address in case they've forgotten it.
If the Controller detects that the data has been updated by another device, it asks the user whether they'd like to use the device or cloud data. If the latter, the server data will overwrite the device. For the first save after startup, the Controller does not ask: it silently adopts the newer cloud data and displays the notice 'Transferring more recent data from another device'. The user is only asked on later saves.
The server maintains up to four daily backup copies of the data. When a new set of data is saved to the server and more than 24 hours have passed since the last save, the server will save the old version as a backup, deleting the oldest backup if there are more than four. The user can request restoring the local data from one of the previous backups.
The Controller tracks the following events:
isActive property and the onActivityChange() methodisOnline property and the onConnectivityChange() methodview.resize() and the onResize() method