Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworked the example to address some design issues #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
```
24 changes: 11 additions & 13 deletions app/index.js
Original file line number Diff line number Diff line change
@@ -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);
96 changes: 61 additions & 35 deletions app/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: perhaps push would be a more descriptive name here? To me, open vs replace isn't super clear about the difference between the two.

_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;
46 changes: 26 additions & 20 deletions app/views/view-1.js
Original file line number Diff line number Diff line change
@@ -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 ){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be neater and less prone to typos to use the default export?

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 => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Spacing on the arguments looks a bit odd on the event handlers here compared to other files

console.log("view-1 Button Clicked!");
/* Navigate to another screen */
views.open("view-3", { granularity : "seconds" });
} );
}
48 changes: 15 additions & 33 deletions app/views/view-2.js
Original file line number Diff line number Diff line change
@@ -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" });
});
}
Loading