Form Handling using RxJS and TypeScript

luixaviles

Luis Aviles

Posted on December 12, 2020

Form Handling using RxJS and TypeScript

RxJS and TypeScript are becoming more and more popular these days. It's feasible to use both technologies alone or even as part of any JavaScript library or framework.

It's an interesting fact to know that the RxJS project is using TypeScript actively and it helped to find bugs when the library was migrating from JavaScript.

In this article, we'll see a Reactive approach to handle HTML forms using TypeScript and RxJS.

HTML Forms

HTML forms are widely used in Web Applications nowadays. They are essential every time you want to register a user, send a message through a contact form, or collect any data from your users. At the same time, the HTML vocabulary can help you to define a Form with simple syntax:

<form>
   Add HTML Form Controls here: text input, checkboxes, etc
</form>
Enter fullscreen mode Exit fullscreen mode

An HTML Form Control element can be used to receive an event or collect data within the HTML form context.

A simple Login Form

Let's get started with a simple form:

login-form-rxjs-typescript

This may be a simple form. However, there are different ways to handle it from a developer perspective.

Form Definition

Let's consider the following login form implementation:

<div class="container">
  <form class="form">
    <div class="form-group">
      <label for="email">Email address</label>
      <input type="email" class="form-control" id="email" />
    </div>
    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" class="form-control" id="password" />
    </div>
    <button id="submit" type="button" class="btn btn-primary">Submit</button>
  </form>
</div>
Enter fullscreen mode Exit fullscreen mode

The previous code uses Bootstrap, which is a powerful CSS framework that contains several ready-to-use styles.

The TypeScript Model

TypeScript is all about good and safe typing. Let's be sure to define an Interface for our data model:

// user.ts
export interface User {
  email?: string;
  password?: string;
}
Enter fullscreen mode Exit fullscreen mode

Right after that, we can define a new variable to "contain" our User model:

let userModel: User = {};
Enter fullscreen mode Exit fullscreen mode

The DOM Elements

Since we have our data model defined, it is time to define the variables that allow us to access the DOM elements:

// DOM Elements - declaration
let buttonElement: HTMLButtonElement;
let emailInput: HTMLInputElement;
let passwordInput: HTMLInputElement;
Enter fullscreen mode Exit fullscreen mode

These variables are ready to have a reference to the HTML Form controls we defined before in the markup. Pay attention to the types for each variable.

At some point in the code it will be necessary to obtain the elements of the DOM:

// DOM elements - assignation
buttonElement = document.getElementById("submit") as HTMLButtonElement;
emailInput = document.getElementById("email") as HTMLInputElement;
passwordInput = document.getElementById("password") as HTMLInputElement;
Enter fullscreen mode Exit fullscreen mode

In the JavaScript world, we can use the getElementById method to get the Element associated with the given id property. You may use the querySelector() method as an alternative too.

One important note to point here is that document.getElementById will return an HTMLElement object. This is an interface that represents any HTML element.

This means we have a general type and it would be better to apply the type assertion from TypeScript:

// as-syntax
buttonElement = document.getElementById("submit") as HTMLButtonElement;

// angle-bracket syntax
buttonElement = <HTMLButtonElement>document.getElementById("submit");
Enter fullscreen mode Exit fullscreen mode

The DOM Events as Observables

One way to handle DOM events from JavaScript is through an Event Listener. For example, to handle the click event of the button we can do:

buttonElement.addEventListener('click', function() {
  // Handle click event here
});
Enter fullscreen mode Exit fullscreen mode

However, in the Reactive Programming approach, we'll need to consider streams and Observables.

Let's continue with the definition of every event we can handle at this point: "input"(from HTML Input elements) and "click"(from HTML Button element) as follows:

// DOM events as Observables - declaration
let emailChange$: Observable<InputEvent>;
let passwordChange$: Observable<InputEvent>;
let submit$: Observable<MouseEvent>;
Enter fullscreen mode Exit fullscreen mode

