Enabling Hot Module Replacement with Angular 2 Upgraded App - angularjs

We're using the upgrade adapter to setup an Angular 1/2 Hybrid app. We're bootstrapping the app like this (As described in the Angular docs https://angular.io/guide/upgrade):
class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, ['ngApp'], { strictDi: true });
}
}
For our HMR setup, we're using Angular CLI and following these instructions https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/configure-hmr.md
The problem is that when HMR runs and tries running that bootstrap code again, it attempts to rebootstrap the Angular 1 app, which results in an error that the Angular 1 module has already been bootstrapped.
Trying to only conditionally run that code if angular 1 hasn't been bootstrapped also won't work, because while the component does refresh, it doesn't reload with the new updates.
We've resorted to something that seems to work, but is less than ideal, which is removing he angular 1 app element, and recreating it, so that the bootstrap doesn't error.
Something like this:
ngDoBootstrap() {
// reinitialize angular 1 app
var element = document.getElementById('ng-app');
let windowRef = <any>window;
if(!windowRef.appContents) {
windowRef.appContents = element.innerHTML;
} else {
document.body.removeChild(element);
element = document.createElement('div');
element.id = 'ng-app';
element.innerHTML = windowRef.appContents;
document.body.appendChild(element);
}
this.upgrade.bootstrap(element, ['ngApp'], { strictDi: true });
}
While a bit hacky, that at least seems to get the HMR working. However, because it is causing the entire angular 1 app to rebootstrap, rather than just refreshing the module that changed, this is only marginally faster than a simple livereload.
Is there any better way to rebootstrap a hybrid app to allow for HMR to work?

Related

Using NgRef on downgraded Angular 5 component in AngularJs 1.7.2

I am working on a project in Angular 1.7.2 that utilizes some components that were built in Angular 5/6. We are downgrading the components using the downgradeComponent tool and everything is working just fine.
We recently added a new component that we need to integrate with but we need to access the components properties as well. I was looking into the ngRef directive but that does not seem to be working and I'm unable to find any other ngRef examples outside of the Angular documentation. When I add the ngRef and bind it to a variable in the current scope, it never gets assigned. Any help would be appreciated!
Angular 5 component
export class ImportedComponent implements OnInit {
variable1: boolean = true;
variable2: boolean = false;
constructor(private certService: ImportedComponent) { }
ngOnInit() {
this.variable1 = true;
this.variable2 = false
}
}
Html - w/ AngularJS 1.7.2
<imported ng-ref="importedProperty" ></imported>
<custom-button ng-if="importedProperty.variable1" [disabled]="!importedProperty.variable2"></custom-button>
Downgrading
angular
.module("blah", [])
.directive(
"imported",
downgradeComponent({ component: ImportedComponent }) as angular.IDirectiveFactory
);
The downgrading for the imported component is working because the HTML is showing up and I'm able to see the console.log()s occurring from their end but when I try to access importedProperty, I get undefined (or empty object if I initialize it as such in my scope prior)
I ended up working with the component owner who is now passing the data back as an event. I then listen for that event and use those properties accordingly

When migrating from Angular 1.5 to 1.6 routing is ran inside unit tests

I've migrated my app to Angular 1.6 and suddenly unit test aren't working anymore. After investigating a little I found out that routing is enabled when running unit tests.
The code that I have added extra to the $routeProvider is this one:
$routeProvider.whenAuthenticated = function (path, route) {
route.resolve = route.resolve || {};
angular.extend(route.resolve, {
isAuthenticated: ['Auth', function (auth) {
return auth.authenticate();
}]
});
return $routeProvider.when(path, route);
};
and
$routeProvider
.whenAuthenticated('/', {
redirectTo: '/dashboards'
});
In Angular 1.5 the auth.authenticate() is never hit, while in Angular 1.6 the app tries to navigate to one of my routes and then auth.authenticate() is hit, and the url changes to "/dashboards".
Does anybody have an idea about what changed in 1.6? Do I need to do something extra to disable routing while testing?
In 1.6 the way the $route service and its dependencies are instantiated has changed, and will - by default - be instantiated early on.
Migrating from Previous Versions

No provider for $scope! error when using a angular 1 directive in an angular 2 app

