Build a reactive split-flap display with Angular

Maxime - Jun 9 '23 - - Dev Community

Hello!

I've decided to make a follow up from RxJS: Advanced challenge to build a reactive split-flap display and build UI for it as it's always more pleasant to play around with the final app than emit into a subject to see the text change.

Here's a live demo of what we'll be building, that you can play with 🔥:

If you haven't read the previous article I mentioned above, you should really read it first before jumping on this one as the reactive split-flap logic is already built and we'll only be focusing on the Angular side of things here.

Porting the split-flap code to an Angular service

I'm going to be creating a new Stackblitz where I'll import most of the code we built in the first part and integrate that in a more Angular way.

I'll copy over the entire utils.ts file (except for the part where I was manipulating the DOM manually).

Now, we'll create a new service SplitFlapService that'll reuse our reactive split-flap code and wrap up the creation of new instances:

@Injectable({ providedIn: 'root' })
export class SplitFlapService {
  public getSplitFlapInstance() {
    const input$$ = new Subject<string>();

    const splitFlap$ = input$$.pipe(
      map(inputToBoardLetters),
      switchScan(
        (acc, lettersOnBoard) =>
          combineLatest(
            lettersOnBoard.map((letter, i) =>
              from(getLettersFromTo(acc[i], letter)).pipe(concatMap(letter => of(letter).pipe(delay(150)))),
            ),
          ),
        BASE,
      ),
    );

    return {
      input$$,
      splitFlap$,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

So far, so good.

Creating the view

First of all, we know we're going to have an input and that our split-flap only support a given number of chars. So we'll start by creating a custom validator for our FormControl that validates the input:

function allowedLettersValidator(): ValidatorFn {
  return (control: FormControl<string>): ValidationErrors | null => {
    const allValidLetters = control.value
      .toUpperCase()
      .split('')
      .every(letter => REVERSE_LOOKUP_LETTERS[letter] !== undefined);

    if (allValidLetters) {
      return null;
    }

    return { invalidLetters: true };
  };
}
Enter fullscreen mode Exit fullscreen mode

Nothing too fancy. We loop on each of the letters in the input and check if they are defined in our allowed chars. If not, we return an object { invalidLetters: true } that'll make the input invalid.

Now, I won't define our split-flap component here. It's only a presentational component which takes an input for all the letters to display and display them. Nothing more so there's little interest in showing that. Feel free to look the code in the Stackblitz in the src/app/split-flap folder.

Finally, we can focus on the component that will use our new service, instanciate a split-flap and use it to show in the view based on an input, the associated split-flap display.

First of all, we inject our service and create an instance of a split-flap:

public splitFlapInstance = inject(SplitFlapService).getSplitFlapInstance();
Enter fullscreen mode Exit fullscreen mode

Then we create a form control for it, with our custom validator:

public fc = new FormControl('', allowedLettersValidator());
Enter fullscreen mode Exit fullscreen mode

Last but not least, we bind the value of our form control to the input$$ of our split-flap (without forgetting to unsubscribe when the component is destroyed):

private subscription: Subscription | null = null;

constructor() {
  this.subscription = this.fc.valueChanges
    .pipe(
      filter(() => this.fc.valid),
      debounceTime(300)
    )
    .subscribe(this.splitFlapInstance.input$$);
}

public ngAfterDestroy() {
  this.subscription?.unsubscribe();
}
Enter fullscreen mode Exit fullscreen mode

Our view can then have the input and the associated split-flap component:

<mat-form-field>
  <mat-label>Input</mat-label>
  <input matInput [formControl]="fc" />
  <mat-error *ngIf="fc.invalid">Invalid chars used</mat-error>
</mat-form-field>

<app-split-flap [letters]="splitFlapInstance.splitFlap$ | async"></app-split-flap>
Enter fullscreen mode Exit fullscreen mode

Here's the final project:

Conclusion

I always like to start thinking about my streams in a really isolated way and once I've got everything working, then integrate where needed. It allows me to make sure I have a reusable code and keep my focus on the core logic, then the view.

Observables integrates of course really well with Angular thanks to a lot of the default API using and the wonderful async pipe.


I hope you enjoyed this article, if you did let me know with a reaction and eventually drop a comment. It's always nice to hear back from people who took the time to read a post 😄! If you gave a go to the challenge yourself, share a link to your Stackblitz or let us know how far you went too!

If you're interested in more articles about Angular, RxJS, open source, self hosting, data privacy, feel free to hit the follow button for more. Thanks for reading!

Found a typo?

If you've found a typo, a sentence that could be improved or anything else that should be updated on this blog post, you can access it through a git repository and make a pull request. Instead of posting a comment, please go directly to https://github.com/maxime1992/my-dev.to and open a new pull request with your changes. If you're interested how I manage my dev.to posts through git and CI, read more here.

Follow me

           
Dev Github Twitter Reddit Linkedin Stackoverflow

You may also enjoy reading

. . . . . . . . . . . . . . . . . .