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
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
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
Included Markup Files - dd.html
Included Markup Files - mm.html
Included Markup Files - yy.html
Using this
ng-switch
pattern with parsing code similar to...
Format Parsing
...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. Thattooltip
, 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
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.
No comments:
Post a Comment