¿Cómo inyectar ventana en un servicio?


110

Estoy escribiendo un servicio Angular 2 en TypeScript que utilizará localstorage. Quiero inyectar una referencia al navegador windowobjeto en mi servicio, ya que no quiero hacer referencia a las variables globales como 1.x angular $window.

¿Cómo puedo hacer eso?

Respuestas:


134

Esto me funciona actualmente (2018-03, angular 5.2 con AoT, probado en angular-cli y una compilación de paquete web personalizado):

Primero, cree un servicio inyectable que proporcione una referencia a la ventana:

import { Injectable } from '@angular/core';

// This interface is optional, showing how you can add strong typings for custom globals.
// Just use "Window" as the type if you don't have custom global stuff
export interface ICustomWindow extends Window {
    __custom_global_stuff: string;
}

function getWindow (): any {
    return window;
}

@Injectable()
export class WindowRefService {
    get nativeWindow (): ICustomWindow {
        return getWindow();
    }
}

Ahora, registre ese servicio con su AppModule raíz para que pueda inyectarse en todas partes:

import { WindowRefService } from './window-ref.service';

@NgModule({        
  providers: [
    WindowRefService 
  ],
  ...
})
export class AppModule {}

y luego donde necesitas inyectar window:

import { Component} from '@angular/core';
import { WindowRefService, ICustomWindow } from './window-ref.service';

@Component({ ... })
export default class MyCoolComponent {
    private _window: ICustomWindow;

    constructor (
        windowRef: WindowRefService
    ) {
        this._window = windowRef.nativeWindow;
    }

    public doThing (): void {
        let foo = this._window.XMLHttpRequest;
        let bar = this._window.__custom_global_stuff;
    }
...

También es posible que desee agregar nativeDocumenty otros globales a este servicio de una manera similar si los usa en su aplicación.


editar: actualizado con sugerencia de Truchainz. edit2: Actualizado para angular 2.1.2 edit3: Se agregaron notas de AoT edit4: Añadiendo el anytipo de nota de solución alternativa edit5: Solución actualizada para usar un WindowRefService que corrige un error que recibía al usar la solución anterior con una compilación diferente edit6: agregando un ejemplo de escritura de ventana personalizada


1
Tener el @Inject en los parámetros del constructor arrojó un montón de errores para mí, como ORIGINAL EXCEPTION: No provider for Window!. Sin embargo, quitarlo solucionó el problema. Usar solo las 2 primeras líneas globales fue suficiente para mí.
TrieuNomad

Interesante ^^ Tendré que probarlo en un par de proyectos de demostración más, sin @Injectque aparecieran No provider for Windowerrores. ¡Eso es bastante bueno no necesitar el manual @Inject!
elwyn

En 2.1.2 tuve que usar @Inject(Window)para que esto funcionara
James Kleeh


2
@Brian sí, todavía está accediendo window, pero con el servicio intermedio permite eliminar windowcosas nativas en pruebas unitarias, y como mencionas para SSR, se puede proporcionar un servicio alternativo que expone una ventana simulada / noop para el servidor. La razón por la que menciono AOT es que varias de las primeras soluciones para envolver la ventana se rompieron en AOT cuando se actualizó Angular.
elwyn

34

Con el lanzamiento de angular 2.0.0-rc.5 se introdujo NgModule. La solución anterior dejó de funcionar para mí. Esto es lo que hice para solucionarlo:

app.module.ts:

@NgModule({        
  providers: [
    { provide: 'Window',  useValue: window }
  ],
  declarations: [...],
  imports: [...]
})
export class AppModule {}

En algún componente:

import { Component, Inject } from '@angular/core';

@Component({...})
export class MyComponent {
    constructor (@Inject('Window') window: Window) {}
}

También puede usar un OpaqueToken en lugar de la cadena 'Ventana'

Editar:

El AppModule se usa para arrancar su aplicación en main.ts de esta manera:

import { platformBrowserDynamic  } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)

