From 8dbddaa6213d3d6f5d1c8e2f57a097ed735d5c46 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 21:18:28 -0500 Subject: [PATCH 01/10] Generic selectors --- view.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/view.js b/view.js index 015e08b..6949ade 100644 --- a/view.js +++ b/view.js @@ -1,25 +1,70 @@ import document from "document"; import { display } from "display"; -export const $ = $wrap( document ); +/** + * + * @param {*} selector = the sequence of '.class-name' | '#element-id' | 'element-type' + * @param {*} el = optional search root + */ +export function $( query, el ){ + const selectors = query.match(/\.|#|\S+/g); + let root = el || document; + + for( let i = 0; root && i < selectors.length; i++ ){ + const s = selectors[ i ]; + root = s === '#' ? $id( selectors[ ++i ], root ) : + s === '.' ? $classAndType( 'getElementsByClassName', selectors[ ++i ], root ) : + $classAndType( 'getElementsByTypeName', s, root ); + } -export function $wrap( element ){ - return selector => { - if( selector ){ - const symbol = selector.substr( 1 ); - return selector[ 0 ] === '.' ? - element.getElementsByClassName( symbol ) : - element.getElementById( symbol ); + return root; +} + +// Search subtrees by id... +function $id( id, arr ){ + if( Array.isArray( arr ) ){ + const res = []; + + for( let el of arr ){ + const x = el.getElementById( id ); + if( x ) res.push( x ); } + + return res; + } + + return arr.getElementById( id ); +} - return element; +// Search subtrees by class or type... +function $classAndType( method, arg, arr ){ + if( Array.isArray( arr ) ){ + const res = []; + + for( let el of arr ){ + for( let el2 of el[ method ]( arg ) ){ + res.push( el2 ); + } + } + + return res; } + + return arr[ method ]( arg ); } +export function $wrap( element ){ + return selector => selector ? $( selector, element ) : element; +} + +$.wrap = $wrap; + export function $at( selector ){ return $wrap( $( selector ) ); } +$.at = $at; + export class View { // el = $( '#your-view-id' ) _subviews = []; From 0ed1f93d9eaafcd7c0f76fca9620e4ef2adc652c Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 21:44:31 -0500 Subject: [PATCH 02/10] Updated docs --- README.md | 32 ++++++++++++++++++++++---------- view.js | 6 +----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3aa3d37..7c76b17 100644 --- a/README.md +++ b/README.md @@ -10,33 +10,46 @@ Copy `view.js` file to your project. ### DOM selectors -#### `function` $( selector ) +#### `function` $( query, [ element ] ) -Global jQuery-style `$` selector to access SVG DOM elements returning raw elements. No wrapping is performed, the raw element or elements array is returned. Just two simple selectors are supported: +jQuery-style `$` query to access SVG DOM elements. No wrapping is performed, the raw element or elements array is returned. +If an `element` argument is provided, the element's subtree will be searched; otherwise the search will be global. -- `$( '#id-of-an-element' )` - will call `document.getElementById( 'id-of-an-element' )` -- `$( '.class-name' )` - will call `document.getElementsByClassName( 'class-name' )` +The `query` is the space-separated sequence of the following simple selectors: -When called without arguments, returns the `document`. +- `#id-of-an-element` - will call `element.getElementById( 'id-of-an-element' )` and return en element. +- `.class-name` - will call `element.getElementsByClassName( 'class-name' )` and return elements array. +- `element-type` - will call `element.getElementsByTypeName( 'element-type' )` and return elements array. + +If all of the selectors in the query are `#id` selectors, the single element will be returned. Otherwise, an array of elements will be returned. ```javascript import { $ } from './view' // Will search for #element globally $( '#element' ).style.display = 'inline'; + +// Find the root element of the screen, then show all underlying elements having "hidden" class. +$( '#my-screen .hidden' ).forEach( ({ style }) => style.display = 'inline' ); + +// The same sequence made in two steps. See `$at()` function. +const screen = $( '#my-screen' ); +$( '.hidden', screen ).forEach( ({ style }) => style.display = 'inline' ); + ``` +> Avoid repeated ad-hoc $-queries. Cache found elements when possible. See Elements Group pattern. + #### `function` $at( id-selector ) -Create the $-function to search in the given DOM subtree. -Used to enforce DOM elements isolation for different views. +Create the $-function to search in the given DOM subtree. Used to enforce DOM elements isolation for different views. When called without arguments, returns the root element. ```javascript import { $at } from './view' -const $ = $at( '#myscreen' ); +const $ = $at( '#my-screen' ); // Make #myscreen visible $().style.display = 'inline'; @@ -47,10 +60,9 @@ $( '#element' ).style.display = 'inline'; #### `function` $wrap( element ) -Create the $-function to search in the given DOM subtree wrapping the given element. Internally, +Create the $-function to search in the given DOM subtree wrapping the given element. ```javascript -const $ = $wrap( document ); const $at = selector => $wrap( $( selector ) ); ``` diff --git a/view.js b/view.js index 6949ade..bfd05f1 100644 --- a/view.js +++ b/view.js @@ -1,11 +1,7 @@ import document from "document"; import { display } from "display"; -/** - * - * @param {*} selector = the sequence of '.class-name' | '#element-id' | 'element-type' - * @param {*} el = optional search root - */ +// Main DOM search method. export function $( query, el ){ const selectors = query.match(/\.|#|\S+/g); let root = el || document; From 9165e9ceda6934b4384956f7b6777455d2296695 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 21:51:39 -0500 Subject: [PATCH 03/10] Added view.onRender() --- README.md | 8 +++++--- view.js | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7c76b17..80d4260 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,8 @@ View is the stateful group of elements. The difference from the elements group i - `view.el` - optional root view element. Used to show and hide the view when its mounted and unmounted. - `view.mount()` - make the `subview.el` visible, call the `subview.onMount()` hook. - `view.onMount()` - place to insert subviews and register events listeners. -- `view.render()` - render the view and all of its subviews. +- `view.render()` - render the view and all of its subviews if the display is on. No-op otherwise. +- `view.onRender()` - place actual UI update code here. - `view.unmount()` - hide the `subview.el`, unmount all the subviews, call the `view.onUnmount()` hook. - `view.onUnmount()` - place to unregister events listeners. - `view.insert( subview )` - insert and mount the subview. @@ -132,7 +133,8 @@ class Timer extends View { minutes = $( '#minutes' ); seconds = $( '#seconds' ); - render(){ + + onRender(){ const { ticks } = this; this.minutes.text = Math.floor( ticks / 60 ); this.seconds.text = ( ticks % 60 ).toFixed( 2 ); @@ -154,7 +156,7 @@ It's the singleton which is globally accessible through the `Application.instanc - `Application.instance` - access an application instance. - `Application.switchTo( 'screen' )` - switch to the screen which is the member of an application. - `app.screen` - property used to retrieve and set current screen view. -- `app.render()` - render all the subviews, _if display is on_. Is called automaticaly when display goes on. +- `app.render()` - render all the subviews, _if display is on_. It's called automaticaly when display goes on. ```javascript class MyApp extends Application { diff --git a/view.js b/view.js index bfd05f1..f09c58c 100644 --- a/view.js +++ b/view.js @@ -107,10 +107,17 @@ export class View { } render(){ - for( let subview of this._subviews ){ - subview.render(); + if( display.on ){ + for( let subview of this._subviews ){ + subview.render(); + } + + this.onRender(); } - } + } + + // Callback called on render + onRender(){} } export class Application extends View { @@ -130,13 +137,6 @@ export class Application extends View { instance.screen = instance[ screenName ]; } - render(){ - // Prevent render when screen is off. - if( display.on ){ - super.render(); - } - } - // Application is the singleton. Here's the instance. static instance = null; From 9e0c0b5864e813329812f2b4b432f4f1478c9139 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 21:53:36 -0500 Subject: [PATCH 04/10] Removed redundant code --- view.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/view.js b/view.js index f09c58c..d4f8660 100644 --- a/view.js +++ b/view.js @@ -53,14 +53,10 @@ export function $wrap( element ){ return selector => selector ? $( selector, element ) : element; } -$.wrap = $wrap; - export function $at( selector ){ return $wrap( $( selector ) ); } -$.at = $at; - export class View { // el = $( '#your-view-id' ) _subviews = []; From 3240147a2e480554105a43b0ec49f123e3569d51 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 21:55:26 -0500 Subject: [PATCH 05/10] minor optimization --- view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/view.js b/view.js index d4f8660..05c953a 100644 --- a/view.js +++ b/view.js @@ -1,9 +1,11 @@ import document from "document"; import { display } from "display"; +const querySplitter = /\.|#|\S+/g; + // Main DOM search method. export function $( query, el ){ - const selectors = query.match(/\.|#|\S+/g); + const selectors = query.match( querySplitter ); let root = el || document; for( let i = 0; root && i < selectors.length; i++ ){ From 12c8b3cacb746780f1f3d2ba9d7d4972cf4dd784 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 22:24:29 -0500 Subject: [PATCH 06/10] Added project boilerplate --- app/index.js | 23 +++++++++++++++++++++++ app/screen1.js | 24 ++++++++++++++++++++++++ app/screen2.js | 22 ++++++++++++++++++++++ view.js => app/view.js | 6 ++++-- resources/index.gui | 4 ++++ resources/screen1.gui | 4 ++++ resources/screen2.gui | 3 +++ resources/styles.css | 4 ++++ resources/widgets.gui | 6 ++++++ 9 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 app/index.js create mode 100644 app/screen1.js create mode 100644 app/screen2.js rename view.js => app/view.js (99%) create mode 100644 resources/index.gui create mode 100644 resources/screen1.gui create mode 100644 resources/screen2.gui create mode 100644 resources/styles.css create mode 100644 resources/widgets.gui diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..a8f57c3 --- /dev/null +++ b/app/index.js @@ -0,0 +1,23 @@ +import { Application } from './view' +import { Screen1 } from './screen1' +import { Screen2 } from './screen2' + +class MultiScreenApp extends Application { + screen1 = new Screen1(); + screen2 = new Screen2(); + + onMount(){ + this.screen = this.screen2; // Same as Application.switchTo( 'screen1' ); + + document.onkeypress = this.switchScreens; + } + + switchScreens = ({ key }) => { + if( key === 'down' ){ + Application.switchTo( this.screen === this.screen1 ? 'screen2' : 'screen1' ); + } + } + +} + +MultiScreenApp.start(); \ No newline at end of file diff --git a/app/screen1.js b/app/screen1.js new file mode 100644 index 0000000..b30e549 --- /dev/null +++ b/app/screen1.js @@ -0,0 +1,24 @@ +import { View, $at } from './view' +import { clock } from 'clock' + +const $ = $at( '#screen-1' ); + +export class Screen1 extends View { + el = $(); + time = $( '#current-time' ); + + onMount(){ + clock.granularity = 'seconds'; + clock.ontick = () => this.render(); + } + + onUnmount(){ + clock.granularity = 'off'; + clock.ontick = null; + } + + onRender(){ + this.time.text = new Date(); + } +} + diff --git a/app/screen2.js b/app/screen2.js new file mode 100644 index 0000000..f11ff42 --- /dev/null +++ b/app/screen2.js @@ -0,0 +1,22 @@ +import { View, $at } from './view' + +const $ = $at( '#screen-2' ); + +export class Screen1 extends View { + el = $(); + + onMount(){ + // TODO: insert subviews... + // TODO: subscribe for events... + } + + onUnmount(){ + // TODO: unsubscribe from events... + } + + onRender(){ + // TODO: put DOM manipulations here... + // Call this.render() to update UI. + } +} + diff --git a/view.js b/app/view.js similarity index 99% rename from view.js rename to app/view.js index 05c953a..f766eea 100644 --- a/view.js +++ b/app/view.js @@ -122,6 +122,10 @@ export class Application extends View { // Current application screen. set screen( view ){ if( this._screen ) this.remove( this._screen ); + + // Poke the display so it will be on after the screen switch... + display.poke(); + this.insert( this._screen = view ).render(); } @@ -129,8 +133,6 @@ export class Application extends View { // Switch the screen static switchTo( screenName ){ - // Poke the display so it will be on after the screen switch... - display.poke(); const { instance } = Application; instance.screen = instance[ screenName ]; } diff --git a/resources/index.gui b/resources/index.gui new file mode 100644 index 0000000..bbfcec5 --- /dev/null +++ b/resources/index.gui @@ -0,0 +1,4 @@ + + + + diff --git a/resources/screen1.gui b/resources/screen1.gui new file mode 100644 index 0000000..9475ce6 --- /dev/null +++ b/resources/screen1.gui @@ -0,0 +1,4 @@ + + Current time: + ------------ + \ No newline at end of file diff --git a/resources/screen2.gui b/resources/screen2.gui new file mode 100644 index 0000000..625b1e4 --- /dev/null +++ b/resources/screen2.gui @@ -0,0 +1,3 @@ + + Screen 2 + \ No newline at end of file diff --git a/resources/styles.css b/resources/styles.css new file mode 100644 index 0000000..66019a6 --- /dev/null +++ b/resources/styles.css @@ -0,0 +1,4 @@ +text { + font-family: System-Regular; + fill: white; +} \ No newline at end of file diff --git a/resources/widgets.gui b/resources/widgets.gui new file mode 100644 index 0000000..91d38ce --- /dev/null +++ b/resources/widgets.gui @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From f553f19b5dba0248e6dc938a5120d79a7f62387f Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Mon, 1 Jan 2018 22:48:52 -0500 Subject: [PATCH 07/10] Updated docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 80d4260..fc0bc20 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ An application for the Fitbit Ionic can quickly become a mess. This micro-framew ## Installation -Copy `view.js` file to your project. +This repository contains the starting boilerplate for the multi-screen project. You could copy all the files as they are, +or just copy the `/app/view.js` file to your project. ## API From 372e0a68b74bc404b8ffaa5c94349b4d423c0f58 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Tue, 2 Jan 2018 00:34:17 -0500 Subject: [PATCH 08/10] Added comments --- app/index.js | 12 +++++++++--- app/screen1.js | 43 ++++++++++++++++++++++++++++++++++++------- resources/screen1.gui | 4 ++-- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/app/index.js b/app/index.js index a8f57c3..1883adb 100644 --- a/app/index.js +++ b/app/index.js @@ -6,18 +6,24 @@ class MultiScreenApp extends Application { screen1 = new Screen1(); screen2 = new Screen2(); + // Called once on application's start... onMount(){ - this.screen = this.screen2; // Same as Application.switchTo( 'screen1' ); + // Set initial screen. + // Same as Application.switchTo( 'screen1' ), which might be used to switch screen from anywhere. + this.screen = this.screen2; - document.onkeypress = this.switchScreens; + document.onkeypress = this.onKeyPress; } - switchScreens = ({ key }) => { + // Event handler, must be pinned down to the class to preserve `this`. + onKeyPress = ({ key }) => { if( key === 'down' ){ + // Just switch between two screens we have. Application.switchTo( this.screen === this.screen1 ? 'screen2' : 'screen1' ); } } } +// Create and start the application. MultiScreenApp.start(); \ No newline at end of file diff --git a/app/screen1.js b/app/screen1.js index b30e549..c86553c 100644 --- a/app/screen1.js +++ b/app/screen1.js @@ -4,21 +4,50 @@ import { clock } from 'clock' const $ = $at( '#screen-1' ); export class Screen1 extends View { - el = $(); - time = $( '#current-time' ); + // Root view element used to show/hide the view. + el = $(); // Just the #screen-1 element. + + // Element group. + time = new Time(); + + // The view state. + seconds = 0; onMount(){ + // Subscribe for the clock... clock.granularity = 'seconds'; - clock.ontick = () => this.render(); + clock.ontick = this.onTick; } - onUnmount(){ - clock.granularity = 'off'; - clock.ontick = null; + onTick = () => { + // Update the state and force render. + this.seconds++; + this.render(); } onRender(){ - this.time.text = new Date(); + // Render the elements group. + this.time.render( this.seconds ); + } + + onUnmount(){ + // Unsubscribe from the clock + clock.granularity = 'off'; + clock.ontick = null; } } +// Elements group. Used to group the DOM elements and their update logic together. +class Time { + // Elements... + minutes = $( '#minutes' ); + seconds = $( '#seconds' ); + + // UI update method(s). Can have any name, it's just the pattern. + // Element groups have no lifecycle hooks, thus all the data required for UI update + // must be passed as arguments. + render( seconds ){ + this.minutes.text = ( seconds / 60 ) | 0; + this.seconds.text = seconds % 60; + } +} \ No newline at end of file diff --git a/resources/screen1.gui b/resources/screen1.gui index 9475ce6..c854622 100644 --- a/resources/screen1.gui +++ b/resources/screen1.gui @@ -1,4 +1,4 @@ - Current time: - ------------ + -- \ No newline at end of file From 0c7a270ccd8ecb279fc01d612be4802889007c37 Mon Sep 17 00:00:00 2001 From: Vlad Balin Date: Tue, 2 Jan 2018 12:03:07 -0500 Subject: [PATCH 09/10] Minor fixes --- app/index.js | 3 +-- app/screen1.js | 0 app/screen2.js | 8 +++++++- app/view.js | 0 package.json | 15 +++++++++++++++ resources/index.gui | 2 +- resources/screen1.gui | 2 +- resources/screen2.gui | 0 resources/styles.css | 10 ++++++---- resources/widgets.gui | 2 +- 10 files changed, 32 insertions(+), 10 deletions(-) mode change 100644 => 100755 app/index.js mode change 100644 => 100755 app/screen1.js mode change 100644 => 100755 app/screen2.js mode change 100644 => 100755 app/view.js create mode 100755 package.json mode change 100644 => 100755 resources/index.gui mode change 100644 => 100755 resources/screen1.gui mode change 100644 => 100755 resources/screen2.gui mode change 100644 => 100755 resources/styles.css mode change 100644 => 100755 resources/widgets.gui diff --git a/app/index.js b/app/index.js old mode 100644 new mode 100755 index 1883adb..a709ecc --- a/app/index.js +++ b/app/index.js @@ -21,8 +21,7 @@ class MultiScreenApp extends Application { // Just switch between two screens we have. Application.switchTo( this.screen === this.screen1 ? 'screen2' : 'screen1' ); } - } - + } } // Create and start the application. diff --git a/app/screen1.js b/app/screen1.js old mode 100644 new mode 100755 diff --git a/app/screen2.js b/app/screen2.js old mode 100644 new mode 100755 index f11ff42..b044953 --- a/app/screen2.js +++ b/app/screen2.js @@ -1,19 +1,25 @@ import { View, $at } from './view' +// Create the root selector for the view... const $ = $at( '#screen-2' ); -export class Screen1 extends View { +export class Screen2 extends View { + // Specify the root view element. + // When set, it will be used to show/hide the view on mount and unmount. el = $(); + // Lifecycle hook executed on `view.mount()`. onMount(){ // TODO: insert subviews... // TODO: subscribe for events... } + // Lifecycle hook executed on `view.unmount()`. onUnmount(){ // TODO: unsubscribe from events... } + // Custom UI update logic, executed on `view.render()`. onRender(){ // TODO: put DOM manipulations here... // Call this.render() to update UI. diff --git a/app/view.js b/app/view.js old mode 100644 new mode 100755 diff --git a/package.json b/package.json new file mode 100755 index 0000000..19dbe67 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "fitbit": { + "appUUID": "d84e4e76-0ded-40f2-98f7-f4de4dc27fc4", + "appType": "app", + "appDisplayName": "boilerplate", + "iconFile": "resources/icon.png", + "wipeColor": "#607d8b", + "requestedPermissions": [], + "i18n": { + "en": { + "name": "boilerplate" + } + } + } +} \ No newline at end of file diff --git a/resources/index.gui b/resources/index.gui old mode 100644 new mode 100755 index bbfcec5..96d3d5e --- a/resources/index.gui +++ b/resources/index.gui @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/resources/screen1.gui b/resources/screen1.gui old mode 100644 new mode 100755 index c854622..1ada958 --- a/resources/screen1.gui +++ b/resources/screen1.gui @@ -1,4 +1,4 @@ - -- -- \ No newline at end of file diff --git a/resources/screen2.gui b/resources/screen2.gui old mode 100644 new mode 100755 diff --git a/resources/styles.css b/resources/styles.css old mode 100644 new mode 100755 index 66019a6..a5b727e --- a/resources/styles.css +++ b/resources/styles.css @@ -1,4 +1,6 @@ -text { - font-family: System-Regular; - fill: white; -} \ No newline at end of file +.defaultText { + font-size: 32; + font-family: System-Regular; + font-weight: regular; + text-length: 32; +} diff --git a/resources/widgets.gui b/resources/widgets.gui old mode 100644 new mode 100755 index 91d38ce..1892304 --- a/resources/widgets.gui +++ b/resources/widgets.gui @@ -3,4 +3,4 @@ - \ No newline at end of file + From 6f876702c43a2d04e8a55af5761aa4cc3dfc9fcd Mon Sep 17 00:00:00 2001 From: kmpm Date: Sun, 7 Jan 2018 21:34:19 +0100 Subject: [PATCH 10/10] bugfixes and actually showing something --- app/index.js | 1 + app/screen1.js | 2 +- resources/screen1.gui | 2 +- resources/styles.css | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/index.js b/app/index.js index a709ecc..2fcd76b 100755 --- a/app/index.js +++ b/app/index.js @@ -1,3 +1,4 @@ +import document from "document"; import { Application } from './view' import { Screen1 } from './screen1' import { Screen2 } from './screen2' diff --git a/app/screen1.js b/app/screen1.js index c86553c..762b92a 100755 --- a/app/screen1.js +++ b/app/screen1.js @@ -1,5 +1,5 @@ import { View, $at } from './view' -import { clock } from 'clock' +import clock from 'clock' const $ = $at( '#screen-1' ); diff --git a/resources/screen1.gui b/resources/screen1.gui index 1ada958..6cd069d 100755 --- a/resources/screen1.gui +++ b/resources/screen1.gui @@ -1,4 +1,4 @@ -- - -- + -- \ No newline at end of file diff --git a/resources/styles.css b/resources/styles.css index a5b727e..558ceb1 100755 --- a/resources/styles.css +++ b/resources/styles.css @@ -1,6 +1,7 @@ -.defaultText { +text { font-size: 32; font-family: System-Regular; font-weight: regular; text-length: 32; + fill: white; }