ponyfoo.com

Why I Write Plain JavaScript Modules

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

These are short-form “thoughts”, in addition to the usual longer-form articles in the blog. The goal is to publish one of these every weekday. I’d love to know what you think. You may send your questions to thoughts@ponyfoo.com. I’ll try to answer them over email and I may publish them here, with your approval. I also write thoughts about the current state of front-end development, and opinions on other people’s articles. You can use the form to the right (near the bottom in mobile) to subscribe via email.

Our web needs better primitive libraries. We’ve been relying for too long – far too long – on jQuery. Most popular UI components are tied to jQuery, part of a comprehensive framework – and it’s usually hard to extract the component as a standalone library. Nowadays we may not develop as many jQuery plugins as we’ve used to, but the situation is far more severe now.

Today, many popular libraries – UI components or otherwise shiny client-side JavaScript things – are bound to the author’s preferred coding style. Thus, we create things like react-dnd, angular-dragdrop, or backbone-draganddrop-delegation. Out of the three, none are backed by a library providing the primitives into drag and drop, without the tight framework bindings – such as dragula.

It all comes down to composability and portability.

While dragula needs to be integrated into each one of those frameworks (React, Angular, Backbone), doing so usually takes few lines of code. Here’s one such example using react, dragula, and plain JavaScript someone posted on a GitHub issue for dragula.

dragula([...], {
  direction: 'horizontal',
}).on('cloned', function (clone) {
  clone.removeAttribute('data-reactid');
  var descendents = clone.getElementsByTagName('*');
  Array.prototype.slice.call(descendents).forEach(function (child) {
    child.removeAttribute('data-reactid');
  });
});

Could dragula get rid of data-reactid attributes when it clones things? Allow me to answer that question using a meme.

This is JavaScript!
This is JavaScript!

That being said, there’s no good reason for dragula to do so. Why should dragula know about React? Instead, we introduced a feature where whenever dragula clones a DOM element, it emits an event. In practical terms, this is about the same as getting rid of data-reactid ourselves, but we’ve moved the responsibility of knowing about that particular attribute to whoever uses React.

As I wrote this post, I made react-dragula into a library. It’s just a wrapper – a mighty thin wrapper – around dragula.

function reactDragula () {
  return dragula.apply(this, atoa(arguments)).on('cloned', cloned);
  function cloned (clone) {
    rm(clone);
    atoa(clone.getElementsByTagName('*')).forEach(rm);
  }
  function rm (el) {
    el.removeAttribute('data-reactid');
  }
}

* atoa casts array-like objects into true arrays.

Going through the trouble is worth it because if somebody wants to write an Angular directive for dragula it’s also easy for them to do so, and they seldom have to do anything to integrate it with Backbone – Backbone isn’t that “smart”, so we don’t have to rewrite our code to fit its awkward architecture.

Portability across Frameworks

Without lower level libraries like dragula or xhr we’ll end up reinventing the wheel for an entire afterlife of eternity in hell. Don’t get me wrong – I’m a big fan of reinventing the wheel. I’ve reinvented my fair share of wheels, Twitter reinvented RSS, etc. But, reinventing the wheel as a pointless exercise in porting a library from a framework to another is just wasteful.

When I wrote react-dragula, I didn’t have to fork dragula and repurpose it for React. When I wrote angular-dragula, I didn’t have to fork dragula either. I guess at this point you might argue that “nobody seems to be forking things and repurposing them for other frameworks”, but that’s beside the point.

The point in question is that developing a library that specifically targets a framework is a waste of your time, because when you eventually move on to the next framework (this is JavaScript, you will) you’ll kick yourself over tightly coupling the library to the framework.

Sure, it involves a bit more of work and design thinking. You have to figure out how the library would work without your framework or choice (or any framework for that matter) first – and then wrap that in another module that molds them into the framework’s paradigm.

The react-dragula example was too easy, right? All I did was add an event listener, getting rid of data-reactid attributes. Even though it “looks easy” in hindsight, the naïve approach would’ve been to just write it to conform to React right off the bat – skipping the vanilla implementation entirely. Thus, we’d be ignoring the opportunity to provide hooks where we could later adjust the library to play nice with React, saving ourselves from the painful experience of maintaining multiple libraries that do essentially the same thing.

