Laravel API and Angular Client Tutorial – Part 6 Client Form File Upload
Introduction
In this tutorial we will update the Angular Client app to and create a form to upload a file to our Laravel API. We will use the Angular reactive forms for validation. Lastly, we will create an Angular guard to make the post form only accessible to logged-in users.
Create Post Component
To generate a new component run this command.
ng generate component components/post/post-create --flat --skip-tests --inline-style
This will create the component files in the components/post directory i.e.
src/app/components/post/post-create.component.html
src/app/components/post/post-create.component.ts
We can now use this component to create a new route.
src\app\app-routing.module.ts
const routes: Routes = [ { path: '', component: HomeComponent, }, { path: 'oauth/callback', component: AuthComponent }, { path: 'post', component: PostCreateComponent, canActivate: [PostGuardGuard] }, ];
To navigate to the new page, you have to update the page navigation bar link if have not already.
src\app\app.component.html
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> <a class="navbar-brand" href="#">Photo & Video Sharing App</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" routerLink="/">Home <span class="sr-only">(current)</span></a> </li> </ul> <ul class="navbar-nav"> <li class="nav-item" *ngIf="isAuthenticated"> <a class="nav-link" routerLink="post">Post</a> </li> // ommitted </ul> </div> </nav> <main role="main" class="container"> <router-outlet></router-outlet> </main>
Creating the Form
First we have to create a FormGroup
instance. This object will automatically validate the values entered and have properties that we can use to show feedback to user. You can read more about it here.
src\app\components\post\post-create.component.ts
export class PostCreateComponent implements OnInit { public formGroup: FormGroup; constructor( private formBuilder: FormBuilder ) { } ngOnInit() { this.formGroup = this.formBuilder.group({ title: this.formBuilder.control('', [Validators.required]), file: this.formBuilder.control('', [Validators.required]), }); } }
In this code, we first inject a class called FormBuilder
. This will create the FormGroup
instance for us. We then created the FormGroup
inside ngOnInit
method.
src\app\components\post\post-create.component.html
<div class="row"> <div class="col-md-6 offset-md-3"> <form [formGroup]="formGroup" novalidate> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" [ngClass]="{ 'is-invalid' : formGroup.get('title').invalid && (formGroup.get('title').touched || formGroup.get('title').submitted) }" id="title" formControlName="title"> </div> <div class="custom-file mb-3"> <input type="file" class="custom-file-input" [ngClass]="{ 'is-invalid' : formGroup.get('file').invalid && (formGroup.get('file').touched || formGroup.get('file').submitted) }" id="file" formControlName="file"> <label class="custom-file-label" for="file"></label> </div> <button class="btn btn-primary" type="submit">Post</button> </form> </div> </div>
In the form
tag we set the formGroup
attribute to formGroup
which we instantiated in the component. Second, we set the formControlName
for each fields.
To get the a field in a FormGroup
we can call the get
method e.g. formGroup.get("title")
. From these fields, we can get the touched
or submitted
flags and there are other ones as well.
From these features we set the is-invalid
HTML class to show the field is red which is available in bootstrap.
In the screenshot above the fields shows in red if you click or touch on them and not enter a value.
Next, we have to update the file
field to display the filename when you select one. We also need to store the file was selected.
For this we need to have a properties to hold these values and update them via an event.
src\app\components\post\post-create.component.ts
export class PostCreateComponent implements OnInit { // omitted public fileName = 'Choose file...'; public file: any = null; // omitted onFileSelect(e) { this.fileName = e.target.files[0].name; this.file = e.target.files[0]; } }
src\app\components\post\post-create.component.html
// omitted <div class="custom-file mb-3"> <input type="file" class="custom-file-input" [ngClass]="{ 'is-invalid' : formGroup.get('file').invalid && (formGroup.get('file').touched || formGroup.get('file').submitted) }" id="file" formControlName="file" (change)="onFileSelect($event)"> <label class="custom-file-label" for="file">{{ fileName }}</label> </div> //omitted
In the codes above we set the (change)
event of the file input and change the label accordingly. We also store the file in a property for later use when we actually upload the file.
Now we want the form to show the validation when we just click the “Post” button. To do this we can force the formGroup
to mark all fields as touched.
src\app\components\post\post-create.component.ts
export class PostCreateComponent implements OnInit { // omitted submit() { this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { return; } } // omitted }
In the above code we mark the fields as touched and check the form if it is invalid.
src\app\components\post\post-create.component.html
<div class="row"> <div class="col-md-6 offset-md-3"> <form [formGroup]="formGroup" (ngSubmit)="submit()" novalidate> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" [ngClass]="{ 'is-invalid' : formGroup.get('title').invalid && (formGroup.get('title').touched || formGroup.get('title').submitted) }" id="title" formControlName="title"> </div> <div class="custom-file mb-3"> <input type="file" class="custom-file-input" [ngClass]="{ 'is-invalid' : formGroup.get('file').invalid && (formGroup.get('file').touched || formGroup.get('file').submitted) }" id="file" formControlName="file" (change)="onFileSelect($event)"> <label class="custom-file-label" for="file">{{ fileName }}</label> </div> <button class="btn btn-primary" type="submit">Post</button> </form> </div> </div>
We call the submit
method when the form is submitted.
Uploading the file
First we need to add the method to upload the file via AJAX to PostService
class.
export class PostService { // omitted post(title: string, file: any) { const form = new FormData(); form.append('title', title); form.append('file', file); return this.http.post(environment.apiUrl + '/posts', form); } }
The code is pretty simple. We use FormData
since we can’t use JSON to upload the file, the we just post to our API.
Next is we inject the PostService
to our component then update the submit
method.
export class PostCreateComponent implements OnInit { constructor( private formBuilder: FormBuilder, private postService: PostService ) { } // omitted submit() { this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { return; } this.postService.post( this.formGroup.get('title').value, this.file ).subscribe(() => { this.formGroup.reset(); this.file = null; this.fileName = 'Choose file...'; alert('Post successful!'); }); } }
In the codes above, we simply subscribe to the post
method of the PostService
and clear the fields after finishing.
Testing it out
That is it for now. Thanks for reading!
References
- Tutorial Source Codes –
node_modules
directory is not included so please runnpm install
. - Part 1 API Authentication
- Part 2 Client OAuth Login
- Part 3 API Get Photos and Videos
- Part 4 Client Get Photos and Videos
- Part 5 API File Upload
- https://angular.io/guide/reactive-forms