JavaScript widgets with Browserify
Published on
Apr 22, 2016
Theory is good, practice is better! That's why I (aka @rougemine) created a tiny demo project in order to explain the concept of JS widgets with Browserify :-)
The pattern is simple : a widgets-initializer-from-dom component is responsible for looking for HTML elements with the "data-widget-module-id" attribute. For each of these elements, it requires the given module by its id, and creates a new instance of the module, passing the DOM node as only argument.
From then, each instance can live its own life, adding specific behavior to the target HTML element. That's all!
The only third-party library we need is a JavaScript modules bundler : its role is to analyze CommonJS JavaScript modules from an "entry point", parses its dependencies recursively, and creates a bundle of it into a single file - with each module dependencies automatically injected.
I could have used any bundler here, but the one used for the demo is Browserify. It's simple, mimics Node.js API, and can be also handy for its system of "transforms" - which are kind of preprocessors, that can do a lot of things. You can look at this list for more information about them.
In this demo the JavaScript classes are used for clarity, but of course it can be used with basic JS functions (EcmaScript 2015 specific features, like classes, are converted at build time by the "babelify" Browserify transform in this demo). The anatomy of a widget is very simple, you can look at this minimalistic test widget to see it. I have included a few test widgets on the demo ; they all add an ugly border around their node for demo purpose:
- a tooltip widget (which can adapt its behavior by reading specific "data-" attributes on its node)
- a lorem picture widget, as an example of a very simple widget
- a date picker widget ; this one is here to demonstrate that a widget can easily embed CSS resources, thanks to Browserify's "brfs" transformer. It parses JS modules at build time, and replaces occurrences of `fs.readFileSync` (which is Node's equivalent of PHP's `file_get_content()`) with the content of the target file.
- an Ajax form widget : this is the only one of this demo that could be useful in the real world :-) It simply prevents form submit, and replaces it with an Ajax call. The data returned by the server is added to a temporary container, which allows the server to decide which behavior it will add on the HTML page. This is following the principle which I like a lot : "Server-generated JavaScript Responses", described here by DHH, creator of Ruby on Rails.
The 2 heavy Single Page Applications of the project I am currently working on are only powered by widgets and server-side JS responses. No framework! :-)
The only tricky part is that Browserify resolves dependencies dynamically from a start module. But because the widgets modules are not required anywhere, Browserify doesn't include them in the bundled Javascript file.
My solution is to do a hacky-but-efficient-even-on-a-real-project parsing of the project templates files. We look for "data-widget-module-id" occurrences, and add each of these modules in the bundle. This is managed here.
I tried to keep this demo project minimalistic, and did not use any Grunt or gulp build tool - everything is simply done by NPM scripts, described in the root package.json file. Tasks like LESS compilation (with a "watch" option), Browserify JavaScript modules bundle (also with a "watch" option), HTTP test server and stuff are handled in the "scripts" section of the package.json file.
The other package.json file, in "front-end-assets/js", is only here to handle front-end dependencies - jQuery, Foundation, and misc libraries are downloaded from NPM.
I have been using this pattern for a few years on multiple projects - with Require.js first, and now with Browserify. Its main advantages are that it's very simple, does not need any heavy framework knowledge, and it "scales" : there are dozens of JS modules on my current project, they are all independent, and it works well. If I need to send or receive information between modules, I can send domain events on the HTML document (which is used as a central message bus).
Do you like this pattern? I would be happy to see your feedback on Twitter.
Comments