Skip to content

Modify Streams with User Input

Before we get into the next addition to our stream, let’s briefly recap what our gifsLoaded$ stream does so far. This is the general flow:

  1. Gets a stream of values from the pagination$ stream
  2. It will start that stream with one single emission of null automatically
  3. It will take those emissions and then switch to the fetchFromReddit stream, but we use concatMap because we want to wait for each request to Reddit to complete before addressing the next emission

The key thing we are going to address this lesson is that, at the moment, the subreddit we are using to pull in GIFs is hardcoded to the gifs subreddit. We want our user to be able to supply whatever subreddit they like to pull in GIFs from.

This means that we want to achieve the following:

  1. The user should be able to type a subreddit into a form control
  2. The gifsLoaded$ stream will now emit GIFs from that particular subreddit (and it will get rid of any GIFs from the previous subreddit)

The question is, how do we modify gifsLoaded$ to support this behavior?

Adding a subredditChanged$ Source

Now we want to react to the user changing the subreddit, so we are dealing with a new source of data now and we are going to have to add that to our service.

It can be hard to figure out what that source of data should be exactly. Your first instinct, and one that would technically work, is to just create a new source that is a Subject:

subredditChanged$ = new Subject<string>();

You could just next this with whatever subreddit you want to change to, and react to that by incorporating it into the gifsLoaded$ source.

This faces the same problem as our imperative approach to the videoLoadComplete$ source. It will work, but it requires an imperative call to make it work and we have no idea what subredditChanged$ is just by looking at it. Again, sometimes it might make sense to do that, but it is worth first considering what exactly our data source is and if we can just access that directly.

We are going to be reacting to a user entering data into a form. That form will be associated with a Form Control. What we really want to do is react to the value of that control changing.

The good news is that we can just define that form control directly in our service. This will give the service direct access to the values as they change, and we can also bind the form in our template to the form control in the service.

export class RedditService {
private http = inject(HttpClient);
subredditFormControl = new FormControl();

A FormControl has a valueChanges property which is an observable stream that will emit every time its value changes — just what we need!

private subredditChanged$ = this.subredditFormControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
startWith('gifs'),
map((subreddit) => (subreddit.length ? subreddit : 'gifs'))
);

We’re using valueChanges from the form control to create our source here… but what is all this other stuff?

Consider that we want to launch a request to the Reddit API every time the value in that form control changes. If I type chemicalreactiongifs that means the form control will change 20 times (one for each letter) and 20 requests to Reddit will be launched — really, we only want to launch one request when the user is done typing.

So, we use debounceTime which will cause the stream not to emit any values until it has received no new value for 300ms. This might still cause unnecessary requests if the user types slowly or if they pause in the middle, but it should remove a lot of unnecessary requests. We also use distinctUntilChanged which will cause the stream not to emit a value if the new value is the same as the old value (no need to re-fetch the same data). We also make use of startWith because, just like before, we still want to launch an initial request for the gifs subreddit without waiting for the user to actually type something.

Finally, we also want to handle the situation where the user clears the search bar such that there is no search term — in this case we want to default back to using the gifs value so we map to that if the search term is empty.

Reacting to the subreddit changing

Now that we have our source, we can incorporate it into our gifsLoaded$ stream so that the gifsLoaded$ source reacts to the subreddit changing by emitting gifs for that subreddit.

//sources
pagination$ = new Subject<string | null>();
private subredditChanged$ = this.subredditFormControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
startWith('gifs'),
map((subreddit) => (subreddit.length ? subreddit : 'gifs'))
);
private gifsLoaded$ = this.subredditChanged$.pipe(
switchMap((subreddit) =>
this.pagination$.pipe(
startWith(null),
concatMap((lastKnownGif) =>
this.fetchFromReddit(subreddit, lastKnownGif, 20)
)
)
)
);

Now we first take values from the subredditChanged$ source, and then we switch to what was previously the beginning of our stream — the pagination$ source.

This time we use switchMap because we do want to cancel any previous requests if we get a new value. If subredditChanged$ emits a value of checmicalrea our stream will cause a request to Reddit for that value to be launched, but if we get another value of chemicalreactiongifs before that request finishes, we want to cancel it and switch to the new value because we don’t need it anymore.

Notice that we are also supplying the subreddit value from the form to our fetchFromReddit method now.

The last thing we are going to do in our service is to add a reducer to handle the subredditChanged$ source emitting.

this.subredditChanged$.pipe(takeUntilDestroyed()).subscribe(() => {
this.state.update((state) => ({
...state,
loading: true,
gifs: [],
lastKnownGif: null,
}));
});

Although it is ultimately the gifsLoaded$ source that sets our gifs, we still want to make some state changes whenever the subreddit is changed — specifically we want to reset everything: the loading, gifs, and lastKnownGif states.

Once again, if you test the application now you might be disappointed… because it should work the same as before. Our subredditChanged$ source is just using the default value of gifs and we have no way to update that value from within the application.

Now we are going to extend our user interface to allow us to interact with that subredditFormControl. To do this we are going to create another dumb UI component for the home feature called SearchBarComponent.

We are going to add some Angular Material stuff to this component to make it look nicer, but forget that for now. See how far you can get in creating this component by yourself — it should take a FormControl as an input, and bind that control to an input field that the user can type into.

If you are really up for a challenge, see if you can look up the Angular Material documentation for inputs to see how you can use it to style your input field.

import { Component, input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@Component({
selector: 'app-search-bar',
template: `
<mat-toolbar>
<mat-form-field appearance="outline">
<input
matInput
placeholder="subreddit..."
type="text"
[formControl]="subredditFormControl()"
/>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</mat-toolbar>
`,
imports: [
ReactiveFormsModule,
MatToolbarModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
],
styles: [
`
mat-toolbar {
height: 80px;
}
mat-form-field {
width: 100%;
padding-top: 20px;
}
`,
],
})
export class SearchBarComponent {
subredditFormControl = input.required<FormControl>();
}

As well as the basic functionality of binding the FormControl you can see we are making use of the mat-toolbar, mat-form-field, mat-icon, and matInput from Angular Material to make this input look nicer. We have also added some styling of our own.

The stuff we are using from Angular Material here works just the same as our stuff — it’s just components and directives that we are importing into our component.

Now we can use this search bar in our HomeComponent.

template: `
<app-search-bar
[subredditFormControl]="redditService.subredditFormControl"
></app-search-bar>
@if (redditService.loading()){
<mat-progress-spinner mode="indeterminate" diameter="50" />
} @else {
<app-gif-list
[gifs]="redditService.gifs()"
infiniteScroll
(scrolled)="redditService.pagination$.next(redditService.lastKnownGif())"
class="grid-container"
/>
}
`,
imports: [
GifListComponent,
InfiniteScrollDirective,
SearchBarComponent,
MatProgressSpinnerModule,
],
styles: [
`
mat-progress-spinner {
margin: 2rem auto;
}
`,
],

As well as adding the search bar component, we are now also making use of our loading state and displaying a loading spinner if necessary.

If you test the application now, you should see that we are able to change the subreddit and when we do that all of that previous GIFs will be cleared. Then, once the new gifs have loaded in, we will see them. Keep in mind that you will need to supply a subreddit that actually contains gifs. A couple you can use for testing are: funny and chemicalreactiongifs.