Para obtener más información sobre NgModule, lea la documentación de Angular 2: https://angular.io/docs/ts/latest/guide/ngmodule.html


19

Puede inyectarlo después de haber configurado el proveedor:

import {provide} from 'angular2/core';
bootstrap(..., [provide(Window, {useValue: window})]);

constructor(private window: Window) {
    // this.window
}

pero cuando cambio el window.varcontenido de la página no cambia
Ravinder Payal

6
Esto no funcionó en Safari ya que Windows no es inyectable. Tuve que crear mi propio tipo Injectable que contenía las propiedades de Window que necesitaba. Un mejor enfoque puede haber sido crear un servicio como se describe en las otras respuestas
daveb

Este enfoque no funciona, porque useValue en realidad crea una copia del valor que le proporcionas. Ver: github.com/angular/angular/issues/10788#issuecomment-300614425 . Este enfoque funcionaría si lo cambiara para usar useFactory y devolver el valor de una devolución de llamada.
Levi Lindsey

18

Puede obtener una ventana del documento inyectado.

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

export class MyClass {

  constructor(@Inject(DOCUMENT) private document: Document) {
     this.window = this.document.defaultView;
  }

  check() {
    console.log(this.document);
    console.log(this.window);
  }

}

15

Para que funcione en Angular 2.1.1 tuve que hacer una @Injectventana usando una cadena

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

y luego burlarte de esto

