Skip to content

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>

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>

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;
}

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

let userModel: User = {};

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;

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;

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");

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
});

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>;

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");

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

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

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;
  });

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;
  });

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)
  );
}

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;
});

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 }));

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.