Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions aio/content/examples/upgrade-lazy-load-ajs/e2e/src/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { browser, element, by, ExpectedConditions } from 'protractor';

describe('Lazy Loading AngularJS Tests', function () {
const pageElements = {
homePageHref: element(by.cssContainingText('app-root nav a', 'Home')),
homePageParagraph: element(by.css('app-root app-home p')),
ajsUsersPageHref: element(by.cssContainingText('app-root nav a', 'Users')),
ajsUsersPageParagraph: element(by.css('app-root app-angular-js div p')),
notFoundPageHref: element(by.cssContainingText('app-root nav a', '404 Page')),
notFoundPageParagraph: element(by.css('app-root app-app404 p')),
};

beforeAll(async() => {
await browser.get('/');
});

it('should display \'Angular Home\' when visiting the home page', async() => {
await pageElements.homePageHref.click();

const paragraphText = await pageElements.homePageParagraph.getText();

expect(paragraphText).toEqual('Angular Home');
});

it('should display \'Users Page\' page when visiting the AngularJS page at /users', async() => {
await pageElements.ajsUsersPageHref.click();
await loadAngularJS();

const paragraphText = await pageElements.ajsUsersPageParagraph.getText();

expect(paragraphText).toEqual('Users Page');
});

it('should display \'Angular 404\' when visiting an invalid URL', async() => {
await pageElements.notFoundPageHref.click();

const paragraphText = await pageElements.notFoundPageParagraph.getText();

expect(paragraphText).toEqual('Angular 404');
});

// Workaround for https://github.com/angular/protractor/issues/4724
async function loadAngularJS() {
// Abort if `resumeBootstrap` has already occured
if (await browser.executeScript(`return '__TESTABILITY__NG1_APP_ROOT_INJECTOR__' in window;`)) {
return;
}

// Might have to re-insert the 'NG_DEFER_BOOTSTRAP!' if the name has been changed since protractor loaded the page
if (!await browser.executeScript('window.name.includes(\'NG_DEFER_BOOTSTRAP!\')')) {
await browser.executeScript('window.name = \'NG_DEFER_BOOTSTRAP!\' + name');
}

// Wait for the AngularJS bundle to download and initialize
await browser.wait(ExpectedConditions.presenceOf(element(by.css('app-root app-angular-js'))), 5000, 'AngularJS app');

// Run the protractor pre-bootstrap logic and resumeBootstrap
// Based on https://github.com/angular/protractor/blob/5.3.0/lib/browser.ts#L950-L969
{
let moduleNames = [];
for (const {name, script, args} of browser.mockModules_) {
moduleNames.push(name);
await browser.executeScriptWithDescription(script, 'add mock module ' + name, ...args);
}

await browser.executeScriptWithDescription(
// TODO: must manually assign __TESTABILITY__NG1_APP_ROOT_INJECTOR__ (https://github.com/angular/angular/issues/22723)
`window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__ = angular.resumeBootstrap(arguments[0]) `
+ `|| angular.element('app-angular-js').injector();`,
'resume bootstrap',
moduleNames
);
}

// Wait for the initial AngularJS page to finish loading
await browser.waitForAngular();
}
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"projectType": "cli-ajs"
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { LazyLoaderService } from '../lazy-loader.service';

@Component({
selector: 'app-angular-js',
template: '<div ng-view></div>'
})
export class AngularJSComponent implements OnInit {
constructor(private lazyLoader: LazyLoaderService, private elRef: ElementRef) {}
export class AngularJSComponent implements OnInit, OnDestroy {
constructor(
private lazyLoader: LazyLoaderService,
private elRef: ElementRef
) {}

ngOnInit() {
this.lazyLoader.load(this.elRef.nativeElement);
}


ngOnDestroy() {
this.lazyLoader.destroy();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { App404Component } from './app404/app404.component';
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { Injectable } from '@angular/core';
import * as angular from 'angular';

@Injectable({
providedIn: 'root'
})
export class LazyLoaderService {
bootstrapped = false;
private app: angular.auto.IInjectorService;

load(el: HTMLElement): void {
if (this.bootstrapped) {
return;
}

import('./angularjs-app').then(app => {
try {
app.bootstrap(el);
this.bootstrapped = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router destroys the component that holds the ng-view, so the options are bootstrap every time, or add a custom RouteReuseStrategy to retain the component and the root element. @jasonaden said bootstrapping was the recommendation for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is definitely NOT recommended by me 😁
Main reasons:

a. Depending on how you do the bundling, you may end up loading and running the same code repeatedly (and unnecessarily).
This doesn't seem to be an issue with how cli+webpack work at the time (AngularJS is only loaded/parsed once).
b. Since you are not cleaning up (e.g. by means of $rootScope.$destroy()), a bunch of listeners (e.g. on document or window) are left behind. In addition to that, these listeners may hold on to other stuff, leading to memory leaks.
c. As a result of the above, AngularJS state is retained, which could lead to buggy (and really hard to debug) behavior, esp. if you have multiple AngularJS routes.

Let's discuss this offline.

this.app = app.bootstrap(el);
} catch (e) {
console.error(e);
}
});
}

destroy() {
if (this.app) {
this.app.get('$rootScope').$destroy();
}
}
}
4 changes: 3 additions & 1 deletion aio/content/guide/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,8 @@ As of Angular version 8, lazy loading code can be accomplished simply by using t

The service uses the `import()` method to load your bundled AngularJS application lazily. This decreases the initial bundle size of your application as you're not loading code your user doesn't need yet. You also need to provide a way to _bootstrap_ the application manually after it has been loaded. AngularJS provides a way to manually bootstrap an application using the [angular.bootstrap()](https://docs.angularjs.org/api/ng/function/angular.bootstrap) method with a provided HTML element. Your AngularJS app should also expose a `bootstrap` method that bootstraps the AngularJS app.

To ensure any necessary teardown is triggered in the AngularJS app, such as removal of global listeners, you also implement a method to call the `$rootScope.destroy()` method.

<code-example path="upgrade-lazy-load-ajs/src/app/angularjs-app/index.ts" header="angularjs-app">
</code-example>

Expand All @@ -886,7 +888,7 @@ In your Angular application, you need a component as a placeholder for your Angu
<code-example path="upgrade-lazy-load-ajs/src/app/angular-js/angular-js.component.ts" header="src/app/angular-js/angular-js.component.ts">
</code-example>

When the Angular Router matches a route that uses AngularJS, the `AngularJSComponent` is rendered, and the content is rendered within the AngularJS [`ng-view`](https://docs.angularjs.org/api/ngRoute/directive/ngView) directive.
When the Angular Router matches a route that uses AngularJS, the `AngularJSComponent` is rendered, and the content is rendered within the AngularJS [`ng-view`](https://docs.angularjs.org/api/ngRoute/directive/ngView) directive. When the user navigates away from the route, the `$rootScope` is destroyed on the AngularJS application.

### Configure a custom route matcher for AngularJS routes

Expand Down
23 changes: 23 additions & 0 deletions aio/tools/example-zipper/customizer/package-json/cli-ajs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"scripts": [
{ "name": "ng", "command": "ng" },
{ "name": "build", "command": "ng build --prod" },
{ "name": "start", "command": "ng serve" },
{ "name": "test", "command": "ng test" },
{ "name": "lint", "command": "ng lint" },
{ "name": "e2e", "command": "ng e2e" }
],
"dependencies": [
"angular",
"angular-route"
],
"devDependencies": [
"@angular/cli",
"@types/angular",
"@types/angular-route",
"@types/jasminewd2",
"jasmine-spec-reporter",
"karma-coverage-istanbul-reporter",
"ts-node"
]
}
5 changes: 5 additions & 0 deletions aio/tools/examples/example-boilerplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ BOILERPLATE_PATHS.schematics = [
'angular.json'
];

BOILERPLATE_PATHS['cli-ajs'] = [
...cliRelativePath,
'package.json'
];

const EXAMPLE_CONFIG_FILENAME = 'example-config.json';

class ExampleBoilerPlate {
Expand Down
56 changes: 56 additions & 0 deletions aio/tools/examples/shared/boilerplate/cli-ajs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "angular.io-example",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^8.0.0",
"@angular/common": "^8.0.0",
"@angular/compiler": "^8.0.0",
"@angular/core": "^8.0.0",
"@angular/forms": "^8.0.0",
"@angular/platform-browser": "^8.0.0",
"@angular/platform-browser-dynamic": "^8.0.0",
"@angular/router": "^8.0.0",
"angular": "1.7.8",
"angular-in-memory-web-api": "^0.8.0",
"angular-route": "1.7.8",
"core-js": "^2.5.4",
"rxjs": "^6.5.1",
"tslib": "^1.9.0",
"web-animations-js": "^2.3.1",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.800.0",
"@angular/cli": "^8.0.0",
"@angular/compiler-cli": "^8.0.0",
"@angular/language-service": "^8.0.0",
"@types/angular": "^1.6.47",
"@types/angular-route": "^1.3.5",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~5.0.0",
"jasmine-core": "~2.99.1",
"jasmine-marbles": "^0.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.4.4"
}
}
2 changes: 2 additions & 0 deletions aio/tools/examples/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"@nguniversal/common": "^8.0.0-rc.1",
"@nguniversal/express-engine": "^8.0.0-rc.1",
"@nguniversal/module-map-ngfactory-loader": "^8.0.0-rc.1",
"angular": "1.7.8",
"angular-in-memory-web-api": "github:brandonroberts/in-memory-web-api-bazel#50a34d8",
"angular-route": "1.7.8",
"core-js": "^2.5.4",
"express": "^4.14.1",
"rxjs": "^6.5.1",
Expand Down
10 changes: 10 additions & 0 deletions aio/tools/examples/shared/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,16 @@ amdefine@>=0.0.4:
dependencies:
tslib "^1.9.0"

angular-route@1.7.8:
version "1.7.8"
resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.7.8.tgz#d502aa605dcbb253a93e844c0adf51c9bc36b9fa"
integrity sha512-VVk89PH0fsY5kfbx+N7IVX1IwnaPWYhMGY0uA+rjej2v1sjvrTx1SLkxUK4E0UpW1hXeLJhN7ncBcwoBiPtAtA==

angular@1.7.8:
version "1.7.8"
resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.8.tgz#b77ede272ce1b261e3be30c1451a0b346905a3c9"
integrity sha512-wtef/y4COxM7ZVhddd7JtAAhyYObq9YXKar9tsW7558BImeVYteJiTxCKeJOL45lJ/+7B4wrAC49j8gTFYEthg==

ansi-colors@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.1.0.tgz#dcfaacc90ef9187de413ec3ef8d5eb981a98808f"
Expand Down