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.
No comments:
Post a Comment