I wired up a
proof of concept
responsive browser UI that implements an application's views
as a series of panels. A single panel holds the UI focus at any given time,
and each panel is compact enough to render on a smart phone,
so on a phone sized device the UI renders a single panel at a time, but
on a larger device the UI may render 2, 3, or 4 adjoining panels depending
on the device size. For example, the
demo,
application just navigates through a tree of panels (a decision-tree control flow
is common in apps). On a typical laptop the UI renders up to 4 adjoining
panels where the right-most panel is the "focus" of the app, and a panel's
parent is to its left. If we make the browser window smaller, then the
UI eventually removes the left-most panel from the screen, and continues
removing the left panel as the window shrinks until only the focus-panel
remains.
I started on the demo while thinking about how to design
an application that from the beginning behaves in a reasonable way on a phone, tablet,
or PC.
I like the idea of
using a responsive design
to implement one web app that works well across
devices. It's too much work
to build a bunch of separate "apps" for iOS, Android, etc. with
custom UI designs for phone and tablet.
I recently started playing around with purecss
(a CSS framework forked from yui)
that includes an implementation of a "responsive grid".
Responsive grids like those in
Twitter bootstrap
and purecss leverage
CSS3 media queries
to implement a web page layout that behaves differently
on phone, tablet, and PC sized screens.
I implemented the demo with typescript and
YUI -
the code is on github.
The UI manager follows patterns like those in YUI's app framework.
The application registers views (panels) with the manager, and associates each panel with
a route based on the panel's parent and a basename filter. The view manager
then takes care of deciding which panels to render where when the application triggers a change in route.
The view manager also registers
a click-handler, so a.little-route links trigger a route change, but the application
can also manage the YUI router directly.
Eventually we wind up with application code like this:
// home page info
var versionInfo = Y.Node.create("<div id='app-info'><span class='app-title'>LittleEvents</span><br>Version 0.0 2013/06/21</p></div>");
var homePage = new Y.View();
homePage.get("container").append(versionInfo);
// router
var router = new Y.Router(
{ root: "/littleware_apps/blog/gridDemo.html" }
);
// inject homepage and router into view manager
var app = appModule.ViewManager.getFactory().create("app", "div#app", homePage, router);
//
// passed to registerPanel (below) - notifies panel of new path on view change.
// could also just add a listener on the router ...
//
var viewListener = function (panelStatus) {
if (panelStatus.state.name == "VISIBLE") {
var oldPath = panelStatus.panel.view.get("pathParts").join("/");
if (oldPath != panelStatus.path) {
//log.log("Setting new path: " + oldPath + " != " + panelStatus.path);
panelStatus.panel.view.set("pathParts", panelStatus.path.split("/"));
app.markPanelDirty(panelStatus.panel.id);
}
}
};
// register panels according to position in control-flow tree
app.registerRootPanel(panel1, panel1.id, viewListener);
// panel2 is a "child" of panel1
app.registerPanel(panel2, panel1.id, function () { return true; }, viewListener );
app.registerPanel(panel3, panel2.id, function () { return true; }, viewListener );
app.show();
..., and a library interface like this:
export module ViewManager {
...
/**
* Little helper manages the display of panels based
* on the routes triggered in router and size of display
* (phone, tablet, whatever).
* Assumes tree-based panel app.
*
* @class Manager
*/
export interface Manager {
name: string;
/**
* Root div under which to manage the panel UI
*/
container: Y.Node;
/**
* Info panel embedded in home page at path "/" - splash screen,
* version info, whatever placed above the "route index"
* @property homePage {Y.View}
*/
homePage: Y.View;
router: Y.Router;
/**
* List of routes to sort and display in the "index" on the home page
* along with the "root" panel paths.
*/
routeIndex: string[];
/**
* Child panel of homePage - "config"
* is reserved for internally managed configuration panels.
*/
registerRootPanel(
panel: LittlePanel,
baseName: string,
listener: (PanelStatus) => void
);
/**
* Register panels along with id of its parent, and a baseFilter
* that either accepts or rejects a route basename.
* For example, given some route /path/to/parent/bla/foo/frick,
* then for each element of the path [path,to,parent,bla,foo,frick],
* test that element against the children of a parent panel
* to determine which panel to associate with that route.
*
* @method registerPanel
*/
registerPanel(
panel: LittlePanel,
parentId: string,
routeFilter: (string) => bool,
listener: (PanelStatus) => void
);
/**
* By default the manager does not re-render a panel when it
* becomes "visible" unless the panel is "dirty".
* If the panel is already visible, then re-render panel once
* call stack clears: Y.later( 0, () => render() ).
*
* @method markPanelDirty
*/
markPanelDirty(panelId: string);
/**
* Triggers the manager to render its initial view (depends on the active route),
* and begin responding to routing and dirty-panel notifications in the "Active"
* ManagerState. NOOP if already active.
*
* @method show
*/
show();
}
export interface Factory {
/**
* Create a new view manager that manipulates DOM in the given selector
*
* @param name alphanumeric to associate with this manager - used as a key
* in persistence store
* @param selector CSS selector for div under which to build view
*/
create(name: string, selector: string, homePage: Y.View, router: Y.Router): Manager;
//create( name:string, selector: string): Manager;
/**
* Load manager state from persistent storage
*/
load( name:string ): Manager;
}
...
}
Anyway - I'm pretty happy with how things fit together in the demo,
but I won't really know how well this works until I use it with
a couple apps. We'll see how it goes.