tl;dr; view the demo
I’ve been doing a lot of front-end development these past couple years, in between bouts of the complete opposite. I guess you might call it developer pivoting, which seems to happen frequently with startups [otherwise known as fighting fires]. I enjoy being able to push my limits every time I have the chance to revisit the same problem I encountered in the past, but always armed with a better skill-set than before; this especially applies to UI. Since the release of Bootstrap, I’ve taken notice to the seemingly endless amount of new techniques and libraries available for solving interface problems better. The reinvention, or building upon, was no-doubt absolutely necessary - not justHacker News propoganda. Building large, complex web applications [which need to support all browsers back to _Internet Explorer 7_] can be difficult, especially now that there’s mobile and tablets to account for. I set out to learn these new tools, and in the process, had a little bit more fun building UI than I normally do.
[play 2.0 & backbone.js]
Let’s let history tell the story. Two years ago, I would have written a PHP-based application in CodeIgniter, and heavily mixed Javascript and PHP
, using a library like YUI as the AJAX wrapper for building UI elements from data sets:
YAHOO.util.Event.addListener(window, "load", function () { YAHOO.example.XHR_JSON = function () { var myColumnDefs = [{ key: "status", label: "Status", sortable: true, resizeable: true }, { key: "name", label: "Name", sortable: true, resizeable: true }]; var myDataSource = new YAHOO.util.DataSource("doThis.php?someParam=<? echo $someParam; ?>&someOtherParam=<? echo $someOtherParam; ?>"); myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON; myDataSource.connXhrMode = "queueRequests"; myDataSource.responseSchema = { resultsList: "ResultSet.Result", fields: ["status", "name"] }; singleSelectDataTable = new YAHOO.widget.DataTable("single", myColumnDefs, myDataSource, { selectionMode: "single", width: "100%" }); singleSelectDataTable.on('initEvent', function () { YAHOO.util.Dom.setStyle(singleSelectDataTable.getTableEl(), 'width', '100%'); }); // Subscribe to events for row selection singleSelectDataTable.subscribe("rowMouseoverEvent", singleSelectDataTable.onEventHighlightRow); singleSelectDataTable.subscribe("rowMouseoutEvent", singleSelectDataTable.onEventUnhighlightRow); singleSelectDataTable.subscribe("rowClickEvent", singleSelectDataTable.onEventSelectRow); singleSelectDataTable.subscribe("rowSelectEvent", function (trEl, record) { var status = trEl.record._oData.status; var name = trEl.record._oData.name; ... this stuff continues }(); });
One year ago, I would have been writing a Java-based application in Play! 1.2.4, and echoing HTML inside Javascript
. I remember re-rendering the headers of ul
blocks and having to create my own repaint
methods [because of CSS issues] when building UI elements from data sets:
function HTMLCreator() { this.createDivTableHeader = function () { var html = '<li class="ticket-header">'; html += '<ul>'; html += '<li class="ticket-header-ticket" style="width: 10%;">Status</li>'; html += '<li class="ticket-header-activity" style="width: 20%;">Duration</li>'; html += '<li class="ticket-header-user" style="width: 20%;">Agent</li>'; html += '<li class="ticket-header-priority" style="width: 20%;">Caller</li>'; html += '<li class="ticket-header-age" style="width: 20%;">Company</li>'; html += '<li class="ticket-header-age" style="width: 5%;">Flags</li>'; html += '<li class="ticket-header-age" style="width: 5%;">Priority</li>'; html += '</ul>'; html += '</li>'; return html; }; ... this stuff continues }
It makes me want to cry/pout when looking back. Building sustainable, reusable features like that took forever. I didn’t know any better, and the learning curve of the shiny new frameworks like Backbone.js was rather steep; especially when you wanted to throw in learning trueCoffeeScript at the same time [because it just seemed like the thing to do].
When decision had to be made on architecture at at 2lemetry, we chose Play! 2.0 and Scala as our web application back-end/language; it supports the massive amounts of Java libraries out there, and works with AKKA. We adopted the framework before it was even released, and that only caused a few headaches :)
It also had multiple features we were looking for in terms of front-end development, namely compiled CoffeeScript, routes, and templates
. Something I think is especially important to note: do not use Scala templates to render collections of data to the page
; it makes it much harder to manipulate.
[pre-reqs]
[standards]
- All CoffeeScript, all the time.
No Javascript inside HTMl files
.- Format HTML.
- Format CoffeeScript: two-space indents.
- No
""
, only''
in Javascripts, or no double-quotes.
[functionality]
The application takes into account things I ran into on a regular basic across any application, but standardizes a workflow using the concept of Pages[more on that later]. It starts with two arbitrarily named Pages, named as Actions; we all know how quickly what needs to show up first on a dashboard can change in development cycles. The first page displays a Collection in table
form. Models can be added to the Collection, or removed from it, and DOM elements are manipulated in the process. The second page displays a Collection in div
form, and updates when a WebSocket server running locally [a message is triggered by button-click], pushes a new Model to the page.
There are some generic helpers that do some nice modifications to Collections, as well as handle browser compatibility - geographic location is pulled when not using Internet Explorer.
On a Backbone.js level, nothing is assumed as working out of the box - all Collection and Modelparse
methods are overridden, as this seemed to be encountered more frequently than anything. The CompositeViews and ItemViews provided by the Marionette plugin all reflect listening to the Backboneon 'change'
events for this overriding.
Other features that came from real-world use-cases:
- A WebSocket Model which opens connection when then second page is viewed; the connection is than closed when the page changes.
- A generic implementation of a Model[how I would build breadcrumbs], and one that pulls from a local API.
[marionette]
When initially exploring Backbone and CoffeeScript together, Views confused me; there was way too much DOM referencing inside the Javascript - I’m still not OK with double-references, but at least there’s some transparency in the way I approach it. Marionette provides a very modular approach to building UI with it’s Regions and Layouts, and even though the learning-curve was doubly-steep, it makes the Javascript files more readable.
[run]
In order to run the application, you’ll need to download Play!, have Java installed, and generally know how to do stuff.
Franks-MacBook-Pro:Desktop franklovecchio$ git clone https://github.com/franklovecchio/playback.git Cloning into playback... remote: Counting objects: 151, done. remote: Compressing objects: 100% (113/113), done. remote: Total 151 (delta 38), reused 139 (delta 29) Receiving objects: 100% (151/151), 146.67 KiB | 96 KiB/s, done. Resolving deltas: 100% (38/38), done. Franks-MacBook-Pro:Desktop franklovecchio$ cd playback Franks-MacBook-Pro:playback franklovecchio$ sudo play run
[ide]
Import the project into your editor of choice; if it’s Eclipse, like in my case, the Scala plugin and Play! do not always play well. Some things to be aware of:
When adding new template files, you have to specify
Ok
in the controller,play run
the application, thenrefresh
the Eclipse project to be able to reference the newly added template. It’s a bit silly.You won't need to do this for the demo application, just if you're creating a new one
.Lots of CoffeeScript files slow down the app compile-time, but only when packaged Javascripts are changed, and it’s quite noticeable. I’m not sure why they haven’t made this better. I’ve even tried an alternative compiler to no avail.
[templating]
Using Play!’s Scala templates, we have the ability to manage our assets in one place. These templates require a .scala
prefix on any HTML file you wish to make a template. It makes it really easy to pass data from the server side to the templates, but I only use them for resources [I’m trying to be as framework-agnostic as possible]. Think of them as something similar to Mustache or Underscore.
Our main application template is located at /app/views/app.scala.html
. It is strictly a dependency management tool for passing resources to another template, main.scala.html
, where we’ll hold all our static resources. By doing this we can define separate applications
, if we like, inside a single Play! app, [think PROD, STAGING, and DEV]. There isn’t a way to share client-side resources between two separate Play! apps like there is on the server-side [vis-a-vis plugins], and creating separate apps without using separate git branches
seemed to work well like this. The whole file is displayed below, and references every non third-party Javascript we’re using [typically everything we wrote]:
@() @main( css = List(), headJs = List(), bodyJs = List(), analytics = "Google Analytics ID" ) { @layout( // CSS List( // App "app.css", // Action1 "action1.css", // Action2 "action2.css" ), // _Javascripts_ // Listed in order, each one being dependent (or not) on the other. List( // Namespaces (window object) "app/namespaces.js", // Utils and Logging "utils/utils.js", "utils/logger.js", // App "app/events.js", "app/state.js", "app/ui.js", "app/errors.js", "app/routes.js", // Helpers "helpers/compatibility.js", "helpers/collections.js", // Models "models/item.js", // Generic, think breakcrumbs "models/thing.js", "models/socket.js", // Collections "collections/items.js", "collections/things.js", // Pages // App "views/region.js", "views/layout.js", "views/nav.js", // Action1 "pages/action1/view.js", // Marionette Layout "pages/action1/events.js", // View logic "pages/action1/state.js", // View state, as long as the page is rendered "pages/action1/ui.js", // DOM manipulation // Action2 "pages/action2/view.js", "pages/action2/events.js", "pages/action2/state.js", "pages/action2/ui.js", "app/app.js" // Start the app here ) ) }
It’s pretty self-explanatory, and everything at bottom of the List is dependent on the files described above it. Pages are displayed in blocks, and are an agnostic concept that came from developing these applications. A Page is simply an Eclipse package and has four class files associated with it [UI isn’t always used].
The main.scala.html
template holds all references to the third-party libraries we are using to make the application browser compatible back to Internet Explorer 7; there’s nothing like building on the shoulders of giants
:
@( css: List[String], headJs: List[String], bodyJs: List[String], analytics: String )(content: Html) <!-- css: css file path headJs: javascript file paths DOM relevant bodyJs: javascript file paths loaded at the end of the page content: html body --> <!doctype html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=8" /> <title>Playback</title> <!-- Bootstrap styles. Makes it easy to develop quickly. --> <link rel="stylesheet" href="/assets/stylesheets/third-party/bootstrap.min.css"> <link rel="stylesheet" href="/assets/stylesheets/third-party/bootstrap-responsive.min.css"> <link rel="stylesheet" href="/assets/stylesheets/third-party/prettify.css"> <!-- Injected head CSS from Scala templates. --> @for(cssFile <- css) { <link rel="stylesheet" href="/assets/stylesheets/@cssFile"> } <!-- Logging http://benalman.com/code/projects/javascript-debug/docs/files/ba-debug-js.html --> <!-- Takes care of IE issues with console, adds built in ability to turn on/off logs, adds various states. --> <script src="/assets/_Javascripts_/debug.min.js"></script> <!-- jQuery --> <script src="/assets/_Javascripts_/jquery-1.7.1.min.js"></script> <!-- Takes care of IE issues with jQuery. --> <script src="/assets/_Javascripts_/augment.js"></script> <!-- Bootstrappy --> <script src="/assets/_Javascripts_/prettify.js"></script> <script src="/assets/_Javascripts_/less-1.3.0.min.js"></script> <!-- Backbone/Underscore/Marionette --> <script src="/assets/_Javascripts_/json2.js"></script> <script src="/assets/_Javascripts_/underscore.min.js"></script> <script src="/assets/_Javascripts_/backbone.min.js"></script> <script src="/assets/_Javascripts_/backbone.marionette-0.8.1.min.js"></script> <!-- Injected head _Javascripts_ from Scala templates --> @for(jsFile <- headJs) { <script src="/assets/_Javascripts_/@jsFile"></script> } </head> <body data-spy="scroll" data-target=".subnav" data-offset="50" onload="prettyPrint();"> @content <!-- Bootstrap! --> <script src="/assets/_Javascripts_/bootstrap.min.js"></script> <!-- Injected body _Javascripts_ go at the end of the page for faster loading. --> @for(jsFile <- bodyJs) { <script src="/assets/_Javascripts_/@jsFile"></script> } <!-- Google Analytics --> <!-- Allow multiple applications to be displayed with a dynamic ID --> <div id="google-analytics" class="hide">@analytics</div> <script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', $('div#google-analytics').html()]); _gaq.push(['_trackPageview']); (function () { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script> </body> </html>
[namespaces]
I may have overdone it a bit, but after reading through various blogs and StackOverflow rants, I decided that the ability to know what’s in a global namespace at any given time could be useful, even if they’re a bit long in this case [App.Pages.Views.Action1 could easily be App.P.V.A1]. All class files are scoped separately than their instantiated counterparts - I’m not sure if this is good practice or not, but it works. Nothing is ever set in window
except for App
, Portal
, and Helpers
:
# Holds the Marionette application. window.Portal = { } # Holds the entire application. window.App = { } # Holds class references to all Helpers. window.Helpers = { } # Holds class references for app/pages to all Views, Events, UI, Errors, and states. App.Config = { } App.Views = { } App.Events = { } App.UI = { } App.State = { } App.Errors = { } # Holds instantiated app references to global Config, Events, UI, Errors, and states. App.Global = { } # Holds instantiated page references to all Views, Events, UI, Errors, and states. App.Pages = { } App.Pages.Views = { } App.Pages.Events = { } App.Pages.UI = { } App.Pages.State = { } # Holds instantiated references to all Helpers. App.Helpers = { } # Holds class references to all Backbone Models (that are not considered Helpers). App.Models = { } # Holds class references to all Backbone Collections. App.Collections = { } # Holds class references to all Marionette Regions. App.Regions = { } # Holds class references to all Marionette Layouts. App.Layouts = { } # Holds class references to all Marionette ItemViews. App.ItemViews = { } # Holds class references to all Marionette CompositeViews. App.CompositeViews = { } # Holds instantiated View references. App.Rendered = { } # Holds all global data at an application level. App.Data = { } # Holds all global data at a page level. App.Data.Pages = { }
[logging]
By appending a GET parameter of ?log=true
, we have the ability to turn on logging to the application, and it’s Internet Explorer safe! By default, logging is turned off
.
For application logging, I got tired of writing debug.info 'MyClass.myMethod()
really quickly. The solution is to prepend every method but constructor
with @trace
to make debug.info
happen automatically when the method is called. Note: one minor issue is that App.SomeSpace.MyClass
logs as MyClass
.
[routes]
In /app/assets/javascripts/app/app.coffee
, where the application is started from a Backbone perspective, I made it so that creating a page does not require modification of the Routes
class; instead, you’d simply follow the convention for creating a Page, and you can set the default route programatically:
Note: using some Googled trickery, routes are not case sensitive!
ROUTES = 'action1/:page': 'standardRouter' 'action2/:page': 'standardRouter' '*path': 'defaultRouter' ... # Start Backbone history. App.Router = new Routes( routes: ROUTES ) App.Router.setDefaultRoute 'action1/1' Backbone.history.start()
The reason for following :view/:page
is simple; it’s what I could break down every URL to. If I wanted to keep state via the URL in a more complicated fashion, I’d simply override standardRouter
and delimit the data passed through.
[conventions]
I think the best way to follow the Page process is to show modifications necessary for adding one. Before we do that, let’s look at some conventions used:
- In Scala template files, always reference DOM elements that are bound to Backbone Views with a
do-
prefix. - In Scala template files, always reference DOM elements that are bound to our UI classes, so we don’t go on a renaming spree and break stuff.
- Pages are alwaysMarionette Layouts; it makes it easy to add new features by just adding more Regions
- Pages are always numbered; it makes it easier to swap references, placement, and makes routing simpler. Most of the time, this works really well.
- Pages are generic references to Actions, as when developing an application, the page functionality - and it’s name - might change on completion.
- States for Pages are cleared when the view changes. If you need to keep something in state globally, you can use the App.State class.
- Templates reference data via
obj.someId
to avoid breaking when an attribute is undefined, even if it does render asundefined
. - All Pages have their own CSS files, and classes inside them are prefixed based on their name; this makes it easier to track down issues at-a-glance.
[application core]
The entire Backbone application instantiation can be summed up by 42 lines of CoffeeScript:
ROUTES = 'action1/:page': 'standardRouter' 'action2/:page': 'standardRouter' '*path': 'defaultRouter' # App. App.Global.Events = new Events() App.Global.UI = new UI() App.Global.State = new State() App.Global.Errors = new Errors() # Helpers. App.Helpers.Compatibility = new Helpers.Compatibility() App.Helpers.Collections = new Helpers.Collections() # Geolocation. if !App.Helpers.Compatibility.isIE() App.Global.Events.geoLocate() # Start Marionette. App.Portal = new Backbone.Marionette.Application() App.Portal.start { } # Don't pass it anything # Setup default Marionette Layouts/Regions. App.Global.State.setDefaultRegion new App.Regions.Default() App.Global.State.setDefaultLayout new App.Layouts.Default() # Render the application HTML wireframe. App.Global.State.getDefaultRegion().show App.Global.State.getDefaultLayout() App.Global.State.getNavRegion().show new App.Views.Nav() # Start Backbone history. App.Router = new Routes( routes: ROUTES ) App.Router.setDefaultRoute 'action1/1' Backbone.history.start()
The process, on each page reload:
- Declare routes.
- Instantiate application classes [these are equivalent to Pages ].
- Geo-locate.
- Setup/render the default application template with Marionette.
- Start the Backbone Router.
[adding a page]
To create a third page in this app, we’ll call it Action3
, we’d start off by adding an Eclipse package called /app/assets/javascripts/pages/action3
. Pages require four new Javascript files:
/app/assets/javascripts/pages/action3/events.coffee
/app/assets/javascripts/pages/action3/state.coffee
/app/assets/javascripts/pages/action3/ui.coffee
/app/assets/javascripts/pages/action3/view.coffee
In order to simplify pages, I’m using the eval()
function to instantiate Page classes from a single defined route object inside /app/assets/javscripts/app/app.coffee
[view the source of the Routes class, it’s pretty cool]. We need a reference to our new route, now:
ROUTES = 'action1/:page': 'standardRouter' 'action2/:page': 'standardRouter' 'action3/:page': 'standardRouter' # Hey, I'm new. '*path': 'defaultRouter'
Next, we need to add the template references. First, we’ll create the new Scala template as /app/views/action3.scala.html
using standard Bootstrap CSS. You’ll note that each Page can reference it’s own container-type:
@() <script type="text/template" id="template-action3"> <div class="container-fluid offset-from-top"> <div class="row-fluid"> <div class="span1"> Action3 </div> <div class="span11"> <div id="region-content"> <!-- page content --> </div> </div> </div> </div> </script>
In order for the application to know about this Scala template, we need to reference it where all Pages are defined, in /app/views/layout.scala.html
:
@(css: List[String], js: List[String]) <!-- Global Templates --> @nav() <!-- Page Templates --> @action1() @action2() @action3() <!-- Hey, I'm new. --> <!-- Wireframe --> <div id="default-region"> <script type="text/template" id="template-layout"> <div id="region-nav"> <!-- navbar --> </div> <div id="region-content"> <!-- content --> </div> </script> </div> <!-- Injected --> @for(cssFile <- css) { <link rel="stylesheet" href="/assets/stylesheets/@cssFile"> } @for(jsFile <- js) { <script src="/assets/_Javascripts_/@jsFile"></script> }
[views]
All pages are defined as Marionette Layouts, as it makes it easy to add new Marionette Regions
, which typically hold Collections
, Models
, or even nested Layouts
. A simple example creates a single, swappable content region:
class App.Views.Action3 extends Backbone.Marionette.Layout template: '#template-action3' regions: content: '#region-content' @trace close: () -&gt; @remove() @unbind()
If you wanted to add a button-click to this View, you would first modify the Scala template, noting that we add a do-
prefix to the DOM ID, so as to note the View encapsulates it. It’s easy to miss what’s bound and what’s not once your application gets bigger, and this seems to help:
@() <script type="text/template" id="template-action2"> <!-- Bound: button#do-something Always add a comment, always prefix View events with 'do-' prefix. --> <div class="container-fluid offset-from-top"> <div class="row-fluid"> <div class="span1"> Action2 <button id="do-add_item" class="btn btn-medium">Simulate WebSocket Add Item</button> </div> <div class="span11"> <div id="region-content"> <!-- page content --> </div> </div> </div> </div> </script>
That View event needs to be referenced in the Javascript next, so the first file to be modified is /app/assets/javscripts/pages/action3/view.coffee
:
class App.Views.Action3 extends Backbone.Marionette.Layout template: '#template-action3' regions: content: '#region-content' events: 'click button#do-something': 'doSomething' @trace doSomething: (event) -> App.Pages.Events.Action3.doSomething() @trace close: () -> @remove() @unbind()
The doSomething
method references a method of the same name in the Events class related to the page; it's a good idea to keep all logic inside the Events classes of Pages
.
[events]
Since Page logic is contained in the Events classes, they can grow to be quite large. Here’s a brief overview of what I see happening most of the time on Page render:
- Check if something is defined.
- If not defined, go somewhere else.
- If defined, request some data, render it to the Page.
In the code, this boils down to:
- Check if something is defined in
continueToLoad
, returnfalse
if it’s not, and referenceApp.Router.navigate 'action/1', true
as the redirect tool.Always uses the Backbone Router to navigate
. - Swap the global content Region out with our Layout in
doBeforeLayouts
. renderPage1
renders the Layout, and could swap in requested data.
class App.Events.Action3 extends Backbone.Model @trace initialize: () -> # Any checks that need to be done to show page. @trace continueToLoad: () -> return true # Render the page. @trace doBeforeLayouts: () -> App.Pages.State.Action3.setLayout new App.Views.Action1() App.Global.State.getContentRegion().show App.Pages.State.Action3.getLayout() @trace renderPage1: () -> # Add data if you wish. @trace router: (page) -> if !@continueToLoad() return @doBeforeLayouts() switch page when 1 @renderPage1() # Non-render methods @trace doSomething: (event) -> App.Pages.UI.Action3.showSomeAlert()
[ui]
Anytime I’ve used jQuery to manipulate elements on a large scale, it got messy really fast
. The best way I could think to keep this in check was to move all references of jQuery manipulation to a separate helper. Because it’s so prevalent, it just made sense to package this UI
class with all Pages. In the constructor, single references are made to the elements being manipulated - this works really well for managing complicated stuff
:
class App.UI.Action3 constructor: () -> @someAlert ='div#some_alert' @trace showSomealert: () -> $(@someAlert).slideDown()
[state]
Managing global variables has always been a pain in the ass in Javascript, which is probably why I went so namespace crazy above. Every Page
has a State class which holds, at the very least, its Layout reference. Every time the Backbone Router is called and the Page is rendered, the globals are cleared. In it’s most basic form:
class App.State.Action3 extends Backbone.Model @trace initialize: () -> # Set App.Data.Pages.Action3 = { } @trace setLayout: (layout) -> App.Data.Pages.Action3.layout = layout @trace getLayout: () -> App.Data.Pages.Action3.layout
That’s all she wrote for now. I didn’t go through the Helper classes much, as the code for those is well-documented in the source…and mostly self-explanatory. I’ll work on making this a more exhaustive tutorial. I hope this gives you an idea of how to structure your front-end applications in a more well-formed way!