diff --git a/README.md b/README.md index 40fdb06..5747cbe 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,9 @@ export class HomeComponent { In situations where the Pixel ID needs to be changed dynamically, this can be done using `initialize()` with the new Pixel ID as an optional argument. **Notes:** - A Pixel ID still needs to be provided when importing ***ngx-pixel*** in the module. -- The previous instance should be removed with `remove()` before initializing a new Pixel ID. +- The previous instance should be removed with `remove()` before initializing a new Pixel ID. +- This approach should **not** be used in combination with serverside rendering (SSR). As the module is initialized on each request, the Pixel script will default to the ID provided in the module. + ## Disabling ***ngx-pixel*** Disabling works very similar to *enabling* from within a component and looks like this: @@ -147,7 +149,7 @@ export class HomeComponent { --- # What's next? -- [ ] Adding Angular Universal SSR support. +- [X] Adding Angular Universal SSR support. - [ ] Adding tests. - [ ] Removing all Facebook scripts on removal, not just the initial script. diff --git a/projects/pixel/README.md b/projects/pixel/README.md index 40fdb06..1cc89b9 100644 --- a/projects/pixel/README.md +++ b/projects/pixel/README.md @@ -121,7 +121,8 @@ export class HomeComponent { In situations where the Pixel ID needs to be changed dynamically, this can be done using `initialize()` with the new Pixel ID as an optional argument. **Notes:** - A Pixel ID still needs to be provided when importing ***ngx-pixel*** in the module. -- The previous instance should be removed with `remove()` before initializing a new Pixel ID. +- The previous instance should be removed with `remove()` before initializing a new Pixel ID. +- This approach should **not** be used in combination with serverside rendering (SSR). As the module is initialized on each request, the Pixel script will default to the ID provided in the module. ## Disabling ***ngx-pixel*** Disabling works very similar to *enabling* from within a component and looks like this: @@ -147,7 +148,7 @@ export class HomeComponent { --- # What's next? -- [ ] Adding Angular Universal SSR support. +- [X] Adding Angular Universal SSR support. - [ ] Adding tests. - [ ] Removing all Facebook scripts on removal, not just the initial script. diff --git a/projects/pixel/package.json b/projects/pixel/package.json index e1de42c..357b428 100644 --- a/projects/pixel/package.json +++ b/projects/pixel/package.json @@ -7,7 +7,7 @@ "url": "https://niels.codes" }, "license": "MIT", - "version": "1.0.2", + "version": "1.1.0", "repository": { "type": "git", "url": "https://github.com/NielsKersic/ngx-pixel" diff --git a/projects/pixel/src/lib/pixel.module.ts b/projects/pixel/src/lib/pixel.module.ts index f07f35c..7334b99 100644 --- a/projects/pixel/src/lib/pixel.module.ts +++ b/projects/pixel/src/lib/pixel.module.ts @@ -1,5 +1,6 @@ import { PixelConfiguration } from './pixel.models'; -import { ModuleWithProviders, NgModule } from '@angular/core'; +import { Inject, ModuleWithProviders, NgModule, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; import { PixelService } from './pixel.service'; @NgModule({ @@ -9,11 +10,14 @@ export class PixelModule { private static config: PixelConfiguration | null = null; - constructor( private pixel: PixelService ) { + constructor( + private pixel: PixelService, + @Inject(PLATFORM_ID) platformId: Object + ) { if (!PixelModule.config) { throw Error('ngx-pixel not configured correctly. Pass the `pixelId` property to the `forRoot()` function'); } - if (PixelModule.config.enabled) { + if (PixelModule.config.enabled && isPlatformBrowser(platformId)) { this.pixel.initialize(); } } diff --git a/projects/pixel/src/lib/pixel.service.ts b/projects/pixel/src/lib/pixel.service.ts index 08218ee..a85b4d5 100644 --- a/projects/pixel/src/lib/pixel.service.ts +++ b/projects/pixel/src/lib/pixel.service.ts @@ -1,20 +1,32 @@ import { PixelEventName, PixelConfiguration, PixelEventProperties } from './pixel.models'; -import { Inject, Injectable, Optional } from '@angular/core'; +import { Inject, Injectable, Optional, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { filter } from 'rxjs/operators'; -declare var fbq: any; +declare const fbq: any; @Injectable({ providedIn: 'root' }) export class PixelService { + private doc: Document; + private renderer: Renderer2 + constructor( @Inject('config') private config: PixelConfiguration, - @Optional() private router: Router + @Inject(DOCUMENT) private injectedDocument: any, + @Inject(PLATFORM_ID) private platformId: Object, + @Optional() private router: Router, + private rendererFactory: RendererFactory2 ) { + // DOCUMENT cannot be injected directly as Document type, see https://github.com/angular/angular/issues/20351 + // It is therefore injected as any and then cast to Document + this.doc = injectedDocument as Document; + this.renderer = rendererFactory.createRenderer(null, null); + if (router) { // Log page views after router navigation ends router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(event => { @@ -58,8 +70,12 @@ export class PixelService { track( eventName: PixelEventName, properties?: PixelEventProperties - ): void { - if(!this.isLoaded()) { + ): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + if (!this.isLoaded()) { console.warn('Tried to track an event without initializing a Pixel instance. Call `initialize()` first.'); return; } @@ -80,7 +96,11 @@ export class PixelService { * @param properties Optional properties of the event */ trackCustom(eventName: string, properties?: object): void { - if(!this.isLoaded()) { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + if (!this.isLoaded()) { console.warn('Tried to track an event without initializing a Pixel instance. Call `initialize()` first.'); return; } @@ -97,6 +117,9 @@ export class PixelService { * @param pixelId The Facebook Pixel ID to use */ private addPixelScript(pixelId: string): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } const pixelCode = ` var pixelCode = function(f,b,e,v,n,t,s) @@ -110,16 +133,20 @@ export class PixelService { fbq('init', '${pixelId}'); fbq('track', 'PageView');`; - const scriptElement = document.createElement('script'); - scriptElement.setAttribute('id', 'pixel-script'); - scriptElement.type = 'text/javascript'; - scriptElement.innerHTML = pixelCode; - document.getElementsByTagName('head')[0].appendChild(scriptElement); + + const scriptElement = this.renderer.createElement('script'); + this.renderer.setAttribute(scriptElement, 'id', 'pixel-script'); + this.renderer.setAttribute(scriptElement, 'type', 'text/javascript'); + this.renderer.setProperty(scriptElement, 'innerHTML', pixelCode); + this.renderer.appendChild(this.doc.head, scriptElement); } /** Remove Facebook Pixel tracking script from the application */ private removePixelScript(): void { - const pixelElement = document.getElementById('pixel-script'); + if (!isPlatformBrowser(this.platformId)) { + return; + } + const pixelElement = this.doc.getElementById('pixel-script'); if (pixelElement) { pixelElement.remove(); } @@ -127,8 +154,11 @@ export class PixelService { /** Checks if the script element is present */ private isLoaded(): boolean { - const pixelElement = document.getElementById('pixel-script'); - return !!pixelElement; + if (isPlatformBrowser(this.platformId)) { + const pixelElement = this.doc.getElementById('pixel-script'); + return !!pixelElement; + } + return false; } }