LiveLoveApp logo

Solution - Pipe Function

Solution

import { from, fromEvent, pipe } from 'rxjs';
import { bufferCount, map, mergeMap, sequenceEqual } from 'rxjs/operators';

const PASSCODE = [1, 1, 1, 1];
const buttons = document.querySelectorAll('.btn');

const verifyPasscode = () =>
  pipe(
    bufferCount<number>(4),
    mergeMap((passcode) => from(passcode).pipe(sequenceEqual(from(PASSCODE))))
  );

fromEvent<MouseEvent>(buttons, 'click')
  .pipe(
    map((event: MouseEvent) => {
      const target = event.target as HTMLButtonElement;
      return parseInt(target.dataset.key!, 10);
    }),
    verifyPasscode()
  )
  .subscribe({
    error: console.error,
    next: console.log,
    complete: () => console.log('complete')
  });

See example on codesandbox

Let's review the solution above:

  • First, we define the verifyPasscode function that returns the Observable from the pipe() function.
  • Within the pipe() function we use the bufferCount() operator to buffer 4 next notifications and then emit an Observable that immediately emits a single next notification of all of the values in an array.
  • We then use the mergeMap() operator that receives the passcode array of numbers. Using the from() operator we create a new Observable that emits each number value in sequence and then completes. Within the pipe() of this new Observable we use the sequenceEqual() operator to compare the Observable against the known PASSCODE Observable, which we also create using the from() operator.

Bonus Solutions

import { from, fromEvent, pipe } from 'rxjs';
import {
  bufferCount,
  map,
  mergeMap,
  sequenceEqual,
  tap,
  delay
} from 'rxjs/operators';

const PASSCODE = [1, 1, 1, 1];
const buttons = document.querySelectorAll('.btn');
const dots = document.querySelectorAll('.dot');
const clear = (el: HTMLElement) => el.classList.remove('bg-black');
const fill = (el: HTMLElement) => el.classList.add('bg-black');

const clearElements = <T>(elements: NodeList) =>
  pipe(
    delay(1000),
    tap(() =>
      elements.forEach((el) => clear(el as HTMLElement))
    ) as OperatorFunction<T, T>
  );

const fillElements = <T>(elements: NodeList) => {
  let index = 0;
  return pipe(
    tap(() => {
      if (index < PASSCODE.length) {
        const el = dots.item(index) as HTMLElement;
        fill(el);
        index++;
      } else {
        dots.forEach((el) => clear(el as HTMLElement));
        index = 0;
      }
    }) as OperatorFunction<T, T>
  );
};

const verifyPasscode = () =>
  pipe(
    bufferCount<number>(4),
    mergeMap((passcode) => from(passcode).pipe(sequenceEqual(from(PASSCODE))))
  );

fromEvent<MouseEvent>(buttons, 'click')
  .pipe(
    map((event: MouseEvent) => {
      const target = event.target as HTMLButtonElement;
      return parseInt(target.dataset.key!, 10);
    }),
    fillElements(dots),
    verifyPasscode(),
    clearElements(dots)
  )
  .subscribe({
    error: console.error,
    next: console.log,
    complete: () => console.log('complete')
  });

See example on codesandbox