Integrating File Upload in An Angular Application

Introduction

In this tutorial, I like to discuss something I have covered in my previous article -- how to upload a file in a single-page web application. One of my previous tutorials was related to uploading files from an AngularJS web application. In this new tutorial, I like to discuss how to upload a file from an Angular based web application. The approaches for the two are almost the same. There are, however, subtle differences that are worthy of discussion. For example, when developing an Angular based web application, usually it is developed as a separate project from the Spring Boot based service application project. If I have to perform integration testing of the two, I have to make some CORS configurations on the Spring Boot project so that the two can communicate successfully. The way for an Angular application to upload a file is almost the same as the way of uploading a file from an AngularJS application. The programming syntax is totally different. To make this tutorial more interesting, I will also describe the steps of integrating ng-bootstrap with the Angular application code so that I can use modal popups. These are not the most interesting points. Please read on, the section where I explain how to extract file data from the file input field will be the most worthy part of this tutorial. I promise you won't be disappointed.

Overall Architecture

The sample application I prepared for this tutorial is a single-page web application. On the page, there is one button. When a user clicks on this button, a popup will be displayed. On the popup, the user can use the file input field to specify the file to be uploaded. There will be two buttons, one is to upload the file; the other one closes the popup. Once the user successfully uploaded the file and clicked the close button to close the popup, the base page will display the file if the file is an image file. If the user uploads a non-image file, the base page will display a broken image.

The front-end application is developed in a node.js project. Most of the discussion will be focused on this project. I will explain how to set up the project, add the ng-bootstrap popup, and send file upload to the back end. The backend web application is developed as a Spring Boot web application. The backend web application has two request handler methods. One is used to handle the file uploading request. The other one is used to send the file data back to the front-end web application for displaying.

This tutorial will be focused on the node.js project. In the first half of the tutorial, I will begin with the node.js project setup. Next, I will explain how to integrate the ng-bootstrap component into the Angular application. At the end of this first part, I will show you how to perform the file upload operation from the Angular application. I will also point out the very useful thing I have learned. In the second half of the tutorial, I will cover the work in the Spring Boot project. Because the two projects are done separately, CORS configuration is needed so that the Angular application can correctly communicate with the Spring Boot application. I need to revisit this part as a reminder that CORS configuration is vital to today's web applications.

In the next section, I will start the discussion with the Angular application, first with the project setup.

The Angular Project

As I have mentioned before, the process of developing an Angular application is different from the same process for an AngularJS application. The source codes of the web application must be in a node.js project. There is a tutorial from the Angular framework's official site that teaches you how to create the Angular application. Alternatively, you can modify an existing Angular project for some new purposes. This alternative route is what I took for this tutorial. While I was doing that, I did some upgrades to the project. If you take a look at the node.js project file package.json, you will see that:

  • I use the Angular framework of version 14.1.0.
  • 6.2.1
  • I use the fontawesome fonts of version 6.2.1. This might be a bit old.
  • I use the ng-bootstrap library of version 13.1.0.

This is the package.json file of my project:

{
  "name": "angular-fileupload-app",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^14.1.0",
    "@angular/common": "^14.1.0",
    "@angular/compiler": "^14.1.0",
    "@angular/core": "^14.1.0",
    "@angular/forms": "^14.1.0",
    "@angular/platform-browser": "^14.1.0",
    "@angular/platform-browser-dynamic": "^14.1.0",
    "@angular/router": "^14.1.0",
    "@fortawesome/fontawesome-free": "^6.2.1",
    "@ng-bootstrap/ng-bootstrap": "^13.1.0",
    "bootstrap": "^5.2.0",
    "jquery": "^3.6.0",
    "rxjs": "~7.5.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^14.1.1",
    "@angular/cli": "~14.1.1",
    "@angular/compiler-cli": "^14.1.0",
    "@types/jasmine": "~4.0.0",
    "jasmine-core": "~4.2.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.0.0",
    "typescript": "~4.7.2"
  }
}

One other important file is angular.json. This file contains the configuration of the Angular project. Suppose you want to add extra JavaScript or CSS files to the application to be packaged for production use. This is the file where I can add the JQuery and Bootstrap JavaScript files and styles files. If you are interested in knowing how to integrate JQuery, Bootstrap, and font-awesome components, please take a look at the content of this file.

