Angularjs simple file download causes router to redirect

Angularjs

Angularjs Problem Overview


HTML:

<a href="mysite.com/uploads/asd4a4d5a.pdf" download="foo.pdf">

Uploads get a unique file name while there real name is kept in database. I want to realize a simple file download. But the code above redirects to / because of:

$routeProvider.otherwise({
    redirectTo: '/', 
    controller: MainController
});

I tried with

$scope.download = function(resource){
    window.open(resource);
}

but this just opens the file in a new window.

Any ideas how to enable a real download for any file type?

Angularjs Solutions


Solution 1 - Angularjs

https://docs.angularjs.org/guide/$location#html-link-rewriting

> In cases like the following, links are not rewritten; instead, the > browser will perform a full page reload to the original link. > > - Links that contain target element Example:
<a href="/ext/link?a=b" target="_self">link</a>

> - Absolute links that go to a different domain Example:
<a href="http://angularjs.org/">link</a>

> - Links starting with '/' that lead to a different base path when base is defined Example:
<a href="/not-my-base/link">link</a>

So in your case, you should add a target attribute like so...

<a target="_self" href="example.com/uploads/asd4a4d5a.pdf" download="foo.pdf">

Solution 2 - Angularjs

We also had to develop a solution which would even work with APIs requiring authentication (see this article)

Using AngularJS in a nutshell here is how we did it:

Step 1: Create a dedicated directive

// jQuery needed, uses Bootstrap classes, adjust the path of templateUrl
app.directive('pdfDownload', function() {
return {
    restrict: 'E',
    templateUrl: '/path/to/pdfDownload.tpl.html',
    scope: true,
    link: function(scope, element, attr) {
        var anchor = element.children()[0];

        // When the download starts, disable the link
        scope.$on('download-start', function() {
            $(anchor).attr('disabled', 'disabled');
        });

        // When the download finishes, attach the data to the link. Enable the link and change its appearance.
        scope.$on('downloaded', function(event, data) {
            $(anchor).attr({
                href: 'data:application/pdf;base64,' + data,
                download: attr.filename
            })
                .removeAttr('disabled')
                .text('Save')
                .removeClass('btn-primary')
                .addClass('btn-success');

            // Also overwrite the download pdf function to do nothing.
            scope.downloadPdf = function() {
            };
        });
    },
    controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) {
        $scope.downloadPdf = function() {
            $scope.$emit('download-start');
            $http.get($attrs.url).then(function(response) {
                $scope.$emit('downloaded', response.data);
            });
        };
    }] 
});

Step 2: Create a template

<a href="" class="btn btn-primary" ng-click="downloadPdf()">Download</a>

Step 3: Use it

<pdf-download url="/some/path/to/a.pdf" filename="my-awesome-pdf"></pdf-download>

This will render a blue button. When clicked, a PDF will be downloaded (Caution: the backend has to deliver the PDF in Base64 encoding!) and put into the href. The button turns green and switches the text to Save. The user can click again and will be presented with a standard download file dialog for the file my-awesome.pdf.

Our example uses PDF files, but apparently you could provide any binary format given it's properly encoded.

Solution 3 - Angularjs

If you need a directive more advanced, I recomend the solution that I implemnted, correctly tested on Internet Explorer 11, Chrome and FireFox.

I hope it, will be helpfull.

HTML :

<a href="#" class="btn btn-default" file-name="'fileName.extension'"  ng-click="getFile()" file-download="myBlobObject"><i class="fa fa-file-excel-o"></i></a>

DIRECTIVE :

