92

I have an Angular 2 application. For mocking the Document object in tests, I'd like to inject it to the service like:

import { Document } from '??' 

@Injectable()
export class MyService {
  constructor(document: Document) {}
}

The Title service of Angular uses the internal getDOM() method.

Is there any simple way to inject the Document to the service? Also, how should I reference it in the providers array?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
RJo
  • 15,631
  • 5
  • 32
  • 63

4 Answers4

175

This has been supported by Angular for a while.

You can use the DOCUMENT constant provided by the @angular/common package.

Description of the DOCUMENT constant (taken from the API documentation):

A DI Token representing the main rendering context. In a browser, this is the DOM Document.

An example is as shown below:

my-service.service.ts:

import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable()
export class MyService {
  constructor(@Inject(DOCUMENT) private document: Document) {}
}

my-service.service.spec.ts

import { provide } from '@angular/core';
import { DOCUMENT } from '@angular/common';

import { MyService } from './my-service';

class MockDocument {}

describe('MyService', () => {
  beforeEachProviders(() => ([
    provide(DOCUMENT, { useClass: MockDocument }),
    MyService
  ]));

  ...
});
Edric
  • 24,639
  • 13
  • 81
  • 91
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 2
    And in the real implementation, we should provide the actual document instance, e.g., `declare const document: Document;` and then `bootstrap(provide(Document, { useValue: document }));` – RJo May 30 '16 at 09:37
  • Why would that be necessary? – Günter Zöchbauer May 30 '16 at 09:38
  • Since failing to do that gives me exception: `ORIGINAL EXCEPTION: No provider for Document!`. Unless you can give an example where this is not necessary? – RJo May 30 '16 at 10:10
  • I will accept the answer, if you can give a working example of both use cases: production and tests. – RJo May 30 '16 at 10:33
  • 3
    @GünterZöchbauer it looks like [DOCUMENT is deprecated](https://angular.io/api/platform-browser/DOCUMENT). Any idea how to do this once it's gone? For example, how would I set the favicon dynamically? – adamdport Jul 27 '17 at 17:00
  • You can alwasy use plain JS/TS `document.title = 'foo';` but I guess https://angular.io/api/core/Renderer2 is the replacement (not tried myself yet) – Günter Zöchbauer Jul 27 '17 at 17:05
  • 12
    @adamdport - import { DOCUMENT } from '@angular/common'; instead of '@angular/platform-browser' – Tom Mettam Sep 21 '17 at 12:53
  • 1
    This answer is confusing (it seems all over the place - where is bootstrap(provide.. supposed to be called?). Is it out of date? Is there an equivalent answer that actually shows what is supposed to go where? Note that I don't need to mock anything: I'm attempting to inject DOCUMENT for an app for production. See also: https://stackoverflow.com/questions/50920734/cant-resolve-all-parameters-for-when-consuming-injectable-from-a-package – Dave Nottage Jun 26 '18 at 06:29
  • I updated my answer (not sure about the test part, haven't used Angular since quite some time) – Günter Zöchbauer Jun 26 '18 at 07:10
  • @GünterZöchbauer Thanks, however where does myDocumentImpl come from? – Dave Nottage Jun 26 '18 at 23:41
  • PS: I'm not sure this method is even going to resolve my issue - it might be a bug? – Dave Nottage Jun 26 '18 at 23:59
  • 1
    @DaveNottage I updated again. I'm somewhat out of practice in Angular. – Günter Zöchbauer Jun 27 '18 at 06:52
  • @GünterZöchbauer Still doesn't help in my scenario – Dave Nottage Jun 27 '18 at 06:58
  • 4
    As type, you can use `HTMLDocument` instead of `any`: `@Inject(DOCUMENT) private document: HTMLDocument` – edwin Jul 25 '18 at 13:17
  • Not sure if this works for server-side rendering. The injected document might be of a different type. Besides that, thanks for the update. – Günter Zöchbauer Jul 25 '18 at 13:25
  • Does anyone know what this is on the server? Can I use DOCUMENT.getElementById for instance? – Crhistian Ramirez May 15 '20 at 00:20
  • I'm pretty sure you can't (never tried SSR). There is no document on the server. If you inject it you get an empty facade. – Günter Zöchbauer May 15 '20 at 03:46
  • This approach is the same way official material components access global document object as well: https://github.com/angular/components/blob/2b1d84e2bc1d7295e53a753211e99a0e73110b45/src/cdk/drag-drop/drag-drop-registry.ts#L64 – 千木郷 Oct 19 '20 at 14:35
36

I'm unable to comment directly on adamdport's question (not yet 50 rep points), but here it is as stated in the angular docs.

Blockquote @GünterZöchbauer it looks like DOCUMENT is deprecated. Any idea how to do this once it's gone? For example, how would I set the favicon dynamically?

Instead of importing from platform browser like so:

import { DOCUMENT } from '@angular/platform-browser';

Import it from angular common:

import {DOCUMENT} from '@angular/common';
Ruud Voost
  • 461
  • 4
  • 5
11

in addition to @Günter Zöchbauer's answer.

Angular define DOCUMENT as an InjectionToken

export const DOCUMENT = new InjectionToken<Document>('DocumentToken');

dom_tokens.ts

And inject it with document in browser.ts

{provide: DOCUMENT, useFactory: _document, deps: []}


export function _document(): any {
  return document;
}

Therefore, when we use it, we just need to inject @Inject(DOCUMENT)

or use the token directly in deps:[DOCUMENT]

maxisam
  • 21,975
  • 9
  • 75
  • 84
  • This works in AOT (production) when you need the actual document. It prevents "ERROR in Error encountered resolving symbol values statically" – Marcel van der Drift Jun 30 '17 at 18:50
  • Doesn't work for me in AOT, at least for Angular 6. I'm receiving the error "Can't resolve all parameters for..". See also: https://stackoverflow.com/questions/50920734/cant-resolve-all-parameters-for-when-consuming-injectable-from-a-package – Dave Nottage Jun 27 '18 at 00:13
  • does this resolve correctly if the angular app is rendered in a non-browser? – The.Wolfgang.Grimmer Nov 18 '20 at 05:33
0
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable()
export class MyService {
  constructor(@Inject(DOCUMENT) private document) {}
}

It's the ": Document" that's causing the problem.