Welcome to today’s blog.
In today’s post I will discuss Angular component refactoring.
In software development you will have heard of the concept of separation of concerns. This is not a new concept and has been around for quite a while. We also have a concept in software development best practices where the principles known as SOLID are used, and the first principle, the separation of concerns does not solely apply to pure code, but it can also apply to other artifacts in the software development environment including:
- Code
- Scripts
- Configurations
- User Interfaces
- Architectures
I will be showing how the separation of concerns principle can be applied for user interfaces within an Angular application.
Refactoring a Typical Component
What I have is a typical component that is a file uploading component that does the following:
- Allows a user to upload a file.
- Checks the validity of the file.
- Uploads the file to a server database.
- Displays the uploaded files.
The first way we might implement the above is to create a whole self-contained component, which is one component and performs all the above processes. Sounds great, but wait a moment … what happens if something breaks in the code and the whole component falls over?
Let us see what the component looks like in terms of its code base.
The component is declared in the app home page as shown:
<div style="text-align:center">
<h1>
Welcome to the File Uploader Viewer App!
</h1>
</div>
<router-outlet></router-outlet>
<app-file-uploader></app-file-uploader>
The HTML template for the component looks like this:
<mat-card>
<mat-card-title>
<div><b>Select Files to Upload</b></div>
<br />
<div class="user-warning">{{allowedFileTypeNotice}}</div>
<div class="user-warning">{{filelengthLimitationNotice}}</div>
<br />
</mat-card-title>
<mat-card-content>
<input type="file" id="upload-files-id" name="formFile" multiple
(change)="fileSelectionChanged($event)">
</mat-card-content>
</mat-card>
<app-file-selected-view
[inputFiles]="selectedFiles"
[errorFiles]="invalidFiles"
[inputStatusFiles]="fileListStatus">
</app-file-selected-view>
<mat-card>
<div>
<button type="button" class="btn btn-primary"
id="btnUpload"
(click)="eventUploadFiles($event.value)">
Upload Files
</button>
</div>
</mat-card>
<app-file-viewer [downloadFiles]="downloadedFiles"></app-file-viewer>
The component view contains the following sub-components and / or controls:
- A HTML file upload section.
- A file selected component to display the selected files.
- An upload button.
- A file viewer component that downloads and displays uploaded files.
Which sections of the above view can we keep, and which ones can we decouple?
In the next section, I will show how to nominate which components to keep as they are, and which ones to decouple into separate code, and or components.
Which Parts of the Component can be Decoupled?
The HTML controls we can keep on for now as they are used to receive input or actions from the user. The file selected component we can separate, but since it is dependent on the files being uploaded and the upload action, we would require two levels of integration to get the component separated. We can defer this if we find a need to re-use the files selected in another part of the application. The file viewer however is dependent only on files uploaded to the server, so this would be the component we can decouple.
Looking at the existing implementation for the file uploader, the source for uploading the files is shown:
this.fileManagerApi.uploadFiles(this.formData).subscribe(
res => {
console.log(this.selectedFiles.length + " files uploaded.");
this.startDownloadFiles.next(true);
},
err =>
{
var errMsg = err.error.error ? err.error.error : err.message;
console.log(this.selectedFiles.length + " files not uploaded. Error: "
+ errMsg);
}
);
Here we see an observable event that is set to trigger the downloading process.
The event definition is a behavior subject which when set to true will commence the downloading of files:
startDownloadFiles: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
The download is triggered when the download condition observable is set.
ngOnInit() {
console.log("starting upload component");
...
this.startDownloadFiles.subscribe(res =>
{
if (!res)
return;
this.startDownload();
})
}
The download task simply calls our Web API and sets the downloaded files list accordingly:
startDownload()
{
console.log("starting download of files..");
this.fileManagerApi.downloadFiles().subscribe(
res =>
{
this.downloadedFiles = res;
console.log("finished download of files..");
}
)
}
The file viewer component, which is the one we wish to decouple basically is a div showing all entries from the download list:
<mat-card>
<mat-card-title>
<div><b>Downloaded Files</b></div>
</mat-card-title>
<mat-card-content>
<div *ngIf="downloadFiles.length > 0">
<div style="display: flex; flex-wrap: wrap; border-style: solid;
border-width: 1px; background-color: cadetblue;">
<div style="flex: 0 40%;"><u><b>FILENAME</b></u></div>
<div style="flex: 0 20%;"><u><b>FILESIZE</b></u></div>
</div>
<mat-list *ngFor="let file of downloadFiles">
<div style="display: flex; flex-wrap: wrap; border-style: solid;
border-width: 1px;">
<div class="grid-row" style="flex: 0 40%;">{{file.fileName}}</div>
<div class="grid-row" style="flex: 0 40%;">{{file.fileSize}}</div>
</div>
</mat-list>
</div>
<div *ngIf="downloadFiles.length === 0">
<p>No files have been downloaded</p>
</div>
</mat-card-content>
</mat-card>
The file viewer code is very minimal, whose only purpose is to set the download files from within the uploader component and display the file metadata in the HTML template:
import { Component, Input, OnInit } from '@angular/core';
import { FileDownloadView } from '../models/fileDownloadView';
@Component({
selector: 'app-file-viewer',
templateUrl: './file-viewer.component.html',
styleUrls: ['./file-viewer.component.scss']
})
export class FileViewerComponent implements OnInit {
@Input() downloadFiles: FileDownloadView[] = [];
constructor() { }
ngOnInit() {
}
}
As discussed earlier, our goal is to take the rather monolithic control that contains multiple components and levels of responsibilities and transform it to two components that each serve more focused concerns. The diagram outlining our goal is shown:
One of the things we noticed was the use of an observable to trigger the download. This observable can be converted into an EventEmitter, which provides a component with the ability to output data and make this output data available to another component. This also allows our component to act like a re-useable control with both inputs and outputs.
For the following steps assume the parent component containing our upload component is app-root.
Steps Taken in the Refactoring Process
The steps we take to transform the upload component to divest itself away from the download functionality are:
Step 1. Add an event emitter to upload component
@Output() triggerDownloadFiles: EventEmitter< oolean> = new EventEmitter();
Step 2. Remove the download observable (BehaviorSubject)
startDownloadFiles: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
Step 3. Add the download observable to the parent component (possibly app.component)
startDownloadFiles: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
Step 4. Add the emitter output to the parent control’s HTML declaration
<router-outlet></router-outlet>
<app-file-uploader (triggerDownloadFiles)=”startDownload($event)”></app-file-uploader>
Step 5. Remove the app viewer HTML declaration from the file upload component HTML
<app-file-viewer [downloadFiles]=”downloadedFiles”></app-file-viewer>
Step 6. Add the app viewer HTML declaration below the file upload component HTML
<router-outlet></router-outlet>
<app-file-uploader (triggerDownloadFiles)=”startDownload($event)”></app-file-uploader>
<app-file-viewer></app-file-viewer>
Step 7. Declare the StartDownload() implementation within the parent component
import { Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'angular-file-uploader-viewer';
startDownloadFiles$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
startDownload(value: any)
{
console.log('download started! val=' + value);
this.startDownloadFiles$.next(true);
}
}
Step 8. Decorate the file viewer with an input for commencing downloads
import { Component, Input, OnInit, NO_ERRORS_SCHEMA } from '@angular/core';
import { FileDownloadView } from '../models/fileDownloadView';
import { FileManagerApiService } from '../services/fileManagerApi.service';
@Component({
selector: 'app-file-viewer',
templateUrl: './file-viewer.component.html',
styleUrls: ['./file-viewer.component.scss']
})
export class FileViewerComponent implements OnInit {
downloadFiles: FileDownloadView[] = [];
@Input()
set commenceDownload(value: boolean)
{
if (value)
this.startDownload();
}
constructor(
private fileManagerApiService: FileManagerApiService) { }
ngOnInit() {
}
startDownload()
{
console.log("starting download of files..");
this.testFileManagerApi.downloadFiles().subscribe(
res =>
{
this.downloadFiles = res;
console.log("number downloaded files = " + res.length);
console.log("finished download of files..");
}
)
}
}
Step 9. Declare the download commencement input parameter for the application viewer component’s HTML declaration
<div style="text-align:center">
<h1>
Welcome to the File Uploader Viewer App!
</h1>
</div>
<router-outlet></router-outlet>
<app-file-uploader (triggerDownloadFiles)="startDownload($event)">
</app-file-uploader>
<app-file-viewer [commenceDownload]="(startDownloadFiles$ | async)">
</app-file-viewer>
To summarize what we have done.
- Separated components app-file-viewer from app-file-uploader
- Moved the download event into the app-root.
- Introduced an output event EventEmitter within the app-file-uploader component.
- Moved the download file logic into the app-file-viewer component.
- Ensure the download logic runs when the event emitter from the file uploader triggers the StartDownload() method in app-root.
- The asynchronous observable startDownloadFiles$ then provides the input commenceDownload for app-file-viewer which then commences downloading the files.
That sounds quite complex and quite a fair bit to digest, however, once you have refactored at least two components, the exercise should be second nature.
Once you understand how this separation / decoupling pattern works you can apply it to your own application components and separate those.
That is all for today’s post.
I hope you have found it useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.