Вы находитесь на странице: 1из 10

05/08/2019 Upload File with AngularJS using ngResource - CodeProject

Upload File with AngularJS using ngResource


Han Bo Sun

3 Aug 2019 MIT

In this tutorial, I will be discussing the approach of uploading files using ngResource with AngularJS.

Download demo project - 495.5 KB

Introduction
The traditional way of uploading a file is to use a form submission. This approach works extremely well with application
implemented with Spring MVC or ASP.NET MVC frameworks. In this tutorial, I will be discussing a different approach of uploading a
file. In this tutorial, the file upload can be packed inside a JSON object and handled by a RESTful web service.

This tutorial will begin with an overview of the sample application's architecture. Then, I will discuss the way of creating a file upload
HTML component using Bootstrap. Afterwards, we will explore the way AngularJS can be used to manipulate the HTML elements.
Believe me, manipulating the HTML elements like JQuery is necessary in this tutorial. And we will get to use $scope.$apply().
Files from local drive can be read by JavaScript's FileReader object, which transforms binary into BASE64 encoded text string.
This allows us to pack the file content into a JSON object to be sent. On the server side, the request handling method will parse the
file content and revert the BASE64 string back into a binary file.

The Overall Architecture


For this tutorial, I have created a sample program to demonstrate the functionalities that would be discussed in this tutorial. This
application will be a Spring Boot based application. It hosts a RESTFul web service that can handle only one kind of request - our
request for file upload. It will handle the file being uploaded. This method will take a JSON object that has two properties, one is a
short file name, and the other is the content of the file. This method will simply save the file to a hardcoded location like: c:\temp\.

The application also contains an index page which is actually a single-page web application implemented using AngularJS. The
application has a single input that can load a file from disk. Once file is selected, the application will load the file content and get the
file name (just the file name, not the path). It packs the two into a JSON object and sends it to the RESTFul web service.

Here is a screenshot of the sample application:

The hardest parts of this sample application are:

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 1/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

How to manipulate the HTML elements on the page to select a file from local disk drive.
How to read the file and transform the file into a string so that it can be transported to the backend web service.
On server side, how to read the input file content and save it back as a file.

All these will be explained in the next few sections. When I first started working on the sample application, the very first task I had to
do is design an HTML element that can get a file from local disk. Let's see how this is done.

The File Input Component


There is an input control available in HTML that does this. I know. What I need is not this predefined input control. What I need is a
readonly text field, and a button grouped together. The button can be clicked and pops up the File Open dialog so it can be used to
select a file. The text field will display the file name. With Bootstrap, this is very easy to define:

<div class="input-group">
<input type="file" id="fileUploadField" style="display: none;">
<input type="text" id="fileNameDisplayField" class="form-control"

ng-model="vm.uploadFileName" placeholder="File to Upload" readonly="readonly">


<div class="input-group-addon btn btn-default"

ng-click="vm.clickSelectFile()"><i class="glyphicon glyphicon-file"></i></div>


</div>

In the code snippet above, there are some attributes specific for AngularJS. You can ignore these for now. The outer <div>
element has CSS class "input-group". It can smash multiple input control into one. With the above code snippet, it will create
an input control like this:

Inside the outer <div>, there are three HTML elements:

An input field with type of "file". This field will be invisible because I am not going to display it. It exists so that I can use it
to hold the metadata of the chosen file. And I use its functionality to choose a file.
A text input file that is readonly, can be used to display the file name (without the folder path). I choose it to be readonly
because I don't want the user to modify the value. This is just a preference. If you wish, you can make it editable.
A button with a File icon. It can be click and select a file. The button defined via another div using CSS class "input-
group-addon".

Let's circle back, to the question why I don't use the HTML default file controller. If you make it visible by removing the style
attribute, you can see that it is a text field with a button. It is similar to the component I just created. The ugly aspect of this default
one is that it is not customizable. And it will not fit with the other Bootstrap controls. It is best to create something that looks like
the rest of the components. And, there, you have it.

Now we have the file upload component. It is time to implement the behavior which can select a file and pack it as an JSON object.

AngularJS Code Walkthrough


The single page application is defined in the file app.js. It is located at src\main\resources\static\assets\app\js folder. In order for the
Angular application to work, I have to implement several functionalities:

