May 25th, 2023 How RxHTML works By Jeffrey M. Barber

I had a great conversation with a friend at lunch about how RxHTML works since I’m pivoting towards a “concierge engineering company”. Today, I want to illustrate how the full stack web app side of Adama works.

Our journey starts with the current focus around single page applications. In the spirit of keeping things simple, we leverage a single file (site.rx.html) to define the web application. The current tools turns that file into three files. First, tailwind is fantastic as it can scan the input HTML and emit a style sheet. Adama is responsible for compiling the site into javascript (site.js) with a shell (200.html) to introduce the javascript.

Just upload these three files to a service that handles 200.html appropriately, and you have an SPA. Let’s dig in!

Digging into <forest> with <page>

For reasons I don’t remember, the top HTML element within an RxHTML document is <forest> as this will bind all the pages together. Within the <forest>, there are a multitude of <page> elements to define routable paths.

<forest>
  <page uri="/">
    Hello World
  </page>
</forest>

This will generate a 200.html like:

<!DOCTYPE html>
<html>
 <head>
  <link rel="stylesheet" href="/style.css">
   <script src="https://aws-us-east-2.adama-platform.com/libadama.js">
   </script>
   <script src="/site.js"></script>
 </head>
 <body></body>
 <script>RxHTML.init();</script>
</html>

RxHTML is defined in libadama.js which is the result of combining connection.js, tree.js, and rxhtml.js together. RxHTML is defined in rxhtml.js.

The associated site.js is as follows:

(function($){
  $.PG(['fixed',''], function(b,a) {
    b.append($.T(' Hello World '));
  });
})(RxHTML);

Now, this isn’t exciting beyond showing that the RxHTML runtime is responsible for page routing (via $.PG) and the building the dom. The variables are generated to help minify the file, but in this context

b.append($.T(' Hello World '));

means “append the text node containing ‘Hello World’ in the DOM element b which happens to be body”. Curiously, $.PG has more structure it is is an instruction set to tear down the given URI. The current instruction is to pull a fixed (i.e. constant) string off that matches the empty string. Let’s add another page to further illustrate how routing works.

Multiple pages

<forest>
  <page uri="/">
    <a href="/blog">Blog</a>
  </page>
  <page uri="/blog">
    <a href="/blog/1">Page 1</a>
    <a href="/blog/2">Page 2</a>
  </page>
  <page uri="/blog/$page:number">
  </page>
</forest>

This causes site.js to explode! (a little bit)

(function($){
  $.PG(['fixed',''], function(b,a) {
    var c = $.E('a');
    $.HREF(c,'/blog');
    c.append($.T('Blog'));
    b.append(c);
  });
  $.PG(['fixed','blog'], function(b,a) {
    var c = $.E('a');
    $.HREF(c,'/blog/1');
    c.append($.T('Page 1'));
    b.append(c);
    var c = $.E('a');
    $.HREF(c,'/blog/2');
    c.append($.T('Page 2'));
    b.append(c);
  });
  $.PG(['fixed','blog','number','page'], function(b,a) {
  });
})(RxHTML);

The three invocations of $.PG illustrate three routable pages, and the logic of first to is to respectively match the strings “/” and “/blog” while the third matches the fix prefix “/blog” and then after the slash a number which will be stored as page in the viewstate. We will talk about the variables get recalled in a bit. The function bodies contain blocks like

    var c = $.E('a');
    $.HREF(c,'/blog');
    c.append($.T('Blog'));
    b.append(c);

which mirror

<a href="/blog">Blog</a>

At core, we are turning the HTML into a runtime (i.e. $) driven javascript to build the DOM. This allows us to introduce why RxHTML has Rx (i.e. reactive) in the name. So, let’s backtrack with another simple example.

Lookups

<forest>
  <page uri="/$name:string">
    Hello <lookup path="view:name">
  </page>
</forest>

Which has the modest site.js of:

(function($){
  $.PG(['string','name'], function(b,a) {
    b.append($.T(' Hello '));
    b.append($.L($.pV(a),'name'));
  });
})(RxHTML);

The key difference is

b.append($.L($.pV(a),'name'));

