Styling and Refinements
Now we just have a few remaining loose ends to finish up — there are a few minor changes we are going to make just to provide a better experience in the application, and to make it look a little nicer.
Adding Titles and Comments
At the moment, we are only displaying the actual gif, but it would be nice to
see the title as well. We will update our GifListComponent to include this, as
well as linking to the comment thread on Reddit for the gif.
Before we do that, we are going to create a little utility.
import { InjectionToken } from '@angular/core';
export const WINDOW = new InjectionToken<Window>('The window object', { factory: () => window,});We have seen something like this before — we did a similar thing when accessing
the localStorage API. The reasoning is the same again here — the window
object is a browser API and an Angular application does not necessarily always
run in the context of a browser. An injection token is a safer design because we
can provide alternate implementations based on the environment the application
is running in. We are not doing that here, and technically it isn’t necessary if
we know our application is only ever going to run in the browser context, but it
is a good principle to keep in mind.
I will leave you a bit of an assignment before we continue. We are going to
implement a few features all at once for our GifListComponent. Take your best
shot at implementing the following and then I will post my solution — you will
likely need to look up the Angular Material documentation.
- Use an Angular Material toolbar to display the title of each gif
- Inject our
WINDOWtoken and use it to launch the link for the particular gif in the browser when the user clicks somewhere in the toolbar. My solution uses an Angular Material icon as the link to click - Add a message that displays if there are no
gifs
Click here to reveal solution
Solution
import { Component, input, inject } from '@angular/core';import { WINDOW } from '../../shared/utils/injection-tokens';import { Gif } from '../../shared/interfaces';import { MatToolbarModule } from '@angular/material/toolbar';import { MatIconModule } from '@angular/material/icon';import { MatButtonModule } from '@angular/material/button';import { GifPlayerComponent } from './gif-player.component';
@Component({ selector: 'app-gif-list', template: ` @for (gif of gifs(); track gif.permalink){ <div> <app-gif-player [src]="gif.src" [thumbnail]="gif.thumbnail" data-testid="gif-list-item" ></app-gif-player> <mat-toolbar color="primary"> <span>{{ gif.title }}</span> <span class="toolbar-spacer"></span> <button mat-icon-button (click)="window.open('https://reddit.com/' + gif.permalink)" > <mat-icon>comment</mat-icon> </button> </mat-toolbar> </div> } @empty { <p>Can't find any gifs 🤷</p> } `, imports: [ GifPlayerComponent, MatToolbarModule, MatIconModule, MatButtonModule, ], styles: [ ` div { margin: 1rem; filter: drop-shadow(0px 0px 6px #0e0c1ba8); }
mat-toolbar { white-space: break-spaces; }
p { font-size: 2em; width: 100%; text-align: center; margin-top: 4rem; } `, ],})export class GifListComponent { gifs = input.required<Gif[]>(); window = inject(WINDOW);}Whilst we are dealing with some styling, let’s also add our global styles in.
.toolbar-spacer { flex: 1 1 auto;}
.grid-container { display: grid; align-items: center; grid-template-columns: 1;}
@media (min-width: 600px) { .grid-container { grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); }}<body class="mat-typography mat-app-background"> <app-root></app-root></body>This should have things looking pretty good! We’re still going to tackle just one more challenge though.
Handling Errors
There is a very high chance the user will encounter an error whilst using this
application. We search for gifs as soon as they enter a search term, so if they
were in the process of writing chemicalreactiongifs and they stopped at
chemicalre we are going to fail to get any gifs because that subreddit doesn’t
exist. Likewise if they accidentally made a typo or something.
So, we are going to add some proper error handling. We’ve already added some
state for our error in the RedditService now we just need to set it in there.
private error$ = new Subject<string | null>(); this.error$.pipe(takeUntilDestroyed()).subscribe((error) => this.state.update((state) => ({ ...state, error, })) ); private handleError(err: HttpErrorResponse) { // Handle specific error cases if (err.status === 404 && err.url) { this.error$.next(`Failed to load gifs for /r/${err.url.split('/')[4]}`); return; }
// Generic error if no cases match this.error$.next(err.statusText); }Ultimately, we are just going to set our error as a string in our state. I’ve
created this method to show how you might create some more complex handling for
error messages. We just need to display a message for a 404 error when the
subreddit is not found, but you could add whatever custom handling you like in
there. We generate the error string we want and then just call next on our
error$ source.
Now we just need to call our handleError method.
catchError((err) => { this.handleError(err); return EMPTY; }),This will deal with setting the error in our state, now we just need to display it. To do this we are again going to make use of Angular Material and use the “snack bar” which is a good way to pop up some kind of quick message.
import { Component, effect, inject } from '@angular/core';import { RouterOutlet } from '@angular/router';import { RedditService } from './shared/data-access/reddit.service';import { MatSnackBar } from '@angular/material/snack-bar';
@Component({ selector: 'app-root', imports: [RouterOutlet], template: ` <router-outlet></router-outlet> `, styles: [],})export class AppComponent { redditService = inject(RedditService); snackBar = inject(MatSnackBar);
constructor() { effect(() => { const error = this.redditService.error();
if (error !== null) { this.snackBar.open(error, 'Dismiss', { duration: 2000 }); } }); }}We inject MatSnackBar and use that to display a message whenever our error
state changes by using an effect. By adding this to the root component, it
allows us to display the error message globally in the application, although
this application only has one page anyway so it doesn’t really matter.
Try going to a subreddit that doesn’t exist now, like gi, and you should see
the error message pop up.
…and that should do it! We now have a pretty respectable looking application.