Better HTML->Javascript Integration

Article is a work in progress! I’ve been meaning to write it for 2 years but never quite know what to say or how to introduce the concept. I doubt the concept is new, I just don’t see it used very often.

This article is meant to present a solution to calling Javascript code from HTML, where the Javascript takes one or many arguments which may or may not be generated on the server-side. I believe the “common” approaches to this are not optimal and either conflict with best-practices regarding Performance or are simply too hard to use.

What do we want?

  • <script src="..."> at the bottom, just before the </body>
  • Javascript calls must be parameterizable (eg. with data from the server)
  • Javascript concatenated & minified to keep requests to a minimum (obviously)
  • Execute Javascript ASAP!

If you don’t yet minify your Javascript in Production you can stop reading here since you don’t seem to care about performance. I consider these points mandatory and they WILL have an impact on your page load times, which might have a serious impact on your User.

How do we get there?

It should look something like:

<head>
</head>
<body>
  <h1>My Super Page</h1>
  <div id="target"></div>

  <script src="/js/jquery+app.js"></script>
  <script>
    myFeature($("#target"), {config: true, data: big_json_blob_from_server});
  </script>
</body>

While this seems to do everything we want it is usually not what I see on the Web since it rarely is that simple. HTML is constructed in Template Engines that employ various techniques to enable “reuse” of HTML and more often than not you’ll have some kind of “Layout” to fit it all together. Now you get the dilemma of having Javascript coupled to specific HTML but since we can’t execute Javascript just yet (remember it is available just before the </body>) we have to resort to some trickery. Either your Template Engine allows to target specific areas of the output (eg. HTML goes here, JS goes here) or more commonly you resort to using jQuery.ready.

So in practice things look more like:

<head>
  <script src="/js/jquery.js"></script>
</head>
<body>
  <h1>My Super Page</h1>
  
  <!-- START some/include.html -->
  <div id="target"></div>
  <script>
    $(function() {
        myFeature($("#target"), {config: true, data: big_json_blob_from_server});
      });
  </script>
  <!-- END some/include.html -->

  <script src="/js/app.js"></script>
</body>

Since we rely on $(function() {}); (which is jQuery for: execute function when ready) we need to have jQuery available early, so we move that to the <head>. One blocking Javascript in the <head> is probably fine, might even be via a CDN to further reduce delay. Also I don’t quite like leaving “marker” elements (eg. #target) in the DOM just to refer to a specific place.

To my knowledge using “ready” is “recommended” in jQuery, other frameworks might suggest other ways. But it is very common to either use the document.onload or DOM ready events.

What can we do?

Eliminate all inline Javascript from HTML.

Yes, it is that simple. Well, we still want to be able to parameterize our function calls and refer to specific places in our HTML. So we put some extra data into the DOM, but please do not use data-attr="huge-json-blob-from-server" ever, thanks.

Instead I’d recommend:

<head>
</head>
<body>
  <h1>My Super Page</h1>
  
  <!-- START some/include.html -->
  <script type="shadow/run" data-fn="my_app.feature1" data-ref="self">
    {config: true,
     data: big_json_blob_from_server}
  </script>
  <!-- END some/include.html -->

  <script async src="/js/app.js"></script>
</body>

Couple that with /js/app.js

// contents of jquery.js here
// contents of api-helper.js

my_app.feature1 = function(target, args) {
  // target is the <script> dom node aka our position in the DOM
  // insert a new node before/after it, replace it, ...
  // args is whatever the body of <script> is after JSON.parse
};

api.ready("my_app.feature1");

// more of my super app

api.ready is an imaginary function that just does

document.querySelectorAll('script[data-fn="my_app.feature1"]')

takes the contents of the <script> Tag, runs it through JSON.parse and passes that and itself as arguments to the function defined at my_app.feature1.

Where is the code?

Well, I have been using this technique for over 2 years now and it is working quite well. But I don’t use jQuery and all my code is ClojureScript. I tried to present the general concept since I feel it is applicable for every library/language. ClojureScript provides a lot of neat things that make this concept even easier to use (namespaces, dependency graphs, …). Plain JS might require some work.

Anyways, on the client this looks like:

(ns my.app
  (:require [shadow.api :refer (ns-ready)]))

(defn ^:export feature1 [node {:keys [config data} :as args}]
  (js/console.log "feature1" node args))
  
(ns-ready)

On the server:

(let [my-data (get-data-from-db)]
  (hiccup/html
    [:body
      [:h1 "My Super Page"]
      (cljs 'my.app/feature1 :self {:config true :data my-data})]))

The client code is available here, you are welcome to use it. Server code is really simple so I won’t include it here. Note that I use EDN as transport format. You might also do this with transit or plain JSON, whichever you prefer. Another neat side effect is that the DOM now is pure data again and all code is where it should be.

Add a Comment