which translates to “append the text node which is bound to a value ($.L) that can be sourced at the path ($.pV(a)) the key of ‘name’.” The view state was addressed because of the path prefix “view:”, and the view state is just a reactive tree. In RxHTML, the are two trees: the view state and the data state. The interesting aspect of this is that if the view state changes, then the information flow from the tree hits exactly the associated text node.

This requires diving into how trees work, so let’s look at a minimal setup of using tree.js.

How trees work

// define a new tree
var viewstate = new AdamaTree();

// attach a subscriber to the tree
viewstate.subscribe({name: function(name) {
    console.log("name was changed to " + name);
    document.getElementById('name').innerHTML = name;
  },
  'date': function(new_date) {
    console.log("the date is now " + new_date);
  }
});

// merge updates into the tree
viewstate.update({name: "Adama", date: "Today!"});

// updates can be done partially
viewstate.update({name: "Roslin"});
viewstate.update({date: 'Tomorrow'});

With this in mind, the line

$.L($.pV(a),'name')

translates to something like (it’s not exactly as there is some serious complexity in building individual subscription functions across multiple values).

function() {
  var node = document.createTextNode("");
  viewstate.subscribe(name: function(name) {
    node.nodeValue = name;
  });
}

Here, the functional closure is the binding linkage between the node and the new value skipping the need to lookup a node by id. The historical origin of RxHTML stems from how repetitive it became binding data changes to DOM changes both with vanilla.js and React.

Much can be said about how to leverage viewstate to build interactive local applications, but let’s punt on that for now. We focus now on how to bring data into the picture with a new site.rx.html.

Connections and multiplicity

<forest>
    <page uri="/$key:string">
        <connection space="chat" key="{view:key}">
            <div rx:iterate="lines">
                <lookup path="who" />: <lookup path="what" /><br />
            </div>
            <form rx:action="send:say">
                <input type="text" name="what" />
                <button type="submit">Say</button>
            </form>
        </connection>
    </page>
</forest>

which manifests a bunch of javascript in site.js:

(function($){
  $.PG(['string','key'], function(b,a) {
    var c=$.RX(['key']);
    c.name='default';
    c.space='chat';
    $.Y2($.pV(a),c,'key','key',function(d) {
      c.key=d['key']
      c.__();
    });
    c.identity=true;
    c.redirect='/sign-in';
    $.CONNECT(a,c);
    var d=$.RX([]);
    d.name='default';
    $.P(b,a,d,function(b,e) {
      var f = $.E('div');
      $.IT(f,e,'lines',false,function(g) {
        var h = $.E('div');
        h.append($.L(g,'who'));
        h.append($.L(g,'what'));
        var i = $.E('br');
        h.append(i);
        return h;
      });
      b.append(f);
      var f = $.E('form');
      $.aSD(f,e,'say');
      $.onS(f,'success',$.pV(e),'send_failed',false);
      $.onS(f,'failure',$.pV(e),'send_failed',true);
      var i = $.E('input');
      i.setAttribute('type','text');
      i.setAttribute('name','what');
      f.append(i);
      var i = $.E('button');
      i.setAttribute('type','submit');
      i.append($.T('Say'));
      f.append(i);
      b.append(f);
    },function(b,e) {
    });
    d.__();
  });
})(RxHTML);

Let’s break down the $.CONNECT bit:

    var c=$.RX(['key']);
    c.name='default';
    c.space='chat';
    $.Y2($.pV(a),c,'key','key',function(d) {
      c.key=d['key']
      c.__();
    });
    c.identity=true;
    c.redirect='/sign-in';
    $.CONNECT(a,c);

This is going to establish a connected named default with the coordinates from the site.rx.html. In the above example, the connection has a reactive input denoted via $.Y2. The purpose of $.Y2 is to query for a value from a tree and set it as a parameter to a reactive. In this case, the html

<connection space="chat" key="{view:key}">

has an attribute that uses a handlebars inspired template language to query the view. This will connect the page’s URI to the connection such that changes to the page will manifest in connection changes.

The <connection> element is associated with an implicit <pick> element which select a connect by name to use as the current data tree. You can have multiple connections on the same page at any time, but it requires selecting the connection via a <pick> which is implicit in this case. The <pick> generates

$.P(b,a,d,function(b,e) { /* stuff */});

Inside the nested function for $.P defines the behavior when a connection is established. There are two new things to address.

First, the $.IT bit.

$.IT(f,e,'lines',false,function(g) {
  var h = $.E('div');
  h.append($.L(g,'who'));
  h.append($.L(g,'what'));
  var i = $.E('br');
  h.append(i);
  return h;
});

This is going to look up the “lines” field within the tree. However, the ‘lines’ field is a list. The related function then is the instruction for how to handle a new item in the list. The DOM is then managed such that the DOM elements mirror the order of the list. When items from lines disappear, their DOM element disappears. When items appear, the function is called and the DOM element is inserted. If the ordering of items within lines changes, then the associated DOM elements are re-ordered.

Forms and actions

Second, the $.aSD and $.onS bits.

var f = $.E('form');
$.aSD(f,e,'say');
$.onS(f,'success',$.pV(e),'send_failed',false);
$.onS(f,'failure',$.pV(e),'send_failed',true);
var i = $.E('input');
i.setAttribute('type','text');
i.setAttribute('name','what');
f.append(i);
var i = $.E('button');
i.setAttribute('type','submit');
i.append($.T('Say'));
f.append(i);
b.append(f);

$.aSD means “the given form has an action to send a message to the current connection via the indicated channel”. $.onS attaches behavior to the success and failure of the form. In this cases, the viewstate is going to see a send_failed value go from nothing to either true or false.

   ...
            </div>
            <div rx:if="view:send_failure">
                Oh no! We failed to send the message!
            </div>
            <form rx:action="send:say">
   ...

which introduces the new bit of js to site.js

var f = $.E('div');
$.IF(f,$.pV(e),'send_failure',true,false,function(h,g) {
   h.append($.T(' Oh no! We failed to send the message! '));
},function(h,g) {
});
b.append(f);

This is how feedback in RxHTML works. Let’s return to the question of building interactive applications using a new site.rx.html.

Interactive viewstate

<forest>
    <page uri="/">
        <div rx:load="set:counter=0">
            <lookup path="view:counter" />
        </div>
        <button rx:click="inc:counter"> [+] </button>
        <button rx:click="dec:counter"> [-] </button>
        <button rx:click="set:counter=0"> [reset] </button>
    </page>
</forest>

Which turns into

(function($){
  $.PG(['fixed',''], function(b,a) {
    var c = $.E('div');
    $.onS(c,'load',$.pV(a),'counter',0);
    c.append($.L($.pV(a),'counter'));
    b.append(c);
    var c = $.E('button');
    $.onD(c,'click',$.pV(a),'counter', 1);
    c.append($.T(' [+] '));
    b.append(c);
    var c = $.E('button');
    $.onD(c,'click',$.pV(a),'counter', -1);
    c.append($.T(' [-] '));
    b.append(c);
    var c = $.E('button');
    $.onS(c,'click',$.pV(a),'counter',0);
    c.append($.T(' [reset] '));
    b.append(c);
  });
})(RxHTML);

The slightly familiar $.onS will set the value of the counter to zero both at the start and when the reset button is clicked. This adds $.onD which adds the given value (in this case either +1 for inc and -1 for dec) to the view state variable. In this document, we have seen set, inc, and dec functions. There are a few more in documentation like raise, lower, toggle, and ones related to decision making.

Conclusion

This has been a rough whirlwind tour of how RxHTML works from the bottom up. The goal at hand is to build entire products with RxHTML and see how far I can push this in terms of usability and power. As the various inflight projects get bigger, I imagine I’ll have more interesting challenges ahead of me. It’s exciting to have a new superpower since the key advantage of RxHTML is that I’m focused entirely on templating without any glue.

I use tailwind to define the look and feel, and then I bind the look and field to data via a connection to the backend powered by Adama. As such, I’m spending more time focused on solving the domain problem rather than glue. Recently, I have even introduced a new meta level called EdHTML where I generate RxHTML from the backend’s reflected schema, and I’m able to fly.

I’m approaching the rails moment where I can design the backend, and then poof… a product pops out.