Sebastian Gomez
Implementing Observables from Events in JS
In this post we are going to explore how to implement observables and how reactive and functional programming is changing the way we approach our code. Get ready for a journey full of knowledge and fun.
Reactive and functional programming
Reactive and functional programming is a growing trend in the development world, but it is still not widely known, especially among people who are just starting out. In this post we are going to explore the purest definition of an observable and how to implement the most common observable generator function, just like RxJS does, so we can understand the concept in depth.
Understanding observables
The best way to approach observables is to understand that they are the combination of two design patterns in JavaScript: the iterator pattern, which we explain in this post about the iterator pattern, and the observer pattern, which we explain in this post about the observer pattern.
Note: verify that both links resolve to the correct sibling posts before publishing. The iterator pattern post exists on this site; confirm the exact URL of the observer pattern post.
After reading about these two patterns and having an idea in mind of what they are, an observable is nothing more than a function in JavaScript with another function inside it that returns an object (JSON) with three functions: onNext, onError and onCompleted.
Implementing an observable in JavaScript
Here is how to implement an observable in JavaScript using the classic prototype syntax (ES5):
// Classic prototype style (ES5)
// This version uses constructor functions and prototypes, the style that came before ES6 classes.
function Observable(forEach) {
// We define the 'Observable' constructor function that takes a 'forEach' argument and creates a new Observable object.
this._forEach = forEach;
// We assign the forEach function received as an argument to the '_forEach' property of the Observable object.
}
Observable.prototype = {
// We define the properties of the 'prototype' object of the 'Observable' constructor function.
forEach: function (onNext, onError, onCompleted) {
// We declare a 'forEach' method on the prototype of 'Observable' that takes three arguments: onNext, onError and onCompleted.
if (typeof onNext === "function") {
// If the first argument 'onNext' is a function, then...
return this._forEach({
// ...we run the function stored in '_forEach' with an object that contains the properties and their values.
onNext: onNext,
// 'onNext' is assigned the value passed as the first argument.
onError: onError || function () {},
// If 'onError' is provided, it is assigned to the 'onError' property. Otherwise, an empty function is assigned.
onCompleted: onCompleted || function () {},
// If 'onCompleted' is provided, it is assigned to the 'onCompleted' property. Otherwise, an empty function is assigned.
});
} else {
// If the first argument 'onNext' is not a function, then...
return this._forEach(onNext);
// ...we run the function stored in '_forEach' with the first argument 'onNext' as the parameter.
}
},
};As you can see, it is a function called Observable that receives a function called forEach as a parameter. This has everything to do with the forEach function we use with arrays in JavaScript. This function is implemented internally inside the Observable to make it part of the observable's own definition.
Capturing events with observables
Imagine you want to show a console.log("Clicked"); every time a person clicks a button on your website. To do this we are going to create a static function inside the Observable definition we created earlier, which will turn a DOM event into an observable:
Observable.fromEvent = function (dom, eventName) {
// We add a static 'fromEvent' method to the 'Observable' object.
// This method takes two arguments: a DOM element 'dom' and an event name 'eventName'.
return new Observable(function forEach(observer) {
// The function creates and returns a new 'Observable' object.
// The 'forEach' function passed as an argument to the constructor takes an 'observer' as a parameter.
var handler = (e) => observer.onNext(e);
// We create a 'handler' that is an arrow function that receives an event 'e' and calls the 'onNext' method of the 'observer', passing it 'e'.
dom.addEventListener(eventName, handler);
// We register the 'handler' on the DOM element 'dom' for the event specified in 'eventName'.
return {
// We return an object that has a single 'dispose' property.
dispose: () => {
// The 'dispose' property is an arrow function that...
dom.removeEventListener(eventName, handler);
// ...removes the 'handler' previously registered on the DOM element 'dom' for the event specified in 'eventName'.
},
};
});
};The fromEvent function receives a DOM element and the name of the event we want to track, and returns an observable that gives us back each click event as it happens. Let's see how it is used:
const button = document.getElementById("button");
// We declare a constant 'button' and assign it the DOM element with the ID "button".
const clicks = Observable.fromEvent(button, "click");
// We declare a constant 'clicks' and assign it the result of calling the static 'fromEvent' method of 'Observable',
// passing it the DOM element 'button' and the event name "click". This creates an 'Observable' object that listens for the
// click event on the button.
const clicksSubscription = clicks.forEach((event) => {
// We declare a constant 'clicksSubscription' and assign it the result of calling the 'forEach' method on the
// 'clicks' object. This function takes an arrow function as an argument that receives an 'event'.
console.log("The user clicked", event);
// Inside the arrow function, we log a "The user clicked" message followed by the 'event' object.
});Now we are going to see how we can create observables from events using the modern ES6 (ES2015) class syntax, the differences with the earlier style and how to subscribe and unsubscribe from observables. Join us on this exciting journey.
Using ES6 (ES2015) classes for observables
When using the ES6 class syntax, the first thing we need to keep in mind is that the forEach function is replaced by convention with the subscribe function. In addition, we will use the reserved words class and static to create our observable implementation. Let's see how we implement the pure observable definition with classes:
class Observable {
// We declare an 'Observable' class.
constructor(subscribe) {
// We define the class constructor, which takes a 'subscribe' argument.
this._subscribe = subscribe;
// We assign the 'subscribe' function received as an argument to the '_subscribe' property of the Observable object instance.
}
subscribe(observer) {
// We declare a 'subscribe' method on the 'Observable' class that takes an 'observer' argument.
return this._subscribe(observer);
// We run the function stored in the '_subscribe' property of the Observable object instance,
// passing it the 'observer' argument and returning the result.
}
}Thanks to the class and constructor definition, the observable implementation is shorter. Also, in this new definition we do not care how the observer receives the onNext, onError and onCompleted functions (which by convention are changed to next, error and complete).
Implementing fromEvent with ES6 (ES2015) classes
Now let's see how to implement the fromEvent function to create observables from events using classes:
class Observable {
// We declare an 'Observable' class.
// ...
// The constructor and other methods that are not shown in this code snippet should be included here.
static fromEvent(domElement, eventName) {
// We declare a static 'fromEvent' method on the 'Observable' class that takes two arguments: a DOM element 'domElement' and an event name 'eventName'.
return new Observable(function subscribe(observer) {
// The function creates and returns a new 'Observable' object.
// The 'subscribe' function passed as an argument to the constructor takes an 'observer' as a parameter.
const handler = (ev) => { observer.next(ev) };
// We create a 'handler' that is an arrow function that receives an event 'ev' and calls the 'next' method of the 'observer', passing it 'ev'.
domElement.addEventListener(eventName, handler);
// We register the 'handler' on the DOM element 'domElement' for the event specified in 'eventName'.
return {
// We return an object that has a single 'unsubscribe' property.
unsubscribe() {
// The 'unsubscribe' property is a function that...
domElement.removeEventListener(eventName, handler);
// ...removes the 'handler' previously registered on the DOM element 'domElement' for the event specified in 'eventName'.
}
}
});
}
}Running observables
Running the observable is done in a way similar to the earlier style, but instead of using the forEach function, we use the subscribe function:
// We create a constant called 'button' that stores the HTML element with the ID "button"
const button = document.getElementById("button");
// We create a constant called 'clicks' that uses our own Observable implementation to observe the 'click' event of the HTML element stored in the 'button' constant
const clicks = Observable.fromEvent(button, "click");
// We create a constant called 'clicksSubscription' that subscribes a callback function that runs every time a 'click' event is detected
const clicksSubscription = clicks.subscribe({
// The 'next' function is called every time a 'click' event is detected and shows a log message in the console
next(e) {
console.log("Click in the button");
},
});Although there are differences in how the simplest pure definition of an observable is implemented between the earlier style and the modern one, invoking and running the observable is very similar in both cases.
Cold observables
Observables are "cold" or "lazy" by nature, which means they do nothing unless someone is listening to them. To unsubscribe from an observable, you can use the dispose method (earlier style) or unsubscribe (modern style) that comes with the subscription:
// Old standard
clicksSubscription.dispose();
// New standard
clicksSubscription.unsubscribe();Conclusions
Implementing observables with ES6 (ES2015) classes is shorter and easier to understand thanks to the use of class and constructor.
With the class syntax, the forEach function is replaced by the subscribe function, and the onNext, onError and onCompleted functions are changed to next, error and complete.
Observables are "cold" or "lazy" and only run when someone is listening to them. To unsubscribe, you use the dispose method (earlier style) or unsubscribe (modern style).
Suggested exercises
- Create an observable that shows a message every time the person moves the mouse over a specific element on the page.
- Implement an observable that runs when a key is pressed in a text field, and filters only the events from the alphabetic keys (a z).
- Design an observable that combines events from multiple elements on the page and performs actions based on the sequence of events.
3-point summary
- ES6 (ES2015) classes make it easier to create observables thanks to class and constructor definitions, and the
subscribefunction replaces theforEachfunction. - The
fromEventfunction lets us create observables from DOM events, making it easier to handle events in web applications. - Observables are cold and only run when they have at least one observer, which gives us greater control over when they run and when they stop.
I hope this post has been useful to you and has helped you understand the nature of observables in JavaScript. Do not hesitate to leave a comment if you have a question, a suggestion, or if you want to add some other functionality. If you liked it, share this post on your social networks using the links below. Until next time.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.