DEV Community

Cover image for RegExp syntax for Observables: Never Been Easier!
Kostia Palchyk
Kostia Palchyk

Posted on • Updated on

RegExp syntax for Observables: Never Been Easier!

Imagine we need to implement a drag-n-drop behavior.

For that, we have three event streams: mousemove$, mousedown$, mouseup$. So we want to capture mousemove$ events after mousedown$ emitted and before mouseup$.

Let's draw a marble diagram of the event streams we have:

mousedown$  --o------------
mousemove$  -o-o-o-o-o-o-o-
mouseup$    ------------o--
Every event stream is represented with separate line.
o stands for event on the stream. - is a separator to represent passing time

For better readability let's substitute all the os to respective letters of the streams:

mousedown$  --d------------
mousemove$  -m-m-m-m-m-m-m-
mouseup$    ------------u--
'd' will now stand for mouse-down event, 'm' for mouse-move, 'u' for mouse-up

Now that we have distinct event names, we can simplify our diagram to a single line of events:

events$     -mdm-m-m-m-mum-

Let's remove the time - signs as well, we don't them:

events$      mdmmmmmum

Okay, to rephrase our task in terms of the new diagram: we need to capture the m events between d and u emissions.

🤔

Hmm...

"we need ms between d and u"...

Sounds familiar...

Ah! If that was a string, we could easily do it with a regular expression:

/dm*u/.exec('mdmmmum')

Would give us the needed dmmmu without trailing mouse-move m events...

Right?

If only we had a library to select events from streams with regexes...

🤩 Introducing Regular Expressions for Rx

With exprs-rx package you can now query your streams with regular expressions like syntax!

mousedown$  --d------------
mousemove$  -m-m-m-m-m-m-m-
mouseup$    ------------u--

                 dm*u

result$     --dm-m-m-m-mu|

Benefits:

  • clear and extensible syntax
  • no need to remember dozens of operators
  • no need to import them, which reduces bundle size
  • with more implementations to come — one syntax for all stream libraries (monetjs, kefirjs, baconjs, TC39 Observables, etc)

Example

Here's our drag-n-drop example in JS:

import { exec } from 'exprs-rx';

const item = document.getElementById('item');

const D = fromEvent(item, 'mousedown');
const M = fromEvent(document, 'mousemove');
const U = fromEvent(document, 'mouseup');

exec(
  'DM*U'         // <- regular expression
  , { D, M, U }  // <- streams that will be used
)
  .pipe(
    // ... apply any operators ...
  )
  .subscribe(console.log);

Repo //github.com/expressions-for-reactive-streams/js-rx-exprs

Currently, the library supports capital letters A-Z that represent a single emission of a corresponding stream.

And a repeat A* notation that will consume multiple events from the corresponding stream, until completion or until next entry in the regex matches.

And much more to come: (), [], !, etc!

For regex specification and plans on vocabulary, please, see this repo github.com/expressions-for-reactive-streams/spec-regular-expressions

Try it

Install it

npm i exprs-rx

🙌 Outro

The idea is to extend the expressions syntax to cover 80% of routine RxJS tasks. And maybe add combination commands. The possibilities are endless!

For upcoming amazing updates — follow me here and on twitter

If you've found this post worth sharing — please, use this tweet:

🙂 Thanks!

Latest comments (5)

Collapse
 
deanius profile image
Dean Radcliffe

Wow! Please show how to do the Konami code! [up, up, down, down, left, right, left, right, b, a, enter];..

Might just be me, but I think your solution might compare well against:

gist.github.com/MartinSeeler/f89dd...

Collapse
 
kosich profile image
Kostia Palchyk

Hey, Dean!

Yeah! In the second part I'm exploring an operator based approach, where
exec('DM*U' , { D, M, U }) transforms into query(D, some(M), U) (note that some is an operator example). And with that format we potentially could define our custom operators to match particular keydown events. So, I think, with a few tweaks it could look something like this:

let keydown$ = fromEvent(document, 'keydown');
const [up, down] = [38,40].map(code => Operator(keydown$, e => e.key == code));
query(up, up, down, down, /* ... */ )
  .subscribe(() => console.log('CHEATER!!11elf'));
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bradtaniguchi profile image
Brad

If only we had a library to use regular expressions on streams

Whoa... RxJs and Reactive Programming can be hard. Regular Expressions can be hard. Seems like combining the two could make whatever your doing harder.

clear and extensible syntax

I don't consider regex to be clear if you don't use them regularly. (Which hopefully is most people hehe)

What would be a real-world use-case for getting --dm-m-m-m-mu| as a use case? Maybe testing?

Collapse
 
kosich profile image
Kostia Palchyk

Hey, Brad!

Almost a year after I think I heard you 🙂
I've created an operator-based API!

// > Mouse DnD

// get dom element to drag
const item = document.getElementById('item');

// capture mouse down, up and move
const D = fromEvent(item, 'mousedown');
const M = fromEvent(document, 'mousemove');
const U = fromEvent(document, 'mouseup');

// listen to a query (_DM*_U)* & log it
query(
  some(mute(D), some(M), mute(U))
)
  .subscribe(({ x, y }) => console.log(x,y));

Check it out: github.com/erql

What do you think?

Collapse
 
kosich profile image
Kostia Palchyk • Edited

Hi, Brad!

Because I might've confused everyone with strings and regexes comparisons, just to be sure:
We don't get a string "--dm-m-m-m-mu|". Marble diagrams I used are only for visualization of what's going on with Observables. We work with an actual live stream of events.

When we apply a selector, what we get — is a stream of events, that match the selector.

For testing purposes, please see this amazing package rxjs-marbles by @cartant (it's actually used in this package testing flow)

Sorry, if that was obvious, just wanted to make sure we're on the same page 🙂

Now. To the main point:

Whoa... RxJs and Reactive Programming can be hard. Regular Expressions can be hard. Seems like combining the two could make whatever your doing harder.

Yep, both might be hard! And have you seen that regexp for email validation? Sheesh!

And Observables: I've been using RxJS for about 3 years now, have created a playground for observables and am currently working on a RxJS-based framework. Still I'm often confused with that toolset of operators:

Take the mouse Drag and Drop example: if I were to implement that in pure RxJS, I would start with this:

mouseDown$.pipe(
  switchMap(() => mouseMove$),
  takeUntil(mouseUp$),
  repeat()
)

Then I'd realize that I'm doing a resubscription on repeat(), that could've been omitted. So I dig in my memory and under a thick layer of TV shows, I finally find a windowToggle operator:

mouseMove$.pipe(
  windowToggle(mouseDown$, mouseUp$)
)

Only I forget at first that it accepts a function as a second argument.

mouseMove$.pipe(
  windowToggle(mouseDown$, () => mouseUp$)
)

And then if later I were required to add those trailing mouseDown$ and mouseUp$ events to the output stream w/o an additional subscription — ooff. That code grows fast.

Now with a query expression, the same effect might be achieved with:

exec(
  'DM*U',
  { D: mouseDown$, M: mouseMove$, U: mouseUp$ }
)

My point is that it's sometimes easier to use the declarative regex-like syntax.
And with library evolving — we'll get better options to express ourselves.

Hope this really-really long reply will let you look at the idea at another angle.

Cheers!