How to click the button with the file icon to pop up the File Open dialog.
Once a file is selected, how to save the file metadata and extract the single file name to display in the readonly text field.
How to load the file content as BASE64 string.

What is not really a challenge is sending the BASE64 file content string to the backend web service. This is done with
ngResource. I will discuss this at the end of this section.

Getting File Metadata from Disk

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 2/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

Since I have to define my own file upload component and the default file upload input is hidden, there has to be a way to link the
button click to the default file upload input. Turns out, it is pretty easy to do. The default file upload input has a button. All I need to
do is associate my button click to the click of the file upload button click.

In order to do this, I have to use something similar to JQuery. AngularJS has a built-in function called angular.element(). It
can be used to select elements from the page. Before I continue, I just want to say that it is OK to query the DOM elements in the
page. My reasons are:

It is a sample application, to demo a concept.


It is contained, it is somewhat well designed. And it is maintainable.
It is the easiest way to accomplish what I need to accomplish.

Back to the design. When a user clicks on the button with the file icon, it will trigger the hidden file upload input to be clicked. Here
is how:

vm.clickSelectFile = function () {
angular.element("#fileUploadField").click();
};

On the index.html, the button with the file icon is defined as this:

<div class="input-group-addon btn btn-default"

ng-click="vm.clickSelectFile()"><i class="glyphicon glyphicon-file"></i></div>

As you can see, the directive ng-click references the function vm.clickSelectFile(). So when this button is clicked, the
JavaScript code above will do a query on the DOM tree and find the hidden input of file upload and invoke its click() method.
When the user does the click, the File Open dialog will popup. After choosing a file and closing this popup, the read-only text field
does not display anything. This is expected unless I add more functionalities to the application. The next functionality I had to add is
that when the hidden file upload input is assigned a file, it will notify the text field to display the file name, which is discussed next.

Display Selected File Name


As described, choosing a file with the hidden file upload input does not automatically make the text field display the file name. This
problem is also easy to solve. All we need to do is to have the hidden file upload input handle the selection change event. During
the event handling, it will assign the file name to the angular scope variable which is bound to the text field. The problem that
would be apparent is that assigning the value to the scope variable, but it will not automatically refresh the value display on the text
field. To fix this, we have to call $scope.$apply(). This will do the refreshing and make the text appear.

To have the file upload input handle the selection change event, what I had to do is use angular.element() to query the
hidden file upload input and assign an event handling method to it:

angular.element("#fileUploadField").bind("change", function(evt) {
if (evt) {
...
}
});

As you can see, this is the same way one would do with JQuery. Query the element by its id to get a reference, then bind the event
handling method to the event. In this case, it is the change event of the element that will be handled.

Next, I need to use the full path file name of the selected file, and return only the file name, not the folder path. For this, I just came
up with a simple heuristic. The file name will either be a Windows full path file name with drive letter, and backslashes; or UNIX
based full path file name with slashes. It is one way or the other, so here is how I extract the file name:

var fn = evt.target.value;
if (fn && fn.length > 0) {
var idx = fn.lastIndexOf("/");
if (idx >= 0 && idx < fn.length) {
vm.uploadFileName = fn.substring(idx+1);
} else {
idx = fn.lastIndexOf("\\");
if (idx >= 0 && idx < fn.length) {
vm.uploadFileName = fn.substring(idx+1);
https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 3/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

}
}
}
$scope.$apply();

The code snippet above first looks for the last slash in the file name. If found, I just take the rest of the file name after this character.
If the last slash is not found, then I will try again by finding the last back slash in the file name. If found, then I again will take the rest
of the file name after this character. If both characters are not found, nothing is changed for the scope variable.
vm.uploadFileName is the scope variable. Once the file name is assigned to the scope variable, I need to call
$scope.$apply() to make sure the text field would display the file name. Why would I do this? Normally, AngularJS would
handle view element to model binding automatically. But, in this case where I explicitly query the element and bind an event
handler method, the automatic binding of the data model to view is not set. So to explicitly do the model to view data refresh, just
call $scope.$apply().

What is next? It is the hardest part of this tutorial. I need to load the file as BASE64 encoded string. It will be discussed next.

Loading File Content as BASE64 Encoded String


Once a file is selected, all we have is a file name, and maybe some other file related metadata. The next step is to load the data.
After some searching, it turns out to be very easy as well. JavaScript provided an object type called FileReader. There are two
cool things about this object type:

