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.
End Result
Steps
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
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:
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. In src/app/graphql.module.ts we set our API url const 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 dataschema:
- https://api.spacex.land/graphql/# The client side queries to turn into servicesdocuments:
- src/**/*.graphql# Where to output the services and the list of pluginsgenerates:
./src/app/services/spacexGraphql.service.ts:
plugins:
- typescript
- typescript-operations
- typescript-apollo-angular
In package.json add a script "codegen": "gql-gen" then npm run codegen to generate the Angular Services.
To make it look nice we add Angular Material ng add @angular/material then in the app.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';exportclassLaunchListComponent{constructor(privatereadonlypastLaunchesService: PastLaunchesListGQL){}// Please be careful to not fetch too much, but this amount lets us see lazy loading imgs in actionpastLaunches$=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><sectionclass="container"><mat-card*ngFor="let launch of pastLaunches"
[routerLink]="['/', launch.id]"
><mat-card-header><imgheight="50"
width="50"
mat-card-avatarloading="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><imgheight="300"
width="300"
mat-card-imageloading="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!
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';exportclassLaunchDetailsComponent{constructor(privatereadonlyroute: ActivatedRoute,privatereadonlylaunchDetailsService: LaunchDetailsGQL){}launchDetails$=this.route.paramMap.pipe(map((params)=>params.get('id')asstring),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"><sectionstyle="padding-top: 20px;"><mat-cardstyle="width: 400px; margin: auto;"><mat-card-header><mat-card-title>{{ launchDetails.mission_name }}</mat-card-title></mat-card-header><imgheight="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><sectionclass="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>
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';constmilliSecondsInDay=1000*3600*24;// Cast as any because typescript typing haven't updated yetconstrtf=new(Intlasany).RelativeTimeFormat('en');
@Pipe({name: 'relativeTime'})exportclassRelativeTimePipeimplementsPipeTransform{transform(utcTime: string): string{constdiffInMillicseconds=newDate(utcTime).getTime()-newDate().getTime();constdiffInDays=Math.round(diffInMillicseconds/milliSecondsInDay);returnrtf.format(diffInDays,'day');}}
Add the pipe to our launch card in src/app/launch-list/launch-list.component.html
请发表评论