I have an angular 2 App built with angular-cli and I need to use an angular 1 directive in one of my components (to re-use it from a different application). I followed the steps from:
https://angular.io/docs/ts/latest/guide/upgrade.html#!#using-angular-1-component-directives-from-angular-2-code
But now I got to this error and cannot get past it. I am using angular2.0.2 (I managed to build a hybrid app in the past with the beta version but it was an angular1 app and I used angular 2 components with downgrade function of the adapter).
In my app.module.ts I have:
import { UpgradeAdapter } from '#angular/upgrade';
const upgradeAdapter = new UpgradeAdapter(forwardRef(() => AppModule));
const HeroDetail = upgradeAdapter.upgradeNg1Component('heroDetail');
#NgModule({
imports: [
BrowserModule,
...
],
declarations: [
...
HeroDetail
]
})
export class AppModule { }
and my hero-detail.component.ts looks like this:
export const heroDetail = {
templateUrl: 'hero-detail.html',
controller: function() {
}
};
and my hero-detail.html looks like this:
<h2>Windstorm details!</h2>
I try to use the directive in another angular 2 component simply by adding in the template:
When I run ng serve, the application compiles fine but when I try to load the page I get the mentioned error.
Any suggestions on how I can move forward with this?
It seems you have incorrect bootstrap logic.
It's actually not quite obvious, make sure that:
you don't bootstrap any ng2 component with #NgModule({bootstrap:[ ... ]}). Instead, you should have empty ngDoBootstrap() { } method in your main module.
root template is ng1 template. I.e. in your index.html you should have only ng1 components or downgraded ng2 components. You can have ng2 component as a root, but you need to downgrade it first.
Official upgrade guide contains an example of DOM structure:
... which ensures that ng2 injectors have all required providers from ng1.

referencing an amd module(arcgis) in webpack app

I'm building a react app with webpack and i need to incorporate arcgis maps into a react component. I have know idea how to bring this into my project. I've tried creating an arcgis directory with an index.js of the built javascript and trying to reference that:
import {Map} from 'arcgis/index'
That doesn't work. I then just tried to include the css/js script tags directly into my index.html but when I try to require them, like in the example, webpack obviously can't find them. Is there some way to tell webpack to ignore require calls in my src file so it gets handled by the browser? I'm trying and failing at doing the following:
import React from 'react'
export default class EsriMap extends React.Component {
componentDidMount() {
const _this = this
require(["esri/map", "dojo/domReady!"], function(Map) {
var map = new Map(_this.refs.map, {
center: [-118, 34.5],
zoom: 8,
basemap: "topo"
});
});
}
render() {
return (
<div ref="map"></div>
)
}
}
You may want to try this https://github.com/tomwayson/esri-webpack-babel .
This method is nice because it doesn't bog down the build. You pull in the ESRI Api from the CDN, and tell webpack that it's an external.
//Add this...
externals: [
// Excludes any esri or dojo modules from the bundle.
// These are included in the ArcGIS API for JavaScript,
// and its Dojo loader will pull them from its own build output
function (context, request, callback) {
if (/^dojo/.test(request) ||
/^dojox/.test(request) ||
/^dijit/.test(request) ||
/^esri/.test(request)
) {
return callback(null, "amd " + request);
}
callback();
}
],
//And this to you output config
output: {
libraryTarget: "amd"
},
When your app loads you bootstrap you webpack modules using Dojo in a script tag.
<!-- 1. Configure and load ESRI libraries -->
<script>
window.dojoConfig = {
async: true
};
</script>
<script src="https://js.arcgis.com/4.1/"></script>
<!-- Load webpack bundles-->
<script>
require(["Angular/dist/polyfills.bundle.js", "Angular/dist/vendor.bundle.js", "Angular/dist/app.bundle.js"], function (polyfills, vendor, main) { });
</script>
I've got it working with an Angular 2 App I'm working on. The only downside is I haven't yet got the unit tests to run right using Karma. I've only been working on that a few hours now.. Hope to have a solution to the testing issue soon.
#getfuzzy's answer will work well as long as you don't need to lazy load the ArcGIS API (say for example only on a /map route).
For that you will want to take the approach I describe in this answer
This blog post explains why you need to use one of these two approaches and explains how they work as well as the pros/cons of each.
I think you can try using bower version of esrijsapi. Doc link

Cordova + Angularjs + Device Ready