Pay attention (again) to the Generic Types used in every variable type. These generic types will tell the TypeScript compiler the specific type of object that will "flow" through those observables. Also, by a code convention, the $ sign is used at the end of the variable name to represent a stream.

We have the variables needed to assign the Observables. It's time to use the fromEvent operator from RxJS, which turns an event into an Observable:

// DOM events as Observables - assignation
emailChange$ = fromEvent<InputEvent>(emailInput, "input");
passwordChange$ = fromEvent<InputEvent>(passwordInput, "input");
submit$ = fromEvent<MouseEvent>(buttonElement, "click");
Enter fullscreen mode Exit fullscreen mode

The fromEvent is a creation operator and it's feasible to use it without the generic type:

submit$ = fromEvent(buttonElement, "click"); // Observable<Event>
Enter fullscreen mode Exit fullscreen mode

The disadvantage is obvious: we will have a general type that might need the type assertion later to handle the object properly.

input-event mouse-event

Subscribe to Event-Observables

Let's start processing the email value as a stream:

emailChange$
  .pipe(map((event: InputEvent) => (event.target as HTMLInputElement).value))
  .subscribe(email => {
    console.log("email Value: ", email);
    userModel.email = email;
  });
Enter fullscreen mode Exit fullscreen mode

This is what is happening:

  • emailChange$ is an Observable. Picture this: you have a data flow ready to be processed. How are you going to process them? By using functions (pipeable operators in terms of RxJS).
  • .pipe() function provides a readable way to use operators together. For example, you could have a kind of "combination" of Linux commands: ls -l | grep 'json | sort where you're using | as a pipe to perform operations or actions one after another.
  • map() operator comes first and allows to "extract" the value property from the HTMLInputElement(the input field).
  • event.target as HTMLInputElement is needed since event.target will return the EventTarget interface by default, and the HTMLInputElement interface provides the methods and properties for <input> elements.
  • subscribe() call is "needed" to call the Observable. It behaves similar to when you call a function.
  • Once the Observable is called, it returns the email value and the User model can be updated.

We can apply the same logic to process the password value changes:

passwordChange$
  .pipe(map((event: InputEvent) => (event.target as HTMLInputElement).value))
  .subscribe(password => {
    console.log("password Value:", password);
    userModel.password = password;
  });
Enter fullscreen mode Exit fullscreen mode

See the final value is assigned to the userModel.password property.

May you guess a potential improvement here? The email and password value handling are the same. We can create a common function to avoid code duplication as follows:

function getValueFromInputEvent(
  event: Observable<InputEvent>
): Observable<string> {
  return event.pipe(
    tap(event => console.log("event.target", event.target)),
    map((event: InputEvent) => (event.target as HTMLInputElement).value)
  );
}
Enter fullscreen mode Exit fullscreen mode

The previous function receives an Observable<InputEvent>, and returns the string value as an Observable too!

Let's apply that function on emailChange$ stream:

emailChange$.pipe(getValueFromInputEvent).subscribe(email => {
  console.log("email Value: ", email);
  userModel.email = email;
});
Enter fullscreen mode Exit fullscreen mode

This is a short version, and easier to read too. You can apply the same function to process passwordChange$ stream.

Finally, we're ready to process the "click" event. Following the same logic, let's subscribe and process the User model at the end.

submit$
  .pipe(tap((event: MouseEvent) => console.log(event)))
  .subscribe(() => console.log("Sending User", { userModel }));
Enter fullscreen mode Exit fullscreen mode

Source Code Project

Find the complete project running in StackBlitz. Don't forget to open the browser's console to see the results.

You can follow me on Twitter and GitHub to see more about my work.


This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit thisdotlabs.com.

This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit thisdot.co.

💖 💪 🙅 🚩
luixaviles
Luis Aviles

Posted on December 12, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related