All I have to do is pass the file name to the object's readAsDataURL() to do the file loading.
The loading of file is done asynchronously. And when the loading is done, I can provide a callback method that can call the
web service to do the actual upload.

Here is the entire source code of the file loading, and upload the file:

vm.doUpload = function () {
vm.uploadSuccessful = false;
var elems = angular.element("#fileUploadField");
if (elems != null && elems.length > 0) {
if (elems[0].files && elems[0].files.length > 0) {
let fr = new FileReader();
fr.onload = function(e) {
if (fr.result && fr.result.length > 0) {
var uploadObj = {
fileName: vm.uploadFileName,
uploadData: fr.result
};

sampleUploadService.uploadImage(uploadObj).then(function(result) {
if (result && result.success === true) {
clearUploadData();
vm.uploadSuccessful = true;
}
}, function(error) {
if (error) {
console.log(error);
}
});
}
};

fr.readAsDataURL(elems[0].files[0]);
} else {
vm.uploadObj.validationSuccess = false;
vm.uploadObj.errorMsg = "No file has been selected for upload.";
}
}
};

There are a couple points regarding the above code that are worthy of mention. First, I need to get the file name (full path) of the
selected file. By querying the element of the default upload file input, I will get a reference of it. Then, I will use its property called

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 4/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

.files to get a reference to all the selected files of this input element. The default file upload input can select multiple files at
once, this is why an array is used to hold these files instead of an object reference to just a single file. Here is the code snippet:

var elems = angular.element("#fileUploadField");


if (elems != null && elems.length > 0) {
if (elems[0].files && elems[0].files.length > 0) {
// This is where we can get the file name and use it for loading
...
}
}

Next, I need to create a FileReader object, and read the file using method readAsDataUrl(). Here is the code snippet:

let fr = new FileReader();


fr.onload = function(e) {
if (fr.result && fr.result.length > 0) {
// This is where we actually upload the file content to the web service.
...
}
};

fr.readAsDataURL(elems[0].files[0]);

As shown above, I created an object of FileReader and have variable fr reference it. Then, I supplied the object with a call back
method for its property onload. The method supplied will call my AngularJS service object to do the file upload. Next, I will
discuss how this service object works.

AngularJ Service for File Upload


In my previous article, "AngularJS ngResource Tutorial", I mentioned that upload data will be covered in a future article. This is the
future article I have promised. The problem I had in my previous tutorial is that I don't have a way of loading the file as BASE64
encoded string. Here, I do. And the rest (of uploading a file) is actually pretty easy to do.

Here is the full source code of the service object definition:

(function () {
"use strict";
var mod = angular.module("uploadServiceModule", [ "ngResource" ]);
mod.factory("sampleUploadService", [ "$resource",
function ($resource) {
var svc = {};

var restSvc = $resource(null, null, {


"uploadImage": {
url: "./uploadImage",
method: "post",
isArray: false,
data: {
fileName: "@fileName",
uploadData: "@uploadData"
}
}
});

svc.uploadImage = function (imageUpload) {


return restSvc.uploadImage(imageUpload).$promise;
};

return svc;
}
])
})();

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 5/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

If you have read my previous article, "AngularJS ngResource Tutorial", it is going to be easy for you to understand the above service
object defintion. I defined a factory object called "sampleUploadService". The factory object returns an object that acts as a
service, in it, there is just one method called uploadImage().

The uploadImage() method uses ngResource object to call the backend web service. The ngResource object which I
have defined has just one action method inside, also called "uploadImage". The action method sees HTTP Post for
communication with the back end. The url property defines the web service url. And it expects a single object as response, not
an array of objects. Finally, the data that is sent to the backend will be a JSON object, which has two properties: one is the file name
and the other is BASE64 encoded file content.

Let's get back to the end of the last section, where I was about to post the code snippet of how to use the above AngularJS service
(or shall we say factory) to invoke the backend web service. I stop there because I think it is best to show how the AngularJS service
is defined first. Now that is revealed, time to see how the AngularJS service can be used. Here it is:

var uploadObj = {
fileName: vm.uploadFileName,
uploadData: fr.result
};

sampleUploadService.uploadImage(uploadObj).then(function(result) {
if (result && result.success === true) {
clearUploadData();
vm.uploadSuccessful = true;
}
}, function(error) {
if (error) {
console.log(error);
}
});