Finally, I want to mention another important file that is used for the compilation of the Angular project. It is the tsconfig.json file. This file is auto-generated. And it is modifiable. I have made two changes in the section called "compileOptions" The first one is the outDir. I changed the value to a folder where the front-end application can be included in the Java project so that when the Java project is compiled and packaged, the front-end web application can be integrated into the back-end service project as one. The other line is the one I added:

...
    "strictPropertyInitialization": false,
...

This is a very important line. It allows me to create properties in a TypeScript class without initializing it via its constructor. And this allows me to initialize a property of the class via annotation (a.k.a via dependency injection). This is also related to the most interesting "part" that I want to point out in the next section.

There is also the tsconfig.app.json. This is used for compiling production deployment. You can change the "outdir" property in the compileOptions section. This will set the output folder of the front-end application for production deployment.

The next section will be big. In it, I will be explaining in detail the Angular application design. The most interesting "part" that I have mentioned several times will also be explained in this section.

How to Upload a File in Angular Application

The Angular application has two parts. The first part is the index page which has a button and also displays the file that is being uploaded (limited to image files only). The second part is the popup the user can specify the file to be uploaded. Since the second part is the most interesting, I will start with that. Everything that is fun is all in this popup component. I will first start with the popup component's HTML markup page. The markup code looks like this:

<div class="modal-header">
   <h4 class="modal-title">Upload File</h4>
   <button type="button" class="btn-close" aria-label="Close" (click)="cancelPopup()"></button>
</div>
<form>
<div class="modal-body">
   <div class="mb-1">
      <label for="uploadFileField" class="form-label">Select a file</label>
      <input class="form-control" type="file"
             name="uploadFileField" id="uploadFileField" #fileToUpld />
   </div>
</div>
<div class="modal-footer">
   <button type="button" class="btn btn-outline-dark" (click)="uploadFile()">Upload</button>
   <button type="button" class="btn btn-outline-dark" (click)="closePopup()">Close</button>
</div>
</form>

I am using the modal component from Bootstrap 5. The modal popup can be divided into three parts: a header, a body, and a footer. In the header portion, I added an "x" button that closes the modal. The event handler for this button is a method called cancelPopup(). We will get to this in the later part of this section. In the body portion, I used only one file input field. This is where the user can select a file to upload. And this is where the most interesting part of this tutorial is coming up. The one from the header cancels the upload operation. The bottom button closes the popup and passes the uploaded file info to the index page so the file (if it is an image file) can be displayed.

To upload and display the file, I have to perform several operations. First, I need to get the file data from the file input field. Next, I need to pack the file content into a request and send it to the back-end service. Once the backend service receives the file content, it can be saved to disk as a file. Then the back-end service will send a response with the file name back to the Angular application. It will be the popup modal component that holds the HTTP response from the back end. When a user clicks the "Close" button at the bottom, the response data which contains file info would be passed to the index page. When the modal closes and the image file info is available, the index page code will construct an URL that displays the file (if it is an image file).

Let me start with the field for selecting a file. The markup of this field looks like this:

...
      <input class="form-control" type="file"
             name="uploadFileField" id="uploadFileField" #fileToUpld />
...

This input field has no "ngModel" associated. All it is a tag like this: #fileToUpld. This is an important tag that would allow me to get a reference of this HTML field in the Angular code allowing me to initialize a property of this popup component via annotation. This is the most interesting part of this tutorial. Once I can get a reference for this specific field, I can code it to extract the content of the uploading file.

Let me jump into the Angular code of the popup component and reveal to you how this field is being used:

import { Component, ViewChild, ElementRef } from '@angular/core';
...
...
@Component({
   ...
})
export class UploadPopupComponent {
   @ViewChild("fileToUpld")
   private _fileUploadField: ElementRef;
   
   ...
   ...
   public uploadFile(): void {
      if (this._fileUploadField &&
        this._fileUploadField.nativeElement) {
           if (this._fileUploadField.nativeElement.files && this._fileUploadField.nativeElement.files.length > 0) {
              let fileData: any = this._fileUploadField.nativeElement.files[0];
    ...
    ...
          }
      }
   }
   ...
   ...
}

The above codes can be a lttle hard if you never worked on Angular before (I know this from first hand experience). In the markup of the popup component, I have defined a input field that could hold a file. The definition contains the tag #fileToUpld. In the above Angular component class, I need a reference to this input field. This is the line that declares and initializes the element reference property:

   @ViewChild("fileToUpld")
   private _fileUploadField: ElementRef;

