Showing posts with label Angular. Show all posts
Showing posts with label Angular. Show all posts

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.

Friday, January 20, 2017

Gather Ye Rosebuds: thoughts regarding collecting date values

One very common data types that we collect from users is a date value, whether it be a birthday or other anniversary or even something like a booking date for an event. Unfortunately, most of the date collection methods I've seen in the past two decades are sub par and the failure is even more pronounced when we consider the experience for those using assistive technology.

We've made some progress - we now, in HTML5, have a date type INPUT tag - but even there the experience is lacking. If we add in other considerations - like internationalization - we slide even further down this slippery slope, opting to do things like collect the date parts, i.e., month, day, and year, individually to make sure we have a valid date that's gathered in a manner that is somewhat familiar to the user.

Of course we have tools like datepicker, but unfortunately those are typically only partial solutions. Sure, they improve the interface for a number of users, but most still fall short, especially for those using screen readers...and that situation becomes even worse when they insert a calendar widget into a popup and focus is not managed well.

What's the solution?

Ideally, a tool that would automatically generate a visual calendar in the most common layout - a table with seven columns with each week in a row - and some common controls - like the ability to change the month and/or year using a next/previous button or a drop down list. The control should be in the flow of the document and it should include, at the very least, ARIA-* and role attributes so the experience doesn't suck if you're using a screen reader. Oh, and you should be able to navigate it easily using the keyboard, a mouse, or a touch-enabled device.

I don't really think that's asking too much. All it's really asking is that we, as UI engineers, consider how the experiences we are building are impacting people who are not like us. Maybe that's more along the lines of "everything and a kite" than "not too much", but I like to think we can do better than just OK. I'd like to think that when we talk about the MVP we're not really thinking about the MVE (Minimum Viable Effort - look for that in an upcoming post).

Now for a little good news. I've begun building such an interface. Since everyone seems to be hot after Angular these days, that's what I've used and I've built a prototype with a calendar directive. Granted, it's not 100% complete - there's still a little work to do to improve the accessibility and internationalization - but it's about 99 percent there. (You can see the prototype at http://prototypes.cathmhaol.com/ng-calendar/.)

The directive is not yet on my github, but it will be as soon as I've rounded a few of the rough corners, but you can easily download the source and the CSS (which includes a special trick that forces the table cells into squares) from the prototype if you're dying to get started building out a better date collector.

As always, happy coding.

20 February, 2017
The interface is complete and is available on GitHub at https://github.com/hrobertking/angular/tree/master/ng-calendar. A brief demonstration is below.

Thursday, December 22, 2016

Watching the Children

One of the things I've been working on is an AngularJS component that groups elements into a single field...which I thought would be simple. As it turned out, I learned more than I thought I would.

First, let me describe the scenario. The original block of code, that came out of a collaboration between designers and front-end developers, was inadequate. Here's what the code was (sort of) like...


Original Angular Markup

<div class="row">   <label>{{fieldLabel}}</label>   <select name="{{id}}_day" ng-model="day">     <option ng-repeat="option in ::days track by ::$index">       {{option}}     </option>   </select>   <select name="{{id}_month" ng-model="month">     <option ng-repeat="option in ::months track by ::$index">       {{option}}     </option>   </select>   <select name="{{id}}_year" ng-model="year">     <option ng-repeat="option in ::years track by ::$index">       {{option}}     </option>   </select>   <tooltip ></tooltip> </div>


Looking at the code I immediately knew that this was not written by someone who was familiar with several concepts every engineer must keep in mind - remember, we are getting paid to think - things like flexibility for localization, and more importantly, accessibility.

My first lesson

My first thought (beyond the obvious lack of accessibility practice, like having a linked label) was "why are they not using an input with a date type attribute". When I did some accessibility testing of my own, however, I learned that the user experience with an HTML5 date type input was far below what I would consider sub-par. Sure, the HTML5 date type has a better visual experience, but if you're relying on something like VoiceOver™ it's very unclear how you're even supposed to interact with the input. If you're relying on VoiceOver™, you'd have a much better experience if the input format (e.g., year/month/day or year-month-day) were described and you were left to enter it in a text type input.

