diff --git a/README.md b/README.md
index 2b5afb7..87a6dc9 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,116 @@ added in Fitbit OS 4.0.
`/app/views/` contains a JavaScript file for each view in the application.
`/app/index.js` initializes the list of views.
-When `views.navigate()` is called, the current view is unloaded and all event
+When `views.replace()` or `views.open()` is called, the current view is unloaded and all UI event
handlers are unregistered. The JavaScript for the selected view is dynamically
-loaded, and the document for the selected view is loaded.
+loaded, and the document for the selected view is loaded.
The `BACK` button can be used to navigate to the previous view.
+
+## Viewport
+
+Viewport is an object which knows the view names and able to switch between them.
+
+View is a JS module in `app/views` folder having exported `initialize()` method. View is loaded dynamically when it's shown and unloaded when it's replaced with another view.
+
+#### views.initialize()
+
+Initialize views linking view names with JS controller modules.
+View name must correspond to a file name in `resources/views` folder excluding `.gui` extension.
+
+```javascript
+import views from "./views";
+
+/**
+ * Definition for each view in the resources/views folder, and the associated
+ * JavaScript module is lazily loaded alongside its view.
+ */
+views.initialize({
+ 'view-1' : () => import("./views/view-1"),
+ 'view-2' : () => import("./views/view-2"),
+ 'view-3' : () => import("./views/view-3")
+});
+
+setTimeout( () => {
+ // Initialize application loading the first view.
+ views.replace("view-1");
+}, 1000 );
+```
+
+#### views.replace( name, options )
+
+Replace the currently displayed view with another view of the given name. Optionally, pass options to view's `initialize()` function.
+
+Old views will automatically unsubscribe from all UI event handlers, but not the handlers from sensors and clock. You will need to do the latter in view's clean-up function optionally returned by view's `initialize()`.
+
+#### views.open( name, options )
+
+Works similar to `views.replace()`, but open another view on top of the existing one, so the `back` button will open the previous view.
+
+#### views.back()
+
+If view was opened with `views.open()`, return to the parent view. Works similar to hardware `back` button.
+
+#### views.current
+
+The name of the currently displayed view.
+
+#### views.context
+
+Global context object available for all views to share information between them. Initially it is empty.
+
+This object can be handy as Fitbit OS doesn't have modules cache thus storing globals in modules won't work. Each dynamically loaded view will load its own instance of the module.
+
+## Views
+
+#### initialize( views, options )
+
+Any view module must export initialize function this function takes viewport as its first argument, and an optional `options` object passed to `views.replace()` or `views.open()`.
+
+All view logic like events subscribption must be placed inside of initialize.
+
+```javascript
+import document from "document";
+
+export function initialize( views ){
+ /**
+ * When this view is mounted, setup elements and events.
+ */
+ const btn = document.getElementById("v2-button");
+
+ /**
+ * Sample button click with navigation.
+ */
+ btn.addEventListener("click", evt => {
+ console.log("view-2 Button Clicked!");
+ /* Navigate to another screen */
+ views.replace("view-3", { granularity : "seconds" });
+ });
+}
+```
+
+`initialize()` may return the clean-up function to unsubscribe from the sensors and clock when the view is closed.
+
+```javascript
+import document from "document";
+import clock from "clock";
+
+export function initialize( views, { granularity } ){
+ // Subscribe for clock updates...
+ clock.granularity = granularity; // seconds, minutes, hours
+
+ clock.ontick = function(evt) {
+ console.log(evt.date.toString());
+ };
+
+ ...
+
+ // View init functions can return clean-up functions executed before the view is unloaded.
+ // No need to unsubscribe from DOM events, it's done automatically.
+ return () => {
+ // Unsubscribe from clock.
+ clock.granularity = "off";
+ clock.ontick = void 0;
+ }
+}
+```
\ No newline at end of file
diff --git a/app/index.js b/app/index.js
index 1dd00e2..db4d497 100755
--- a/app/index.js
+++ b/app/index.js
@@ -1,19 +1,17 @@
-import { init } from "./views";
+import views from "./views";
/**
* Definition for each view in the resources/views folder, and the associated
* JavaScript module is lazily loaded alongside its view.
*/
-const views = init(
- [
- ["view-1", () => import("./views/view-1")],
- ["view-2", () => import("./views/view-2")],
- ["view-3", () => import("./views/view-3")]
- ],
- "./resources/views/"
-);
+views.initialize({
+ 'view-1' : () => import("./views/view-1"),
+ 'view-2' : () => import("./views/view-2"),
+ 'view-3' : () => import("./views/view-3")
+});
+
+setTimeout( () => {
+ // Initialize application loading the first view.
+ views.replace("view-1");
+}, 1000 );
-// Select the first view (view-1) after 1 second
-setTimeout(() => {
- views.navigate("view-1");
-}, 1000);
diff --git a/app/views.js b/app/views.js
index b1a7711..d91a39a 100755
--- a/app/views.js
+++ b/app/views.js
@@ -4,47 +4,73 @@
import document from "document";
/**
- * Initialize the views module with views from a specific folder.
- * @param {Object[]} _views An array of views containing the view filename excluding
- * its file extension, and an associated JavaScript `import()`.
* @param {string} _prefix The folder name where the view files reside.
+ * Initialize the views module with views from a specific folder.
+ * @param {Object} _views An map of view names to an associated JavaScript `import()`.
*/
-export function init(_views, _prefix) {
- let views = _views;
- let viewsPrefix = _prefix;
- const viewsSuffix = ".gui";
- let viewSelected;
+
+let _views;
+let _unmount;
+let _history = [];
+let _options = [];
+let _currentOptions;
+
+const viewport = {
+ initialize( views ) {
+ _views = views;
+ },
+
+ // Currently selected view name.
+ current : null,
+
+ // Global context shared across all views.
+ context : {},
/**
- * Select a specific view by its index. The view's associated JavaScript is
- * loaded and executed, and the current view is replaced by the selected one.
- * @param {number} _index The array position of the view to be selected.
+ * Navigate to a specific view using its view name.
+ * @param {string} viewName The name of a .gui file, excluding its path or file extension.
+ * @param {object} options Object with options to be passed to view's initialize() function.
*/
- const select = _index => {
- const [viewGUI, viewJSLoader] = views[_index];
- viewSelected = viewGUI;
- viewJSLoader()
- .then(({ init }) => {
- document.replaceSync(`${viewsPrefix}${viewGUI}${viewsSuffix}`);
- init({ navigate });
+ replace( viewName, options ){
+ if( _unmount ){
+ _unmount();
+ }
+
+ _views[ viewName ]()
+ .then(({ initialize }) => {
+ document.replaceSync(`./resources/views/${viewName}.gui`);
+
+ if( _history.length ){
+ document.addEventListener( "keypress", evt => {
+ if( evt.key === "back"){
+ evt && evt.preventDefault();
+ viewport.back( evt );
+ }
+ });
+ }
+
+ viewport.current = viewName;
+ _currentOptions = options;
+ _unmount = initialize( viewport, options );
})
- .catch(() => {
- console.error(`Failed to load view JS: ${viewGUI}`);
+ .catch( e => {
+ console.error( e );
+ console.error(`Failed to load view JS: ${viewName}`);
});
- };
+ },
- /**
- * Navigate to a specific view using its view name.
- * @param {string} _viewName The name of a .gui file, excluding its path or
- * file extension.
- */
- const navigate = _viewName => {
- const index = views.indexOf(views.filter(el => el[0] == _viewName)[0]);
- select(index);
- };
-
- return {
- navigate,
- viewSelected: () => viewSelected
- };
+ /** Open the view as subview, so back button can be used to navigate back */
+ open( viewName, options ){
+ _history.push( this.current );
+ _options.push( _currentOptions );
+
+ viewport.replace( viewName, options );
+ },
+
+ /** If view was opened with views.open(), close it and navigate to the previous view */
+ back(){
+ viewport.replace( _history.pop(), _options.pop() );
+ }
}
+
+export default viewport;
\ No newline at end of file
diff --git a/app/views/view-1.js b/app/views/view-1.js
index aba6e95..14cda96 100755
--- a/app/views/view-1.js
+++ b/app/views/view-1.js
@@ -1,26 +1,32 @@
import document from "document";
-let views;
-
-export function init(_views) {
- views = _views;
- console.log("view-1 init()");
- onMount();
-}
-
/**
- * When this view is mounted, setup elements and events.
+ * View module must export initialize() function.
+ * @param {*} views - the global viewport object used to perform navigation between views.
+ * @param {*} options - optional parameter passed to `views.replace( options )` or `views.open( options )` call
*/
-function onMount() {
- let btn = document.getElementById("v1-button");
- btn.addEventListener("click", clickHandler);
-}
+export function initialize( views, options ){
+ console.log("view-1 init()");
-/**
- * Sample button click with navigation.
- */
-function clickHandler(_evt) {
- console.log("view-1 Button Clicked!");
- /* Navigate to another screen */
- views.navigate("view-2");
+ const btn2 = document.getElementById("v2-button");
+
+ /**
+ * Open view-2 as subview. `views.open()` enables "back" button in subview.
+ */
+ btn2.addEventListener( "click", evt => {
+ console.log("view-1 Button Clicked!");
+ /* Navigate to another screen */
+ views.open("view-2");
+ } );
+
+ const btn3 = document.getElementById("v3-button");
+
+ /**
+ * Open view-2 as subview and pass granularity as a parameter.
+ */
+ btn3.addEventListener( "click", evt => {
+ console.log("view-1 Button Clicked!");
+ /* Navigate to another screen */
+ views.open("view-3", { granularity : "seconds" });
+ } );
}
diff --git a/app/views/view-2.js b/app/views/view-2.js
index 9d85b17..430f2bb 100755
--- a/app/views/view-2.js
+++ b/app/views/view-2.js
@@ -1,37 +1,19 @@
import document from "document";
-let views;
+export function initialize( views ){
+ console.log("view-2 initialize()");
-export function init(_views) {
- views = _views;
- console.log("view-2 init()");
- onMount();
-}
+ /**
+ * When this view is mounted, setup elements and events.
+ */
+ const btn = document.getElementById("v2-button");
-/**
- * When this view is mounted, setup elements and events.
- */
-function onMount() {
- let btn = document.getElementById("v2-button");
- btn.addEventListener("click", clickHandler);
- document.addEventListener("keypress", keyHandler);
-}
-
-/**
- * Sample button click with navigation.
- */
-function clickHandler(_evt) {
- console.log("view-2 Button Clicked!");
- /* Navigate to another screen */
- views.navigate("view-3");
-}
-
-/**
- * Sample keypress handler to navigate backwards.
- */
-function keyHandler(evt) {
- if (evt.key === "back") {
- evt.preventDefault();
- views.navigate("view-1");
- }
-}
+ /**
+ * Sample button click with navigation.
+ */
+ btn.addEventListener("click", evt => {
+ console.log("view-2 Button Clicked!");
+ /* Navigate to another screen */
+ views.replace("view-3", { granularity : "seconds" });
+ });
+}
\ No newline at end of file
diff --git a/app/views/view-3.js b/app/views/view-3.js
index f4ec482..1c0b96e 100755
--- a/app/views/view-3.js
+++ b/app/views/view-3.js
@@ -1,37 +1,32 @@
import document from "document";
+import clock from "clock";
-let views;
+export function initialize( views, { granularity } ){
+ console.log("view3 initialize()");
-export function init(_views) {
- views = _views;
- console.log("view-3 init()");
- onMount();
-}
+ // Subscribe for clock updates...
+ clock.granularity = granularity; // seconds, minutes, hours
-/**
- * When this view is mounted, setup elements and events.
- */
-function onMount() {
- let btn = document.getElementById("v3-button");
- btn.addEventListener("click", clickHandler);
- document.addEventListener("keypress", keyHandler);
-}
+ clock.ontick = function(evt) {
+ console.log(evt.date.toString());
+ };
-/**
- * Sample button click with navigation.
- */
-function clickHandler(_evt) {
- console.log("view-3 Button Clicked!");
- /* Navigate to another screen */
- views.navigate("view-1");
-}
+ let btn = document.getElementById("v3-button");
+
+ /**
+ * Replace current view with view-2. Back button would still return us to view-1.
+ */
+ btn.addEventListener("click", evt => {
+ console.log("view-3 Button Clicked!");
+ /* Navigate to another screen */
+ views.replace("view-2");
+ });
-/**
- * Sample keypress handler to navigate backwards.
- */
-function keyHandler(evt) {
- if (evt.key === "back") {
- evt.preventDefault();
- views.navigate("view-2");
+ // View init functions can return clean-up functions executed before the view is unloaded.
+ // No need to unsubscribe from DOM events, it's done automatically.
+ return () => {
+ // Unsubscribe from clock.
+ clock.granularity = "off";
+ clock.ontick = void 0;
}
-}
+}
\ No newline at end of file
diff --git a/resources/views/view-1.gui b/resources/views/view-1.gui
index 7b8e2c1..2e955e8 100644
--- a/resources/views/view-1.gui
+++ b/resources/views/view-1.gui
@@ -1,6 +1,9 @@
diff --git a/resources/views/view-2.gui b/resources/views/view-2.gui
index 92529d1..b965fba 100644
--- a/resources/views/view-2.gui
+++ b/resources/views/view-2.gui
@@ -1,6 +1,6 @@
diff --git a/resources/views/view-3.gui b/resources/views/view-3.gui
index d1a20c0..aaf9621 100644
--- a/resources/views/view-3.gui
+++ b/resources/views/view-3.gui
@@ -1,6 +1,6 @@