bnieland bnieland - 3 months ago 19
Javascript Question

Recognizing and exact series of events in RxJS

Using RxJS, I want to fire an event if the user types 'a' then 'b' then 'c'.

I do not want the event fired if they enter 'a' then 'b' then 'z' then 'c'.

Here is my codepen of the work I have done so far (in TypeScript).

class App1 {
private divStream: HTMLElement;
private divResult: HTMLElement;

constructor(divStream: HTMLElement, divResult: HTMLElement) {
this.divStream = divStream;
this.divResult = divResult;
}

start() {
var filterByCharacter = (expectedCharater) => {
return (char) => { return char === expectedCharater; };
};

var values = ['a', 'b', 'b', 'c', 'b'];

var obChars = Rx.Observable.fromArray(values);
obChars.subscribe((k) => {
divStream.innerHTML += "<div style='color: blue'>" + ":: " + k + " ::" + "</div>";
},
(err) => {
divStream.innerHTML += "<div style='background-color: blue' > " + 'Error: ' + err + " </div>";
},
() => {
divStream.innerHTML += "<div style='background-color: blue'>" + ":: finished ::" + "</div>";
}
);

function log(text: string) {
divResult.innerHTML += "<div style='color: green'>" + text + "</div>";
}
var obA: Rx.Observable<string> = obChars.filter(filterByCharacter('a'));
var obB: Rx.Observable<string> = obChars.filter(filterByCharacter('b'));
var obC: Rx.Observable<string> = obChars.filter(filterByCharacter('c'));

let aSteps: Rx.Observable<any>[] = [];
aSteps.push(obA.take(1).do(() => { log("a"); }).ignoreElements());
aSteps.push(obB.take(1).do(() => { log("b"); }).ignoreElements());
aSteps.push(obC.take(1).do(() => { log("c"); }));
let steps: Rx.Observable<any> = Rx.Observable.concat<any>(aSteps);
var source = steps
.takeUntil(Rx.Observable.timer(100 * values.length));

var subscription = source.subscribe(
function (x) {
log("Next: " + x);
},
function (err) {
divResult.innerHTML += "<div style='background-color: green'>Error: " + err + "</div>";
},
function () {
divResult.innerHTML += "<div style='background-color: green' > " + 'Completed' + "</div>";
});
}

stop() {
clearTimeout(this.timerToken);
}

}

window.onload = () => {
var app = new App1(document.getElementById('divStream'), document.getElementById('divResult'));
app.start();
};

Answer

This seems to be working, it uses a simple state machine and is generalizable to recognize any basic regular expression. The regular expression recognized here is *abc* :

function noop () {}

var keyUp$ = 
  Rx.Observable.fromEvent(ta_input, 'keyup')
               .map(function(ev){return ev.keyCode});

var stateMachine$ = keyUp$
  .scan(function (state, keyCode) {
    if (String.fromCharCode(keyCode) === state.password[state.index]) {
      state.index++;
      if (state.index === state.password.length) {
        state.found = true;
      }
    } 
    else {
      state.index = 0;
      state.found = false;
    }
    return state;
}, {password : 'ABC', index : 0, found: false})
  .filter(function (state){return state.found})
  .take(1)

stateMachine$.subscribe(noop)

To check it, run the jsffidle, and type abc in the textarea. For some reason, your password has to be in caps, but that part should be easy to fix. When the abc substring is detected, the stateMachine$ observable emits found and completes.