An Introduction to machine.javascript

This is the first in a series of [hopefully many] blog posts on the topic of machine.javascript, an open source framework for helping developers create testable, deployable and well-factored UIs on the client in JavaScript. It covers some of my exposure to tools in this space and an overview of what machine.javascript consists of and how to use it. This post is tightly coupled to Part 2 in the series and each should not be read without considering the context of the other.

My experience with JavaScript MVC

As I’ve gotten involved in my professional and personal capacities with rich, client-based UIs in te browser, one of the most indispensible patterns I’ve had at my disposal would have to be “MVC”. I put MVC in scare-quotes because it is, in my opinion, one of the most overloaded terms in software development today. The most basic, valid definition for MVC in the context of this post, though, is to say that it is about creating patterns for application development that deliberately seek to segregate control and rendering logic which are optimized for the particular platform (in this case, JavaScript in the browser).

With this in mind, the Eleutian team (notably Daniel Tabuenca) produced a kick-ass library that I always felt really attacked the problem of building complex, client-side applications in JavaScript in an effective, sensical fashion. It could have to do with the fact that this framework was the first I ever used in this particular solution/problem space, but I’ve reviewed/experimented with several other frameworks since and never really liked any of them. Some reasons for this:

  • Many of them (JavaScript MVC in particular) are very heavy, requiring things like having Rhino installed to do command-line scaffolding (wtf?!)
  • Others are tightly coupled in the manner in which view/template rendering is handled. Modern JavaScript development in the browser is interesting because there are several different ways to “get there”, when it comes to how you programatically display content. Options include, but aren’t limited to: Direct DOM manipulation via jQuery/other frameworks ( $(‘#container’).append(‘
    ’); ), JavaScript Templating (EJS, Pure, etc) and/or tools/plugins that take structured data and directly output markup (jQuery.datatables, jqplot, etc). Eleutian’s framework was originally tightly coupled to EJS for template rendering, but this has since changed (as I will explain shortly).
  • Some frameworks get the abstraction wrong, in my opinion. The scope of what I, the developer, want when doing client-side work is pretty narrow: aggregate events around some subset of the DOM and have a uniform way of manipulating/regenerating the markup in that subset in response to said events. The premise of “smart models” in an isolated browser environment (wiring up AJAX calls to the server) is way too much abstraction for me and, in the laissez-faire world of JavaScript, strikes me as very open to “entering a world of pain” scenarios.

From my time with the Eleutian team, one of the things I missed the most was the MVC framework I had used when I worked there. And while I can by no means take credit for creating or expanding machine.javascript, I can definitely say that I was able to harass Aaron Jensen and Dan into releasing the libraries in question as OSS (although they claim they were going to do it anyways). Hence, the github repo and this blog post.

What is machine-javascript?

At the heart of it, machine-javascript consists of two components, each an individual javascript file:

  • machine-includer.js — Specifies a “script loader” that works in a “nested” fashion (as opposed to “flat” includers like LABjs). It allows you to specify your dependencies in a manner similar to how you would in server-side code and then load it all (in a cascading fashion) during the page load. Like other script loaders, it is often not practical for performance-critical/production uses, but is great for dev/debug work and provides a critical advantage: The nested, per-file include() statements provide get hints for a recursive script parser to build a list of dependencies which you could use to create a single, bundled for your markup. The include() function that it creates can handle both external script loads (when passed a string) or evaluate functions passed to it. Regardless of which, both are evaluated in order of specification based upon the dependency tree created when looking at how invokations of include() are specified in files and the same external script is only loaded once.
  • machine-controller.js — Contains a prototype for the Machine.Controller object, which can be inherited in your “controllers” and provides a straightforward framework for specifying events and rendering logic on a piece of the DOM that the controller will be associated with.

Leveraging these two components, I will demonstrate how to create a simple controller with machine-javascript.

A Simple Example

Let’s consider the simplest possible hello world case, utilizing machine-javascript.

<head>
      <title>Hello, world!</title>
      <script type="text/javascript" src="machine-includer.js"></script>
    </head>

In our page’s tag section, we have a single script include. All we are going to load in this page in an explicit, static fashion is the machine-includer.js script.

Somewhere down in the , we have a chunk of markup that looks something like:

      // this is the initial setup for machine.includer
      Machine.Includer.configure({scriptLocations : {'.*':''}});

      // include our external dependencies
      include('jquery.js');
      include('HelloWorldController.js');

      // and kick it all off once the page loads
      include(function() {
        $(function() {
          var hw = new HelloWorldController();
          hw.render();
          $('#container').append(hw.domRoot);
        });
      });

      include.load();

  </script>
    <div id="container"></div>

So this is interesting; We do some inital configuration for machine-includer.js, then load our external scripts that the page is dependant upon and then instantiate an object (defined in an external dependency) and attach one of its properties to the DOM via jquery’s append() function.

Let’s take each signifigant chunk on its own…

  // this is the initial setup for machine.includer
  Machine.Includer.configure({scriptLocations : {'.*':''}});`</pre>

This is the only initialization call needed to set up machine.includer.js; after this, you can safely call include(). The hash that is passed into configure() has one notable parameter: scriptLocations. It is a series of pairs that say to the includer, “Hey, if you encounter the regex pattern in the left component in an includer URL, please prepend it with the contents of the right component.” This means that, if you had some hinting from the server based on environment (dev, production, etc), you could configure machine-includer.js to mangle the script loads done via include() so they actually called out to an external CDN or local folder, depending on the runtime environment.

For example, consider if your static content was delivered by a CDN like Cloudfront in your production setting, but was served from a local Scripts directory in the same web server when run in the dev/test environments.

In an ASP.NET MVC WebForms template with a strongly-typed view that had an Environment property hooked up to some mythical enum or the like (this applies just as easily to other server frameworks) the server-side template might look like:

      // this is the initial setup for machine.includer
      <% 
        var prefix = Model.Env == Env.Production ? 'http://s3.yourdomain.com/path/'
          : '/Scripts/';
      %>
      Machine.Includer.configure({scriptLocations : {'^static.*': '<%= prefix %>'}});

In this way, you can drive how your scripts are loaded at runtime with a simple, per-page check to the ambient environment.

      // include our external dependencies
      include('jquery.js');
      include('HelloWorldController.js');`</pre>

In this chunk of code, we are declaring our dependencies for this page, utilizing calls to the include() function with strings for the paths to the files in question. As with