fix(ssr): fix angular global window and document references

This commit is contained in:
Adam Bradley
2019-02-25 16:43:41 -06:00
committed by GitHub
parent ceaef7eed2
commit f44c17e03b
19 changed files with 200 additions and 214 deletions

View File

@ -3,10 +3,10 @@ import { defineCustomElements } from '@ionic/core/loader';
import { Config } from './providers/config';
import { IonicWindow } from './types/interfaces';
export function appInitialize(config: Config) {
export function appInitialize(config: Config, doc: Document) {
return (): any => {
const win: IonicWindow | undefined = window as any;
if (typeof win !== 'undefined') {
const win: IonicWindow | undefined = doc.defaultView as any;
if (win) {
const Ionic = win.Ionic = win.Ionic || {};
Ionic.config = config;

View File

@ -82,11 +82,13 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
this.activateWith(context.route, context.resolver || null);
}
}
this.nativeEl.componentOnReady().then(() => {
if (this._swipeGesture === undefined) {
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', this.nativeEl.mode === 'ios');
}
});
if ((this.nativeEl as any).componentOnReady) {
this.nativeEl.componentOnReady().then(() => {
if (this._swipeGesture === undefined) {
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', this.nativeEl.mode === 'ios');
}
});
}
}
get isActivated(): boolean {

View File

@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { CommonModule, DOCUMENT } from '@angular/common';
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { IonicConfig } from '@ionic/core';
@ -141,7 +141,8 @@ export class IonicModule {
useFactory: appInitialize,
multi: true,
deps: [
ConfigToken
ConfigToken,
DOCUMENT
]
}
]

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { ActionSheetOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -7,7 +8,7 @@ import { OverlayBaseController } from '../util/overlay';
providedIn: 'root',
})
export class ActionSheetController extends OverlayBaseController<ActionSheetOptions, HTMLIonActionSheetElement> {
constructor() {
super('ion-action-sheet-controller');
constructor(@Inject(DOCUMENT) doc: any) {
super('ion-action-sheet-controller', doc);
}
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { AlertOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -7,7 +8,7 @@ import { OverlayBaseController } from '../util/overlay';
providedIn: 'root',
})
export class AlertController extends OverlayBaseController<AlertOptions, HTMLIonAlertElement> {
constructor() {
super('ion-alert-controller');
constructor(@Inject(DOCUMENT) doc: any) {
super('ion-alert-controller', doc);
}
}

View File

@ -43,9 +43,8 @@ export class Config {
export const ConfigToken = new InjectionToken<any>('USERCONFIG');
function getConfig(): CoreConfig | null {
const win: IonicWindow | undefined = window as any;
if (typeof win !== 'undefined') {
const Ionic = win.Ionic;
if (typeof (window as any) !== 'undefined') {
const Ionic = (window as IonicWindow).Ionic;
if (Ionic && Ionic.config) {
return Ionic.config;
}

View File

@ -23,14 +23,23 @@ export class DomController {
}
function getQueue() {
const Ionic = (window as any).Ionic;
if (Ionic && Ionic.queue) {
return Ionic.queue;
const win = typeof (window as any) !== 'undefined' ? window : null as any;
if (win != null) {
const Ionic = win.Ionic;
if (Ionic && Ionic.queue) {
return Ionic.queue;
}
return {
read: (cb: any) => win.requestAnimationFrame(cb),
write: (cb: any) => win.requestAnimationFrame(cb)
};
}
return {
read: (cb: any) => window.requestAnimationFrame(cb),
write: (cb: any) => window.requestAnimationFrame(cb)
read: (cb: any) => cb(),
write: (cb: any) => cb()
};
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { LoadingOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -7,7 +8,7 @@ import { OverlayBaseController } from '../util/overlay';
providedIn: 'root',
})
export class LoadingController extends OverlayBaseController<LoadingOptions, HTMLIonLoadingElement> {
constructor() {
super('ion-loading-controller');
constructor(@Inject(DOCUMENT) doc: any) {
super('ion-loading-controller', doc);
}
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { proxyMethod } from '../util/util';
@ -8,13 +9,16 @@ const CTRL = 'ion-menu-controller';
})
export class MenuController {
constructor(@Inject(DOCUMENT) private doc: any) {
}
/**
* Programmatically open the Menu.
* @param [menuId] Optionally get the menu by its id, or side.
* @return returns a promise when the menu is fully opened
*/
open(menuId?: string): Promise<boolean> {
return proxyMethod(CTRL, 'open', menuId);
return proxyMethod(CTRL, this.doc, 'open', menuId);
}
/**
@ -25,7 +29,7 @@ export class MenuController {
* @return returns a promise when the menu is fully closed
*/
close(menuId?: string): Promise<boolean> {
return proxyMethod(CTRL, 'close', menuId);
return proxyMethod(CTRL, this.doc, 'close', menuId);
}
/**
@ -35,7 +39,7 @@ export class MenuController {
* @return returns a promise when the menu has been toggled
*/
toggle(menuId?: string): Promise<boolean> {
return proxyMethod(CTRL, 'toggle', menuId);
return proxyMethod(CTRL, this.doc, 'toggle', menuId);
}
/**
@ -47,7 +51,7 @@ export class MenuController {
* @return Returns the instance of the menu, which is useful for chaining.
*/
enable(shouldEnable: boolean, menuId?: string): Promise<HTMLIonMenuElement> {
return proxyMethod(CTRL, 'enable', shouldEnable, menuId);
return proxyMethod(CTRL, this.doc, 'enable', shouldEnable, menuId);
}
/**
@ -57,7 +61,7 @@ export class MenuController {
* @return Returns the instance of the menu, which is useful for chaining.
*/
swipeEnable(shouldEnable: boolean, menuId?: string): Promise<HTMLIonMenuElement> {
return proxyMethod(CTRL, 'swipeEnable', shouldEnable, menuId);
return proxyMethod(CTRL, this.doc, 'swipeEnable', shouldEnable, menuId);
}
/**
@ -66,7 +70,7 @@ export class MenuController {
* If the menuId is not specified, it returns true if ANY menu is currenly open.
*/
isOpen(menuId?: string): Promise<boolean> {
return proxyMethod(CTRL, 'isOpen', menuId);
return proxyMethod(CTRL, this.doc, 'isOpen', menuId);
}
/**
@ -74,7 +78,7 @@ export class MenuController {
* @return Returns true if the menu is currently enabled, otherwise false.
*/
isEnabled(menuId?: string): Promise<boolean> {
return proxyMethod(CTRL, 'isEnabled', menuId);
return proxyMethod(CTRL, this.doc, 'isEnabled', menuId);
}
/**
@ -87,20 +91,20 @@ export class MenuController {
* @return Returns the instance of the menu if found, otherwise `null`.
*/
get(menuId?: string): Promise<HTMLIonMenuElement> {
return proxyMethod(CTRL, 'get', menuId);
return proxyMethod(CTRL, this.doc, 'get', menuId);
}
/**
* @return Returns the instance of the menu already opened, otherwise `null`.
*/
getOpen(): Promise<HTMLIonMenuElement> {
return proxyMethod(CTRL, 'getOpen');
return proxyMethod(CTRL, this.doc, 'getOpen');
}
/**
* @return Returns an array of all menu instances.
*/
getMenus(): Promise<HTMLIonMenuElement[]> {
return proxyMethod(CTRL, 'getMenus');
return proxyMethod(CTRL, this.doc, 'getMenus');
}
}

View File

@ -1,4 +1,5 @@
import { ComponentFactoryResolver, Injectable, Injector } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ComponentFactoryResolver, Inject, Injectable, Injector } from '@angular/core';
import { ModalOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -12,8 +13,9 @@ export class ModalController extends OverlayBaseController<ModalOptions, HTMLIon
private angularDelegate: AngularDelegate,
private resolver: ComponentFactoryResolver,
private injector: Injector,
@Inject(DOCUMENT) doc: any
) {
super('ion-modal-controller');
super('ion-modal-controller', doc);
}
create(opts: ModalOptions): Promise<HTMLIonModalElement> {

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { PickerOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -7,7 +8,7 @@ import { OverlayBaseController } from '../util/overlay';
providedIn: 'root',
})
export class PickerController extends OverlayBaseController<PickerOptions, HTMLIonPickerElement> {
constructor() {
super('ion-picker-controller');
constructor(@Inject(DOCUMENT) doc: any) {
super('ion-picker-controller', doc);
}
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { BackButtonEventDetail, Platforms, getPlatforms, isPlatform } from '@ionic/core';
import { Subject, Subscription } from 'rxjs';
@ -12,6 +13,7 @@ export interface BackButtonEmitter extends Subject<BackButtonEventDetail> {
export class Platform {
private _readyPromise: Promise<string>;
private win: any;
/**
* @hidden
@ -40,22 +42,24 @@ export class Platform {
*/
resize = new Subject<void>();
constructor() {
constructor(@Inject(DOCUMENT) private doc: any) {
this.win = doc.defaultView;
this.backButton.subscribeWithPriority = function(priority, callback) {
return this.subscribe(ev => {
ev.register(priority, callback);
});
};
proxyEvent(this.pause, document, 'pause');
proxyEvent(this.resume, document, 'resume');
proxyEvent(this.backButton, document, 'ionBackButton');
proxyEvent(this.resize, window, 'resize');
proxyEvent(this.pause, doc, 'pause');
proxyEvent(this.resume, doc, 'resume');
proxyEvent(this.backButton, doc, 'ionBackButton');
proxyEvent(this.resize, this.win, 'resize');
let readyResolve: (value: string) => void;
this._readyPromise = new Promise(res => { readyResolve = res; });
if ((window as any)['cordova']) {
document.addEventListener('deviceready', () => {
if (this.win && this.win['cordova']) {
doc.addEventListener('deviceready', () => {
readyResolve('cordova');
}, { once: true });
} else {
@ -106,7 +110,7 @@ export class Platform {
*
*/
is(platformName: Platforms): boolean {
return isPlatform(window, platformName);
return isPlatform(this.win, platformName);
}
/**
@ -129,7 +133,7 @@ export class Platform {
* ```
*/
platforms(): string[] {
return getPlatforms(window);
return getPlatforms(this.win);
}
/**
@ -172,14 +176,14 @@ export class Platform {
* [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir)
*/
get isRTL(): boolean {
return document.dir === 'rtl';
return this.doc.dir === 'rtl';
}
/**
* Get the query string parameter
*/
getQueryParam(key: string): string | null {
return readQueryParam(window.location.href, key);
return readQueryParam(this.win.location.href, key);
}
/**
@ -193,45 +197,48 @@ export class Platform {
* Returns `true` if the app is in portait mode.
*/
isPortrait(): boolean {
return window.matchMedia('(orientation: portrait)').matches;
return this.win.matchMedia && this.win.matchMedia('(orientation: portrait)').matches;
}
testUserAgent(expression: string): boolean {
return navigator.userAgent.indexOf(expression) >= 0;
const nav = this.win.navigator;
return !!(nav && nav.userAgent && nav.userAgent.indexOf(expression) >= 0);
}
/**
* Get the current url.
*/
url() {
return window.location.href;
return this.win.location.href;
}
/**
* Gets the width of the platform's viewport using `window.innerWidth`.
*/
width() {
return window.innerWidth;
return this.win.innerWidth;
}
/**
* Gets the height of the platform's viewport using `window.innerHeight`.
*/
height(): number {
return window.innerHeight;
return this.win.innerHeight;
}
}
function readQueryParam(url: string, key: string) {
const readQueryParam = (url: string, key: string) => {
key = key.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + key + '=([^&#]*)');
const results = regex.exec(url);
return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null;
}
};
function proxyEvent<T>(emitter: Subject<T>, el: EventTarget, eventName: string) {
el.addEventListener(eventName, (ev: Event | undefined | null) => {
// ?? cordova might emit "null" events
emitter.next(ev != null ? (ev as any).detail as T : undefined);
});
}
const proxyEvent = <T>(emitter: Subject<T>, el: EventTarget, eventName: string) => {
if ((el as any)) {
el.addEventListener(eventName, (ev: Event | undefined | null) => {
// ?? cordova might emit "null" events
emitter.next(ev != null ? (ev as any).detail as T : undefined);
});
}
};

View File

@ -1,4 +1,5 @@
import { ComponentFactoryResolver, Injectable, Injector } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ComponentFactoryResolver, Inject, Injectable, Injector } from '@angular/core';
import { PopoverOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -12,8 +13,9 @@ export class PopoverController extends OverlayBaseController<PopoverOptions, HTM
private angularDelegate: AngularDelegate,
private resolver: ComponentFactoryResolver,
private injector: Injector,
@Inject(DOCUMENT) doc: any
) {
super('ion-popover-controller');
super('ion-popover-controller', doc);
}
create(opts: PopoverOptions): Promise<HTMLIonPopoverElement> {

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { ToastOptions } from '@ionic/core';
import { OverlayBaseController } from '../util/overlay';
@ -7,7 +8,7 @@ import { OverlayBaseController } from '../util/overlay';
providedIn: 'root',
})
export class ToastController extends OverlayBaseController<ToastOptions, HTMLIonToastElement> {
constructor() {
super('ion-toast-controller');
constructor(@Inject(DOCUMENT) doc: any) {
super('ion-toast-controller', doc);
}
}

View File

@ -1,26 +1,26 @@
import { proxyMethod } from './util';
export class OverlayBaseController<Opts, Overlay> {
constructor(private ctrl: string) {}
constructor(private ctrl: string, private doc: Document) {}
/**
* Creates a new overlay
*/
create(opts?: Opts): Promise<Overlay> {
return proxyMethod(this.ctrl, 'create', opts);
return proxyMethod(this.ctrl, this.doc, 'create', opts);
}
/**
* When `id` is not provided, it dismisses the top overlay.
*/
dismiss(data?: any, role?: string, id?: string): Promise<void> {
return proxyMethod(this.ctrl, 'dismiss', data, role, id);
return proxyMethod(this.ctrl, this.doc, 'dismiss', data, role, id);
}
/**
* Returns the top overlay.
*/
getTop(): Promise<Overlay> {
return proxyMethod(this.ctrl, 'getTop');
return proxyMethod(this.ctrl, this.doc, 'getTop');
}
}

View File

@ -1,15 +1,15 @@
export function proxyMethod(ctrlName: string, methodName: string, ...args: any[]) {
const controller = ensureElementInBody(ctrlName);
export function proxyMethod(ctrlName: string, doc: Document, methodName: string, ...args: any[]) {
const controller = ensureElementInBody(ctrlName, doc);
return controller.componentOnReady()
.then(() => (controller as any)[methodName].apply(controller, args));
}
export function ensureElementInBody(elementName: string) {
let element = document.querySelector(elementName);
export function ensureElementInBody(elementName: string, doc: Document) {
let element = doc.querySelector(elementName);
if (!element) {
element = document.createElement(elementName);
document.body.appendChild(element);
element = doc.createElement(elementName);
doc.body.appendChild(element);
}
return element as HTMLStencilElement;
}