The file content is stored in the property "result" of the FileReader object. It is only available after the call of
readAsDataURL() is completed asynchronously. When it is done, the object's onload() callback will be invoked, where the
above code snippet is located. In the above code snippet, I created a new object called uploadObj. And I assigned the file name
without the full path and the file content which is in fr.result to the properties of the new object.

Finally, I used my service sampleUploadService and called the uploadImage(). That is all it took. It is very simple. If you
read my previous tutorial, you will know how to handle the event when the HTTP call finishes.

The last thing I want to cover here is the format of the file content. It is not just the simple BASE64 encoded string. The first
part is the media type, start with the prefix "data:". It is followed by a semicolon. Then it is the encoding type. This will always be
"base64", and followed by a comma. Finally it is the BASE64 encoded string. Here is a short sample:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZkAAAHVCAIAAACs
XRGOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjw...

This is the last of the JavaScript code logic. In the next section, I will cover the Java side of code logic.

Spring Boot Web Service


The JavaScript part of code logic is over, time to check out the Spring Boot web service that handles the file upload. First, I would
like to show the Java class that represents the upload file object. This class is called UploadObject. Here it is:

package org.hanbo.boot.rest.models;

public class UploadObject


{
private String fileName;
private String uploadData;

public String getUploadData()


{
return uploadData;
}
https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 6/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

public void setUploadData(String uploadData)


{
this.uploadData = uploadData;
}

public String getFileName()


{
return fileName;
}

public void setFileName(String fileName)


{
this.fileName = fileName;
}
}

I also need an object as a response. For this, I created another object type called GenericResponse. Here it is:

package org.hanbo.boot.rest.models;

public class GenericResponse


{
private String id;
private boolean success;
private String detailMessage;

public String getId()


{
return id;
}

public void setId(String id)


{
this.id = id;
}

public boolean isSuccess()


{
return success;
}

public void setSuccess(boolean success)


{
this.success = success;
}

public String getDetailMessage()


{
return detailMessage;
}

public void setDetailMessage(String detailMessage)


{
this.detailMessage = detailMessage;
}
}

The final part of this tutorial, of the sample program, is the RESTFul controller that handles the file upload. In order to demo the
decoding of the file data, I created the request handling method to decode the file content, and save the file to disk.

Here is the full code snippet:

@RequestMapping(value="/uploadImage", method=RequestMethod.POST)
public ResponseEntity<GenericResponse> uploadImage(
@RequestBody
UploadObject uploadObj
)
https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 7/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

