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:
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.
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 theEventTarget
interface by default, and theHTMLInputElement
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 theUser
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.
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.
Top comments (0)