This repository was archived by the owner on May 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlaxar-angular-adapter.js
271 lines (217 loc) · 9.57 KB
/
laxar-angular-adapter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
* Copyright 2016-2017 aixigo AG
* Released under the MIT license.
* http://laxarjs.org/license
*/
/**
* Provides an AngularJS v1.x adapter factory for a laxarjs bootstrapping context.
* https://github.com/LaxarJS/laxar/blob/master/docs/manuals/adapters.md
*
* Because in AngularJS v1.x module registry and injector are global, there are certain restrictions when
* bootstrapping multiple LaxarJS applications in the same Browser window:
* - Currently, all AngularJS modules must be available when bootstrapping the first instance, and the
* widget modules for all bootstrapping instances must be passed to the first invocation.
* - Multiple bootstrapping instances share each other's AngularJS modules (e.g. controls).
*
* @module laxar-angular-adapter
*/
import ng from 'angular';
import { assert } from 'laxar';
import { name as idModuleName } from './lib/directives/id';
import { name as widgetAreaModuleName } from './lib/directives/widget_area';
import { name as profilingModuleName } from './lib/profiling/profiling';
import { name as visibilityServiceModuleName } from './lib/services/visibility_service';
import { name as localizeModuleName } from './lib/filters/localize';
// exported for unit tests
export const ANGULAR_SERVICES_MODULE_NAME = 'axAngularGlobalServices';
export const ANGULAR_MODULE_NAME = 'axAngularWidgetAdapter';
export const technology = 'angular';
// Due to the nature of AngularJS v1.x, we can only create the AngularJS modules once,
// so we keep certain injections global.
let injectorCreated = false;
let $rootScope;
let $controller;
let $compile;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
export function bootstrap( { widgets, controls }, laxarServices ) {
const api = {
create,
serviceDecorators
};
// register controllers under normalized module names that can also be derived from the widget.json name:
const controllerNames = {};
widgets.forEach( ({ descriptor, module }) => {
controllerNames[ descriptor.name ] = `${capitalize( module.name )}Controller`;
} );
const activeWidgetServices = {};
// Instantiate the AngularJS modules and bootstrap angular, but only the first time!
if( !injectorCreated ) {
injectorCreated = true;
createAngularServicesModule();
createAngularAdapterModule();
}
if( ng.mock ) {
ng.mock.module( ANGULAR_SERVICES_MODULE_NAME );
ng.mock.module( ANGULAR_MODULE_NAME );
( widgets ).concat( controls )
.forEach( _ => { ng.mock.module( _.module.name ); } );
}
// LaxarJS EventBus works with native promise.
// To be notified of eventBus ticks, install a listener.
laxarServices.heartbeat.registerHeartbeatListener( () => { $rootScope.$digest(); } );
const anchorElement = document.createElement( 'DIV' );
anchorElement.style.display = 'none';
document.body.appendChild( anchorElement );
ng.bootstrap( anchorElement, [ ANGULAR_MODULE_NAME ] );
return api;
///////////////////////////////////////////////////////////////////////////////////////////////////////////
function serviceDecorators() {
return {
axGlobalEventBus: decorateEventBusForDigest,
axEventBus: decorateEventBusForDigest,
axContext( context ) {
return ng.extend( $rootScope.$new(), context );
}
};
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// eslint-disable-next-line valid-jsdoc
function create( { widgetName, anchorElement, services, provideServices } ) {
const widgetScope = services.axContext;
const { id } = widgetScope.widget;
activeWidgetServices[ id ] = services;
createController();
return {
domAttachTo,
domDetach,
destroy
};
////////////////////////////////////////////////////////////////////////////////////////////////////////
function createController() {
const controllerName = controllerNames[ widgetName ];
services.$scope = widgetScope;
provideServices( services );
$controller( controllerName, services );
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Synchronously attach the widget DOM to the given area.
*
* @param {HTMLElement} areaElement
* The widget area to attach this widget to.
* @param {String} templateHtml
* The AngularJS HTML template to compile and link to this widget
*/
function domAttachTo( areaElement, templateHtml ) {
if( templateHtml === null ) { return; }
const element = ng.element( anchorElement );
element.html( templateHtml );
areaElement.appendChild( anchorElement );
$compile( anchorElement )( widgetScope );
if( !$rootScope.$$phase ) {
widgetScope.$digest();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
function domDetach() {
const parent = anchorElement.parentNode;
if( parent ) {
parent.removeChild( anchorElement );
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
function destroy() {
widgetScope.$destroy();
delete activeWidgetServices[ id ];
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
function decorateEventBusForDigest( eventBus ) {
const result = Object.create( eventBus );
result.publish = ( ...args ) => tap( eventBus.publish( ...args ) );
result.publishAndGatherReplies = ( ...args ) => tap( eventBus.publishAndGatherReplies( ...args ) );
return result;
function tap( promise ) {
promise.then = ( onFulfilled, onRejected ) =>
Promise.prototype.then.call( promise, intercept( onFulfilled ), intercept( onRejected ) );
return promise;
}
function intercept( callback ) {
return typeof callback !== 'function' ?
callback :
( ...args ) => {
try {
return callback( ...args );
}
finally {
$rootScope.$evalAsync( () => {} );
}
};
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
function createAngularServicesModule() {
// Here we ensure availability of globally public laxar services for directives and other services
ng.module( ANGULAR_SERVICES_MODULE_NAME, [] )
.factory( 'axConfiguration', () => laxarServices.configuration )
.factory( 'axGlobalEventBus', () => decorateEventBusForDigest( laxarServices.globalEventBus ) )
.factory( 'axGlobalLog', () => laxarServices.log )
.factory( 'axGlobalStorage', () => laxarServices.storage )
.factory( 'axHeartbeat', () => laxarServices.heartbeat )
.factory( 'axTooling', () => laxarServices.tooling )
.factory( 'axWidgetServices', () => createWidgetServiceProvider() );
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
function createWidgetServiceProvider() {
return scope => {
let currentScope = scope;
let widgetId = null;
while( !widgetId && currentScope ) {
if( currentScope.widget && typeof currentScope.widget.id === 'string' ) {
widgetId = currentScope.widget.id;
break;
}
currentScope = currentScope.$parent;
}
assert.state( widgetId, 'No widget context found in given scope or one of its parents.' );
const services = activeWidgetServices[ widgetId ];
assert.state( services, `No services found for widget with id ${widgetId}.` );
return services;
};
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
function createAngularAdapterModule() {
const internalDependencies = [
ANGULAR_SERVICES_MODULE_NAME,
idModuleName,
widgetAreaModuleName,
localizeModuleName,
profilingModuleName,
visibilityServiceModuleName
];
const externalDependencies = ( widgets ).concat( controls ).map( _ => _.module.name );
ng.module( ANGULAR_MODULE_NAME, [ ...internalDependencies, ...externalDependencies ] ).run(
[ '$compile', '$controller', '$rootScope', ( _$compile_, _$controller_, _$rootScope_ ) => {
$controller = _$controller_;
$compile = _$compile_;
$rootScope = _$rootScope_;
} ] )
.factory( '$exceptionHandler', () => {
return ( exception, cause ) => {
const msg = exception.message || exception;
laxarServices.log.error(
`There was an exception: ${msg}, \nstack: ${exception.stack}, \n, Cause: ${cause}`
);
};
} );
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// For testing, to reset the global AngularJS module state:
export function reset() {
injectorCreated = false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
function capitalize( _ ) {
return _.replace( /^./, _ => _.toUpperCase() );
}