Sebastian Gomez
Implementing filter as an operator in Observables
In this post we're going to explore how to grow the number of operators on an Observable by adding the filter operator.
Remember from the previous post, where we implemented the map operator on Observables from scratch, much like RxJS does. Now we're going to increase the number of operators we can use on an Observable.
First, let's take a look at the purest definition of an Observable, with a generator function that builds observables from events:
// Define a class called Observable
class Observable {
// The class constructor receives a subscribe function
constructor(subscribe) {
// Store the subscribe function in a private property
this._subscribe = subscribe;
}
// Method to subscribe an observer to the observable
subscribe(observer) {
// Run the subscribe function with the provided observer
return this._subscribe(observer);
}
// Static method that creates an observable from a DOM event
static fromEvent(domElement, eventName) {
// Return a new Observable instance
return new Observable(function subscribe(observer) {
// Define an event handler that calls the observer's next method with the event
const handler = (ev) => {
observer.next(ev);
};
// Add the event handler to the DOM element for the given event name
domElement.addEventListener(eventName, handler);
// Return an object with a method to cancel the event subscription
return {
unsubscribe() {
// Remove the event handler from the DOM element
domElement.removeEventListener(eventName, handler);
},
};
});
}
}With this, you can create Observables that return event information like so:
// Get a reference to the button with ID "button" from the HTML document
const button = document.getElementById("button");
// Create an observable called "clicks" using the static method "fromEvent" of the Observable class,
// subscribing the button to the "click" event
const clicks = Observable.fromEvent(button, "click");
// Subscribe an observer object to the "clicks" observable and store the subscription in "clicksSubscription"
const clicksSubscription = clicks.subscribe({
// Define the observer's "next" method to handle click events on the button
next(e) {
// Log a message indicating the button was clicked and show the event object "e"
console.log("Click in the button", e); // "e" holds all the event information
}
});
// Cancel the subscription to button click events using the "unsubscribe" method of the subscription object
clicksSubscription.unsubscribe(); // This is how you can unsubscribeBut sometimes we need to perform more operations over the data stream. For example, if we want to get only some data from the event, like the mouse position, or if we only want to listen to clicks made on the left side of the button. To do that, we're going to implement the filter function over Observables.
filter example in Arrays
Let's see how filter works on JavaScript Arrays before implementing it on Observables:
[1, 2, 3, 4].filter((x) => x > 3);
// returns [4]filter, just like map, receives a function as a parameter. That function is a validation function and returns true or false. The filter operator returns the element for which the validation function is true.
Implementing filter in Observables
Remember that we defined the map method in the previous post, so here we only add the filter method to the same Observable class:
// Define a "filter" method that takes a validation function as an argument
filter(validationFunction) {
// Store a reference to the current object in the "self" variable
const self = this;
// Return a new observable
return new Observable(function subscribe(observer) {
// Subscribe an observer object to the current observable ("self") and store the subscription in "subscription"
const subscription = self.subscribe({
// Define the observer's "next" method to handle the values emitted by the observable
next(v) {
// If the validation function returns true for the value "v", pass the value to the observer's "next" method
if (validationFunction(v)) {
observer.next(v);
}
},
// Define the observer's "error" method to handle errors emitted by the observable
error(e) {
// Pass the error "e" to the observer's "error" method
observer.error(e);
},
// Define the observer's "complete" method to handle the completion of the observable
complete() {
// Call the observer's "complete" method
observer.complete();
}
});
// Return the subscription
return subscription;
});
}Now let's see how to use the filter operator on Observables to filter the clicks made on the left side of the button. Since filter and map return a new Observable, we can chain them directly:
// Get a reference to the button with ID "button" from the HTML document
const button = document.getElementById("button");
// Create an observable called "clicks" using the static method "fromEvent" of the Observable class,
// subscribing the button to the "click" event
const clicks = Observable.fromEvent(button, "click");
// Filter and transform the click events before subscribing an observer object
const clicksSubscription = clicks
// Apply the "filter" method to allow only events where the mouse X position is less than 40
.filter((ev) => ev.offsetX < 40)
// Apply the "map" method (implemented in the previous post) to transform the event into its mouse X position (offsetX)
.map((ev) => ev.offsetX)
// Subscribe an observer object to the filtered and transformed events
.subscribe({
// Define the observer's "next" method to handle click events on the button
next(offsetX) {
// Log a message indicating the mouse X position at the moment of the click
console.log("Mouse position relative to X " + offsetX); // Only prints if you click to the left of "Me"
},
});Here is the complete example with the filter and map implementation:
https://codepen.io/seagomezar/pen/WdVzxB
Note: verify that the CodePen link is still live before publishing; old short IDs sometimes stop existing.
Conclusions
- We've learned to implement the
filteroperator on Observables to filter and process the events that meet certain criteria. - Operators on Observables, like
filterandmap, let us manipulate and perform operations over the data stream more efficiently. - Operators over Observables follow a similar logic and, for the most part, must return a new Observable. The operations or filters over the values are done in the
nextfunction.
Exercises to practice
- Create an Observable that filters and shows only the clicks made with the right mouse button.
- Implement an Observable that filters and shows only keyboard events with arrow keys (up, down, left, and right).
- Combine the
filterandmapoperators to create an Observable that filters keyboard events and transforms the output into an object that holds relevant information about the pressed key (for example, the key name and whether it was pressed together with a modifier key like Shift, Ctrl, etc.).
3-point summary
- We learned to implement and use the
filteroperator on Observables. - We understood how operators on Observables, like
filterandmap, work to manipulate events. - We saw examples of how to apply
filterandmapin real use cases, like filtering clicks and keyboard events.
Additional ideas to apply what you learned
- Implement an Observable that filters and shows the user's
scrollevents on a web page, but only when the scroll goes upward. - Create an Observable that detects and shows change events (
change) on a form, but only when the entered value meets certain conditions, like a minimum length or a specific format. - Combine the
filterandmapoperators to create an Observable that detectsmousemoveevents and transforms the output into an object that holds relevant information about the mouse position as percentages of the window width and height. - Use the
filterfunction on Observables to implement an autocomplete system that shows suggestions based on the user's input in a text field, but only once the user has typed at least three characters. - Implement an Observable that filters and shows window size change events (
resize), but only when the difference between the previous size and the new size is greater than a specific value.
These extra ideas will help you apply and consolidate what you learned about implementing filter on Observables. Don't forget that you can combine different operators to achieve more complex results tailored to your needs.
That's all, I hope this post is useful to you and that you can apply it to a project you have in mind. Leave me a comment if it helped or if you have any questions, and if you liked it, share it using the social links below. Good luck with your projects and keep exploring the possibilities that Observables offer!
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.