Pass javascript page level variable into bundled Angular2 RC 1 app - javascript

I will try to keep this as short as possible.
The problem I am having is an extension of this SO question
I am trying to pass javascript variable from the "page level" into a service in my angular 2 app. I followed the direction of Gunter in the answer in the above SO question.
I used an opaque token to capture page variable names and pass them into the app constructor. This works perfectly when I am in development, but once I try to bundle the app it stops working. I use gulp-jspm-build to bundle my app and I have mangle set to false to avoid some other errors.
My app lives inside a CMS and the cms pre-processes my apps's index.html and replaces certain tokens with values.
Here is the part of the index.html of my angular app that gets pre processed with token replacement:
<!-- 2. Capture values to pass to app -->
<script type="text/javascript">
var moduleId = parseInt("[ModuleContext:ModuleId]");
var portalId = parseInt("[ModuleContext:PortalId]");
var tabId = parseInt("[ModuleContext:TabId]");
var dnnSF = $.ServicesFramework(moduleId);
if ("[ModuleContext:EditMode]" === 'True') {
var editMode = true;
}
// console.log('editMode = ' + editMode);
</script>
<!-- 3. Replaces with actual path to ststem.config.js -->
[Javascript:{path: "~/my-app/systemjs.config.js"}]
<!-- 4. APP selector where is it rendered-->
<my-app>Loading...</my-app>
Notice the [ModuleContext:ModuleId] - this gets replaced with a number value that I need to use in the angularApp that gets bootstrapped on this page.
So my main.ts file looks like this:
import {bootstrap} from '#angular/platform-browser-dynamic';
import {provide} from '#angular/core';
import {AppComponent} from './app.component';
import {dnnModId, dnnSF, dnnPortalId} from './shared/dnn/app.token';
import {HashLocationStrategy, LocationStrategy} from '#angular/common';
import {ROUTER_PROVIDERS} from '#angular/router';
import {HTTP_PROVIDERS} from '#angular/http';
// declare
declare var $: any;
declare var moduleId: any;
declare var portalId: any;
// the providers & services bootstrapped in this root component
// should be available to the entire app
bootstrap(AppComponent, [
ROUTER_PROVIDERS,
HTTP_PROVIDERS,
provide(LocationStrategy, { useClass: HashLocationStrategy }),
provide(dnnModId, { useValue: moduleId }),
provide(dnnPortalId, { useValue: portalId }),
provide(dnnSF, { useValue: $.ServicesFramework(moduleId) })
]);
I added declare var moduleId: any; so that typescript does not throw compilation errors. But this part is lost when bundled.
Here is how I define my opaque tokens:
import {OpaqueToken} from '#angular/core';
// Opaque tokens create tokens that can be used in the Dependency Injection Provider
export let dnnModId: any = new OpaqueToken('moduleId');
export let dnnPortalId: any = new OpaqueToken('portalId');
export let dnnTabId: any = new OpaqueToken('tabId');
export let dnnSF: any = new OpaqueToken('sf');
MY ERROR
I get an error on the following line:
core_1.provide(app_token_1.dnnModId, { useValue: moduleId
In my bundled .js file for the app.
the error is
app.min.js Uncaught ReferenceError: moduleId is not defined
QUESTION:
Can someone help me figure out why this works in development but not once I bundle my files together?
A huge thanks in advance

This turned out to be an issue with my CMS. My CMS took the javascript files and added them to the top of the page.
I had to change
[Javascript:{path: "~/my-app/systemjs.config.js"}]
to
<script src="/DesktopModules/regentsigns-app/systemjs.config.js"></script>
The top example is used by the CMS token replace function that parses the html page, it placed the bundled angular.min.js file above the selector and inline javascript that captured the global variables.
So by using the simple manual script tag import of the app.js files I fixed the load order issue.

In tsconfig.json, you should have
"module": "commonjs"
under
"compilerOptions"

Related

Import external JS into Angular 8

I am writing application in Angular 8 (8.0.3) and I need to import external JS file which will hold a URL reference for API that can and will change. The problem I am facing is that changes I do into this file after I have compiled Angular app (with ng build --prod), changes are not being picked up inside Angular, but it keeps the data as it was when the app was built.
This is the external JS file:
export function apiUrl() {
return 'http://www.localhost.local/api/v1/';
}
The apiUrl() does return proper value, but if I change it, the updated value is not reflected inside Angular app.
I also created .d.ts. file:
export declare function apiUrl();
Imported the external JS file into index.html:
<script type="module" src="assets/js/api_url.js"></script>
Used it in angular service:
import {apiUrl} from '../../assets/js/api_url';
export class ApiService {
private api_url: string = apiUrl();
constructor(...) {
console.log(apiUrl());
}
}
I did NOT import this file in angular.json and even if I do, nothing changes.
So, how to create and then import an external JS file that after it changes, the Angular app does pick up it changes?
EDIT:
Do note that I do not need a live app reload when this file changes, but I need for the file to be loaded from server each time the Angular app starts.
EDIT 2:
To explain why I need this. The Angular app will be used on internal network to which I do not have access to. The client currently does not know the API URL and even if he would, it can change at any moment. That's the reason why I need this to work.
In that case you should use environments
You can find them in src/environments directory.
For local development you have environment.ts file, for production environment.prod.ts
Example environment.ts:
export const environment = {
production: false,
apiUrl: 'http://www.localhost.local/api/v1/'
};
environment.prod.ts:
export const environment = {
production: true,
apiUrl: 'http://www.producitonurl.com/api/v1/'
};
To use this apiUrl value you need to import environment file in typescript file like this:
You need to import environment.ts not any other environment file, because angular is replacing it while building application
import {environment} from '../environments/environment';
#Injectable({
providedIn: 'root'
})
export class MyApiService {
apiUrl = environment.apiUrl;
}
But in case you want to use method from <script type="module" src="assets/js/api_url.js"></script>, just use window object like this:
private api_url: string = (window as any).apiUrl();