I am developing a mobile application using Cordova and AngularJS. How do I restrict bootstrapping of AngluarJS before Cordova device ready. Basically I don't want to use any of AngularJS controllers before device ready.
Manually bootstrap your Angular app:
Remove your ng-app attribute from your HTML code, so Angular doesn't start itself.
Add something like this to you JavaScript code:
document.addEventListener("deviceready", function() {
// retrieve the DOM element that had the ng-app attribute
var domElement = document.getElementById(...) / document.querySelector(...);
angular.bootstrap(domElement, ["angularAppName"]);
}, false);
Angular documentation for bootstrapping apps.
I'm using the following solution, which allows AngularJS to be bootstrapped when running with Cordova as well as when running directly in a browser, which is where much of my development takes place. You have to remove the ng-app directive from your main index.html page since that's what the manual bootstrapping is replacing.
UPDATE: I've since switched to the following method, which I think is cleaner. It works for Ionic as well as vanilla Cordova/PhoneGap. It should be the last bit of JavaScript to run - perhaps inside a script tag before the /body tag.
angular.element(document).ready(function () {
if (window.cordova) {
console.log("Running in Cordova, will bootstrap AngularJS once 'deviceready' event fires.");
document.addEventListener('deviceready', function () {
console.log("Deviceready event has fired, bootstrapping AngularJS.");
angular.bootstrap(document.body, ['app']);
}, false);
} else {
console.log("Running in browser, bootstrapping AngularJS now.");
angular.bootstrap(document.body, ['app']);
}
});
Here's the older solution I used:
// This is a function that bootstraps AngularJS, which is called from later code
function bootstrapAngular() {
console.log("Bootstrapping AngularJS");
// This assumes your app is named "app" and is on the body tag: <body ng-app="app">
// Change the selector from "body" to whatever you need
var domElement = document.querySelector('body');
// Change the application name from "app" if needed
angular.bootstrap(domElement, ['app']);
}
// This is my preferred Cordova detection method, as it doesn't require updating.
if (document.URL.indexOf( 'http://' ) === -1
&& document.URL.indexOf( 'https://' ) === -1) {
console.log("URL: Running in Cordova/PhoneGap");
document.addEventListener("deviceready", bootstrapAngular, false);
} else {
console.log("URL: Running in browser");
bootstrapAngular();
}
If you run into problems with the http/https detection method, due to, perhaps, loading a Cordova app into the phone from the web, you could use the following method instead:
function bootstrapAngular() {
console.log("Bootstrapping AngularJS");
// This assumes your app is named "app" and is on the body tag: <body ng-app="app">
// Change the selector from "body" to whatever you need
var domElement = document.querySelector('body');
// Change the application name from "app" if needed
angular.bootstrap(domElement, ['app']);
}
// This method of user agent detection also works, though it means you might have to maintain this UA list
if (navigator.userAgent.match(/(iOS|iPhone|iPod|iPad|Android|BlackBerry)/)) {
console.log("UA: Running in Cordova/PhoneGap");
document.addEventListener("deviceready", bootstrapAngular, false);
} else {
console.log("UA: Running in browser");
bootstrapAngular();
}
Note that you still need the same bootstrapAngular function from the first example.
Why manually bootstrap AngularJS with Cordova/PhoneGap/Ionic?
Some people getting here might not know why you would want to do this in the first place. The issue is that you could have AngularJS code that relies on Cordova/PhoneGap/Ionic plugins, and those plugins won't be ready until after AngularJS has started because Cordova takes longer to get up and running on a device than the plain old Javascript code for AngularJS does.
So in those cases we have to wait until Cordova/PhoneGap/Ionic is ready before starting up (bootstrapping) AngularJS so that Angular will have everything it needs to run.
For example, say you are using the NG-Persist Angular module, which makes use of local storage for saving data on a browser, iOS Keychain plugin when running on iOS, and the cordova-plugin-file when running on Android. If your Angular app tries to load/save something right off the bat, NG-Persist's check on window.device.platform (from the device plugin) will fail because the mobile code hasn't completed startup yet, and you'll get nothing but a white page instead of your pretty app.
If you are using Ionic, this solution works for browsers and devices. Credit to romgar on this thread.
window.ionic.Platform.ready(function() {
angular.bootstrap(document, ['<your_main_app']);
});
Still need to remove ng-app from your DOM element.
This solution became more robust when I used:
angular.element(document).ready(function () {
var domElement = document.getElementById('appElement');
angular.bootstrap(domElement, ["angularAppName"]);
});
UPDATE
My suggestion was to put the above within the appropriate deviceready function, e.g.:
document.addEventListener("deviceready", function() {
angular.element(document).ready(function () {
var domElement = document.getElementById('appElement');
angular.bootstrap(domElement, ["angularAppName"]);
});
}, false);
On using the solution from TheHippo:
document.addEventListener("deviceready", function() {
// retrieve the DOM element that had the ng-app attribute
var domElement = document.getElementById(...) / document.querySelector(...);
angular.bootstrap(domElement, ["angularAppName"]);
}, false);
It doesn't work in the browser because "cordova.js" gets resolved by the Cordova or Phonegap building process and is not available in your localhost or emulated testing environment.
Thus the "deviceready" event is never fired. You can simply fire it manually in your browsers console.
var customDeviceReadyEvent = new Event('deviceready');
document.dispatchEvent(customDeviceReadyEvent);
Also make sure, that the bootstrap of angular gets triggered after setting all of you angular modules/controllers/factories/directives etc.
In most cases you probably don't need to block loading your angular app until after deviceready (mind that it can take several seconds for deviceready to fire if you have a lot of plugins).
Instead you can use something like this lib (https://github.com/arnesson/angular-cordova) which solves the deviceready issues for you by automatically buffering calls and then execute them after deviceready has been fired.

Resources