In the case of angular-dragula, I could’ve come up with a directive that just passed the options onto dragula, but that wouldn’t have been very "Angular way"ish. Thus, I came up with the idea of trying to replicate the simple API in dragula in an Angular way. Instead of defining the containers as an array passed to dragula, you could also use directives to group different containers under the same angular-dragula instance.

The example below would use angular-dragula to create two instances of drake, identified as 'foo', and 'bar'.

<div ng-controller='ExampleCtrl'>
  <div dragula='"foo"'></div>
  <div dragula='"foo"'></div>
  <div dragula='"foo"'></div>
  <div dragula='"bar"'></div>
  <div dragula='"bar"'></div>
</div>

If you wanted to listen for events emitted by one of these drake instances, you could do so on the $scope, prefixing the event with the “bag name” and a dot. Here again, I conformed to the Angular style by propagating drake events across the $scope chain, allowing the consumer to leverage Angular event engine. While events in dragula are raised using raw DOM elements, the events emitted across the $scope chain wrap them in angular.element calls, staying consistent with what you’ve come to expect of Angular components.

app.controller('ExampleCtrl', ['$scope',
  function ($scope) {
    $scope.$on('foo.over', function (e, el, container) {
      container.addClass('dragging');
    });
    $scope.$on('foo.out', function (e, el, container) {
      container.removeClass('dragging');
    });
  }
]);

To configure the instances, you’d use the dragulaService in the controller for these containers. The example below makes it so that items in foo containers are copied instead of moved.

app.controller('ExampleCtrl', ['$scope', 'dragulaService',
  function ($scope, dragulaService) {
    dragulaService.options($scope, 'foo', {
      copy: true
    });
  }
]);

In the future, I might add more directives, moving away from the native dragula implementation and towards a more Angular way of handling things. For example, one such directive could be dragula-accepts='method', and it could configure the accepts callback in such a way that the container where the directive is added to only accepts elements that return true when method(item, source) is invoked. A similar dragula-moves='method' directive could determine whether an item can be dragged away from a container, based on the result of calling method(item).

A few more aspects of dragula can be “molded into Angular” in this way.

While dragula doesn’t have a native way of treating containers individually – even when they take part of the same logical unit in the underlying implementation (a drake can have as many containers as needed), we can build the functionality into angular-dragula. That helps us achieve the “Angular way” of writing directives that affect containers individually, rather than writing directives on a container that have knowledge of a series of unrelated DOM elements. Or, even worse, creating a directive where every immediate child element is a dragula container, constraining the use cases for the consumer.

It might involve some extra work, but being able to reuse the code in any future projects makes plain JavaScript modules well worth your time.

Portability across Platforms

Portability isn’t just a matter of writing vanilla client-side JavaScript libraries. An equivalent case may be made for writing libraries that work well in both Node.js and the browser. Consider async: an amazing piece of software in Node.js, that’s just garbage in the client-side. Granted, it was written well before ES6 modules (or even Browserify) became a thing. A similar story can be told about fast-url-parser, a URL parser which underlies many server-side routers but is insanely large for the client-side. Talking about insane, I’ve used sanitize-html in countless opportunities to sanitize HTML on the server-side, but again – repeat with me: freaking huge for the client-side (depends on htmlparser2).

I’ve worked on reimplementing a few of those to work well on the client-side. Naturally, their server-side counterparts are more comprehensive, as they should be. Use cases for server-side JavaScript far outnumber what you need to do on a given site on the client-side for a single visitor. On the client-side, we can get away (should get away) with much smaller libraries and modules.

Here are some examples.

  • contra (2k) is like async for the browser – It’s modular, too. You can just require individual methods (ala lodash)
  • omnibox (1.6k) is like fast-url-parser for the browser
  • insane (2k) is like sanitize-html (100k) for the browser

Then again – huge JavaScript libraries are only worrisome if we actually care about performance when it comes to serving images in the first place – right?

We’re a far way from the “universal JavaScript” fairytale we keep telling ourselves.

Have any questions or thoughts you’d like me to write about? Send an email to thoughts@ponyfoo.com. Remember to subscribe if you got this far!

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments