Wednesday, October 18, 2017

Single use one-way binding

One of the things UI engineers are likely to be asked to do is some sort of one-way binding. The argument typically goes something like this...

Product Owner
We want this to be dynamic.
Engineer 1
All the browser is doing is showing what the server gives it, so the server needs to deliver the information to the browser.
Engineer 2
The server can't send <that information> to the browser because <reasons>, but the browser already has it, so just send back a template that's populated with the data.
Engineer 1
Data bindings are expensive, I don't think that's a good idea.
Engineer 2
<insert your favorite JS framework> does it, so it's OK.
Product Owner
I don't care how it's done, it just needs to be done.

Having had this conversation in nearly every role I've been engaged in over the last decade (more really), I can tell you there are good arguments on both sides because I've made those arguments (on both sides) depending on the specifics of the case.

While I won't reveal what I believe the deciding factors to be - there are just too many variables to make such a generalization - I can reveal a relatively easy way of getting at least some of the functionality...and I'll do it using a real-life case from my experience from nearly a decade ago.

The Case

In order to reduce the number of pages in a process, it was decided that we would do an AJAX call when the user clicked the submit button on page one. The response to that request would be the next page, minus all the extraneous stuff like headers and footers - it would be just the next page in the form. Because of the nature of the application - it involved sensitive information - there were significant restrictions on what information could be stored, and how it could be stored, on the server. There were also performance concerns that limited the amount of information going back and forth in requests and responses.

The Solution

In our case, we simply had to perform some sort of data binding. We agreed, however, that it would be a simple replacement of identified variables - no complex rules governing how variables might be combined, in fact, no business logic at all. This single use, one-way binding - something like the :: in Angular - allowed us to meet the security requirements while also maintaining our payload and performance budgets.

The challenge in this was making something general enough that it could handle any variable efficiently rather than be a string of replace commands. We were able to overcome this by using a feature of the JavaScript replace method that gets little attention - the ability to pass in a function in place of the substitution string.

When a function is passed to the replace method, the match of the regular expression (passed as the first argument in the call) is passed into the function. This allows the engineer to write a regular expression in which a key is wrapped in delimiters, passing the key into the substitution function. Once the substitution function has the key, it's a fairly simple matter of retrieving the variable from the dataset and returning it to the replace function where it's substituted for the original match.

This, of course, was all codified into a small library that looked something like this...
JavaScript
/** * @author H Robert King * @returns {object} * @param {object} config */ function PreProcessor(config) {   /**    * @property delimiters    * @description The start and stop delimiters identifying data elements. Strings are in    * Regular Expression format. Defaults to '<data>' and '<\\/data>'.    * @type {string[]}    */   this.delimiters = {    start: '<data>',    stop: '<\\/data>',   };   /**    * @method setData    * @description Sets the datastore    * @returns {undefined}    * @param {object} data    */   this.setData = function(data) {    dataset = data;   };   /**    * @method parse    * @description Parses a string, replacing data elements with values from the datastore    * @returns {string}    * @param {string} toParse    */   this.parse = function(toParse) {    var keys = Object.keys(dataset);    var parsed = toParse;    var property = '[."\'\\w\\s-_[\\]]+';    var match = new RegExp('(' + this.delimiters.start +    property +    this.delimiters.stop + ')', 'g');    var keyExp = new RegExp(this.delimiters.start +    '(' + property + ')' +    this.delimiters.stop);    if (keys.length) {    /* do the replace */    parsed = parsed.replace(match, function(element) {    var key = keyExp.exec(element)[1];    var path = key.split('.');    var val = dataset;    for (var i = 0; i < path.length; i += 1) {    var isElement = /([^[]+)\[["']?([\w\s-_]+)["']?\]/.exec(path[i]);    if (isElement) {    val = val[isElement[1]][isElement[2]];    } else {    val = val[path[i]];    }    }    return val;    });    }    return parsed;   };   var dataset = config.data;   this.delimiters = config.delimiters || this.delimiters;   return this; }


...and was called like this...
JavaScript
/* Set up the dataset */ var data = {   arr: {     values: [       { text: 'zero' },       { text: 'one' },       { text: 'two' },     ],   },   foo: {     value: 'the foo string',   },   bar: {     str: {       label: 'the bar string',     },   },   situation: 'snafu', }; data.hash = []; data.hash['one'] = { text: 'one' }; data.hash['two'] = { text: 'two' }; /* Create the preprocessor */ var preprocess = new PreProcessor({ data }); /* call the preprocessor */ var show = preprocess.parse('the value of array[1].text is   <data>arr.values[1].text</data>\n   the value of hash["one"].text is <data>hash["one"].text</data>\n   the value of foo is <data>foo.value</data>\n   the value of bar is <data>bar.str.label</data>\n   the value of situation is <data>situation</data>');


While I won't say that this comes up often - it's only come up once or twice at each of my gigs over the last 10 or 15 years - it does reappear with some regularity, so hopefully this is of some help to you when you're asked to do this sort of templating. As always, feel free to take the code and reuse or rework it - the same license is offered here as on my github account - which is basically do what you want with it. As always, I'd appreciate a shout out if you reuse it, but that's not a license requirement.

Happy coding.

No comments:

Post a Comment