The annotation @ViewChild("fileToUpld") links the HTML element (the file input field with tag #fileToUpld) on the popup to this property called "_fileUploadField". This property has the type of ElementRef. This declaration also initializes the property with the actual reference to the HTML element. Remember that in the project's tsconfig.json file, I have this:

...
    "strictPropertyInitialization": false,
...

Without this, the declaration of the ElementRef property will fail compilation. The error would be something about the property of this component was never initialized in the constructor. This property does not need to be initialized. It is probably initialized by dependency injection via the annotation.

Once I have a reference to the file input field, I can extract the file that user selected. This is how it can be done:

if (this._fileUploadField &&
   this._fileUploadField.nativeElement) {
      if (this._fileUploadField.nativeElement.files && this._fileUploadField.nativeElement.files.length > 0) {
         let fileData: any = this._fileUploadField.nativeElement.files[0];
...
...
   }
}

this._fileUploadField.nativeElement is the reference to the actual HTML input field. Since this input field represents selected file or files (it allows the user to select multiple files at once). The native element reference will have a property called files. It is an array that holds the data content of one or more files. Since I assume that one file is uploaded everytime, I would only get the first file in this array:

...
let fileData: any = this._fileUploadField.nativeElement.files[0];
...

For my file input field, I never bound an ngModel property to this field. This is intended. If I bind an ngModel property and when the application runs, there will be an error showing in the debugging console. Binding an ngModel property is completely unnecessary in this case. You are welcome to try it if you don't believe me.

Next, I will show you how to perform the file-uploading operation. We have seen the code logic of extracting the file content from the input field. After getting the content data, we call a service object to upload the file. This is how it was done:

...
   let fileData: any = this._fileUploadField.nativeElement.files[0],
       self = this;
   self.isUploadingMediaFile = true;
   this._fileUploadSvc.uploadFile(fileData)
      .subscribe((resp: any) => {
         if (resp) {
            if (resp.operationSuccess) {
               self._fileName = resp.fileName;
               self.isUploadingMediaFile = false;
               alert("Upload successful.");
            } else {
               self.isUploadingMediaFile = false;
               if (resp.statusMessage && resp.statusMessage.trim() !== "") {
                  alert("Error occurred: " + resp.statusMessage);
               } else {
                  alert("Unable to upload media file. Unknown error.");
               }
            }
         } else {
            self.isUploadingMediaFile = false;
            alert("Response object is inbvalid. Unknown error.");
         }
      }, (error: HttpErrorResponse) => {
         self.isUploadingMediaFile = false;
         console.log(error);
         alert("Failed to upload file. See JavaScript console output for more details.");
      });
...

This is part of the method uploadFile() in the TypeScript file uploadPopup.component.ts. The line that calls the service object to do the uploading is this:

...
this._fileUploadSvc.uploadFile(fileData)
...

The service object is called _fileUploadSvc. It is declared and initialized at the top of the component class. The service class is defined in a TypeScript file called "fileUpload.service.ts". This class is surprisingly simple. It has a constructor that helps initialize the HttpClient object that is needed for the file-uploading operation. Besides the constructor, there is only just one method uploadFile(). Here is the whole class definition:

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

@Injectable({
   providedIn: 'root',
})
export class FileUploadService {
   constructor(private _uploadClient: HttpClient) {
      
   }
   
   public uploadFile(fileData: any): Observable<any> {
      let formData:FormData = new FormData();
      formData.append("MediaUpload", fileData);
      return this._uploadClient.post<any>(environment.apiBaseUrl + "api/fileUpload", formData);
   }
}

The way file uploading works in an Angular application is exactly the same as doing it in an AngularJS application. We need to do an HTTP post and pack the file content in a FormData object. And I honestly didn't realize this when I researched. When all the searches online pointed to the same thing, I realized I was chasing an answer when I already know what it is. FormData is a JavaScript build-in data type. It represents the "ancient" HTTP form request data, including file content. The FormData object is a collection of key-value pairs. On the Spring Boot side, the JavaScript data object FormData is called a MultipartFile object.

The above code segment creates a FormData object. It then adds a key-value pair with the key named MediaUpload, and the value is the file content data. At last, it calls the "httpClient" object to send a POST request against the back-end service. The post request can be sent to the back-end service by calling the HttpClient type's post() method. It takes several parameters. The first is the string value that represents the URL of the service. The second one is the request data for the POST request. The method post() can take more parameters, such as headers collections, URL parameters, and many others. In this simple demonstration application, all we needed for the method parameters are the URL to the back-end service and the content data for the request body.

Let's go back to the index component and take a look at how to invoke the popup modal. In the file index.component.ts, you can find the following code section:

...
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { UploadPopupComponent } from '../uploadPopup/uploadPopup.component';
import { environment } from '../../environments/environment';

@Component({
...
})
export class IndexComponent implements OnInit {
   
...

   constructor(private popup: NgbModal) {
   }
...
   public handleClickUploadBtn(): void {
      const popupInst: NgbModalRef = this.popup.open(UploadPopupComponent);
      popupInst.componentInstance.name = "Choose Media to Upload";
      let result:Promise = popupInst.result;
      let self = this;
      
      result.then((x) => {
         if (x && x.completed) {
            if (x.fileName != null && x.fileName.trim() !== "") {
               self.imageFile = environment.apiBaseUrl + "api/downloadFile/" + x.fileName
            }
         }
      }, (x) => { });
   }
}

To show the popup modal, I have to import the NgbModal and NgbModalRef from the "ng-bootdtrap" library. These are used in the method handleClickUploadBtn(). As you can see, at first, I need to declare an object of type NgbModalRef. This reference is used to hold the reference to the popup modal. After declaring the variable, the popup modal instance can be created by calling the instance property this.popup's method open(). The input parameter is the class definition of the upload popup class type. This is how the popup modal can be displayed. Once the popup modal displays, a reference to it can be obtained. Next, I want to get a promise for the popup modal. Depending on the action taken by the user, closing the popup modal can result in two possible outcomes. One is that the user completes the operation and resolves the promise. The other is that the user cancels the operation and rejects the promise. The way these two outcomes can be handled is the same as what we do in AngularJS. The promise has a method called then(). It takes two parameters. Both are references to the call-back methods. The first method is to handle the completion of the user action on the popup modal. The second method is to handle the case where the user cancels the operation. In the above code segment, I only care about the possibility that the users completed their actions on the popup modal. All it does is check that the user confirms the operation is successful. Then it will construct a URL that can download the file. This URL is used on the view to display the file that was just uploaded.

On the index page, this is how the download URL is used to display the uploaded file:

...
<div class="row" *ngIf="imageFile != null && imageFile.trim() !== ''">
   <div class="col">
      <img [src]="imageFile" class="rounded mx-auto d-block img-fluid" alt="Uploaded file display here." />
   </div>
</div>
...

That is everything for the Angular project. Before I move on, I want to emphasize why I think the use of the annotation of @ViewChild() is the most interesting part of this tutorial. It can be used by injecting elements from the view to the component class. In the component class, the code can perform operations like manipulating the HTML field, extracting the value from the field, changing the style, and many other works one desires. The sample application demonstrates the perfect use of this annotation. Using this annotation, we inject the HTML element into the component class. Then it is possible for the code to extract the file content data from this file input field and use it for the uploading operation.

On the Angular application side, we have seen how the file uploading operation works, and how it calls the backend to download the file and display. It is time to look at how the back end handles these requests. These are nothing new. I will review these known techniques.

Backend Service Request Handling

The backend service for handling the file upload and the file download is not complicated. I have covered these operations in my prior tutorials. I like to cover some important notes I have not mentioned in my previous tutorials. Now I can discuss these here. The backend application is a Spring Boot web application. It has two controllers. One controller is for serving the file uploading requests. The other controller is for serving file-downloading requests. Because the calling client is a separate application, when the two interacts, the CORS configuration must be done on the backend application so that the Angular application can pass requests to it successfully.

I have covered CORS configuration in my previous tutorial. But I will review it here again. The Angular application run on localhost with port 4200. The backend service runs on localhost with port 8080. Because of the port differences, I have to add the localhost URL with port 4200 to the CORS configuration of the backend service. I hardcoded the CORS configuration in the configuration class, like this:

package org.hanbo.boot.rest.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAppConfig
{
   @Bean
   public WebMvcConfigurer corsConfigurer()
   {
      String[] allowDomains = new String[2];
      allowDomains[0] = "http://localhost:4200";

      return new WebMvcConfigurer() {
         @Override
         public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**").allowedOrigins(allowDomains)
                    .allowedMethods("GET", "POST", "HEAD", "PUT", "DELETE");
         }
      };
   }
}