Granted, from an accessibility standpoint there is little we could do that would not improve the experience. I was, however, limited in options - I had to retain the dropdown lists with the only label associated with the group as the primary (visual) interface - so I started where I could. After the first pass - to address the accessibility issues - the code was a little better, even if I did have to add a method to the scope object to set the date value using the three date parts. (Ok, I didn't have to add that, I could still pass the date parts, but adding it and gives me a single point of reference for the date, making validation and other stuff a little easier.)


Accessible Angular Markup

<div class="row">   <label for="{{id}}">{{fieldLabel}}</label>   <input class="visually-hidden" id="{{id}}" name="{{id}}" type="text">   <div aria-hidden="true">     <select name="{{id}}_day"      ng-change="setDate()"      ng-model="day">       <option ng-repeat="option in ::days track by ::$index">         {{option}}       </option>     </select>     <select name="{{id}_month"      ng-change="setDate()"      ng-model="month">       <option ng-repeat="option in ::months track by ::$index">         {{option}}       </option>     </select>     <select name="{{id}}_year"      ng-change="setDate()"      ng-model="year">       <option ng-repeat="option in ::years track by ::$index">         {{option}}       </option>     </select>   </div>   <tooltip ></tooltip> </div>


Although I won't delve into the tooltip directive here, I also made sure it had the ARIA role "status" so when the date was invalid (like a Date of Birth less than 13 years ago for Facebook), I could put that in the tooltip and it would announce to anyone using assistive technology as well as being displayed for anyone.

My second lesson

With the majority of the accessibility issues fixed, I moved on to improving the interface in other ways. There was a significant problem in that the order of the SELECT elements was fixed even though the day-month-year pattern applies to only a few cultures. The directive, as it was coded, would not be flexible enough to handle different formats - different ordering of the date parts - that are common across the globe.

Also, to improve the interface, it would be nice if the number of days were automatically adjusted based on the month and year. Not only does it reduce the cognitive load for poorly labelled elements, it reduces the number of accidental invalid dates like February 30th. Since Angular has two-way binding, and a way to watch properties in a scope for changes, this little modification should have been fairly simple. The good news is that it was. All I had to do was add $watch to the month in the directive and modify the days...oh, and $watch the year as well to account for February's 29th day in leap years.

With the ability to watch the year and month for changes and because JavaScript already has pretty slick slicing and dicing methods for arrays, modifying the number of days was easy peasy, and using days as an array on the scope also meant that passing that array into the ng-options attribute, made perfect sense.

Since I wanted to be able to pass the months array in (to ease localization) this was the perfect time to make that little switch and add a date format to the scope that enabled me to switch around the order of the dropdown lists. A little parsing of the date format that was passed in and I have an order property on the scope that I can use for another of Angular's features - ng-switch.

After these little changes, I had something that looked like this...


Localizable Angular Markup

<div class="row">   <label for="{{id}}">{{fieldLabel}}</label>   <input class="visually-hidden" id="{{id}}" name="{{id}}" type="text">   <div aria-hidden="true">     <ng-switch on="::order[0]">       <div class="datepart" ng-switch-when="d">         <ng-include src="'dd.html'"></ng-include>       </div>       <div class="datepart" ng-switch-when="m">         <ng-include src="'mm.html'"></ng-include>       </div>       <div class="datepart" ng-switch-when="y">         <ng-include src="'yy.html'"></ng-include>       </div>     </ng-switch>     <ng-switch on="::order[1]">       <div class="datepart" ng-switch-when="d">         <ng-include src="'dd.html'"></ng-include>       </div>       <div class="datepart" ng-switch-when="m">         <ng-include src="'mm.html'"></ng-include>       </div>       <div class="datepart" ng-switch-when="y">         <ng-include src="'yy.html'"></ng-include>       </div>     </ng-switch>     <ng-switch on="::order[2]">       <div class="datepart" ng-switch-when="d">         <ng-include src="'dd.html'"></ng-include>       </div>       <div class="datepart" ng-switch-when="m">         <ng-include src="'mm.html'"></ng-include>       </div>       <div class="datepart" ng-switch-when="y">         <ng-include src="'yy.html'"></ng-include>       </div>     </ng-switch>   </div>   <tooltip ></tooltip> </div>


Included Markup Files - dd.html

<select name="{{id}}_day"   ng-change="setDate()"   ng-model="day"   ng-options="days.indexOf(day) as day for day in days"> </select>


Included Markup Files - mm.html

<select name="{{id}_month"   ng-change="setDate()"   ng-model="month"   ng-options="months.indexOf(month) as month for month in months"> </select>


Included Markup Files - yy.html

<select name="{{id}}_year"   ng-change="setDate()"   ng-model="year"   ng-options="years.indexOf(year) as year for year in years"> </select>


Using this ng-switch pattern with parsing code similar to...


Format Parsing

var format = (/^([ymd]*)\W?([ymd]*)\W?([ymd]*)/i)        .exec($scope.dateformat).slice(1, 4); $scope.order = [   format[0].replace(/y+/i, 'y').replace(/m+/i, 'm').replace(/d+/, 'd'),   format[1].replace(/y+/i, 'y').replace(/m+/i, 'm').replace(/d+/, 'd'),   format[2].replace(/y+/i, 'y').replace(/m+/i, 'm').replace(/d+/, 'd') ];


...in the directive means that even a two-part date format, like month/year, will be displayed correctly, and you can use the same datepart identifiers as are used in Angular's date filter.

Third lesson

So now it was worlds better - accessible and ready for localization. That tooltip, though, was going to give me fits. Getting it to display when the input had focus and hide when it didn't was easy...but the SELECT elements are not siblings of the tooltip (preventing CSS from working) and the focus event doesn't bubble so I couldn't add a listener to the element containing the SELECTs that was a sibling to the tooltip. To make matters worse, in Angular, the focus event doesn't fire for a SELECT element until the change event fires. It was like some grand puzzle preventing me from getting the tooltip to hide and show when the focus was on any one of the date parts.

So, how did I solve this little riddle?

First, I thought to use $watch on the SELECT elements in some way. Of course the first problem in attempting that is that by using the ng-switch to help with localization I had prevented the SELECT elements from being present in a way that would allow me to get a hook into them. Add to this the fact that the focus event wasn't firing and I thought I was blocked...but then I remembered I was getting paid to think.

I added the onfocus and onblur attributes to the SELECT items in the markup to by-pass Angular. That fixed the timing problem - the onfocus and onblur fire immediately when focus or blur occur, not when the change event occurs, and I thought I could use them to change the class to indicate focus and use $watch in the directive to watch the class attribute of the SELECT. Unfortunately, that didn't work. Even though I was able to get the class to change by adding onfocus="this.className+='focused'" and onblur="this.className=this.className.replace(/\bfocused\b/, '')" to the SELECT tags, the element instance in the DOM was out of sync with the element instance in the directive. I knew the onfocus and onblur event handlers were key, though, because they allowed me to get the focus event without the change event.

Given that I had to figure out how to get the two references in sync, what I needed was a way to get the onfocus and onblur attributes to reference the directive scope. Referencing the directive scope wasn't enough, however, because I also had to be able to call $digest to get any changes to the scope made outside of Angular (which native event handlers are) recognized. To make that sync easier, I added methods to the scope - one to set a scope property (focused) to true, and one to set the same property to false - and this is key - made sure those methods also called $digest. Next, I changed onfocus and onblur to call the methods I had added to the directive scope in the SELECT elements, and then added the ng-class attribute to the DIV containing the SELECT elements to identify that the field had focus any time the property was true (i.e., ng-class="{'focused': focused}").


Included Markup Files - dd.html

<select name="{{id}}_day"   onblur="angular.element(this).scope().blur();"   onfocus="angular.element(this).scope().focus();"   ng-change="setDate()"   ng-model="day"   ng-options="days.indexOf(day) as day for day in days"> </select>

So, at this point, I was pretty sure it was all set...and, just like in the LEGO movie, everything is awesome.

So, there you go...a quick way to watch the children of a DOM element, sort of.

Happy coding.