Implementing file save functionality with Angular 4


Today, I faced a requirement where I need to implement file save functionality in an Angular 4 application. In this quick post, I will show you how to implement this functionality in Angular 4 using FileSaver.js module.

Step 1: Create an Angular 4 project

I use Angular CLI to generate Angular 4 applications. Navigate to a convenient location on your file system and run the following command.

$ ng new file-save-ng4-example
$ cd file-save-ng4-example
$ npm install # yarn install

$ is used to denote bash prompt. You don’t have to type it on your command-line terminal.

Step 2: Install FileSaver.js dependency

FileSaver.js is an HTML 5 FileSaver functionality. It makes it dead simple to implement file save functionality. FileSaver.js is available as a npm package so you can install it as shown below.

$ npm install --save file-saver

Step 3: Implement file save functionality in frontend

To showcase how to implement file save functionality, we will add a button to AppComponent. When we click on the button then we will make a REST API call to fetch the content.

Let’s start by updating app.component.html to as shown below.

<div style="text-align:center">
<h1>
Export by clicking button below</h1>
<button (click)="saveFile()">Export</button></div>

In the code snippet above, we added a button with click event handled. The saveFile function will exist inside the app.component.ts as shown below.

import { Component } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { saveAs } from 'file-saver/FileSaver';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(private http: Http) { }

  saveFile() {
    const headers = new Headers();
    headers.append('Accept', 'text/plain');
    this.http.get('/api/files', { headers: headers })
      .toPromise()
      .then(response => this.saveToFileSystem(response));
  }

  private saveToFileSystem(response) {
    const contentDispositionHeader: string = response.headers.get('Content-Disposition');
    const parts: string[] = contentDispositionHeader.split(';');
    const filename = parts[1].split('=')[1];
    const blob = new Blob([response._body], { type: 'text/plain' });
    saveAs(blob, filename);
  }
}

In the code shown above, we did following:

  1. We implemented saveFile function in AppComponent. It makes HTTP get request to our service.
  2. Next, we converted Observable to Promise using rxjs toPromise operator.
  3. Next, we read Content-Disposition response header to find the filename.
  4. Next, we constructed Blob object using the response body.
  5. Finally, we passed blob to FileSaver.js saveAs functionality. It take care of the rest of functionality.

It might happen that response.headers.get('Content-Disposition') return value as null even though you are setting it in the response header on server side. It wasted a lot of time to figure out that Angular 4 does not set headers until you set Access-Control-Expose-Headers header in the server side code. Refer to step 4 where I show a sample Java Spring Boot REST end point.

If you run the code now, you will get 404 as service does not exist. To show a full working example, next step covers shows a Spring Boot application that implements the API.

Step 5: Implement REST API

I use Spring Initializer project to scaffold a Spring Boot project. Once you have created that, just replace your application class with the one shown below.

package com.example.filesavebackend;

import java.util.Collections;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry
                        .addMapping("/api/**")
                        .allowedOrigins("http://localhost:4200")
                        .allowedMethods("*");
            }
        };
    }
}

@RestController
class FileResource {

    @GetMapping(path = "/api/files", produces = MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity getFile() {
        String exportedContent = "Hello, World!";
        String filename = "my-file.txt";
        HttpHeaders headers = new HttpHeaders();
        headers.setAccessControlExposeHeaders(Collections.singletonList("Content-Disposition"));
        headers.set("Content-Disposition", "attachment; filename=" + filename);
        return new ResponseEntity<>(exportedContent, headers, HttpStatus.OK);
    }

}

The important thing to note is FileResource. The getFile method implements our REST API. It sets content and required headers. Please note that we are explicitly setting Access-Control-Expose-Headers header. If you don’t set it, then you will not be able to access Content-Disposition header in Angular.

Source code of the full application

You can get the source code of the full application from Github: shekhargulati/filesave-angular4-example

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s