Laravel API and Angular Client Tutorial – Part 4 Client Get Photos and Videos
Introduction
In the last few parts, we had created the authentication API, get the photos and videos, and was able to have the Angular app login work. Now we can update the client app to show the photos or videos. In this tutorial we show the photos and videos in an infinite scrolling page. Lastly, we will update the app to only show the posts when the client is actually logged in.
HTTP Interceptor
Before we create our service to fetch the posts, we will create an HTTP Interceptor. What this does is act like a middleware to the HTTP requests. With this, we can check if the access token exists and add it to the requests without having to do it on all HTTP calls that we do in an Angular application. You can read more about this at https://angular.io/guide/http#http-interceptors.
The HTTP interceptor is a service so we can run this command to create one.
ng generate service services/AppHttpInterceptor --skip-tests
src\app\services\app-http-interceptor.service.ts
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { Observable } from 'rxjs'; import { LocalStorageService } from 'angular-2-local-storage'; @Injectable() export class AppHttpInterceptor implements HttpInterceptor { constructor(private storage: LocalStorageService) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const accessToken = this.storage.get('accessToken'); if (!req.headers.has('Authorization') && accessToken) { const request = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + accessToken) }); return next.handle(request); } return next.handle(req); } }
In our HTTP Interceptor, we checked if our local storage has the access token and add it to the request headers.
Then to register the HTTP interceptor, we update the add an entry to the providers
array in our module.
src\app\app.module.ts
providers: [ { provide: HTTP_INTERCEPTORS, useClass: AppHttpInterceptor, multi: true }, ],
Getting the Data
Now we create another service to get the photos and videos. To do that we can run this command.
ng generate service services/Post --skip-tests
src\app\services\post.service.ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class PostService { constructor(private http: HttpClient) { } public getPosts(page: number): Observable<any> { return this.http.get(environment.apiUrl + '/posts?include=user&page=' + page) .pipe(map((response: any) => { const posts = []; for (const p of response.data) { p.user = response.included.find(u => u.id === p.relationships.user.data.id); posts.push(p); } response.data = posts; return response; })); } }
In the post service we not just get the photos and videos, we get the users as well but it is in the included
array in the response. It is not directly under the each post object so we have to map it to the post objects when we get the data.
Now we can use this service in our src\app\components\home\home.component.ts.
import { Component, OnInit, HostListener } from '@angular/core'; import { PostService } from 'src/app/services/post.service'; import { environment } from 'src/environments/environment'; @Component({ templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { posts: any[] = []; isLoading = false; page = 1; last = 1; fileUrl = ''; constructor( private postService: PostService ) { this.fileUrl = environment.fileUrl; } ngOnInit() { this.loadPosts(); } loadPosts() { this.isLoading = true; this.postService.getPosts(this.page) .subscribe((response: any) => { for (const p of response.data) { this.posts.push(p); } this.last = response.meta.pagination.total_pages; this.isLoading = false; }); } }
Notice in our constructor, we use the have updated fileUrl
with the value from the environment.ts. We also have page
and last
property to keep track of the pagination which we will use for the infinite scroll feature.
src\app\components\home\home.component.html
<div class="row"> <div class="col-md-6 offset-md-3"> <div class="card mb-3" *ngFor="let p of posts"> <video controls="true" *ngIf="p.attributes.file.includes('.mp4'); else elseBlock" [src]="p.attributes.file"></video> <ng-template #elseBlock> <img [src]="fileUrl + '/' + p.attributes.file" class="card-img-top" [alt]="p.attributes.title"> </ng-template> <div class="card-body"> <h5 class="card-title">{{ p.attributes.title }}</h5> <p class="card-text"> <span>by {{ p.user.attributes.name }}</span> <br> <small>{{ p.attributes.created_at }}</small> </p> </div> </div> </div> </div> <div class="text-center" *ngIf="isLoading"> <div class="spinner-border" role="status"> <span class="sr-only">Loading...</span> </div> </div>
In our template, we kept it simple. We just check whether the file ends with .mp4
and use a video
tag, otherwise use img
tag.
Infinite Scroll
We will implement an infinite scroll to our app. Basically the pages will be loaded when the user scrolls down to the bottom of the page.
First is to import HostListener
annotation from angular/core
. We can add this annotations to methods and call it when an event is triggered which in our case is window:scroll
.
src\app\components\home\home.component.ts
import { Component, HostListener, OnInit } from '@angular/core';
@HostListener('window:scroll', ['$event']) onWindowScroll() { if (window.innerHeight + window.scrollY === document.body.scrollHeight && !this.isLoading && this.page < this.last ) { this.page++; this.loadPosts(); } }
If added a check to make sure we don’t call the loadPosts
unnecessarily.
Checking Authentication
Since GET /api/posts
is a protected resource, logging out and loading the posts will respond with error. Ideally we put this logic to the root component and redirect to just homepage but our app is pretty small and loads the content in homepage as well. So we need to check if there is a logged in user in the homepage.
Let us add an isAuthenticated
property and update it when the user has logged-in or logged-out then we can show or hide elements.
Here is the full src\app\components\home\home.component.ts.
import { Component, OnInit, HostListener } from '@angular/core'; import { PostService } from 'src/app/services/post.service'; import { environment } from 'src/environments/environment'; import { AuthService } from 'src/app/services/auth.service'; @Component({ templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { posts: any[] = []; isLoading = false; page = 1; last = 1; isAuthenticated = false; fileUrl = ''; constructor( private authService: AuthService, private postService: PostService ) { this.fileUrl = environment.fileUrl; } ngOnInit() { this.isAuthenticated = this.authService.isAuthenticated; this.authService.onLogout.subscribe(b => { this.isAuthenticated = b; this.posts = []; this.page = 1; this.last = 1; }); this.loadPosts(); } loadPosts() { if (!this.isAuthenticated) { return; } this.isLoading = true; this.postService.getPosts(this.page) .subscribe((response: any) => { for (const p of response.data) { this.posts.push(p); } this.last = response.meta.pagination.total_pages; this.isLoading = false; }); } @HostListener('window:scroll', ['$event']) onWindowScroll() { if (window.innerHeight + window.scrollY === document.body.scrollHeight && !this.isLoading && this.page < this.last ) { this.page++; this.loadPosts(); } } }
First we declared the new isAuthenticated
property, then initialized it in the ngOnInit
method using the injected AuthService
‘s property isAuthenticated
.
Then AuthService
offers an onLogout
event which we can subscribe to and update the isAuthenticated
property.
Additionally, we can add this at the top of our home template.
src\app\components\home\home.component.html
<div class="row" *ngIf="!isAuthenticated"> <div class="col-md-6 offset-md-3"> <h1>Please login first</h1> </div> </div>
That is it for this tutorial. Thanks for reading!