Prototyping reactive interfaces with an adapted JavaScript Observer pattern
Did you think this piece was going to have something to do with React, the JavaScript framework? It doesn’t. Therefore, move along you lot; there’s another bazillion posts out there for you.
By the same token, if you are a Computer Science graduate and already know what an Observer pattern is and why you would use it, you can probably save yourself any further reading.
For the two of you that are still here, this post is documenting how it’s possible to prototype a reactive interface with some vanilla, and relatively straightforward, JavaScript known as an ‘Observer’ pattern.
Thanks to Tom’s input, we will end up with an amended Observer pattern but it has some extra benefits that may suit your needs too.
Introduction
I spend much of my working day prototyping new product features. I enjoy prototyping immensely as you can move fast, fail quickly and iterate until satisfaction. This is most straightforward with ‘flat’ ideas and designs. In that instance it’s mostly just HTML and CSS with a dab of JavaScript for basic interaction.
However, sometimes you want to prototype something a little more ‘alive’. Perhaps more accurately, one may wish to prototype an interface that responds in multiple places to changes in common data or input.
For the purpose of this post, consider prototyping a simple stock buying widget. The stock price will change randomly and based upon the current stock price and our minimum buy price (that the user will input) a ‘Buy Stock’ button will light up or not as the case may be. This is as simple as possible a demonstration as I could imagine but you’ll hopefully appreciate the principle. What this widget does will not be important so much as the way it is doing it.
What can we easily write with JavaScript that can solve this need? Ordinarily, when prototyping something, I write very ‘procedural’ JavaScript; user presses a button, I write an attribute into the body, insert some text somewhere etc. Each function follows another and the ‘state’ of the thing is communicated and checked by interrogating the DOM. For example:
if (document.body.getAttribute("data-widget-enabled" === true)) {
// the widget is enabled
fireNextThing();
}
This is fine to a point but gets messy the more complex the functionality you are prototyping. I found myself constantly following trails in the debugger tools, trying to ascertain which function had set which thing to which value.
In short, things were starting to smell in my code and I felt I needed to find a better tool for the job at hand.
The Observer pattern
My search for the right approach for the job at hand led me to the ‘Observer’ pattern. My first stop on the Google links was a section on the Observer pattern from Addy Osmani’s ‘Learning JavaScript Design Patterns’. This certainly sounded like everything I wanted:
The Observer is a design pattern where an object (known as a subject) maintains a list of objects depending on it (observers), automatically notifying them of any changes to state.
However, in practice, I stumbled when trying to wrap my head around ‘concrete’ Observers and basically get anything working. I left there humbled and confused so my search continued.
The first post I came across that explained something that made sense to me was http://jarrettmeyer.com/2016/04/18/observer-pattern-in-javascript and I managed to get up and running pretty quickly.
That explanation is the basis of how the first example of our stock widget works:
See the Pen ZpqvRW by Ben Frain (@benfrain) on CodePen.
Let’s look under the bonnet. The principle explained in Jarrett’s post is making a function to house your data states:
function Widget() {
this.validInput = false;
this.maxValue = 10;
this.marketOpen = false;
this.livePrice = 10;
this.observers = [];
this.buttonText = "Stock price too high";
}
We need some observers to watch the data states for change. The following function facilitates adding observers into the list of observers that will be notified of changes:
Widget.prototype.addObserver = function (observer) {
this.observers.push(observer);
}
We will then need a ‘notify’ function that loops through each of the observers that we add and notifies them of data changes:
Widget.prototype.notify = function (data) {
this.observers.forEach(function(observer) {
observer.call(null, data);
})
}
You can create an instance of our simple stock widget like this:
var sw = new Widget();
Remember, you can see this implementation here: http://codepen.io/benfrain/pen/ZpqvRW
To make this work we have a standard function that loops and fires another function to adjust the stock price:
(function loop() {
var rand = Math.round(Math.random() * (1000 - 500)) + 3000;
setTimeout(function() {
randomPrice();
loop();
}, rand);
}());
The random price function adjusts the price up or down by 5 and then fires the notify function of our widget (sw.notify
):
function randomPrice() {
var possibleResponses = [1,2];
var rand = possibleResponses[Math.floor(Math.random() * possibleResponses.length)];
if (rand === 1) {
sw.livePrice = sw.livePrice + 5;
} else {
sw.livePrice = Math.max(sw.livePrice - 5, 0);
}
sw.notify({
livePrice: sw.livePrice
});
swPrice.textContent = sw.livePrice;
}
The functions that amend data don’t need to be added as a prototype; any function can amend the data so long as it invokes the notify function of the changes. So, consider this snippet of code. Here, when the user enters a different value we want to update the data and notify any observers:
swInput.addEventListener("input", function(){
sw.maxValue = parseFloat(swInput.value);
sw.notify({
maxValue: sw.maxValue
})
}, false);
An observer looks like this. In this instance, it changes the state of the button depending upon whether it is above or below the ‘Max Buy Price’:
sw.addObserver(function(){
if (sw.livePrice > sw.maxValue) {
swBuy.setAttribute("aria-disabled", "true");
swBuy.textContent = "Stock Price too high";
} else {
swBuy.setAttribute("aria-disabled", "false");
swBuy.textContent = "Buy Now";
}
});
This pattern works quite well but with some limiting caveats:
- Every time something makes a change to the data and you invoke the notify function, every observer updates, regardless of whether the data change is of interest to it or not. For example, say we have the following possibilities:
function Widget() {
this.validInput = false;
this.maxValue = 10;
this.marketOpen = false;
this.livePrice = 10;
this.observers = [];
this.buttonText = "Stock price too high";
}
And we have an observer looking after a certain area of the interface that only cares about the this.livePrice
updating, it will always run, regardless on every change to the data. If your observer is doing any DOM work (for example, setting classes, attributes or updating textContent
or innerHTML
) it will repeat that work every time, whether or not anything it cares about has actually changed.
The second caveat to this pattern is that you must be careful that you don’t create an observer that amends data based upon a change to the same data. For example, this would create an infinite loop:
sw.addObserver(function(){
if (sw.livePrice > sw.maxValue) {
sw.livePrice += sw.livePrice + 5;
sw.notify({livePrice: sw.livePrice})
}
});
An improved Observer pattern for prototyping
Tom hears all my JavaScript woes (poor man). I was relating my unease at every observer running its logic regardless of it’s interest in the piece of data that had changed. He subsequently came up with the following changes. First, the ‘data’ structure stays the same. The observers however declare the properties they are interested in like this:
sw.addObserver({
props: ["livePrice","maxValue"],
callback: function observeAndSetButton() {
if (sw.livePrice > sw.maxValue) {
swBuy.setAttribute("aria-disabled", "true");
swBuy.textContent = "Stock Price too high";
} else {
swBuy.setAttribute("aria-disabled", "false");
swBuy.textContent = "Buy Now";
}
},
});
Let’s talk through how this refined notify function works.
We pass an object instead of a function and the object contains an array containing the properties we are interested in, and then a callback function that we want to execute should any of the properties we are interested in have changed.
The biggest area of change is the notify function. This now checks for invalid property assignment (so I can’t write sw.notify({livePwice: sw.livePrice})
and end up with a livePwice
property alongside a livePrice
one) and then only updates the data if it is different from the value it already has.
Next we filter the observer array into a new array that contains only observers that have interests that align with the properties in the data that have changed.
Then we run the callback function of only those observers that have changes.
Additionally, functions can use the *
symbol as the property they are interested in. That way, they run on every change in the data. For example:
sw.addObserver({
props: ["*"],
callback: function observerEverything() {
// stuff
},
});
The entire notify function now looks like this:
Widget.prototype.notify = function (changes, callback) {
// Loop through every property in changes and set the data to that new value
var prop;
for(prop in changes) {
// First catch any incorrect assignments of the data
if(typeof this[prop] == "undefined") {
console.log("there is no property of name " + prop);
}
// We want to exit if the change value is the same as what we already have, otherwise we update the main object with the new one
if (this[prop] === changes[prop]) {
continue;
} else {
this[prop] = changes[prop];
}
}
// Loop through every observer and check if it matches any of the props in changes
// Do this by filtering the existing array
var matchedObservers = this.observers.filter(hasSomeOfTheChangedProps);
// filter invokes this and returns observers that match into the matchedObservers array
function hasSomeOfTheChangedProps(item) {
// If the props contains a wildcard
if (item.props === "*") {
return true;
}
// Otherwise check if the changed prop is included in the props list
for(var prop2 in changes) {
// To ensure we don't quit the entire loop, we want to return true if the prop2 is in item.props. Otherwise we want to keep looping to check each prop and only quit the function by returning once the entire loop has run
if(item.props.includes(prop2)) {
return true;
}
}
return false;
}
// Now for any observers that care about data that has just been changed we inform them of the changes
matchedObservers.forEach(function (matchingObserver) {
matchingObserver.callback.call(null);
});
};
Here’s a Pen with that new code:
See the Pen rrQNBJ by Ben Frain (@benfrain) on CodePen.
The curious can check the console and notice that although I’ve added another looping function to toggle a different property on a different time delay, the observer only runs if the properties it is interested in (livePrice and maxValue) update.
Summary
Given — we are running a very limited prototype here, so this may seem like unneeded complexity but this amended Observer pattern has proven a worthwhile addition for the prototypes I build that have the potential to grow in functionality and scope.
As ever, I welcome any improvements to this technique in the comments below or I’m on Twitter @benfrain.
Really cool article Ben! You confused me a bit half way through, but I read it one more time, and one more (I tried about 4 times) and it clicked eventually. I started with Jarrett’s post and then came back to yours. Like it so much! Thanks again!