Our goal is to make an Angular app with a list of the past SpaceX launches along with an associated details page. Data is provided via the SpaceX GraphQL API and Angular services are generated via GraphQL Code Generator. We use Apollo Angular to access data from the frontend. The API is free so please be nice and don't abuse it.
-
Generate a new angular application with routing
ng new angular-spacex-graphql-codegen --routing=true --style=css
Make sure to delete the default template in
src/app/app.component.html
-
Install the Apollo VS Code plugin and in the root of the project add
apollo.config.js
module.exports = { client: { service: { name: 'angular-spacex-graphql-codegen', url: 'https://api.spacex.land/graphql/' } } };
This points the extension at the SpaceX GraphQL API so we get autocomplete, type information, and other cool features in GraphQL files. You may need to restart VS Code.
-
Generate our two components:
ng g component launch-list --changeDetection=OnPush
ng g component launch-details --changeDetection=OnPush
Because our generated services use observables we choose OnPush change detection for the best performance.
-
In
src/app/app-routing.module.ts
we setup the routing:import { LaunchListComponent } from './launch-list/launch-list.component'; import { LaunchDetailsComponent } from './launch-details/launch-details.component'; const routes: Routes = [ { path: '', component: LaunchListComponent }, { path: ':id', component: LaunchDetailsComponent } ];
-
Each component will have its own data requirments so we co-locate our graphql query files next to them
# src/app/launch-list/launch-list.graphql query pastLaunchesList($limit: Int!) { launchesPast(limit: $limit) { id mission_name links { flickr_images mission_patch_small } rocket { rocket_name } launch_date_utc } }
# src/app/launch-details/launch-details.graphql query launchDetails($id: ID!) { launch(id: $id) { id mission_name details links { flickr_images mission_patch } } }
Note the first line:
query launchDetails($id: ID!)
When we generate the Angular service the query name is turned into PascalCase and GQL is appended to the end, so the service name for the launch details would be LaunchDetailsGQL. Also in the first line we define any variables we'll need to pass into the query. Please note it's import to include id in the query return so apollo can cache the data. -
We add Apollo Angular to our app with
ng add apollo-angular
. Insrc/app/graphql.module.ts
we set our API urlconst uri = 'https://api.spacex.land/graphql/';
. -
Install Graphql Code Generator and the needed plugins
npm i --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-apollo-angular @graphql-codegen/typescript-operations
-
In the root of the project create a
codegen.yml
file:# Where to get schema data schema: - https://api.spacex.land/graphql/ # The client side queries to turn into services documents: - src/**/*.graphql # Where to output the services and the list of plugins generates: ./src/app/services/spacexGraphql.service.ts: plugins: - typescript - typescript-operations - typescript-apollo-angular
-
In package.json add a script
"codegen": "gql-gen"
thennpm run codegen
to generate the Angular Services. -
To make it look nice we add Angular Material
ng add @angular/material
then in theapp.module.ts
we import the card module and add to the imports array:import { MatCardModule } from '@angular/material/card';
-
Lets start with the list of past launches displayed on the screen:
import { map } from 'rxjs/operators'; import { PastLaunchesListGQL } from '../services/spacexGraphql.service'; export class LaunchListComponent { constructor(private readonly pastLaunchesService: PastLaunchesListGQL) {} // Please be careful to not fetch too much, but this amount lets us see lazy loading imgs in action pastLaunches$ = this.pastLaunchesService .fetch({ limit: 30 }) // Here we extract our query data, we can also get info like errors or loading state from res .pipe(map((res) => res.data.launchesPast)); }
<ng-container *ngIf="pastLaunches$ | async as pastLaunches"> <main> <section class="container"> <mat-card *ngFor="let launch of pastLaunches" [routerLink]="['/', launch.id]" > <mat-card-header> <img height="50" width="50" mat-card-avatar loading="lazy" [src]="launch.links.mission_patch_small" alt="Mission patch of {{ launch.mission_name }}" /> <mat-card-title>{{ launch.mission_name }}</mat-card-title> <mat-card-subtitle >{{ launch.rocket.rocket_name }}</mat-card-subtitle > </mat-card-header> <img height="300" width="300" mat-card-image loading="lazy" [src]="launch.links.flickr_images[0]" alt="Photo of {{ launch.mission_name }}" /> </mat-card> </section> </main> </ng-container>
Notice the cool addition of lazy loading images, if you emulate a mobile device in Chrome and fetch enough launches you should see the images lazy load while you scroll!
To make it look nice we add CSS Grid
.container { padding-top: 20px; display: grid; grid-gap: 30px; grid-template-columns: repeat(auto-fill, 350px); justify-content: center; } .mat-card { cursor: pointer; }
-
Next we make the details page for a launch, we get the id from the route params and pass that to our service
import { ActivatedRoute } from '@angular/router'; import { map, switchMap } from 'rxjs/operators'; import { LaunchDetailsGQL } from '../services/spacexGraphql.service'; export class LaunchDetailsComponent { constructor( private readonly route: ActivatedRoute, private readonly launchDetailsService: LaunchDetailsGQL ) {} launchDetails$ = this.route.paramMap.pipe( map((params) => params.get('id') as string), switchMap((id) => this.launchDetailsService.fetch({ id })), map((res) => res.data.launch) ); }
The HTML looks very similar to the list of launches
<ng-container *ngIf="launchDetails$ | async as launchDetails"> <section style="padding-top: 20px;"> <mat-card style="width: 400px; margin: auto;"> <mat-card-header> <mat-card-title>{{ launchDetails.mission_name }}</mat-card-title> </mat-card-header> <img height="256" width="256" mat-card-image [src]="launchDetails.links.mission_patch" alt="Mission patch of {{ launchDetails.mission_name }}" /> <mat-card-content> <p>{{ launchDetails.details }}</p> </mat-card-content> </mat-card> </section> <section class="photo-grid"> <img *ngFor="let pic of launchDetails.links.flickr_images" [src]="pic" alt="Picture of {{ launchDetails.mission_name }}" height="300" width="300" loading="lazy" /> </section> </ng-container>
Finally we add CSS Grid for the pictures
.photo-grid { padding-top: 30px; display: grid; grid-gap: 10px; grid-template-columns: repeat(auto-fill, 300px); justify-content: center; }
-
npm start
, navigate tohttp://localhost:4200/
, and it should work!
Thanks to the new builtin relative time formating in V8, we can add launched x days ago
-
Generate the pipe:
ng g pipe relative-time --module=app.module --flat=false
-
The pipe takes in the UTC time and returns a formatted string
import { Pipe, PipeTransform } from '@angular/core'; const milliSecondsInDay = 1000 * 3600 * 24; // Cast as any because typescript typing haven't updated yet const rtf = new (Intl as any).RelativeTimeFormat('en'); @Pipe({ name: 'relativeTime' }) export class RelativeTimePipe implements PipeTransform { transform(utcTime: string): string { const diffInMillicseconds = new Date(utcTime).getTime() - new Date().getTime(); const diffInDays = Math.round(diffInMillicseconds / milliSecondsInDay); return rtf.format(diffInDays, 'day'); } }
-
Add the pipe to our launch card in src/app/launch-list/launch-list.component.html
<mat-card-subtitle >{{ launch.rocket.rocket_name }} - launched {{ launch.launch_date_utc | relativeTime }}</mat-card-subtitle >