Torsten Müller

Angular dependency injection: Use case for downloading assets

published Apr 10th, 2020

Angular is a great framework, but you sometimes just need to do things outside the framework. In this case, it was to download a file from the server to directly save it on the user’s hard drive. For this functionality to work, one needs to go outside the framework’s routing mechanism and make use of some underlying HTTP protocol features.

Describing “the problem”

In a recent application I was working on, we needed to provide download links for pdf documents, which were already created and sitting on the server to be downloaded. Usually, I solve this problem with an interplay of the server and the frontend:

The server provides a download URL that contains either the document ID or the name of the file to be downloaded. Any requests to that URL get fulfilled, but the response headers for the request will contain a Content-Disposition: attachment header. This header causes the browser to treat the returned information in different ways, with a value of attachment causing the browser to display a file download dialog in which the user can enter a name for the file on disk. This download also does not unload the currently active Angular application and thus is a great way for users to download files from a web application.

That Content-Disposition header may also include an additional piece of information, which specifies a default filename for the downloaded document. The header then might look like this:

Content-Disposition: attachment; filename="an-awesome-document.pdf"

To implement this functionality, we need to gain access to the window.location object in the browser. This is fairly trivial to accomplish by simply using window in the TypeScript code, but doing so while keeping the application testable is another thing altogether…

Enter: Angular InjectionTokens

Since we’re essentially in a browser context, it would be trivial to use the window object directly in the code whenever we need it. The thing we would lose doing it that way, though, would be to easily test whether an expected value would have been set for the URL.

This is where Angular injection tokens come in handy; They provide a means to create an injectable property and specify what should be provided, when a user requests this particular token. You can define an Injection Token for a window like this:

window-provider.token.ts
import { InjectionToken } from '@angular/core';

export const WindowToken = new InjectionToken('Window');
export function windowProvider() { return window }

In the previous code snippet, we defined an InjectionToken named WindowToken and provided a function that simply returns the global window object. With these two building blocks, we can define a provider, i.e. telling Angular how to use our implementation, in the application’s AppModule as follows:

app.module.ts
@NgModule({
  declarations: [ ... ],
  imports: [ ... ],
  exports: [ ... ],
  providers: [
    { provide: WindowToken, useFactory: windowProvider }
  ]
})
export class AppModule {}

So with this Module definition, we can make use of our new WindowToken in, for example, a service that needs to have access to the browser’s window object. In the following example, I define a DownloadService whose job it is to download a file from the server:

Version 1: Plain HTTP request from Browser

download.service.ts
@Injectable({providedIn: 'root'})
export class DownloadService {

  constructor(@Inject(WindowToken) private window: Window) {}

  public downloadFile(documentId: string) {
    const assetUrl = `${environment.downloadUrl}/${documentId}`;
    this.window.location.href = assetUrl;    
  }
}

The sole method in this service performs the download by being called with a documentId, the unique identifier for the document in the system. Based on this identifier, it composes the URL for the document and uses the injected version of window — notice this.window, i.e. the injected instance for the window object — to directly set the URL in the browser’s navigation bar.

Usually, this would unload the application and replace it with the downloaded asset, but that’s where the Content-Disposition header set by the server, described earlier, comes in: The server sets this header and the browser does not unload the application and instead offers the download dialog where the user can specify where to place the file and what to name it.

Version 2: XMLHttpRequest and extra window

Another option is to download the file’s content using standard Angular HttpService functionality and then manually open a new window and populate that window with the downloaded content. This would look as follows:

download.service.ts
@Injectable({providedIn: 'root'})
export class DownloadService {

  constructor(
      @Inject(WindowToken) private window: Window,
      public http: HttpClient) { }

  public downloadFile(documentId: string) {
    const assetUrl = `${environment.downloadUrl}/${documentId}`;
    this.http.get(assetUrl)
      .subscribe(
        rawData => this.displayDownloadedFile.bind(this)
    )
  }

  public displayDownloadedFile(rawData) {
    const fileContentBlob = new Blob([rawData], { type: 'application/pdf' });
    const dataUrl = this.window.URL.createObjectURL(fileContentBlob);
    this.window.open(dataUrl);
  }
}

In this case, too, the injected window token is used to display the file’s (pdf’s) content in a browser window. This approach uses the standard Angular mechanism for HTTP requests and then creates a Blob out of the downloaded content to display as a data URL.

Discussion

So why is this so complicated?

The Angular routing mechanism by default uses the JavaScript History Api which does not load new pages but simply simulates the change of URLs while at the same time changing the display on the screen. So this means that a simple change in the URL does not reach the server but is processed solely in the browser.

Thus, we need to load the content by one of the two mechanisms described here — and some others not mentioned. Both solutions I presented are using the concept of InjectionToken. The benefit is that in our tests, we can provide a dummy object and count calls to methods as well as analyse passed and set properties. The use of injection tokens thus supports testability of your Angular app which would be impossible, or at least much more complicated, using the stock browser window object.

The mechanisms discussed here for illustration purposes, the downloading of files while not unloading the Angular application, also shows two different paradigms. Which one you choose depends on your requirements, as each one does roughly the same thing, but under different conditions and with different limitations:

  1. The first version, using a simple window.location.href works fairly well, but will not work when loading the document requires the setting of custom request headers. An example would be an authorization mechanism based on the OAuth mechanism, which relies on a Bearer Token contained in the request headers to the server.

  2. When the authentication information for the loading of files and data is stored in secure, HttpOnly cookies, the browser automatically sends them along with the request and thus the first option setting the window.location should work fine.

  3. The second version, using the Blob, permits the user to immediately see the downloaded asset – a pdf, for example – in a separate browser window. This is in contrast to the first version, where the file immediately gets saved to disk.

  4. The second version also allows us to set any kind of request header we desire, for example a header with authentication information that is not managed in browser cookies. It has, however, the drawback that it

    • uses system memory to load and store all the data in the response, then
    • create a Blob out of it and finally
    • open a window showing that blob.

For larger files, memory issues within the browser might come into play, whereas the first version simply downloads a file to disk.

So the choice is up to you. Choose wisely!