This is part four, and possibly the last for awhile, of a multi-part series where I give examples of Reactive design using RxJS.

Taking things a little further, this example shows how to combine two observables and publish the combined values into a subject, which in turns updates a DOM element.

Sample Code

First, the HTML:

<div id="fullName">Full Name</div>

<label for="firstName">First Name</label>
<input type="text" id="firstName">

<label for="lastName">Last Name</label>
<input type="text" id="lastName">

And next, the code:

const { fromEvent, combineLatest, Observable, Subject } = rxjs;
const { filter, map } = rxjs.operators;

// Create an observable attached to the firstName input box
var firstName: Observable<string> = fromEvent(document.querySelector('#firstName'), 'input')
  .pipe(map(event => event.target.value));

// Create an observable attached to the lastName input box
var lastName: Observable<string> = fromEvent(document.querySelector('#lastName'), 'input')
  .pipe(map(event => event.target.value));

// Get the fullName DOM element
var fullNameDOM = document.querySelector('#fullName');

// Create a subject to be used to publish values to the fullName DOM element
var fullName: Subject<string> = new Subject();

// Connect the fullName subject so that when it changes
// it updates the fullName inner text.
fullName.asObservable()
  .subscribe((value) => fullNameDOM.innerText = value);

// Combine the firstName and lastName observables
combineLatest(...[firstName, lastName])
  .subscribe((values) => {
    // Publish the joined values
    fullName.next(values.join(" "));
  });

Explanation

To update the HTML for this example, we add a new label and input element called lastName, and in the code we add a new Observable for the new element in the same way we did for the firstName element.

<label for="lastName">Last Name</label>
<input type="text" id="lastName">
var lastName: Observable<string> = fromEvent(document.querySelector('#lastName'), 'input')
  .pipe(map(event => event.target.value));

Next, instead of just subscribing to firstName we use a special RxJS function called combineLatest, which combines the latest from one or more Observables and creates a new Observable.

This new Observable, when subscribed, passes the observer an array of values that corresponds to the latest value of each of the observables.

So, in this case, our code is:

combineLatest(...[firstName, lastName])
  .subscribe((values) => {
    fullName.next(values.join(" "));
});

When the lambda function passed to subscribe is executed, the values argument contains an array, with the first element in the array being the latest value of firstName, and the second element in the array being the latest value of lastName.

We replace our original subscription which subscribed to firstName with this code, and we use fullName.next(values.join(" ") to join the array (essentially being firstName + " " + lastName) and publish this combined string to fullName, which in turn sends invokes the other subscription that sends it to (value) => fullNameDOM.innerText = value.

Essentially we listen to the two input elements, and when they change we combine then and publish the results to the fullName DOM element.

Something Odd

When you get to this point, you'll notice something odd. The fullName DOM element does not get updated until after both text fields have been modified.

This is because combineLatest will not make up any placeholder values for the Observables it's observing and must wait until all of the Observables have had at least one state change.

There's more than one way to initialize the values if you don't want this behavior, and one way is to initialize the observables through the use of the startWith operator in the observable pipeline by making the following change:

var firstName: Observable<string> = fromEvent(document.querySelector('#firstName'), 'input')
  .pipe(map(event => event.target.value), startWith(""));

var lastName: Observable<string> = fromEvent(document.querySelector('#lastName'), 'input')
  .pipe(map(event => event.target.value), startWith(""));

One way to not solve the problem is by attempting to programmatically initialize the text of the input boxes. Why not? Because this won't fire the input event.

A stretch exercise for the reader is to create a fromInput function that creates an Observable<string> that immediately emits a value as soon as it's subscribed to with the current value of the input box.

Hint: Check out BehaviorSubject.

Final Cleanup

I chose not to show some cleanup code that otherwise would have made the example harder to understand, but every time you subscribe to something, that returns a subscription, and it's important to clean it up correctly.

For more details, check out the RxJS website or use a search engine and look for keywords rxjs unsubscribe takeUntil.

If you want to play with this, check it out on CodePen.