As you can see, the code segment is not hard to understand. All I need to add is an array of allowed domain URLs (including the port numbers in the URL); and all the allowed HTTP request methods to the CORS configuration mappings. If I don't have this done, there will be some error in the browser's debugging console showing that the backend service rejects the requests because the default CORS mappings would not allow these requests to get through.

Next, I want to discuss how to set the upload file size. This is very important. Spring Boot web application has a default upload file size of 1 MB. If you don't set the upload file size to an optimized value, you will be stuck with not being able to upload files more than 1 MB. This can be annoying. It requires a code change and re-deployment to fix. I would rather have this done as early as possible. This can be easily done in the application configuration file. You can find this in the file "src/main/resources/application.properties". All I need is this one line:

spring.servlet.multipart.max-file-size=400MB
spring.servlet.multipart.max-request-size=2GB

The first line specifies the maximum file size of one file. The second line specifies the maximum file size of the whole request. This means that within an HTTP post request, it can attach up to 5 files each can have a size up to 400 MB. It can also mean the request can contain as many files as possible as long as all the file sizes add up not exceeding 2 GB, and individual file's size not excceding the max 400 MB.

Let me show you the controller that handles the file upload:

package org.hanbo.boot.rest.controllers;

import java.io.File;
import java.io.FileOutputStream;