{
if (uploadObj != null && uploadObj.getUploadData() != null)
{
String uploadData = uploadObj.getUploadData();
if (uploadData.length() > 0)
{
String[] splitData = uploadData.split(";");
if (splitData != null && splitData.length == 2)
{
String mediaType = splitData[0];
System.out.println(mediaType);
if (splitData[1] != null && splitData[1].length() > 0)
{
String[] splitAgain = splitData[1].split(",");
if (splitAgain != null && splitAgain.length == 2)
{
String encodingType = splitAgain[0];
System.out.println(encodingType);
String imageValue = splitAgain[1];

byte[] imageBytes = Base64.decode(imageValue);


System.out.println("File Uploaded has " + imageBytes.length + " bytes");
System.out.println("Wrote to file " + "c:\\temp\\" +
uploadObj.getFileName());
File fileToWrite = new File("c:\\temp\\" + uploadObj.getFileName());
try
{
FileUtils.writeByteArrayToFile(fileToWrite, imageBytes);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
}
}
}

GenericResponse resp = new GenericResponse();


UUID randomId = UUID.randomUUID();
resp.setId(randomId.toString().replace("\\-", ""));
resp.setSuccess(true);
resp.setDetailMessage("Upload file is successful.");
ResponseEntity<GenericResponse> retVal = ResponseEntity.ok(resp);

return retVal;
}

In above code snippet, all I'm doing is splitting the string to get to the BASE64 encoded part, which is the actual file content.
Once I got the string, I use Apache Commons' encoding library to turn the string into a byte array. Finally, I save the byte
array to a file located in folder: C:\temp. The file name is the file name from the request. After saving the file, the method returns
back a JSON object of type GenericResponse to the caller.

How to Test
After downloading the source code, please go through all the static content files and rename all the files *.sj to *.js. I had to rename
these files so I can zip them up and send to codeproject.com via email for publishing. The email server scans the zip files and won't
allow me to attach the file because JavaScript files are inside. Anyways, you need to rename these files if you want to run them
locally.

The sample application is a Spring Boot based application, so it must be built before you can start it as a service application. To
build it, you can CD into the base director the sample application. Then run:

mvn clean intsall

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 8/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

The build will succeed. After it is successful, run the following command and start the sample application:

java -jar target\hanbo-ngfileupload-sample-1.0.1.jar

The application will start successfully. When it does, you will see the following output in the commandline console:

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.5.RELEASE)

2019-07-30 22:56:00.949 INFO 12808 --- [ main] org.hanbo.boot.rest.App


: Starting App v1.0.1 on U3DTEST-PC with PID 12808
(C:\Users\u3dadmin\workspace-mars8\ngUploadSample\target\hanbo-ngfileupload-sample-1.0.1.j
ar started by u3dadmin in C:\Users\u3dadmin\workspace-mars8\ngUploadSample)
2019-07-30 22:56:00.957 INFO 12808 --- [ main] org.hanbo.boot.rest.App
: No active profile set, falling back to default profiles: default
2019-07-30 22:56:01.095 INFO 12808 --- [ main]
ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.
context.AnnotationConfigServletWebServerApplicationContext@3a4afd8d: startup date
[Tue Jul 30 22:56:01 EDT 2019]; root of context hierarchy
...
...
2019-07-30 22:56:11.195 INFO 12808 --- [ main] org.hanbo.boot.rest.App
: Started App in 11.453 seconds (JVM running for 12.389)
2019-07-30 22:56:42.343 INFO 12808 --- [nio-8080-exec-3] o.a.c.c.C.[Tomcat].[localhost].[/]
: Initializing Spring FrameworkServlet 'dispatcherServlet'
2019-07-30 22:56:42.343 INFO 12808 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet
: FrameworkServlet 'dispatcherServlet': initialization started
2019-07-30 22:56:42.373 INFO 12808 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet
: FrameworkServlet 'dispatcherServlet': initialization completed in 30 ms

To run the web application in the browser, navigate to the following URL:

http://localhost:8080

Once the page loads, it will display the screenshot displayed earlier in this tutorial. All you need to do is click the button with the file
icon, the File Open dialog would pop up. Use it to select a file and click OK. You will see the file name without the full folder path
would be displayed on the text input. At last, click on the blue button "Upload". When upload succeeds, a green status bar would
display on top of the page and show upload succeeded.

Go to the destination folder C:\temp, and find the file that was just uploaded and saved there.

Summary
This is yet another good tutorial completed. Again, I had fun writing this. For this tutorial, my focus is on file upload, specifically on
how to perform file upload using AngularJS' ngResource. As shown, in order to perform the file upload, I had to load the file and
convert the content into BASE64 encoded string (along with the media type and encoding type). Then this can be packed in a
JSON object, which can be sent to the web service.

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 9/10
05/08/2019 Upload File with AngularJS using ngResource - CodeProject

The web service is written as a Spring Boot web application. The single page application is also packaged in the same application.
The web service has just one request handling method, which takes the JSON object as input. Once it received the request, it will
parse the BASE64 encoded string, converted into a byte array. Then save the byte array into a file. Even though this sample
application does not do anything useful, it demonstrates the upload functionality end to end. It is useful for anyone who needs this.

I hope you enjoyed this tutorial. I certainly had fun writing this. Anyways, enjoy this and I hope it is helpful.

History
7/31/2019 - Initial draft

License
This article, along with any associated source code and files, is licensed under The MIT License

About the Author


Han Bo Sun No Biography provided
Team Leader The Judge Group
United States

Comments and Discussions


0 messages have been posted for this article Visit https://www.codeproject.com/Articles/5163873/Upload-File-with-
AngularJS-using-ngResource to post and view comments on this article, or click here to get a print view with messages.

Permalink Article Copyright 2019 by Han Bo Sun


Advertise Everything else Copyright © CodeProject, 1999-2019
Privacy
Cookies Web03 2.8.190802.3
Terms of Use

https://www.codeproject.com/Articles/5163873/Upload-File-with-AngularJS-using-ngResource?display=Print 10/10