Using downgradeModule in conjunction with downgradeInjectable in an angular / angularjs hybrid application results in error

With angular 5.0 the upgrade module now has the option of using downgradeModule which runs angularjs outside of the angular zone. While experimenting with this I have run into a problem with using downgradeInjectable.
I am receiving the error:
Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.
Bootstrapping angular in angular js works fine
import 'zone.js/dist/zone.js';
import * as angular from 'angular';
/**
* Angular bootstrapping
*/
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { decorateModuleRef } from 'src/environment';
import { AppModule } from 'src/app/app.module';
import { downgradeModule } from '#angular/upgrade/static';
export const bootstrapFn = ( extraProviders ) => {
const platformRef = platformBrowserDynamic( extraProviders );
return platformRef
.bootstrapModule( AppModule )
.then( decorateModuleRef );
};
angular.module( 'app.bootstrap', [
downgradeModule( bootstrapFn ),
] );
However...
Since the bootstrapping takes place after angularjs has been initialized I can no longer get the downgrade injectable working.
Service to be downgraded
import { Injectable, Inject, OnInit } from '#angular/core';
#Injectable()
export class MobileService implements OnInit{
constructor(
#Inject( 'angularjsDependency1' ) public angularjsDependency1 : any,
#Inject( 'angularjsDependency2' ) public angularjsDependency2 : any,
) {}
}
Downgrade injectable attempt
import * as angular from 'angular';
import { downgradeInjectable } from '#angular/upgrade/static';
import { MyService } from 'src/services/myService/myService';
export const myServiceDowngraded = angular.module( 'services.mobileService', [
angularjsDependency1,
angularjsDependency2,
] )
.factory(
'mobileService',
downgradeInjectable( MyService ),
).name;
When "downgradeInjectable( MyService ) runs the angular injector is not available yet since angular hasn't been bootstrapped. Hence the error:
Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.
Does anyone have an idea how I might fix this?
Answers in this thread helped me find a solution, but none contains the holy grail:
Creating a service-boostrap component aside the app's code does not work, because Angular is loaded asynchronously, unlike AngularJS. This gives the same error Trying to get the Angular injector before bootstrapping an Angular module.
Creating a service-bootstrap component wrapping the AngularJS code kind of worked, but then I experienced issues with change detection inside Angular composants, as described in this issue on github.
In the github issue, someone suggested to edit #angular/upgrade source code to change a false to true to force components to be created in the Zone. But in this case it seems to cause performance issues (it seemed to launch ngZone's code multiple times on user events)
In order for the app to work correctly, I needed :
Not to have ng components containing AngularJS components containing Angular components. We need to only have AngularJS containing Angular components.
Make sure that AngularJS components using Angular services are created after a first angular component, named service-bootstrap
To acheive this, I created a slightly modified service-bootstrap component:
import { Component, Output, EventEmitter, AfterViewInit } from "#angular/core";
#Component({
selector: 'service-bootstrap',
template: ``
})
export class ServiceBootstrapComponent implements AfterViewInit{
#Output()
public initialized: EventEmitter<void> = new EventEmitter();
public ngAfterViewInit(){
this.initialized.emit();
}
}
Declared this component as entryComponent in the Angular module and called downgradeComponent to register it in AngularJS:
import { downgradeModule, downgradeInjectable, downgradeComponent } from '#angular/upgrade/static';
const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = platformBrowserDynamic(extraProviders);
return platformRef.bootstrapModule(AppModule);
};
const downgradedModule = downgradeModule(bootstrapFn);
const app = angular.module('ngApp', [
downgradedModule,
'app'
]);
app.directive('serviceBootstrap', downgradeComponent({ component: ServiceBootstrapComponent }));
Then (and the magic happens here), I created a new AngularJS component:
angular.module("app")
.directive('ng1App', ng1AppDirective);
function ng1AppDirective(){
return {
template: `
<service-bootstrap (initialized)="onInit()"></service-bootstrap>
<section ng-if="initialized">
<!-- Your previous app's code here -->
</section>
`,
controller: ng1AppController,
scope: {},
};
}
ng1AppController.$inject = ['$scope'];
function ng1AppController($scope){
$scope.onInit = onInit;
$scope.initialized = false;
function onInit(){
$scope.initialized = true;
}
}
Then, my index.html only referenced this component
<body>
<ng1-app></ng1-app>
</body>
With this approach, I'm not nesting AngularJS components inside Angular components (which breaks change detection in Angular components), and still I ensure that a first Angular component is loaded before accessing the Angular providers.
Note: The answer below follows the convention of calling angular 1.x as angularjs and all angular 2+ versions as simply angular.
Expanding on JGoodgive's answer above, basically, if you're using downgradeModule, then angular module is bootstrapped lazily by angularjs when it needs to render the first angular component. Until then, since the angular module isn't initialised, if you are accessing any angular services inside angularjs using downgradeInjectable, those services aren't available too.
The workaround is to force bootstrapping of the angular module as early as possible. For this, a simple component is needed:
import {Component} from '#angular/core';
#Component({
selector: 'service-bootstrap',
template: ''
})
export class ServiceBootstrapComponent {}
This component doesn't do anything. Now, we declare this component in the top level angular module.
#NgModule({
// ...providers, imports etc.
declarations: [
// ... existing declarations
ServiceBootstrapComponent
],
entryComponents: [
// ... existing entry components
ServiceBootstrapComponent
]
})
export class MyAngularModule {}
Next, we also need to add a downgraded version of this component to angularjs module. (I added this to the top level angularjs module I had)
angular.module('MyAngularJSModule', [
// ...existing imports
])
.directive(
'serviceBootstrap',
downgradeComponent({ component: ServiceBootstrapComponent }) as angular.IDirectiveFactory
)
Finally, we throw in this component in our index.html.
<body>
<service-bootstrap></service-bootstrap>
<!-- existing body contents -->
</body>
When angularjs finds that component in the markup, it needs to initialise angular module to be able to render that component. The intended side effect of this is that the providers etc. also get initialised and are available to be used with downgradeInjectable, which can be used normally.
This was pointed out to me in an angular github thread.
https://github.com/angular/angular/issues/16491#issuecomment-343021511
George Kalpakas's response:
Just to be clear:
You can use downgradeInjectable() with downgradeModule(), but there are certain limitations. In particular, you cannot try to inject a downgraded injectable until Angular has been bootstrapped. And Angular is bootstrapped (asynchronously) the first time a downgraded component is being rendered. So, you can only safely use a downgraded service inside a downgraded component (i.e. inside upgraded AngularJS components).
I know this is limiting enough that you might decide to not use downgradeInjectable() at all - just wanted to make it more clear what you can and can't do.
Note that the equivalent limitation is true when using an upgraded injectable with UpgradeModule: You cannot use it until AngularJS has been bootstrapped. This limitation usually goes unnoticed though, because AngularJS is usually bootstrapped in the Angular module's ngDoBootstrap() method and AngularJS (unlike Angular) bootstraps synchronously.
I had the same issue, and the reasons are explained in the above answer.
I fixed this by dynamically injecting the downgraded angular service using $injector.
Steps
Register your downgraded service to angularjs module
angular.module('moduleName', dependencies)
angular.factory('service', downgradeInjectable(Service));
Inject $injector to your controller and use this to get the downgraded service
const service = this.$injector.get('service');
service.method();
I had the same issue and it sucked up several hours before finding this.
My workaround was to create a ServiceBootstrapComponent that does nothing but injects all the services that we need to downgrade.
I then downgrade that component, mark it as en entry in #NgModule and add it to index.html.
Works for me.
I was getting the same error in our hybrid app. We are using the following versions:
AngularJS 1.7.x
Angular 7.3.x
As mentioned in this answer, I also used a dummy component called <ng2-bootstrap> to force boostrapping of Angular. And then, I created an AngularJS service which checks if Angular has been bootstrapped:
// tslint:disable: max-line-length
/**
* This service can be used in cases where Angular fails with following error message:
*
* `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
*
* Above error occurs because of how `downgradeModule` works.
*/
/*#ngInject*/
export class Ng2BootstrapDetectionService {
private bootstrapDone = false;
constructor(private $q: ng.IQService) {}
public whenBootstrapDone(): ng.IPromise<void> {
if (this.bootstrapDone) {
return this.$q.resolve();
}
const deferred = this.$q.defer<void>();
angular.element(document).ready(() => {
const intervalId = setInterval(() => {
const el = document.querySelector('ng2-bootstrap');
if (el && el.outerHTML.includes('ng-version=')) {
this.bootstrapDone = true;
clearInterval(intervalId);
deferred.resolve();
}
}, 500);
});
return deferred.promise;
}
}
Ng2BootstrapDetectionService can be used like below:
import {NotificationService} from 'ng2-app/notification.service';
// This can be used in cases where you get following error:
// `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
// You will need access to following
// $injector: AngularJS Injector
// Ng2BootstrapDetectionService: our custom service to check bootsrap completion
this.Ng2BootstrapDetectionService
.whenBootstrapDone()
.then(() => {
const notificationService = this.$injector
.get<NotificationService>('ng2NotificationService');
notificationService.notify('my message!');
});
You can find more details about this solution at the end of this blog post.