beforeEach(() => {
  let windowMock: Window = <any>{ };
  TestBed.configureTestingModule({
    providers: [
      ApiUriService,
      { provide: 'Window', useFactory: (() => { return windowMock; }) }
    ]
  });

y en lo ordinario @NgModulelo proporciono así

{ provide: 'Window', useValue: window }

10

En Angular RC4 funciona lo siguiente, que es una combinación de algunas de las respuestas anteriores, en su aplicación raíz, agregue los proveedores:

@Component({
    templateUrl: 'build/app.html',
    providers: [
        anotherProvider,
        { provide: Window, useValue: window }
    ]
})

Luego, en su servicio, etc.inyectelo en el constructor

constructor(
      @Inject(Window) private _window: Window,
)

10

Antes de la declaración @Component, también puede hacerlo,

declare var window: any;

El compilador en realidad le permitirá acceder a la variable de ventana global ahora, ya que la declara como una variable global asumida con el tipo any.

Sin embargo, no sugeriría acceder a la ventana en todas partes de su aplicación, debe crear servicios que accedan / modifiquen los atributos de ventana necesarios (e inyecte esos servicios en sus componentes) para determinar lo que puede hacer con la ventana sin permitirles modificar el todo el objeto de la ventana.


Si realiza una renderización del lado del servidor, su código se romperá, porque en el lado del servidor no tiene ningún objeto de ventana y necesita inyectar el suyo.
Alex Nikulin

9

Solía OpaqueToken para la cadena 'Ventana':

import {unimplemented} from '@angular/core/src/facade/exceptions';
import {OpaqueToken, Provider} from '@angular/core/index';

function _window(): any {
    return window;
}

export const WINDOW: OpaqueToken = new OpaqueToken('WindowToken');

export abstract class WindowRef {
    get nativeWindow(): any {
        return unimplemented();
    }
}

export class BrowserWindowRef extends WindowRef {
    constructor() {
        super();
    }
    get nativeWindow(): any {
        return _window();
    }
}


export const WINDOW_PROVIDERS = [
    new Provider(WindowRef, { useClass: BrowserWindowRef }),
    new Provider(WINDOW, { useFactory: _window, deps: [] }),
];

Y se usa solo para importar WINDOW_PROVIDERSen bootstrap en Angular 2.0.0-rc-4.

Pero con el lanzamiento de Angular 2.0.0-rc.5 necesito crear un módulo separado:

import { NgModule } from '@angular/core';
import { WINDOW_PROVIDERS } from './window';

@NgModule({
    providers: [WINDOW_PROVIDERS]
})
export class WindowModule { }

y acaba de definir en la propiedad de importaciones de mi principal app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { WindowModule } from './other/window.module';

import { AppComponent } from './app.component';

@NgModule({
    imports: [ BrowserModule, WindowModule ],
    declarations: [ ... ],
    providers: [ ... ],
    bootstrap: [ AppComponent ]
})
export class AppModule {}

6

A partir de hoy (abril de 2016), el código de la solución anterior no funciona, creo que es posible inyectar la ventana directamente en App.ts y luego recopilar los valores que necesita en un servicio para el acceso global en la aplicación, pero si prefiere crear e inyectar su propio servicio, una solución mucho más sencilla es esta.

https://gist.github.com/WilldelaVega777/9afcbd6cc661f4107c2b74dd6090cebf

//--------------------------------------------------------------------------------------------------
// Imports Section:
//--------------------------------------------------------------------------------------------------
import {Injectable} from 'angular2/core'
import {window} from 'angular2/src/facade/browser';

//--------------------------------------------------------------------------------------------------
// Service Class:
//--------------------------------------------------------------------------------------------------
@Injectable()
export class WindowService
{
    //----------------------------------------------------------------------------------------------
    // Constructor Method Section:
    //----------------------------------------------------------------------------------------------
    constructor(){}

    //----------------------------------------------------------------------------------------------
    // Public Properties Section:
    //----------------------------------------------------------------------------------------------
    get nativeWindow() : Window
    {
        return window;
    }
}

6

Angular 4 introduce InjectToken y también crean un token para el documento llamado DOCUMENTO . Creo que esta es la solución oficial y funciona en AoT.

Utilizo la misma lógica para crear una pequeña biblioteca llamada ngx-window-token para evitar hacer esto una y otra vez.

Lo he usado en otro proyecto y lo construí en AoT sin problemas.

Así es como lo usé en otro paquete

Aquí está el plunker

En tu módulo

imports: [ BrowserModule, WindowTokenModule ] En tu componente

constructor(@Inject(WINDOW) _window) { }


4

Es suficiente para hacer

export class AppWindow extends Window {} 

y hacer

{ provide: 'AppWindow', useValue: window } 

para hacer feliz a AOT


4

Existe la posibilidad de acceder directamente al objeto de la ventana a través del documento.

document.defaultView == window

4

Aquí hay otra solución que se me ocurrió poco después de que llegué cansado de recibir defaultViewde DOCUMENTuna función de token y comprueba si existe alguna nulo:

import {DOCUMENT} from '@angular/common';
import {inject, InjectionToken} from '@angular/core';

export const WINDOW = new InjectionToken<Window>(
    'An abstraction over global window object',
    {
        factory: () => {
            const {defaultView} = inject(DOCUMENT);

            if (!defaultView) {
                throw new Error('Window is not available');
            }

            return defaultView;
        }
    });

1
Entonces, pongo esto en mi carpeta de proveedores (por ejemplo) y luego uso en el constructor de mi componente este token de inyección. @Inject(WINDOW) private _window: anyy usarlo como el token de inyección de DOCUMENTO proporcionado por Angular?
Sparker73

Sí, eso es todo.
waterplea

Sip. Funciona perfectamente, tanques para esta sencilla solución.
Sparker73

3

Sé que la pregunta es cómo inyectar el objeto de la ventana en un componente, pero parece que estás haciendo esto solo para llegar a localStorage. Si realmente solo desea localStorage, ¿por qué no utilizar un servicio que exponga precisamente eso, como h5webstorage ? Luego, su componente describirá sus dependencias reales, lo que hace que su código sea más legible.


2
Si bien este enlace puede responder a la pregunta, es mejor incluir las partes esenciales de la respuesta aquí y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si cambia la página enlazada.
Todos los trabajadores son esenciales

3

Esta es la respuesta más corta / limpia que he encontrado trabajando con Angular 4 AOT

Fuente: https://github.com/angular/angular/issues/12631#issuecomment-274260009

@Injectable()
export class WindowWrapper extends Window {}

export function getWindow() { return window; }

@NgModule({
  ...
  providers: [
    {provide: WindowWrapper, useFactory: getWindow}
  ]
  ...
})
export class AppModule {
  constructor(w: WindowWrapper) {
    console.log(w);
  }
}

2

Puede usar NgZone en Angular 4:

import { NgZone } from '@angular/core';

constructor(private zone: NgZone) {}

print() {
    this.zone.runOutsideAngular(() => window.print());
}

2

También es una buena idea marcar DOCUMENTcomo opcional. Según los documentos de Angular:

Es posible que el documento no esté disponible en el contexto de la aplicación cuando los contextos de aplicación y de renderizado no son los mismos (por ejemplo, cuando se ejecuta la aplicación en un Web Worker).

A continuación, se muestra un ejemplo del uso de DOCUMENTpara ver si el navegador es compatible con SVG:

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

...

constructor(@Optional() @Inject(DOCUMENT) document: Document) {
   this.supportsSvg = !!(
   document &&
   document.createElementNS &&
   document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect
);

0

@maxisam gracias por ngx-window-token . Estaba haciendo algo similar, pero cambié al tuyo. Este es mi servicio para escuchar eventos de cambio de tamaño de ventana y notificar a los suscriptores.

import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import { WINDOW } from 'ngx-window-token';


export interface WindowSize {
    readonly width: number;
    readonly height: number;
}

@Injectable()
export class WindowSizeService {

    constructor( @Inject(WINDOW) private _window: any ) {
        Observable.fromEvent(_window, 'resize')
        .auditTime(100)
        .map(event => <WindowSize>{width: event['currentTarget'].innerWidth, height: event['currentTarget'].innerHeight})
        .subscribe((windowSize) => {
            this.windowSizeChanged$.next(windowSize);
        });
    }

    readonly windowSizeChanged$ = new BehaviorSubject<WindowSize>(<WindowSize>{width: this._window.innerWidth, height: this._window.innerHeight});
}

Corto y dulce y funciona a las mil maravillas.


0

Obtener un objeto de ventana a través de DI (inyección de dependencia) no es una buena idea cuando las variables globales son accesibles en toda la aplicación.

Pero si no desea usar el objeto de ventana, también puede usar una selfpalabra clave que también apunta al objeto de ventana.


3
Ese no es un buen consejo. Dependency Injection hace que las clases (componentes, directivas, servicios, tuberías, ...) sean más fáciles de probar (por ejemplo, incluso sin un navegador) y más fáciles de reutilizar en diferentes plataformas como la representación del lado del servidor o Web Workers. Podría funcionar para algunos y la simplicidad podría tener algún atractivo, pero desalentar el uso de DI es en mi humilde opinión una mala respuesta.
Günter Zöchbauer

Si realiza una renderización del lado del servidor, su código se romperá, porque en el lado del servidor no tiene ningún objeto de ventana y necesita inyectar el suyo.
Alex Nikulin

-1

¡Mantenlo simple, amigos!

export class HeroesComponent implements OnInit {
  heroes: Hero[];
  window = window;
}

<div>{{window.Object.entries({ foo: 1 }) | json}}</div>

Si realiza una renderización del lado del servidor, su código se romperá, porque en el lado del servidor no tiene ningún objeto de ventana y necesita inyectar el suyo.
Alex Nikulin

-2

En realidad, es muy simple acceder al objeto de la ventana. Aquí está mi componente básico y lo probé.

import { Component, OnInit,Inject } from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';

@Component({
  selector: 'app-verticalbanners',
  templateUrl: './verticalbanners.component.html',
  styleUrls: ['./verticalbanners.component.css']
})
export class VerticalbannersComponent implements OnInit {

  constructor(){ }

  ngOnInit() {
    console.log(window.innerHeight );
  }

}

Si realiza una renderización del lado del servidor, su código se romperá, porque en el lado del servidor no tiene ningún objeto de ventana y necesita inyectar el suyo.
Alex Nikulin
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.