Skip to content

Commit 986a610

Browse files
committed
Add delayed analytics script loading in Angular 7.
1 parent 7c13869 commit 986a610

26 files changed

Lines changed: 7731 additions & 0 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ with.
1010

1111
## My JavaScript Demos - I Love JavaScript!
1212

13+
* [Creating A Proxy For Analytics Libraries In Order To Defer Loading And Parsing Overhead In Angular 7.2.13](https://bennadel.github.io/JavaScript-Demos/demos/delayed-script-load-proxy-service-angular7/)
1314
* [Thought Experiment: Partially-Applying Ng-Template References In Angular 7.2.13](https://bennadel.github.io/JavaScript-Demos/demos/partially-applied-templates-angular7/)
1415
* [Sanity Check: Nested Templates Maintain Lexical Binding In Angular 7.2.13](https://bennadel.github.io/JavaScript-Demos/demos/nested-template-variables-angular7/)
1516
* [Sub-Classing NgForOf In Order To Make It A "Pure" Directive In Angular 7.2.13](https://bennadel.github.io/JavaScript-Demos/demos/ng-pure-for-angular7/)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
# Now that we're using Webpack, we can install modules locally and just ignore
3+
# them since the assets are baked into the compiled modules.
4+
node_modules/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
(function() { "use strict";
2+
3+
window.analytics = {
4+
identify: identify,
5+
track: track
6+
};
7+
8+
function identify( userID, traits ) {
9+
10+
console.group( "analytics-service.js (remote script)" );
11+
console.log( ".identify()" );
12+
console.log( "userID:", userID );
13+
console.log( "traits:", traits );
14+
console.groupEnd();
15+
16+
}
17+
18+
function track( eventID, eventProperties ) {
19+
20+
console.group( "analytics-service.js (remote script)" );
21+
console.log( ".track()" );
22+
console.log( "eventID:", eventID );
23+
console.log( "eventProperties:", eventProperties );
24+
console.groupEnd();
25+
26+
}
27+
28+
})();
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
2+
// Import the core angular services.
3+
import { Injectable } from "@angular/core";
4+
5+
// Import the application components and services.
6+
import { DelayedScriptLoader } from "./delayed-script-loader";
7+
8+
// ----------------------------------------------------------------------------------- //
9+
// ----------------------------------------------------------------------------------- //
10+
11+
// Since I don't have a Type Definition for this demo library, I'm just going to declare
12+
// the interface here and then explicitly cast the global value when I reference it.
13+
interface AnalyticsScript {
14+
identify( userID: UserIdentifier, traits: UserTraits ) : void;
15+
track( eventID: EventIdentifier, eventProperties: EventProperties ) : void;
16+
}
17+
18+
export type UserIdentifier = string | number;
19+
20+
export interface UserTraits {
21+
[ key: string ]: any;
22+
}
23+
24+
export type EventIdentifier = string;
25+
26+
export interface EventProperties {
27+
[ key: string ]: any;
28+
}
29+
30+
// ----------------------------------------------------------------------------------- //
31+
// ----------------------------------------------------------------------------------- //
32+
33+
@Injectable({
34+
providedIn: "root"
35+
})
36+
export class AnalyticsService {
37+
38+
private scriptLoader: DelayedScriptLoader;
39+
40+
// I initialize the analytics service.
41+
constructor() {
42+
43+
this.scriptLoader = new DelayedScriptLoader( "./analytics-service.js", ( 10 * 1000 ) );
44+
45+
}
46+
47+
// ---
48+
// PUBLIC METHODS.
49+
// ---
50+
51+
// I identify the user to be associated with subsequent tracking events.
52+
public identify( userID: UserIdentifier, traits: UserTraits ) : void {
53+
54+
this.run(
55+
( analytics ) => {
56+
57+
analytics.identify( userID, traits );
58+
59+
}
60+
);
61+
62+
}
63+
64+
65+
// I track the given event for the previously-identified user.
66+
public track( eventID: EventIdentifier, eventProperties: EventProperties ) : void {
67+
68+
this.run(
69+
( analytics ) => {
70+
71+
analytics.track( eventID, eventProperties );
72+
73+
}
74+
);
75+
76+
}
77+
78+
// ---
79+
// PRIVATE METHODS.
80+
// ---
81+
82+
// I return a Promise that resolves with the 3rd-party Analytics Script.
83+
private async getScript() : Promise<AnalyticsScript> {
84+
85+
// CAUTION: For the sake of simplicity, I am not going to worry about the case in
86+
// which the analytics scripts fails to load. Ideally, I might create some sort
87+
// of "Null Object" version of the analytics API such that the rest of the code
88+
// can run as expected with various no-op method implementations.
89+
await this.scriptLoader.load();
90+
// NOTE: Since I don't have an installed Type for this service, I'm just casting
91+
// Window to ANY and then re-casting the global service that we know was just
92+
// injected into the document HEAD.
93+
return( ( window as any ).analytics as AnalyticsScript );
94+
95+
}
96+
97+
98+
// I run the given callback after the remote analytics library has been loaded.
99+
private run( callback: ( analytics: AnalyticsScript ) => void ) : void {
100+
101+
this.getScript()
102+
.then( callback )
103+
.catch(
104+
( error ) => {
105+
// Swallow underlying analytics error - they are not important.
106+
}
107+
)
108+
;
109+
110+
}
111+
112+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
:host {
3+
display: block ;
4+
font-size: 18px ;
5+
}
6+
7+
a {
8+
color: red ;
9+
cursor: pointer ;
10+
text-decoration: underline ;
11+
user-select: none ;
12+
-moz-user-select: none ;
13+
-webkit-user-select: none ;
14+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
// Import the core angular services.
3+
import { Component } from "@angular/core";
4+
5+
// Import the application components and services.
6+
import { AnalyticsService } from "./analytics.service";
7+
8+
// ----------------------------------------------------------------------------------- //
9+
// ----------------------------------------------------------------------------------- //
10+
11+
@Component({
12+
selector: "my-app",
13+
styleUrls: [ "./app.component.less" ],
14+
template:
15+
`
16+
<p>
17+
<a (click)="doThis()">Do This</a>
18+
&mdash;
19+
<a (click)="doThat()">Do That</a>
20+
</p>
21+
`
22+
})
23+
export class AppComponent {
24+
25+
private analyticsService: AnalyticsService;
26+
27+
// I initialize the app component.
28+
constructor( analyticsService: AnalyticsService ) {
29+
30+
this.analyticsService = analyticsService;
31+
32+
}
33+
34+
// ---
35+
// PUBLIC METHODS.
36+
// ---
37+
38+
// I execute an action (that we're going to track).
39+
public doThat() : void {
40+
41+
this.analyticsService.track(
42+
"do.that",
43+
{
44+
now: Date.now()
45+
}
46+
);
47+
48+
}
49+
50+
51+
// I execute an action (that we're going to track).
52+
public doThis() : void {
53+
54+
this.analyticsService.track(
55+
"do.this",
56+
{
57+
now: Date.now()
58+
}
59+
);
60+
61+
}
62+
63+
64+
// I get called once after the inputs have been bound for the first time.
65+
public ngOnInit() : void {
66+
67+
this.analyticsService.identify(
68+
"bennadel",
69+
{
70+
group: "admin"
71+
}
72+
);
73+
74+
}
75+
76+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
// Import the core angular services.
3+
import { BrowserModule } from "@angular/platform-browser";
4+
import { NgModule } from "@angular/core";
5+
6+
// Import the application components and services.
7+
import { AppComponent } from "./app.component";
8+
9+
// ----------------------------------------------------------------------------------- //
10+
// ----------------------------------------------------------------------------------- //
11+
12+
@NgModule({
13+
imports: [
14+
BrowserModule
15+
],
16+
declarations: [
17+
AppComponent
18+
],
19+
bootstrap: [
20+
AppComponent
21+
]
22+
})
23+
export class AppModule {
24+
// ...
25+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
2+
export class DelayedScriptLoader {
3+
4+
private delayInMilliseconds: number;
5+
private scriptPromise: Promise<void> | null;
6+
private urls: string[];
7+
8+
// I initialize the delayed script loader service.
9+
constructor( urls: string[], delayInMilliseconds: number );
10+
constructor( urls: string, delayInMilliseconds: number );
11+
constructor( urls: any, delayInMilliseconds: number ) {
12+
13+
this.delayInMilliseconds = delayInMilliseconds;
14+
this.scriptPromise = null;
15+
this.urls = Array.isArray( urls )
16+
? urls
17+
: [ urls ]
18+
;
19+
20+
}
21+
22+
// ---
23+
// PUBLIC METHODS.
24+
// ---
25+
26+
// I load the the underlying Script tags. Returns a Promise.
27+
public load() : Promise<void> {
28+
29+
// If we've already configured the script request, just return it. This will
30+
// naturally queue-up the requests until the script is resolved.
31+
if ( this.scriptPromise ) {
32+
33+
return( this.scriptPromise );
34+
35+
}
36+
37+
// By using a Promise-based workflow to manage the deferred script loading,
38+
// requests will naturally QUEUE-UP IN-MEMORY (not a concern) until the delay has
39+
// passed and the remote-scripts have been loaded. In this case, we're not even
40+
// going to load the remote-scripts until they are requested FOR THE FIRST TIME.
41+
// Then, we will use they given delay, after which the in-memory queue will get
42+
// flushed automatically - Promises rock!!
43+
this.scriptPromise = this.delay( this.delayInMilliseconds )
44+
.then(
45+
() => {
46+
47+
var scriptPromises = this.urls.map(
48+
( url ) => {
49+
50+
return( this.loadScript( url ) );
51+
52+
}
53+
);
54+
55+
return( Promise.all( scriptPromises ) );
56+
57+
}
58+
)
59+
.then(
60+
() => {
61+
62+
// No-op to generate a Promise<void> from the Promise<Any[]>.
63+
64+
}
65+
)
66+
;
67+
68+
return( this.scriptPromise );
69+
70+
}
71+
72+
// ---
73+
// PRIVATE METHODS.
74+
// ---
75+
76+
// I return a Promise that resolves after the given delay.
77+
private delay( delayInMilliseconds: number ) : Promise<any> {
78+
79+
var promise = new Promise(
80+
( resolve ) => {
81+
82+
setTimeout( resolve, delayInMilliseconds );
83+
84+
}
85+
);
86+
87+
return( promise );
88+
89+
}
90+
91+
92+
// I inject a Script tag with the given URL into the head. Returns a Promise.
93+
private loadScript( url: string ) : Promise<any> {
94+
95+
var promise = new Promise(
96+
( resolve, reject ) => {
97+
98+
var commentNode = document.createComment( " Script injected via DelayedScriptLoader. " );
99+
100+
var scriptNode = document.createElement( "script" );
101+
scriptNode.type = "text/javascript";
102+
scriptNode.onload = resolve;
103+
scriptNode.onerror = reject;
104+
scriptNode.src = url;
105+
106+
document.head.appendChild( commentNode );
107+
document.head.appendChild( scriptNode );
108+
109+
}
110+
);
111+
112+
return( promise );
113+
114+
}
115+
116+
}

0 commit comments

Comments
 (0)