Import a javascript library into a custom javascript file which I use in my Angular2 project

I want to include Machine Learning in my Angular2 project through the "synaptic.js" library of JavaScript. I have installed the library through the command
npm install synaptic --save
I want to run the following custom javascript file (myJsFile.js):
function myFunction() {
var A = new Layer(5);
var B = new Layer(2);
A.project(B);
var learningRate = .3;
for (var i = 0; i < 20000; i++)
{
// when A activates [1, 0, 1, 0, 1]
A.activate([1,0,1,0,1]);
// train B to activate [0,0]
B.activate();
B.propagate(learningRate, [0,0]);
}
// test it
A.activate([1,0,1,0,1]);
console.log(B.activate()); // [0.004606949693864496,0.004606763721459169]
}
In my index.html file, I have included the following code:
<script src="js/myJsFile.js"></script>
In my app.component.ts file
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { AuthenticationService } from '../../Services/authentication.service';
declare var myFunction: any;
#Component({
moduleId: module.id,
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
// some code here
// I call f() function inside the app.component.html file in order to run the myFunction() javascript function.
f() {
new myFunction();
}
If, for example, the alert() function was inside the myJsFile.js file, the code would run without any problem. But in my case, I have the following error:
app.component.html:9 ERROR ReferenceError: Layer is not defined
It means that I have to import the synaptic library somewhere.
The usage section in github (https://github.com/cazala/synaptic) points out the following:
var synaptic = require('synaptic'); // this line is not needed in the browser
var Neuron = synaptic.Neuron,
Layer = synaptic.Layer,
Network = synaptic.Network,
Trainer = synaptic.Trainer,
Architect = synaptic.Architect;
If I import the above code into my myJsFile.js, I get the following error:
myJsFile.js:1 Uncaught ReferenceError: require is not defined
Where should I import the synaptic library and how??
I have also found and installed the synaptic.d.ts file, but I don't know how to use it.. Can anyone help me??
Thanks a lot for your time!
The example you have is probably using Node, for using with Angular 2+ you need to use the import syntax.
e.g.
import { Neuron, Layer, Network, Trainer, Architect} from 'synaptic';
Then you can should be able to reference them
Also I would suggest making your myJsFile.js file a ts file and importing the same way. It's just less complicated if everything is imported in js and not in the html.

Externally pass values to an Angular application

I'm thinking of separating our MVC-based website's front-end into few components and I was thinking about using Angular for that purpose (e.g. creating cart application that I can include to my view afterwards).
Problem is that I need to pass few variables into that application and I'm wondering how to do that safely and professionally. I was thinking about something like:
I'm going to ng build --prod --aot
I inject all my scripts to my view
I inject variables passed to view to my app
... and "code" representation for my thoughts:
Controller:
public function viewAction()
{
$this->view->addCss('angular/app/styles.css'); // adds styles for cart app
$this->view->addJS('angular/app/scripts.js'); // adds scripts for cart app
$this->view->setJSVariable('idCustomer', 555); // sets global var idCustomer
}
View:
<!-- bunch of HTML for view ... -->
<script>
// CartApp would be an angular app.
CartApp.use([
idCustomer
]);
</script>
So my question is... would it be possible (and would it be a good solution) to get the CartApp as an object and then make some function (like use in above example) that would set/pass the data? (let's say to some globally provided service/component or anything else). Or is there any other way to do this? Like taking the variables from inside the application from the window object? (as they're going to be bound to the window anyways). Thank you.
So I was going to suggest using input bindings... I've done that before in AngularJS but was surprised to find that using input bindings on the root component isn't supported yet. You can have fun reading this giant issue: https://github.com/angular/angular/issues/1858
The best post I saw there was Rob Wormald's which had a couple of suggestions:
to the initial question, if you really want to grab data from the
root, that's possible via
https://plnkr.co/edit/nOQuXE8hMkhakDNCNR9u?p=preview - note that it's not an input (because there's no angular context outside of it to do the input...) - its a simple string attribute
which you'd need to parse yourself.
ideally though, you'd do as
#robtown suggested and actually pass the data in javascript, rather
than passing it as a DOM string and retrieving / parsing it yourself
(which has never really been a supported case in angular, despite the
explicit-warned-against usage of ng-init in angular1 to accomplish
this) - see https://plnkr.co/edit/PoSd07IBvYm1EzeA2yJR?p=preview for a
simple, testable example of how to do this.
So the 2 good options I saw were:
Add normal HTML attributes to the root component:
<app-root appData='important stuff'></app-root>
and use ElementRef to fetch them:
#Component({
selector: 'app-root'
})
export class AppComponent {
constructor(el: ElementRef) {
console.log(el.nativeElement.getAttribute('appData'));
}
}
Would probably work best if you are just dealing with strings or config flags. If you are passing JSON, you will need to manually parse it.
Have the server render the data as JavaScript and import it in your app:
Have the server render something like this to a script tag or JS file that is loaded before Angular is bootstrapped:
window.APP_DATA = { ids: [1, 2, 3] }
Tell your NgModule about it using a provider:
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppComponent } from './app.component';
#NgModule({
providers: [{ provide: 'AppData', useValue: (<any> window).APP_DATA }],
bootstrap: [AppComponent]
})
export class AppModule { }
And then use it as a normal Angular service:
import {Component, Inject} from '#angular/core';
#Component({
selector: 'app-root'
})
export class AppComponent {
constructor(#Inject('AppData') appData) {
console.log(appData.ids);
}
}