directive('fileDownload',function(){
	return{
		restrict:'A',
		scope:{
			fileDownload:'=',
			fileName:'=',
		},
		
		link:function(scope,elem,atrs){
			

			scope.$watch('fileDownload',function(newValue, oldValue){
				
				if(newValue!=undefined && newValue!=null){
					console.debug('Downloading a new file'); 
					var isFirefox = typeof InstallTrigger !== 'undefined';
					var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
					var isIE = /*@cc_on!@*/false || !!document.documentMode;
					var isEdge = !isIE && !!window.StyleMedia;
					var isChrome = !!window.chrome && !!window.chrome.webstore;
					var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
					var isBlink = (isChrome || isOpera) && !!window.CSS;
					
					if(isFirefox || isIE || isChrome){
						if(isChrome){
							console.log('Manage Google Chrome download');
							var url = window.URL || window.webkitURL;
			                var fileURL = url.createObjectURL(scope.fileDownload);
							var downloadLink = angular.element('<a></a>');//create a new  <a> tag element
	                        downloadLink.attr('href',fileURL);
	                        downloadLink.attr('download',scope.fileName);
	                        downloadLink.attr('target','_self');
	                        downloadLink[0].click();//call click function
		                    url.revokeObjectURL(fileURL);//revoke the object from URL
						}
						if(isIE){
							console.log('Manage IE download>10');
					        window.navigator.msSaveOrOpenBlob(scope.fileDownload,scope.fileName); 
						}
						if(isFirefox){
							console.log('Manage Mozilla Firefox download');
							var url = window.URL || window.webkitURL;
			                var fileURL = url.createObjectURL(scope.fileDownload);
							var a=elem[0];//recover the <a> tag from directive
							a.href=fileURL;
							a.download=scope.fileName;
							a.target='_self';
							a.click();//we call click function
						}
						
						
					}else{
						alert('SORRY YOUR BROWSER IS NOT COMPATIBLE');
					}
				}
			});
			
		}
	}
})

IN CONTROLLER:

$scope.myBlobObject=undefined;
$scope.getFile=function(){
		console.log('download started, you can show a wating animation');
		serviceAsPromise.getStream({param1:'data1',param1:'data2', ...})
   		.then(function(data){//is important that the data was returned as Aray Buffer
   		        console.log('Stream download complete, stop animation!');
   				$scope.myBlobObject=new Blob([data],{ type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
   		},function(fail){
   				console.log('Download Error, stop animation and show error message');
   									$scope.myBlobObject=[];
   								});
   							}; 

IN SERVICE:

function getStream(params){
				 console.log("RUNNING");
				 var deferred = $q.defer();
				 
				 $http({
					 url:'../downloadURL/',
					 method:"PUT",//you can use also GET or POST
					 data:params,
					 headers:{'Content-type': 'application/json'},
				 	 responseType : 'arraybuffer',//THIS IS IMPORTANT
				 	})
	                .success(function (data) {
	                	console.debug("SUCCESS");
	                	deferred.resolve(data);
	                }).error(function (data) {
	                	 console.error("ERROR");
	                	 deferred.reject(data);
	                });
				 
				 return deferred.promise;
				};

BACKEND(on SPRING):

@RequestMapping(value = "/downloadURL/", method = RequestMethod.PUT)
public void downloadExcel(HttpServletResponse response,
		@RequestBody Map<String,String> spParams
		) throws IOException {
		OutputStream outStream=null;
outStream = response.getOutputStream();//is important manage the exceptions here
ObjectThatWritesOnOutputStream myWriter= new ObjectThatWritesOnOutputStream();// note that this object doesn exist on JAVA,
ObjectThatWritesOnOutputStream.write(outStream);//you can configure more things here
outStream.flush();
return;
}

Solution 4 - Angularjs

in template

<md-button class="md-fab md-mini md-warn md-ink-ripple" ng-click="export()" aria-label="Export">
<md-icon class="material-icons" alt="Export" title="Export" aria-label="Export">
	system_update_alt
</md-icon></md-button>

in controller

     $scope.export = function(){ $window.location.href = $scope.export; };

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionUpCatView Question on Stackoverflow
Solution 1 - AngularjsjessegavinView Answer on Stackoverflow
Solution 2 - AngularjsaixView Answer on Stackoverflow
Solution 3 - AngularjshavelinoView Answer on Stackoverflow
Solution 4 - Angularjsnat_jeaView Answer on Stackoverflow