import org.apache.commons.io.IOUtils;
import org.hanbo.boot.rest.models.UploadResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class FileUploadController
{
   private static Logger _logger = LoggerFactory.getLogger(FileUploadController.class);
   
   @Value("${resource.basedir}")
   private String uploadFileBaseDir;
   
   @RequestMapping(value="/api/fileUpload", method=RequestMethod.POST)
   public ResponseEntity<UploadResponse> uploadFile(
         @RequestParam("MediaUpload") MultipartFile srcFile)
   {
      UploadResponse resp = new UploadResponse();
      if (srcFile != null)
      {
         String origFileName = srcFile.getOriginalFilename();
         String contentType = srcFile.getContentType();
         
         System.out.println("Uploaded File Name: " + origFileName);
         System.out.println("Uploaded File MIME Type: " + contentType);
         
         FileOutputStream writeToFile = null;
         try
         {
            String fileName = String.format("%s/%s", uploadFileBaseDir, origFileName);
            File destMediaFile = new File(fileName);
            
            writeToFile = new FileOutputStream(destMediaFile);
            IOUtils.copy(srcFile.getInputStream(), writeToFile);
            writeToFile.flush();
            
            _logger.info(String.format("Uploaded file has been saved to [%s].", fileName));
            
            resp.setFileId("000000001");
            resp.setFileName(origFileName);
            resp.setMimeType(contentType);
            resp.setOperationSuccess(true);
            resp.setStatusMessage(String.format("File [%s] has been saved successfully.", origFileName));
         }
         catch (Exception ex)
         {
            String errorDetails = String.format("Exception occurred when write media file to disk: %s", ex.getMessage());
            _logger.error("MediaMgmtServiceImpl.saveMediaFile: " + errorDetails);
            throw new RuntimeException(ex);
         }
         finally
         {
            if (writeToFile != null)
            {
               try
               {
                  writeToFile.close();
               }
               catch(Exception ex)
               { }
            }
         }
      }
      else
      {
         resp.setFileId("");
         resp.setFileName("");
         resp.setMimeType("");
         resp.setOperationSuccess(false);
         resp.setStatusMessage("HTTP request does not contain a file object.");
      }
      
      return ResponseEntity.ok(resp);
   }
}

The request handling method will take the MultipartFile object as the input. The parameter has the annotation of @RequestParam() because it is a form-based request. The method will extract the file name, the mime type of the file, and the file content stream. I used IOUtils.copy() to copy the data from the request input stream into the destination file output stream. After everything is successful, the method will return an operation result object back to the caller.

This is the controller that handles file download:

package org.hanbo.boot.rest.controllers;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RestController
public class FileDownloadController
{
   private static Logger _logger = LoggerFactory.getLogger(FileDownloadController.class);