How do I declare a dependency on an ng-metadata module?

I have a project that is using ng-metadata (https://github.com/ngParty/ng-metadata) to build a handful of Angular 1.5 modules. I have a test module/component that looks like this:
import { NgModule, Component, Inject, Input, Output, EventEmitter } from 'ng-metadata/core'
import { platformBrowserDynamic } from 'ng-metadata/platform-browser-dynamic'
#Component({
selector: 'test',
template: require('./test.template.html')
})
class TestComponent {
#Input() type: string;
constructor() {
console.log(`test: ${this.type}`)
}
}
#NgModule({
declarations: [TestComponent]
})
class HeroModule {}
platformBrowserDynamic().bootstrapModule(HeroModule)
Everything seems happy when compiled and I'm now attempting to use the module in another project (that is not using ng-metadata but has a compatible version of Angular).
I'm simply including the shims as directed by the ng-metadata docs and the JavaScript file that contains the module described above (built by webpack). I have a new module in this project that wants to list the HeroModule as a dependency. I've tried a few things:
// attempt 1:
angular.module('my-consuming-module', ['ui.router', 'hero'])
// attempt 2:
angular.module('my-consuming-module', ['ui.router', 'heroModule'])
// attempt 3:
angular.module('my-consuming-module', ['ui.router', 'hero-module'])
All always end up with the same Error: $injector:nomod Module Unavailable error from Angular.
If I'm using ng-metadata to build my modules, what are the names I use to list them as dependencies in another project?
Finally figured this out! It's amazing what happens when you carefully read documentation...
Found in the Manual Angular 1 Bootstrap section of ng-metadata's docs:
You can still leverage ng2 way of components registration without ng-metadata bootstrap, but you have to manually create your Angular 1 module from an ng-metadata #NgModule using the bundle helper function.
I ended up being able to do the following:
// REMOVED platformBrowserDynamic().bootstrapModule(HeroModule)
const Ng1AdminModule = bundle(HeroModule).name;
export const AppModule = angular.module('hero', [Ng1AdminModule]);
And then my hero module becomes accessible to the my-consuming-module just as I expected. The bundle helper function was the key to figuring this out.
You need to import those module from their respective locations and inject it inside your angular module
//ensure `angular`, `angular-ui-router` should be there in `map` of systemjs.config.js
import * as angular from 'angular';
import * as uiRouter from 'angular-ui-router';
import { heroModule} from './hero.module';
angular.module('app',[ uiRouter, heroModule]);
Check references here

Categories