In the midst of our work hacking on v3 (and I mean that in the best possible light), we ran across an interesting problem with an even more interesting solution.
Before I get into that, reflecting on this recent problem reminded me that I haven't talked about our development process in a little while.
I thought it might make an interesting exercise to step through my thought process and demonstrate a couple of neat developer habits that worked well. Sound good? Let's go!
There could be myriad reasons why that is the case, from CORS miconfigurations, to network congestion, or just the most common reason of all, that the file on the other end just no longer exists.
For the longest time, we simply ignored the problem. It wasn't significant enough to pose a usability issue, and usually affected a very small subset of user avatars β as you can imagine, active users tend to want to have a working avatar.
However, when setting out to resolve this, we wanted to find a solution that allowed for falling back to the user icon (the coloured circle with the single letter). This complicated matters somewhat, due to how our avatars are generated.
We use the buildAvatar helper as a shorthand in our templates to generate a consistent avatar. It was introduced many years ago to enforce consistency across the Persona theme, as each invocation of the user icon had its own idiosyncrasies. In and of itself, the helpers still does allow for some customization. You can customize the size, shape (square or round), classes, attributes, and even specify an alternative component.
However, the helper did enforce one thing: it displayed an avatar, or if one was not available, it displayed the user icon.
The avatar was an <img />
element, and the user icon was a <span>
element. As opposed to our competitors who dynamically generated an image on the server-side, we opted to keep things simple and use plain HTML and CSS.
If an image failed to load, we could load a fallback image (at this point, how we went about doing that was not considered).
We moved on from this solution because we wanted the appropriate fallback to be the user icon, and not a hardcoded image substitute.
The first solution to mind was to have the server check that the image was loadable. The server could even act as a caching layer, in case the remote image went down.
In the end we discounted this solution because of a few factors:
Continuing my research on handling images that fail to load, I stumbled upon a "solution" that utilised CSS pseudo-elements to generate a fallback image or placeholder.
There were a couple red flags for this one:
I then turned my attention to doing a client-to-server round trip to regenerate the icon. Since we called the buildAvatar helper to generate the image the first time, we could retrieve all necessary data and call it again (just without the broken picture url).
However, things got complicated quickly when I realized that we didn't have the original context for the image.
If all we had was the user picture, how could we get the appropriate user data to call the helper again?
But wait! The generated HTML contains a data-uid
paramater, we're saved!
While we did have the user id, and that technically was enough data to do that round-trip to get the user data, we didn't easily know what arguments were originally passed into that buildAvatar
helper (e.g. whether it was round, what size, etc.)
But not to worry! We could easily parse the attributes back out from the generate HTML, so we could accurately pass those back in...
You see where I am going with this, yes? In my quest to resolve this (admittedly small) problem, I went through a number of solutions, each more complicated than the last.
It's very easy to fall into this type of trap, where you dig yourself deeper and deeper attempting to overengineer a solution. It's essentially the sunk cost fallacy, I've already spent X amount of time on this, what's another couple minutes/hours?
It was precisely at this point that I practiced what I consider the holy grail of developer tools β the rubber duck. I stepped through the problem with my colleague Baris, mostly out of amusement, and quickly ran through my attempted solutions.
At the same time, I stepped back. All of the above had been occurring over the past couple of hours, and there are times when you are so stepped in the context of the problem that you cannot see the forest for the trees.
So what happened? In the span of five minutes, I came up with the actual solution. A solution that surprised me in its relative simplicity.
buildAvatar
does exactly one thing and it did it well: it consumed user data, and it output either an image at a specified dimension, or a user icon with the appropriate CSS to match the desired dimensions.
Either way, an element with known dimensions was generated, so it did not matter whether an image url was available or not.
Knowing this, it was technically possible to generate both the pictureΒ in addition to the user icon, and simply hide the second using CSS. If a user's picture fails to load, we can simply remove it, and the user's icon would magically take its place, since it's no longer hidden because it's no longer the second avatar.
Here is the commit where buildAvatar is updated to show both avatars (with the second one hidden), and the corresponding commit where on error, the image itself is removed from the DOM.
I love when I can replace potentially large solutions with relative one-liners. Happy hacking!
.avatar +.avatar { display: none; }
onError
attribute to the potentially faulty image that contains this.remove()