   @Value("${resource.basedir}")
   private String resourceFileBaseDir;
   
   @RequestMapping(value="/api/downloadFile/{fileName}", method=RequestMethod.GET)
   public ResponseEntity<StreamingResponseBody> downloadFile(
         @PathVariable("fileName") String fileName)
   {
      System.out.println("Received file name: " + fileName);
      ResponseEntity<StreamingResponseBody> retVal = null;
      if (StringUtils.hasText(fileName))
      {
         String fileFullPath = String.format("%s/%s", resourceFileBaseDir, fileName);
         
         try
         {
            File f = new File(fileFullPath);
            if (f.exists() && f.isFile())
            {
               StreamingResponseBody respBody = loadFileDataIntoResponseStream(fileFullPath);
               if (respBody != null)
               {
                  Path path = Paths.get(fileFullPath);
                  String mimeType = Files.probeContentType(path);
                  
                  retVal = ResponseEntity.ok()
                        .header("Content-type", mimeType)
                        .header("Content-length", String.format("%d", f.length()))
                        .body(respBody);
               }
               else
               {
                  retVal = ResponseEntity.notFound().build();
               }
            }
            else
            {
               _logger.error(String.format("File not found: %s", fileFullPath));
               retVal = ResponseEntity.internalServerError().build();
            }
         }
         catch(Exception ex)
         {
            _logger.error(String.format("Exception occurred: %s", ex.getMessage()));
            retVal = ResponseEntity.internalServerError().build();
         }
      }
      else
      {
         _logger.error(String.format("Invalid file name: %s", fileName));
         retVal = ResponseEntity.badRequest().build();
      }
      
      return retVal;
   }
   
   private StreamingResponseBody loadFileDataIntoResponseStream(String imageFileFullPath)
   {
      StreamingResponseBody retVal = null;
      File imageFile = new File(imageFileFullPath);
      if (imageFile.exists() && imageFile.isFile())
      {
         try
         {
            retVal = new StreamingResponseBody()
            {
               @Override
               public void writeTo(OutputStream outputStream) throws IOException
               {
                  FileInputStream fs = null;
                  try
                  {
                     fs = new FileInputStream(imageFile);
                     IOUtils.copy(fs, outputStream);
                     outputStream.flush();
                  }
                  finally
                  {
                     outputStream.close();
                     if (fs != null)
                     {
                        fs.close();
                     }
                  }
               }
            };
         }
         catch (Exception ex)
         {
            _logger.error(String.format("Exception: %s", ex.getMessage()));
            retVal = null;
         }
      }
      else
      {
         _logger.error(String.format("Unable to find image file [%s]. file does not exist. Error 404", imageFileFullPath));
         retVal = null;
      }
      
      return retVal;
   }
}

This controller has two methods. One method is for handling the request. The other one is a helper method that loads the file content from disk into an output stream that can be packed into the HTTP response. In the old days, I would "inject" an HttpServletResponse object as parameter into the request handling method. Then I can write the file content into the output stream of the HttpServletResponse object. This is how we are used to return a large data object back. The new way of doing this is to return an object of type ResponseEntity<StreamingResponseBody>. The StreamingResponseBody would be the wrapper of the file content as an output stream. I think this new way is cleaner than the old way. I will use this for future works.

At this point, I have explained all the important aspects of this tutorial. In the next section, I will show you how to run the sample application.

How to Run the Sample Application

It is time to run the sample application. In this section, I will teach you one more trick. There is a way of integrating the Angular application into the Spring Boot project so you don't have to run two applications separately. First thing first, Let's take a look at the way of running both applications separately.

In the Spring Boot project, the application.properties file contains the base directory for storing the file upload. You must specify this directory with a path that the application can read from and write to:

resource.basedir=/home/<Your home folder>/Projects/DevJunk/AngularFileUpload

First, go to the webapp directory under the base directory. Run the following command to install all the node.js libraries needed for the Angular application:

npm install

It will take a few seconds to a few minutes to download and install the "node_modules" for the Angular project. At the time of writing, I can see two moderate-severity issues with the libraries installed. I am sure if I start the project anew with updated versions of all libraries, the problem will go away. Please ignore this issue. Run the next command to run the Angular application in the terminal window:

ng serve

The application will start up after the build process. It might take a few minutes if it runs for the first time. The subsequent builds would take much less time. And if it runs successfully, the web application will run on the following URL:

http://localhost:4200/

You also need to run the Spring Boot application. If you don't, the Angular application will not work as expected. First, you need to run the Maven build to compile and package the back-end service application. Here is the command:

mvn clean install

After the Java build succeeds, you can run the following command to run the Spring Boot application in the terminal window:

java -jar target/hanbo-angular-file-upload-1.0.1.jar

If you did everything right, both applications should be running at the same time. The URL for testing is: http://localhost:4200/. When you navigate to it for the first time, you will see this:

You can start testing the applications as one application. All you need to do are the following steps:

Step 1: Click the button "Pick file to Upload".

Step 2: When the ng-bootstrap popup shows up, click the file input field to specify a file for upload. Please choose an image file (jpg/png/gif).

Step 3: On the popup, click the button "Upload". When the upload succeeds, it will display an alert window indicate the file uploading is successful.


Step 4: On the popup, click the button "Close" to close the popup. The index page will display the newly uploaded file.

Optional Step: On the popup, if you click the "X" on the title bar. It will cancel the operation. If you have uploaded an image file. The file will not be displayed. If there is an old image displayed, the image will not change to the newly uploaded one.

Now, I will discuss the surprise I have added - the integration of the Angular application into the Spring Boot project. When you build the Angular application, the build will package everything into a few minified files. You can specify the files to be placed into a directory of the Spring Boot project. Then, when you build the Spring Boot application, the Angular application can be part of it. All you need to do is open the index page from the Spring boot application, and you can repeat all the demonstration operation steps on that page.

To specify where to output the final index page and associated JavaScript files, all you need to do is open the angular.json file in the Angular project. In the JSON section "architect" -> "build" -> "options", there is the property "outputPath". You can set the output path to the directory "src/main/resources/static" of the Spring Boot project. I have already set this for you, like this:

...
      "architect": {
        "build": {
          "builder": "@angular-devkit In the s/build-angular:browser",
          "options": {
            "outputPath": "../src/main/resources/static",
            "index": "src/index.html",
...

This is where the static content of a Spring Boot web application can be served to the users. When the Angular application files are generated here, they will become part of the Spring Boot project. Here are the steps to try this integration:

Step 1, go the to directory of the Angular project in the terminal window. Then run the following command:

ng build

Step 2, when the Angular project build completes, go to the Spring Boot project's directory "src/main/resources/static" and check for the files of the Angular application.

Step 3, run the following command in the base folder of the Spring Boot project:

mvn clean install

Step 4, when the Spring Boot project build completes. Go ahead and start the Spring Boot application. You don't need to start up the Angular application separately. It is part of the Spring Boot application. Run the following command to start the Spring boot application in the terminal window:

java -jar target/hanbo-angular-file-upload-1.0.1.jar

Step 5, navigate to this URl and run the demonstration steps:

http://localhost:8080/index.html

It is fun! Isn't it? If you can get to this point, you have done everything correctly. Feel free to explore the source as you please.

Summary

I hope you have enjoyed this tutorial. I recycled a lot of old materials for this one, such as the mechanism for file upload operations, the approach of getting the file content from the file input field, the CORS configuration for communication between two different applications, and the build integration that copies the Angular application files into the Spring Boot project. To me, the biggest takeaway is using annotation @ViewChild() to inject the HTML element field into the Angular component class. From there, I can get the native element reference of the HTML field, and manipulate the field freely. This ability enables me to get the file content from the input field so that I can pass the file content to the Angular service object for uploading. Another good takeaway is that we can package the Angular application as part of the Spring Boot application. In this tutorial, I have described how this is done. However, the node.js application can be hosted in many ways, so it is not necessary to be packaged in a Spring Boot project.

The other concepts I have discussed in this tutorial are similar to the ones I have already covered in my prior tutorials. I just want a tutorial that bridges the gaps between AngularJS and Angular. I believe this tutorial has successfully accomplished this. I had a lot of fun writing this tutorial. I hope you enjoy this. Thank you for reading this tutorial, and good luck to you.

History

7/29/2023 - Initial Draft

Your Comment


Required
Required
Required

All Related Comments

Loading, please wait...
{{cmntForm.errorMsg}}
{{cmnt.guestName}} commented on {{cmnt.createDate}}.

There is no comments to this post/article.