refactor(upgrade): use Bazel packages to avoid symlinks in the source (#29466)

Previously we had to share code between upgrade/dynamic and upgrade/static
by symlinking the `src` folder, which allowed both packages to access
the upgrade/common files.

These symlinks are always problematic on Windows, where we had to run
a script to re-link them, and restore them.

This change uses Bazel packages to share the `upgrade/common` code,
which avoids the need for symlinking the `src` folder.

Also, the Windows specific scripts that fixup the symlinks have also
been removed as there is no more need for them.

PR Close #29466
This commit is contained in:
Pete Bacon Darwin
2019-03-22 09:42:52 +00:00
committed by Jason Aden
parent e5201f92fc
commit 9f54d76ef5
49 changed files with 219 additions and 162 deletions

View File

@ -6,18 +6,15 @@ exports_files(["package.json"])
ng_module(
name = "static",
# Note: There is Bazel issue where Windows symlinks
# aka. junctions are not traversed by glob.
srcs = glob(
[
"*.ts",
"src/common/**/*.ts",
"src/static/**/*.ts",
"src/*.ts",
],
),
deps = [
"//packages/core",
"//packages/platform-browser",
"//packages/platform-browser-dynamic",
"//packages/upgrade/src/common",
],
)

View File

@ -6,19 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
/**
* @module
* @description
* Entry point for all public APIs of this package. allowing
* Angular 1 and Angular 2+ to run side by side in the same application.
*/
export {getAngularJSGlobal, getAngularLib, setAngularJSGlobal, setAngularLib} from './src/common/angular1';
export {downgradeComponent} from './src/common/downgrade_component';
export {downgradeInjectable} from './src/common/downgrade_injectable';
export {VERSION} from './src/common/version';
export {downgradeModule} from './src/static/downgrade_module';
export {UpgradeComponent} from './src/static/upgrade_component';
export {UpgradeModule} from './src/static/upgrade_module';
export {getAngularJSGlobal, getAngularLib, setAngularJSGlobal, setAngularLib} from '../src/common/src/angular1';
export {downgradeComponent} from '../src/common/src/downgrade_component';
export {downgradeInjectable} from '../src/common/src/downgrade_injectable';
export {VERSION} from '../src/common/src/version';
export {downgradeModule} from './src/downgrade_module';
export {UpgradeComponent} from './src/upgrade_component';
export {UpgradeModule} from './src/upgrade_module';
// This file only re-exports content of the `src` folder. Keep it that way.
// This file only re-exports items to appear in the public api. Keep it that way.

View File

@ -1 +0,0 @@
../src

View File

@ -0,0 +1,51 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {IInjectorService} from '../../src/common/src/angular1';
// We have to do a little dance to get the ng1 injector into the module injector.
// We store the ng1 injector so that the provider in the module injector can access it
// Then we "get" the ng1 injector from the module injector, which triggers the provider to read
// the stored injector and release the reference to it.
let tempInjectorRef: IInjectorService|null = null;
export function setTempInjectorRef(injector: IInjectorService) {
tempInjectorRef = injector;
}
export function injectorFactory() {
if (!tempInjectorRef) {
throw new Error('Trying to get the AngularJS injector before it being set.');
}
const injector: IInjectorService = tempInjectorRef;
tempInjectorRef = null; // clear the value to prevent memory leaks
return injector;
}
export function rootScopeFactory(i: IInjectorService) {
return i.get('$rootScope');
}
export function compileFactory(i: IInjectorService) {
return i.get('$compile');
}
export function parseFactory(i: IInjectorService) {
return i.get('$parse');
}
export const angular1Providers = [
// We must use exported named functions for the ng2 factories to keep the compiler happy:
// > Metadata collected contains an error that will be reported at runtime:
// > Function calls are not supported.
// > Consider replacing the function or lambda with a reference to an exported function
{provide: '$injector', useFactory: injectorFactory, deps: []},
{provide: '$rootScope', useFactory: rootScopeFactory, deps: ['$injector']},
{provide: '$compile', useFactory: compileFactory, deps: ['$injector']},
{provide: '$parse', useFactory: parseFactory, deps: ['$injector']}
];

View File

@ -0,0 +1,189 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, NgModuleFactory, NgModuleRef, StaticProvider} from '@angular/core';
import {platformBrowser} from '@angular/platform-browser';
import {IInjectorService, IProvideService, module as angularModule} from '../../src/common/src/angular1';
import {$INJECTOR, $PROVIDE, DOWNGRADED_MODULE_COUNT_KEY, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants';
import {LazyModuleRef, UpgradeAppType, getDowngradedModuleCount, isFunction} from '../../src/common/src/util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {NgAdapterInjector} from './util';
let moduleUid = 0;
/**
* @description
*
* A helper function for creating an AngularJS module that can bootstrap an Angular module
* "on-demand" (possibly lazily) when a {@link downgradeComponent downgraded component} needs to be
* instantiated.
*
* *Part of the [upgrade/static](api?query=upgrade/static) library for hybrid upgrade apps that
* support AoT compilation.*
*
* It allows loading/bootstrapping the Angular part of a hybrid application lazily and not having to
* pay the cost up-front. For example, you can have an AngularJS application that uses Angular for
* specific routes and only instantiate the Angular modules if/when the user visits one of these
* routes.
*
* The Angular module will be bootstrapped once (when requested for the first time) and the same
* reference will be used from that point onwards.
*
* `downgradeModule()` requires either an `NgModuleFactory` or a function:
* - `NgModuleFactory`: If you pass an `NgModuleFactory`, it will be used to instantiate a module
* using `platformBrowser`'s {@link PlatformRef#bootstrapModuleFactory bootstrapModuleFactory()}.
* - `Function`: If you pass a function, it is expected to return a promise resolving to an
* `NgModuleRef`. The function is called with an array of extra {@link StaticProvider Providers}
* that are expected to be available from the returned `NgModuleRef`'s `Injector`.
*
* `downgradeModule()` returns the name of the created AngularJS wrapper module. You can use it to
* declare a dependency in your main AngularJS module.
*
* {@example upgrade/static/ts/lite/module.ts region="basic-how-to"}
*
* For more details on how to use `downgradeModule()` see
* [Upgrading for Performance](guide/upgrade-performance).
*
* @usageNotes
*
* Apart from `UpgradeModule`, you can use the rest of the `upgrade/static` helpers as usual to
* build a hybrid application. Note that the Angular pieces (e.g. downgraded services) will not be
* available until the downgraded module has been bootstrapped, i.e. by instantiating a downgraded
* component.
*
* <div class="alert is-important">
*
* You cannot use `downgradeModule()` and `UpgradeModule` in the same hybrid application.<br />
* Use one or the other.
*
* </div>
*
* ### Differences with `UpgradeModule`
*
* Besides their different API, there are two important internal differences between
* `downgradeModule()` and `UpgradeModule` that affect the behavior of hybrid applications:
*
* 1. Unlike `UpgradeModule`, `downgradeModule()` does not bootstrap the main AngularJS module
* inside the {@link NgZone Angular zone}.
* 2. Unlike `UpgradeModule`, `downgradeModule()` does not automatically run a
* [$digest()](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest) when changes are
* detected in the Angular part of the application.
*
* What this means is that applications using `UpgradeModule` will run change detection more
* frequently in order to ensure that both frameworks are properly notified about possible changes.
* This will inevitably result in more change detection runs than necessary.
*
* `downgradeModule()`, on the other side, does not try to tie the two change detection systems as
* tightly, restricting the explicit change detection runs only to cases where it knows it is
* necessary (e.g. when the inputs of a downgraded component change). This improves performance,
* especially in change-detection-heavy applications, but leaves it up to the developer to manually
* notify each framework as needed.
*
* For a more detailed discussion of the differences and their implications, see
* [Upgrading for Performance](guide/upgrade-performance).
*
* <div class="alert is-helpful">
*
* You can manually trigger a change detection run in AngularJS using
* [scope.$apply(...)](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply) or
* [$rootScope.$digest()](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest).
*
* You can manually trigger a change detection run in Angular using {@link NgZone#run
* ngZone.run(...)}.
*
* </div>
*
* ### Downgrading multiple modules
*
* It is possible to downgrade multiple modules and include them in an AngularJS application. In
* that case, each downgraded module will be bootstrapped when an associated downgraded component or
* injectable needs to be instantiated.
*
* Things to keep in mind, when downgrading multiple modules:
*
* - Each downgraded component/injectable needs to be explicitly associated with a downgraded
* module. See `downgradeComponent()` and `downgradeInjectable()` for more details.
*
* - If you want some injectables to be shared among all downgraded modules, you can provide them as
* `StaticProvider`s, when creating the `PlatformRef` (e.g. via `platformBrowser` or
* `platformBrowserDynamic`).
*
* - When using {@link PlatformRef#bootstrapmodule `bootstrapModule()`} or
* {@link PlatformRef#bootstrapmodulefactory `bootstrapModuleFactory()`} to bootstrap the
* downgraded modules, each one is considered a "root" module. As a consequence, a new instance
* will be created for every injectable provided in `"root"` (via
* {@link Injectable#providedIn `providedIn`}).
* If this is not your intention, you can have a shared module (that will act as act as the "root"
* module) and create all downgraded modules using that module's injector:
*
* {@example upgrade/static/ts/lite-multi-shared/module.ts region="shared-root-module"}
*
* @publicApi
*/
export function downgradeModule<T>(
moduleFactoryOrBootstrapFn: NgModuleFactory<T>|
((extraProviders: StaticProvider[]) => Promise<NgModuleRef<T>>)): string {
const lazyModuleName = `${UPGRADE_MODULE_NAME}.lazy${++moduleUid}`;
const lazyModuleRefKey = `${LAZY_MODULE_REF}${lazyModuleName}`;
const lazyInjectorKey = `${INJECTOR_KEY}${lazyModuleName}`;
const bootstrapFn = isFunction(moduleFactoryOrBootstrapFn) ?
moduleFactoryOrBootstrapFn :
(extraProviders: StaticProvider[]) =>
platformBrowser(extraProviders).bootstrapModuleFactory(moduleFactoryOrBootstrapFn);
let injector: Injector;
// Create an ng1 module to bootstrap.
angularModule(lazyModuleName, [])
.constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Lite)
.factory(INJECTOR_KEY, [lazyInjectorKey, identity])
.factory(
lazyInjectorKey,
() => {
if (!injector) {
throw new Error(
'Trying to get the Angular injector before bootstrapping the corresponding ' +
'Angular module.');
}
return injector;
})
.factory(LAZY_MODULE_REF, [lazyModuleRefKey, identity])
.factory(
lazyModuleRefKey,
[
$INJECTOR,
($injector: IInjectorService) => {
setTempInjectorRef($injector);
const result: LazyModuleRef = {
promise: bootstrapFn(angular1Providers).then(ref => {
injector = result.injector = new NgAdapterInjector(ref.injector);
injector.get($INJECTOR);
return injector;
})
};
return result;
}
])
.config([
$INJECTOR, $PROVIDE,
($injector: IInjectorService, $provide: IProvideService) => {
$provide.constant(DOWNGRADED_MODULE_COUNT_KEY, getDowngradedModuleCount($injector) + 1);
}
]);
return lazyModuleName;
}
function identity<T = any>(x: T): T {
return x;
}

View File

@ -0,0 +1,297 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, SimpleChanges, ɵlooseIdentical as looseIdentical} from '@angular/core';
import {IAttributes, IAugmentedJQuery, IDirective, IDirectivePrePost, IInjectorService, ILinkFn, IScope, ITranscludeFunction} from '../../src/common/src/angular1';
import {$SCOPE} from '../../src/common/src/constants';
import {IBindingDestination, IControllerInstance, UpgradeHelper} from '../../src/common/src/upgrade_helper';
import {isFunction} from '../../src/common/src/util';
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
class Bindings {
twoWayBoundProperties: string[] = [];
twoWayBoundLastValues: any[] = [];
expressionBoundProperties: string[] = [];
propertyToOutputMap: {[propName: string]: string} = {};
}
/**
* @description
*
* A helper class that allows an AngularJS component to be used from Angular.
*
* *Part of the [upgrade/static](api?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AoT compilation.*
*
* This helper class should be used as a base class for creating Angular directives
* that wrap AngularJS components that need to be "upgraded".
*
* @usageNotes
* ### Examples
*
* Let's assume that you have an AngularJS component called `ng1Hero` that needs
* to be made available in Angular templates.
*
* {@example upgrade/static/ts/full/module.ts region="ng1-hero"}
*
* We must create a `Directive` that will make this AngularJS component
* available inside Angular templates.
*
* {@example upgrade/static/ts/full/module.ts region="ng1-hero-wrapper"}
*
* In this example you can see that we must derive from the `UpgradeComponent`
* base class but also provide an {@link Directive `@Directive`} decorator. This is
* because the AoT compiler requires that this information is statically available at
* compile time.
*
* Note that we must do the following:
* * specify the directive's selector (`ng1-hero`)
* * specify all inputs and outputs that the AngularJS component expects
* * derive from `UpgradeComponent`
* * call the base class from the constructor, passing
* * the AngularJS name of the component (`ng1Hero`)
* * the `ElementRef` and `Injector` for the component wrapper
*
* @publicApi
*/
export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
private helper: UpgradeHelper;
private $injector: IInjectorService;
private element: Element;
private $element: IAugmentedJQuery;
private $componentScope: IScope;
private directive: IDirective;
private bindings: Bindings;
// TODO(issue/24571): remove '!'.
private controllerInstance !: IControllerInstance;
// TODO(issue/24571): remove '!'.
private bindingDestination !: IBindingDestination;
// We will be instantiating the controller in the `ngOnInit` hook, when the
// first `ngOnChanges` will have been already triggered. We store the
// `SimpleChanges` and "play them back" later.
// TODO(issue/24571): remove '!'.
private pendingChanges !: SimpleChanges | null;
// TODO(issue/24571): remove '!'.
private unregisterDoCheckWatcher !: Function;
/**
* Create a new `UpgradeComponent` instance. You should not normally need to do this.
* Instead you should derive a new class from this one and call the super constructor
* from the base class.
*
* {@example upgrade/static/ts/full/module.ts region="ng1-hero-wrapper" }
*
* * The `name` parameter should be the name of the AngularJS directive.
* * The `elementRef` and `injector` parameters should be acquired from Angular by dependency
* injection into the base class constructor.
*/
constructor(private name: string, private elementRef: ElementRef, private injector: Injector) {
this.helper = new UpgradeHelper(injector, name, elementRef);
this.$injector = this.helper.$injector;
this.element = this.helper.element;
this.$element = this.helper.$element;
this.directive = this.helper.directive;
this.bindings = this.initializeBindings(this.directive);
// We ask for the AngularJS scope from the Angular injector, since
// we will put the new component scope onto the new injector for each component
const $parentScope = injector.get($SCOPE);
// QUESTION 1: Should we create an isolated scope if the scope is only true?
// QUESTION 2: Should we make the scope accessible through `$element.scope()/isolateScope()`?
this.$componentScope = $parentScope.$new(!!this.directive.scope);
this.initializeOutputs();
}
ngOnInit() {
// Collect contents, insert and compile template
const attachChildNodes: ILinkFn|undefined = this.helper.prepareTransclusion();
const linkFn = this.helper.compileTemplate();
// Instantiate controller
const controllerType = this.directive.controller;
const bindToController = this.directive.bindToController;
if (controllerType) {
this.controllerInstance = this.helper.buildController(controllerType, this.$componentScope);
} else if (bindToController) {
throw new Error(
`Upgraded directive '${this.directive.name}' specifies 'bindToController' but no controller.`);
}
// Set up outputs
this.bindingDestination = bindToController ? this.controllerInstance : this.$componentScope;
this.bindOutputs();
// Require other controllers
const requiredControllers =
this.helper.resolveAndBindRequiredControllers(this.controllerInstance);
// Hook: $onChanges
if (this.pendingChanges) {
this.forwardChanges(this.pendingChanges);
this.pendingChanges = null;
}
// Hook: $onInit
if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) {
this.controllerInstance.$onInit();
}
// Hook: $doCheck
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
const callDoCheck = () => this.controllerInstance.$doCheck !();
this.unregisterDoCheckWatcher = this.$componentScope.$parent.$watch(callDoCheck);
callDoCheck();
}
// Linking
const link = this.directive.link;
const preLink = (typeof link == 'object') && (link as IDirectivePrePost).pre;
const postLink = (typeof link == 'object') ? (link as IDirectivePrePost).post : link;
const attrs: IAttributes = NOT_SUPPORTED;
const transcludeFn: ITranscludeFunction = NOT_SUPPORTED;
if (preLink) {
preLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
}
linkFn(this.$componentScope, null !, {parentBoundTranscludeFn: attachChildNodes});
if (postLink) {
postLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
}
// Hook: $postLink
if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) {
this.controllerInstance.$postLink();
}
}
ngOnChanges(changes: SimpleChanges) {
if (!this.bindingDestination) {
this.pendingChanges = changes;
} else {
this.forwardChanges(changes);
}
}
ngDoCheck() {
const twoWayBoundProperties = this.bindings.twoWayBoundProperties;
const twoWayBoundLastValues = this.bindings.twoWayBoundLastValues;
const propertyToOutputMap = this.bindings.propertyToOutputMap;
twoWayBoundProperties.forEach((propName, idx) => {
const newValue = this.bindingDestination[propName];
const oldValue = twoWayBoundLastValues[idx];
if (!looseIdentical(newValue, oldValue)) {
const outputName = propertyToOutputMap[propName];
const eventEmitter: EventEmitter<any> = (this as any)[outputName];
eventEmitter.emit(newValue);
twoWayBoundLastValues[idx] = newValue;
}
});
}
ngOnDestroy() {
if (isFunction(this.unregisterDoCheckWatcher)) {
this.unregisterDoCheckWatcher();
}
this.helper.onDestroy(this.$componentScope, this.controllerInstance);
}
private initializeBindings(directive: IDirective) {
const btcIsObject = typeof directive.bindToController === 'object';
if (btcIsObject && Object.keys(directive.scope !).length) {
throw new Error(
`Binding definitions on scope and controller at the same time is not supported.`);
}
const context = (btcIsObject) ? directive.bindToController : directive.scope;
const bindings = new Bindings();
if (typeof context == 'object') {
Object.keys(context).forEach(propName => {
const definition = context[propName];
const bindingType = definition.charAt(0);
// QUESTION: What about `=*`? Ignore? Throw? Support?
switch (bindingType) {
case '@':
case '<':
// We don't need to do anything special. They will be defined as inputs on the
// upgraded component facade and the change propagation will be handled by
// `ngOnChanges()`.
break;
case '=':
bindings.twoWayBoundProperties.push(propName);
bindings.twoWayBoundLastValues.push(INITIAL_VALUE);
bindings.propertyToOutputMap[propName] = propName + 'Change';
break;
case '&':
bindings.expressionBoundProperties.push(propName);
bindings.propertyToOutputMap[propName] = propName;
break;
default:
let json = JSON.stringify(context);
throw new Error(
`Unexpected mapping '${bindingType}' in '${json}' in '${this.name}' directive.`);
}
});
}
return bindings;
}
private initializeOutputs() {
// Initialize the outputs for `=` and `&` bindings
this.bindings.twoWayBoundProperties.concat(this.bindings.expressionBoundProperties)
.forEach(propName => {
const outputName = this.bindings.propertyToOutputMap[propName];
(this as any)[outputName] = new EventEmitter();
});
}
private bindOutputs() {
// Bind `&` bindings to the corresponding outputs
this.bindings.expressionBoundProperties.forEach(propName => {
const outputName = this.bindings.propertyToOutputMap[propName];
const emitter = (this as any)[outputName];
this.bindingDestination[propName] = (value: any) => emitter.emit(value);
});
}
private forwardChanges(changes: SimpleChanges) {
// Forward input changes to `bindingDestination`
Object.keys(changes).forEach(
propName => this.bindingDestination[propName] = changes[propName].currentValue);
if (isFunction(this.bindingDestination.$onChanges)) {
this.bindingDestination.$onChanges(changes);
}
}
}

View File

@ -0,0 +1,285 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, NgModule, NgZone, Testability} from '@angular/core';
import {IInjectorService, IIntervalService, IProvideService, ITestabilityService, bootstrap, element as angularElement, module as angularModule} from '../../src/common/src/angular1';
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants';
import {LazyModuleRef, UpgradeAppType, controllerKey} from '../../src/common/src/util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {NgAdapterInjector} from './util';
/**
* @description
*
* An `NgModule`, which you import to provide AngularJS core services,
* and has an instance method used to bootstrap the hybrid upgrade application.
*
* *Part of the [upgrade/static](api?query=upgrade/static)
* library for hybrid upgrade apps that support AoT compilation*
*
* The `upgrade/static` package contains helpers that allow AngularJS and Angular components
* to be used together inside a hybrid upgrade application, which supports AoT compilation.
*
* Specifically, the classes and functions in the `upgrade/static` module allow the following:
*
* 1. Creation of an Angular directive that wraps and exposes an AngularJS component so
* that it can be used in an Angular template. See `UpgradeComponent`.
* 2. Creation of an AngularJS directive that wraps and exposes an Angular component so
* that it can be used in an AngularJS template. See `downgradeComponent`.
* 3. Creation of an Angular root injector provider that wraps and exposes an AngularJS
* service so that it can be injected into an Angular context. See
* {@link UpgradeModule#upgrading-an-angular-1-service Upgrading an AngularJS service} below.
* 4. Creation of an AngularJS service that wraps and exposes an Angular injectable
* so that it can be injected into an AngularJS context. See `downgradeInjectable`.
* 3. Bootstrapping of a hybrid Angular application which contains both of the frameworks
* coexisting in a single application.
*
* @usageNotes
*
* ```ts
* import {UpgradeModule} from '@angular/upgrade/static';
* ```
*
* See also the {@link UpgradeModule#examples examples} below.
*
* ### Mental Model
*
* When reasoning about how a hybrid application works it is useful to have a mental model which
* describes what is happening and explains what is happening at the lowest level.
*
* 1. There are two independent frameworks running in a single application, each framework treats
* the other as a black box.
* 2. Each DOM element on the page is owned exactly by one framework. Whichever framework
* instantiated the element is the owner. Each framework only updates/interacts with its own
* DOM elements and ignores others.
* 3. AngularJS directives always execute inside the AngularJS framework codebase regardless of
* where they are instantiated.
* 4. Angular components always execute inside the Angular framework codebase regardless of
* where they are instantiated.
* 5. An AngularJS component can be "upgraded"" to an Angular component. This is achieved by
* defining an Angular directive, which bootstraps the AngularJS component at its location
* in the DOM. See `UpgradeComponent`.
* 6. An Angular component can be "downgraded" to an AngularJS component. This is achieved by
* defining an AngularJS directive, which bootstraps the Angular component at its location
* in the DOM. See `downgradeComponent`.
* 7. Whenever an "upgraded"/"downgraded" component is instantiated the host element is owned by
* the framework doing the instantiation. The other framework then instantiates and owns the
* view for that component.
* 1. This implies that the component bindings will always follow the semantics of the
* instantiation framework.
* 2. The DOM attributes are parsed by the framework that owns the current template. So
* attributes in AngularJS templates must use kebab-case, while AngularJS templates must use
* camelCase.
* 3. However the template binding syntax will always use the Angular style, e.g. square
* brackets (`[...]`) for property binding.
* 8. Angular is bootstrapped first; AngularJS is bootstrapped second. AngularJS always owns the
* root component of the application.
* 9. The new application is running in an Angular zone, and therefore it no longer needs calls to
* `$apply()`.
*
* ### The `UpgradeModule` class
*
* This class is an `NgModule`, which you import to provide AngularJS core services,
* and has an instance method used to bootstrap the hybrid upgrade application.
*
* * Core AngularJS services
* Importing this `NgModule` will add providers for the core
* [AngularJS services](https://docs.angularjs.org/api/ng/service) to the root injector.
*
* * Bootstrap
* The runtime instance of this class contains a {@link UpgradeModule#bootstrap `bootstrap()`}
* method, which you use to bootstrap the top level AngularJS module onto an element in the
* DOM for the hybrid upgrade app.
*
* It also contains properties to access the {@link UpgradeModule#injector root injector}, the
* bootstrap `NgZone` and the
* [AngularJS $injector](https://docs.angularjs.org/api/auto/service/$injector).
*
* ### Examples
*
* Import the `UpgradeModule` into your top level {@link NgModule Angular `NgModule`}.
*
* {@example upgrade/static/ts/full/module.ts region='ng2-module'}
*
* Then inject `UpgradeModule` into your Angular `NgModule` and use it to bootstrap the top level
* [AngularJS module](https://docs.angularjs.org/api/ng/type/angular.Module) in the
* `ngDoBootstrap()` method.
*
* {@example upgrade/static/ts/full/module.ts region='bootstrap-ng1'}
*
* Finally, kick off the whole process, by bootstraping your top level Angular `NgModule`.
*
* {@example upgrade/static/ts/full/module.ts region='bootstrap-ng2'}
*
* {@a upgrading-an-angular-1-service}
* ### Upgrading an AngularJS service
*
* There is no specific API for upgrading an AngularJS service. Instead you should just follow the
* following recipe:
*
* Let's say you have an AngularJS service:
*
* {@example upgrade/static/ts/full/module.ts region="ng1-text-formatter-service"}
*
* Then you should define an Angular provider to be included in your `NgModule` `providers`
* property.
*
* {@example upgrade/static/ts/full/module.ts region="upgrade-ng1-service"}
*
* Then you can use the "upgraded" AngularJS service by injecting it into an Angular component
* or service.
*
* {@example upgrade/static/ts/full/module.ts region="use-ng1-upgraded-service"}
*
* @publicApi
*/
@NgModule({providers: [angular1Providers]})
export class UpgradeModule {
/**
* The AngularJS `$injector` for the upgrade application.
*/
public $injector: any /*angular.IInjectorService*/;
/** The Angular Injector **/
public injector: Injector;
constructor(
/** The root `Injector` for the upgrade application. */
injector: Injector,
/** The bootstrap zone for the upgrade application */
public ngZone: NgZone) {
this.injector = new NgAdapterInjector(injector);
}
/**
* Bootstrap an AngularJS application from this NgModule
* @param element the element on which to bootstrap the AngularJS application
* @param [modules] the AngularJS modules to bootstrap for this application
* @param [config] optional extra AngularJS bootstrap configuration
*/
bootstrap(
element: Element, modules: string[] = [], config?: any /*angular.IAngularBootstrapConfig*/) {
const INIT_MODULE_NAME = UPGRADE_MODULE_NAME + '.init';
// Create an ng1 module to bootstrap
const initModule =
angularModule(INIT_MODULE_NAME, [])
.constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static)
.value(INJECTOR_KEY, this.injector)
.factory(
LAZY_MODULE_REF,
[INJECTOR_KEY, (injector: Injector) => ({ injector } as LazyModuleRef)])
.config([
$PROVIDE, $INJECTOR,
($provide: IProvideService, $injector: IInjectorService) => {
if ($injector.has($$TESTABILITY)) {
$provide.decorator($$TESTABILITY, [
$DELEGATE,
(testabilityDelegate: ITestabilityService) => {
const originalWhenStable: Function = testabilityDelegate.whenStable;
const injector = this.injector;
// Cannot use arrow function below because we need the context
const newWhenStable = function(callback: Function) {
originalWhenStable.call(testabilityDelegate, function() {
const ng2Testability: Testability = injector.get(Testability);
if (ng2Testability.isStable()) {
callback();
} else {
ng2Testability.whenStable(
newWhenStable.bind(testabilityDelegate, callback));
}
});
};
testabilityDelegate.whenStable = newWhenStable;
return testabilityDelegate;
}
]);
}
if ($injector.has($INTERVAL)) {
$provide.decorator($INTERVAL, [
$DELEGATE,
(intervalDelegate: IIntervalService) => {
// Wrap the $interval service so that setInterval is called outside NgZone,
// but the callback is still invoked within it. This is so that $interval
// won't block stability, which preserves the behavior from AngularJS.
let wrappedInterval =
(fn: Function, delay: number, count?: number, invokeApply?: boolean,
...pass: any[]) => {
return this.ngZone.runOutsideAngular(() => {
return intervalDelegate((...args: any[]) => {
// Run callback in the next VM turn - $interval calls
// $rootScope.$apply, and running the callback in NgZone will
// cause a '$digest already in progress' error if it's in the
// same vm turn.
setTimeout(() => { this.ngZone.run(() => fn(...args)); });
}, delay, count, invokeApply, ...pass);
});
};
(wrappedInterval as any)['cancel'] = intervalDelegate.cancel;
return wrappedInterval;
}
]);
}
}
])
.run([
$INJECTOR,
($injector: IInjectorService) => {
this.$injector = $injector;
// Initialize the ng1 $injector provider
setTempInjectorRef($injector);
this.injector.get($INJECTOR);
// Put the injector on the DOM, so that it can be "required"
angularElement(element).data !(controllerKey(INJECTOR_KEY), this.injector);
// Wire up the ng1 rootScope to run a digest cycle whenever the zone settles
// We need to do this in the next tick so that we don't prevent the bootup
// stabilizing
setTimeout(() => {
const $rootScope = $injector.get('$rootScope');
const subscription =
this.ngZone.onMicrotaskEmpty.subscribe(() => $rootScope.$digest());
$rootScope.$on('$destroy', () => { subscription.unsubscribe(); });
}, 0);
}
]);
const upgradeModule = angularModule(UPGRADE_MODULE_NAME, [INIT_MODULE_NAME].concat(modules));
// Make sure resumeBootstrap() only exists if the current bootstrap is deferred
const windowAngular = (window as any)['angular'];
windowAngular.resumeBootstrap = undefined;
// Bootstrap the AngularJS application inside our zone
this.ngZone.run(() => { bootstrap(element, [upgradeModule.name], config); });
// Patch resumeBootstrap() to run inside the ngZone
if (windowAngular.resumeBootstrap) {
const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap;
const ngZone = this.ngZone;
windowAngular.resumeBootstrap = function() {
let args = arguments;
windowAngular.resumeBootstrap = originalResumeBootstrap;
return ngZone.run(() => windowAngular.resumeBootstrap.apply(this, args));
};
}
}
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '@angular/core';
export class NgAdapterInjector implements Injector {
constructor(private modInjector: Injector) {}
// When Angular locate a service in the component injector tree, the not found value is set to
// `NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR`. In such a case we should not walk up to the module
// injector.
// AngularJS only supports a single tree and should always check the module injector.
get(token: any, notFoundValue?: any): any {
if (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
return notFoundValue;
}
return this.modInjector.get(token, notFoundValue);
}
}

View File

@ -0,0 +1,29 @@
load("//tools:defaults.bzl", "ts_library", "ts_web_test_suite")
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/platform-browser-dynamic",
"//packages/platform-browser/testing",
"//packages/upgrade/src/common",
"//packages/upgrade/src/common/test/helpers",
"//packages/upgrade/static",
],
)
ts_web_test_suite(
name = "test",
static_files = [
"//:angularjs_scripts",
],
deps = [
":test_lib",
],
)

View File

@ -0,0 +1,68 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Ng1Token} from '../../src/common/src/angular1';
import {compileFactory, injectorFactory, parseFactory, rootScopeFactory, setTempInjectorRef} from '../src/angular1_providers';
{
describe('upgrade angular1_providers', () => {
describe('compileFactory', () => {
it('should retrieve and return `$compile`', () => {
const services: {[key: string]: any} = {$compile: 'foo'};
const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true};
expect(compileFactory(mockInjector)).toBe('foo');
});
});
describe('injectorFactory', () => {
it('should return the injector value that was previously set', () => {
const mockInjector = {get: () => undefined, has: () => false};
setTempInjectorRef(mockInjector);
const injector = injectorFactory();
expect(injector).toBe(mockInjector);
});
it('should throw if the injector value is not set', () => {
// Ensure the injector is not set. This shouldn't be necessary, but on CI there seems to be
// some race condition with previous tests not being cleaned up properly.
// Related:
// - https://github.com/angular/angular/pull/28045
// - https://github.com/angular/angular/pull/28181
setTempInjectorRef(null as any);
expect(injectorFactory).toThrowError();
});
it('should unset the injector after the first call (to prevent memory leaks)', () => {
const mockInjector = {get: () => undefined, has: () => false};
setTempInjectorRef(mockInjector);
injectorFactory();
expect(injectorFactory).toThrowError(); // ...because it has been unset
});
});
describe('parseFactory', () => {
it('should retrieve and return `$parse`', () => {
const services: {[key: string]: any} = {$parse: 'bar'};
const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true};
expect(parseFactory(mockInjector)).toBe('bar');
});
});
describe('rootScopeFactory', () => {
it('should retrieve and return `$rootScope`', () => {
const services: {[key: string]: any} = {$rootScope: 'baz'};
const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true};
expect(rootScopeFactory(mockInjector)).toBe('baz');
});
});
});
}

View File

@ -0,0 +1,160 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, Input, NgModule, NgZone, SimpleChanges, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
import {html, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {bootstrap} from './static_test_helpers';
withEachNg1Version(() => {
describe('scope/component change-detection', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should interleave scope and component expressions', async(() => {
const log: string[] = [];
const l = (value: string) => {
log.push(value);
return value + ';';
};
@Directive({selector: 'ng1a'})
class Ng1aComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1a', elementRef, injector);
}
}
@Directive({selector: 'ng1b'})
class Ng1bComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1b', elementRef, injector);
}
}
@Component({
selector: 'ng2',
template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`
})
class Ng2Component {
l = l;
}
@NgModule({
declarations: [Ng1aComponent, Ng1bComponent, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', [])
.directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'}))
.directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'}))
.directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope.l = l;
$rootScope.reset = () => log.length = 0;
});
const element =
html('<div>{{reset(); l(\'1A\');}}<ng2>{{l(\'1B\')}}</ng2>{{l(\'1C\')}}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;');
expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
});
}));
it('should propagate changes to a downgraded component inside the ngZone', async(() => {
const element = html('<my-app></my-app>');
let appComponent: AppComponent;
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'})
class AppComponent {
value?: number;
constructor() { appComponent = this; }
}
@Component({
selector: 'my-child',
template: '<div>{{ valueFromPromise }}</div>',
})
class ChildComponent {
valueFromPromise?: number;
@Input()
set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); }
constructor(private zone: NgZone) {}
ngOnChanges(changes: SimpleChanges) {
if (changes['value'].isFirstChange()) return;
this.zone.onMicrotaskEmpty.subscribe(
() => { expect(element.textContent).toEqual('5'); });
// Create a micro-task to update the value to be rendered asynchronously.
Promise.resolve().then(() => this.valueFromPromise = changes['value'].currentValue);
}
}
@NgModule({
declarations: [AppComponent, ChildComponent],
entryComponents: [AppComponent],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'myApp', downgradeComponent({component: AppComponent}));
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
appComponent.value = 5;
});
}));
// This test demonstrates https://github.com/angular/angular/issues/6385
// which was invalidly fixed by https://github.com/angular/angular/pull/6386
// it('should not trigger $digest from an async operation in a watcher', async(() => {
// @Component({selector: 'my-app', template: ''})
// class AppComponent {
// }
// @NgModule({declarations: [AppComponent], imports: [BrowserModule]})
// class Ng2Module {
// }
// const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
// const ng1Module = angular.module('ng1', []).directive(
// 'myApp', adapter.downgradeNg2Component(AppComponent));
// const element = html('<my-app></my-app>');
// adapter.bootstrap(element, ['ng1']).ready((ref) => {
// let doTimeout = false;
// let timeoutId: number;
// ref.ng1RootScope.$watch(() => {
// if (doTimeout && !timeoutId) {
// timeoutId = window.setTimeout(function() {
// timeoutId = null;
// }, 10);
// }
// });
// doTimeout = true;
// });
// }));
});
});

View File

@ -0,0 +1,178 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, Input, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {bootstrap} from './static_test_helpers';
withEachNg1Version(() => {
describe('content projection', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should instantiate ng2 in ng1 template and project content', async(() => {
// the ng2 component that will be used in ng1 (downgraded)
@Component({selector: 'ng2', template: `{{ prop }}(<ng-content></ng-content>)`})
class Ng2Component {
prop = 'NG2';
ngContent = 'ng2-content';
}
// our upgrade module to host the component to downgrade
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
// the ng1 app module that will consume the downgraded component
const ng1Module = angular
.module('ng1', [])
// create an ng1 facade of the ng2 component
.directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope['prop'] = 'NG1';
$rootScope['ngContent'] = 'ng1-content';
});
const element = html('<div>{{ \'ng1[\' }}<ng2>~{{ ngContent }}~</ng2>{{ \']\' }}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('ng1[NG2(~ng1-content~)]');
});
}));
it('should correctly project structural directives', async(() => {
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
class Ng2Component {
// TODO(issue/24571): remove '!'.
@Input() itemId !: string;
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', [])
.directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope['items'] = [
{id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]},
{id: 'c', subitems: [7, 8, 9]}
];
});
const element = html(`
<ng2 ng-repeat="item in items" [item-id]="item.id">
<div ng-repeat="subitem in item.subitems">{{ subitem }}</div>
</ng2>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(document.body.textContent))
.toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )');
});
}));
it('should instantiate ng1 in ng2 template and project content', async(() => {
@Component({
selector: 'ng2',
template: `{{ 'ng2(' }}<ng1>{{ transclude }}</ng1>{{ ')' }}`,
})
class Ng2Component {
prop = 'ng2';
transclude = 'ng2-transclude';
}
@Directive({selector: 'ng1'})
class Ng1WrapperComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector);
}
}
@NgModule({
declarations: [Ng1WrapperComponent, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive('ng1', () => ({
transclude: true,
template: '{{ prop }}(<ng-transclude></ng-transclude>)'
}))
.directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope['prop'] = 'ng1';
$rootScope['transclude'] = 'ng1-transclude';
});
const element = html('<div>{{ \'ng1(\' }}<ng2></ng2>{{ \')\' }}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('ng1(ng2(ng1(ng2-transclude)))');
});
}));
it('should support multi-slot projection', async(() => {
@Component({
selector: 'ng2',
template: '2a(<ng-content select=".ng1a"></ng-content>)' +
'2b(<ng-content select=".ng1b"></ng-content>)'
})
class Ng2Component {
constructor() {}
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component}));
// The ng-if on one of the projected children is here to make sure
// the correct slot is targeted even with structural directives in play.
const element = html(
'<ng2><div ng-if="true" class="ng1a">1a</div><div' +
' class="ng1b">1b</div></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('2a(1a)2b(1b)');
});
}));
});
});

View File

@ -0,0 +1,820 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectionStrategy, ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, ElementRef, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, Output, SimpleChanges, destroyPlatform} from '@angular/core';
import {async, fakeAsync, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {$apply, bootstrap} from './static_test_helpers';
withEachNg1Version(() => {
describe('downgrade ng2 component', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should bind properties, events', async(() => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['name'] = 'world';
$rootScope['dataA'] = 'A';
$rootScope['dataB'] = 'B';
$rootScope['modelA'] = 'initModelA';
$rootScope['modelB'] = 'initModelB';
$rootScope['eventA'] = '?';
$rootScope['eventB'] = '?';
});
@Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange'
],
template: 'ignore: {{ignore}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
ignore = '-';
literal = '?';
interpolate = '?';
oneWayA = '?';
oneWayB = '?';
twoWayA = '?';
twoWayB = '?';
eventA = new EventEmitter();
eventB = new EventEmitter();
twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => {
const propVal = (this as any)[prop];
if (propVal != value) {
throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`);
}
};
const assertChange = (prop: string, value: any) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
const actualValue = changes[prop].currentValue;
if (actualValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`);
}
};
switch (this.ngOnChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired');
this.eventB.emit('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB');
break;
case 2:
assertChange('interpolate', 'Hello everyone');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', downgradeComponent({
component: Ng2Component,
}));
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
<div>
<ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB"
bindon-two-way-a="modelA" [(two-way-b)]="modelB"
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
$apply(upgrade, 'name = "everyone"');
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
});
}));
it('should bind properties to onpush components', async(() => {
const ng1Module = angular.module('ng1', []).run(
($rootScope: angular.IScope) => { $rootScope['dataB'] = 'B'; });
@Component({
selector: 'ng2',
inputs: ['oneWayB'],
template: 'oneWayB: {{oneWayB}}',
changeDetection: ChangeDetectionStrategy.OnPush
})
class Ng2Component {
ngOnChangesCount = 0;
oneWayB = '?';
}
ng1Module.directive('ng2', downgradeComponent({
component: Ng2Component,
}));
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
<div>
<ng2 [one-way-b]="dataB"></ng2>
</div>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent)).toEqual('oneWayB: B');
$apply(upgrade, 'dataB= "everyone"');
expect(multiTrim(document.body.textContent)).toEqual('oneWayB: everyone');
});
}));
it('should support two-way binding and event listener', async(() => {
const listenerSpy = jasmine.createSpy('$rootScope.listener');
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['value'] = 'world';
$rootScope['listener'] = listenerSpy;
});
@Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
@Input() model = '?';
@Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) {
case 0:
expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC');
break;
case 1:
expect(changes.model.currentValue).toBe('newC');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', downgradeComponent({component: Ng2Component}));
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
<div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}}
</div>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC');
});
}));
it('should run change-detection on every digest (by default)', async(() => {
let ng2Component: Ng2Component;
@Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'})
class Ng2Component {
@Input() value1 = -1;
@Input() value2 = -1;
constructor() { ng2Component = this; }
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', [])
.directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope.value1 = 0;
$rootScope.value2 = 0;
});
const element = html('<ng2 [value1]="value1" value2="{{ value2 }}"></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService;
expect(element.textContent).toBe('0 | 0');
// Digest should invoke CD
$rootScope.$digest();
$rootScope.$digest();
expect(element.textContent).toBe('0 | 0');
// Internal changes should be detected on digest
ng2Component.value1 = 1;
ng2Component.value2 = 2;
$rootScope.$digest();
expect(element.textContent).toBe('1 | 2');
// Digest should propagate change in prop-bound input
$rootScope.$apply('value1 = 3');
expect(element.textContent).toBe('3 | 2');
// Digest should propagate change in attr-bound input
ng2Component.value1 = 4;
$rootScope.$apply('value2 = 5');
expect(element.textContent).toBe('4 | 5');
// Digest should propagate changes that happened before the digest
$rootScope.value1 = 6;
expect(element.textContent).toBe('4 | 5');
$rootScope.$digest();
expect(element.textContent).toBe('6 | 5');
});
}));
it('should not run change-detection on every digest when opted out', async(() => {
let ng2Component: Ng2Component;
@Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'})
class Ng2Component {
@Input() value1 = -1;
@Input() value2 = -1;
constructor() { ng2Component = this; }
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope.value1 = 0;
$rootScope.value2 = 0;
});
const element = html('<ng2 [value1]="value1" value2="{{ value2 }}"></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService;
expect(element.textContent).toBe('0 | 0');
// Digest should not invoke CD
$rootScope.$digest();
$rootScope.$digest();
expect(element.textContent).toBe('0 | 0');
// Digest should not invoke CD, even if component values have changed (internally)
ng2Component.value1 = 1;
ng2Component.value2 = 2;
$rootScope.$digest();
expect(element.textContent).toBe('0 | 0');
// Digest should invoke CD, if prop-bound input has changed
$rootScope.$apply('value1 = 3');
expect(element.textContent).toBe('3 | 2');
// Digest should invoke CD, if attr-bound input has changed
ng2Component.value1 = 4;
$rootScope.$apply('value2 = 5');
expect(element.textContent).toBe('4 | 5');
// Digest should invoke CD, if input has changed before the digest
$rootScope.value1 = 6;
$rootScope.$digest();
expect(element.textContent).toBe('6 | 5');
});
}));
it('should still run normal Angular change-detection regardless of `propagateDigest`',
fakeAsync(() => {
let ng2Component: Ng2Component;
@Component({selector: 'ng2', template: '{{ value }}'})
class Ng2Component {
value = 'foo';
constructor() { setTimeout(() => this.value = 'bar', 1000); }
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng2A', downgradeComponent({component: Ng2Component, propagateDigest: true}))
.directive(
'ng2B', downgradeComponent({component: Ng2Component, propagateDigest: false}));
const element = html('<ng2-a></ng2-a> | <ng2-b></ng2-b>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(element.textContent).toBe('foo | foo');
tick(1000);
expect(element.textContent).toBe('bar | bar');
});
}));
it('should initialize inputs in time for `ngOnChanges`', async(() => {
@Component({
selector: 'ng2',
template: `
ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}`
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
// TODO(issue/24571): remove '!'.
initialValue !: string;
// TODO(issue/24571): remove '!'.
@Input() foo !: string;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component}));
const element = html(`
<ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
});
}));
it('should bind to ng-model', async(() => {
const ng1Module = angular.module('ng1', []).run(
($rootScope: angular.IScope) => { $rootScope['modelA'] = 'A'; });
let ng2Instance: Ng2;
@Component({selector: 'ng2', template: '<span>{{_value}}</span>'})
class Ng2 {
private _value: any = '';
private _onChangeCallback: (_: any) => void = () => {};
private _onTouchedCallback: () => void = () => {};
constructor() { ng2Instance = this; }
writeValue(value: any) { this._value = value; }
registerOnChange(fn: any) { this._onChangeCallback = fn; }
registerOnTouched(fn: any) { this._onTouchedCallback = fn; }
doTouch() { this._onTouchedCallback(); }
doChange(newValue: string) {
this._value = newValue;
this._onChangeCallback(newValue);
}
}
ng1Module.directive('ng2', downgradeComponent({component: Ng2}));
const element = html(`<div><ng2 ng-model="modelA"></ng2> | {{modelA}}</div>`);
@NgModule(
{declarations: [Ng2], entryComponents: [Ng2], imports: [BrowserModule, UpgradeModule]})
class Ng2Module {
ngDoBootstrap() {}
}
platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => {
const adapter = ref.injector.get(UpgradeModule) as UpgradeModule;
adapter.bootstrap(element, [ng1Module.name]);
const $rootScope = adapter.$injector.get('$rootScope');
expect(multiTrim(document.body.textContent)).toEqual('A | A');
$rootScope.modelA = 'B';
$rootScope.$apply();
expect(multiTrim(document.body.textContent)).toEqual('B | B');
ng2Instance.doChange('C');
expect($rootScope.modelA).toBe('C');
expect(multiTrim(document.body.textContent)).toEqual('C | C');
const downgradedElement = <Element>document.body.querySelector('ng2');
expect(downgradedElement.classList.contains('ng-touched')).toBe(false);
ng2Instance.doTouch();
$rootScope.$apply();
expect(downgradedElement.classList.contains('ng-touched')).toBe(true);
});
}));
it('should properly run cleanup when ng1 directive is destroyed', async(() => {
let destroyed = false;
@Component({selector: 'ng2', template: 'test'})
class Ng2Component implements OnDestroy {
ngOnDestroy() { destroyed = true; }
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng1',
() => { return {template: '<div ng-if="!destroyIt"><ng2></ng2></div>'}; })
.directive('ng2', downgradeComponent({component: Ng2Component}));
const element = html('<ng1></ng1>');
platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => {
const adapter = ref.injector.get(UpgradeModule) as UpgradeModule;
adapter.bootstrap(element, [ng1Module.name]);
expect(element.textContent).toContain('test');
expect(destroyed).toBe(false);
const $rootScope = adapter.$injector.get('$rootScope');
$rootScope.$apply('destroyIt = true');
expect(element.textContent).not.toContain('test');
expect(destroyed).toBe(true);
});
}));
it('should properly run cleanup with multiple levels of nesting', async(() => {
let destroyed = false;
@Component({
selector: 'ng2-outer',
template: '<div *ngIf="!destroyIt"><ng1></ng1></div>',
})
class Ng2OuterComponent {
@Input() destroyIt = false;
}
@Component({selector: 'ng2-inner', template: 'test'})
class Ng2InnerComponent implements OnDestroy {
ngOnDestroy() { destroyed = true; }
}
@Directive({selector: 'ng1'})
class Ng1ComponentFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector);
}
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng1ComponentFacade, Ng2InnerComponent, Ng2OuterComponent],
entryComponents: [Ng2InnerComponent, Ng2OuterComponent],
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive('ng1', () => ({template: '<ng2-inner></ng2-inner>'}))
.directive('ng2Inner', downgradeComponent({component: Ng2InnerComponent}))
.directive('ng2Outer', downgradeComponent({component: Ng2OuterComponent}));
const element = html('<ng2-outer [destroy-it]="destroyIt"></ng2-outer>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(element.textContent).toBe('test');
expect(destroyed).toBe(false);
$apply(upgrade, 'destroyIt = true');
expect(element.textContent).toBe('');
expect(destroyed).toBe(true);
});
}));
it('should work when compiled outside the dom (by fallback to the root ng2.injector)',
async(() => {
@Component({selector: 'ng2', template: 'test'})
class Ng2Component {
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng1',
[
'$compile',
($compile: angular.ICompileService) => {
return {
link: function(
$scope: angular.IScope, $element: angular.IAugmentedJQuery,
$attrs: angular.IAttributes) {
// here we compile some HTML that contains a downgraded component
// since it is not currently in the DOM it is not able to "require"
// an ng2 injector so it should use the `moduleInjector` instead.
const compiled = $compile('<ng2></ng2>');
const template = compiled($scope);
$element.append !(template);
}
};
}
])
.directive('ng2', downgradeComponent({component: Ng2Component}));
const element = html('<ng1></ng1>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
// the fact that the body contains the correct text means that the
// downgraded component was able to access the moduleInjector
// (since there is no other injector in this system)
expect(multiTrim(document.body.textContent)).toEqual('test');
});
}));
it('should allow attribute selectors for downgraded components', async(() => {
@Component({selector: '[itWorks]', template: 'It works'})
class WorksComponent {
}
@NgModule({
declarations: [WorksComponent],
entryComponents: [WorksComponent],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'worksComponent', downgradeComponent({component: WorksComponent}));
const element = html('<works-component></works-component>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent)).toBe('It works');
});
}));
it('should allow attribute selectors for components in ng2', async(() => {
@Component({selector: '[itWorks]', template: 'It works'})
class WorksComponent {
}
@Component({selector: 'root-component', template: '<span itWorks></span>!'})
class RootComponent {
}
@NgModule({
declarations: [RootComponent, WorksComponent],
entryComponents: [RootComponent],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'rootComponent', downgradeComponent({component: RootComponent}));
const element = html('<root-component></root-component>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent)).toBe('It works!');
});
}));
it('should respect hierarchical dependency injection for ng2', async(() => {
@Component({selector: 'parent', template: 'parent(<ng-content></ng-content>)'})
class ParentComponent {
}
@Component({selector: 'child', template: 'child'})
class ChildComponent {
constructor(parent: ParentComponent) {}
}
@NgModule({
declarations: [ParentComponent, ChildComponent],
entryComponents: [ParentComponent, ChildComponent],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive('parent', downgradeComponent({component: ParentComponent}))
.directive('child', downgradeComponent({component: ChildComponent}));
const element = html('<parent><child></child></parent>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(document.body.textContent)).toBe('parent(child)');
});
}));
it('should work with ng2 lazy loaded components', async(() => {
let componentInjector: Injector;
@Component({selector: 'ng2', template: ''})
class Ng2Component {
constructor(injector: Injector) { componentInjector = injector; }
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule],
})
class Ng2Module {
ngDoBootstrap() {}
}
@Component({template: ''})
class LazyLoadedComponent {
constructor(public module: NgModuleRef<any>) {}
}
@NgModule({
declarations: [LazyLoadedComponent],
entryComponents: [LazyLoadedComponent],
})
class LazyLoadedModule {
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component}));
const element = html('<ng2></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const modInjector = upgrade.injector;
// Emulate the router lazy loading a module and creating a component
const compiler = modInjector.get(Compiler);
const modFactory = compiler.compileModuleSync(LazyLoadedModule);
const childMod = modFactory.create(modInjector);
const cmpFactory =
childMod.componentFactoryResolver.resolveComponentFactory(LazyLoadedComponent) !;
const lazyCmp = cmpFactory.create(componentInjector);
expect(lazyCmp.instance.module.injector === childMod.injector).toBe(true);
});
}));
it('should throw if `downgradedModule` is specified', async(() => {
@Component({selector: 'ng2', template: ''})
class Ng2Component {
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule],
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component, downgradedModule: 'foo'}));
const element = html('<ng2></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module)
.then(
() => { throw new Error('Expected bootstraping to fail.'); },
err =>
expect(err.message)
.toBe(
'Error while instantiating component \'Ng2Component\': \'downgradedModule\' ' +
'unexpectedly specified.\n' +
'You should not specify a value for \'downgradedModule\', unless you are ' +
'downgrading more than one Angular module (via \'downgradeModule()\').'));
}));
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, Input, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {bootstrap} from './static_test_helpers';
withEachNg1Version(() => {
describe('examples', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should have AngularJS loaded', () => expect(angular.version.major).toBe(1));
it('should verify UpgradeAdapter example', async(() => {
// This is wrapping (upgrading) an AngularJS component to be used in an Angular
// component
@Directive({selector: 'ng1'})
class Ng1Component extends UpgradeComponent {
// TODO(issue/24571): remove '!'.
@Input() title !: string;
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector);
}
}
// This is an Angular component that will be downgraded
@Component({
selector: 'ng2',
template: 'ng2[<ng1 [title]="nameProp">transclude</ng1>](<ng-content></ng-content>)'
})
class Ng2Component {
// TODO(issue/24571): remove '!'.
@Input('name') nameProp !: string;
}
// This module represents the Angular pieces of the application
@NgModule({
declarations: [Ng1Component, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() { /* this is a placeholder to stop the bootstrapper from
complaining */
}
}
// This module represents the AngularJS pieces of the application
const ng1Module =
angular
.module('myExample', [])
// This is an AngularJS component that will be upgraded
.directive(
'ng1',
() => {
return {
scope: {title: '='},
transclude: true,
template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
};
})
// This is wrapping (downgrading) an Angular component to be used in
// AngularJS
.directive('ng2', downgradeComponent({component: Ng2Component}));
// This is the (AngularJS) application bootstrap element
// Notice that it is actually a downgraded Angular component
const element = html('<ng2 name="World">project</ng2>');
// Let's use a helper function to make this simpler
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(element.textContent))
.toBe('ng2[ng1[Hello World!](transclude)](project)');
});
}));
});
});

View File

@ -0,0 +1,134 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken, Injector, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '../../../src/common/src/angular1';
import {$INJECTOR, INJECTOR_KEY} from '../../../src/common/src/constants';
import {html, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {UpgradeModule, downgradeInjectable, getAngularJSGlobal, setAngularJSGlobal} from '../../index';
import {bootstrap} from './static_test_helpers';
withEachNg1Version(() => {
describe('injection', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should downgrade ng2 service to ng1', async(() => {
// Tokens used in ng2 to identify services
const Ng2Service = new InjectionToken('ng2-service');
// Sample ng1 NgModule for tests
@NgModule({
imports: [BrowserModule, UpgradeModule],
providers: [
{provide: Ng2Service, useValue: 'ng2 service value'},
]
})
class Ng2Module {
ngDoBootstrap() {}
}
// create the ng1 module that will import an ng2 service
const ng1Module =
angular.module('ng1Module', []).factory('ng2Service', downgradeInjectable(Ng2Service));
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module)
.then((upgrade) => {
const ng1Injector = upgrade.$injector;
expect(ng1Injector.get('ng2Service')).toBe('ng2 service value');
});
}));
it('should upgrade ng1 service to ng2', async(() => {
// Tokens used in ng2 to identify services
const Ng1Service = new InjectionToken('ng1-service');
// Sample ng1 NgModule for tests
@NgModule({
imports: [BrowserModule, UpgradeModule],
providers: [
// the following line is the "upgrade" of an AngularJS service
{
provide: Ng1Service,
useFactory: (i: angular.IInjectorService) => i.get('ng1Service'),
deps: ['$injector']
}
]
})
class Ng2Module {
ngDoBootstrap() {}
}
// create the ng1 module that will import an ng2 service
const ng1Module = angular.module('ng1Module', []).value('ng1Service', 'ng1 service value');
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module)
.then((upgrade) => {
const ng2Injector = upgrade.injector;
expect(ng2Injector.get(Ng1Service)).toBe('ng1 service value');
});
}));
it('should initialize the upgraded injector before application run blocks are executed',
async(() => {
let runBlockTriggered = false;
const ng1Module = angular.module('ng1Module', []).run([
INJECTOR_KEY,
function(injector: Injector) {
runBlockTriggered = true;
expect(injector.get($INJECTOR)).toBeDefined();
}
]);
@NgModule({imports: [BrowserModule, UpgradeModule]})
class Ng2Module {
ngDoBootstrap() {}
}
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module).then(() => {
expect(runBlockTriggered).toBeTruthy();
});
}));
it('should allow resetting angular at runtime', async(() => {
let wrappedBootstrapCalled = false;
const n: any = getAngularJSGlobal();
setAngularJSGlobal({
bootstrap: (...args: any[]) => {
wrappedBootstrapCalled = true;
n.bootstrap(...args);
},
module: n.module,
element: n.element,
version: n.version,
resumeBootstrap: n.resumeBootstrap,
getTestability: n.getTestability
});
@NgModule({imports: [BrowserModule, UpgradeModule]})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1Module', []);
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module)
.then(upgrade => expect(wrappedBootstrapCalled).toBe(true))
.then(() => setAngularJSGlobal(n)); // Reset the AngularJS global.
}));
});
});

View File

@ -0,0 +1,44 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgZone, PlatformRef, Type} from '@angular/core';
import {UpgradeModule} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
import {$EXCEPTION_HANDLER, $ROOT_SCOPE} from '../../../src/common/src/constants';
export function bootstrap(
platform: PlatformRef, Ng2Module: Type<{}>, element: Element, ng1Module: angular.IModule) {
// We bootstrap the Angular module first; then when it is ready (async) we bootstrap the AngularJS
// module on the bootstrap element (also ensuring that AngularJS errors will fail the test).
return platform.bootstrapModule(Ng2Module).then(ref => {
const ngZone = ref.injector.get<NgZone>(NgZone);
const upgrade = ref.injector.get(UpgradeModule);
const failHardModule: any = ($provide: angular.IProvideService) => {
$provide.value($EXCEPTION_HANDLER, (err: any) => { throw err; });
};
// The `bootstrap()` helper is used for convenience in tests, so that we don't have to inject
// and call `upgrade.bootstrap()` on every Angular module.
// In order to closer emulate what happens in real application, ensure AngularJS is bootstrapped
// inside the Angular zone.
//
ngZone.run(() => upgrade.bootstrap(element, [failHardModule, ng1Module.name]));
return upgrade;
});
}
export function $apply(adapter: UpgradeModule, exp: angular.Ng1Expression) {
const $rootScope = adapter.$injector.get($ROOT_SCOPE) as angular.IRootScopeService;
$rootScope.$apply(exp);
}
export function $digest(adapter: UpgradeModule) {
const $rootScope = adapter.$injector.get($ROOT_SCOPE) as angular.IRootScopeService;
$rootScope.$digest();
}

View File

@ -0,0 +1,134 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule, Testability, destroyPlatform} from '@angular/core';
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {fakeAsync, flush, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeModule} from '@angular/upgrade/static';
import * as angular from '../../../src/common/src/angular1';
import {html, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {bootstrap} from './static_test_helpers';
withEachNg1Version(() => {
describe('testability', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
@NgModule({imports: [BrowserModule, UpgradeModule]})
class Ng2Module {
ngDoBootstrap() {}
}
it('should handle deferred bootstrap', fakeAsync(() => {
let applicationRunning = false;
let stayedInTheZone: boolean = undefined !;
const ng1Module = angular.module('ng1', []).run(() => {
applicationRunning = true;
stayedInTheZone = NgZone.isInAngularZone();
});
const element = html('<div></div>');
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module);
setTimeout(() => { (<any>window).angular.resumeBootstrap(); }, 100);
expect(applicationRunning).toEqual(false);
tick(100);
expect(applicationRunning).toEqual(true);
expect(stayedInTheZone).toEqual(true);
}));
it('should propagate return value of resumeBootstrap', fakeAsync(() => {
const ng1Module = angular.module('ng1', []);
let a1Injector: angular.IInjectorService|undefined;
ng1Module.run([
'$injector', function($injector: angular.IInjectorService) { a1Injector = $injector; }
]);
const element = html('<div></div>');
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module);
tick(100);
const value = (<any>window).angular.resumeBootstrap();
expect(value).toBe(a1Injector);
flush();
}));
it('should wait for ng2 testability', fakeAsync(() => {
const ng1Module = angular.module('ng1', []);
const element = html('<div></div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
const ng2Testability: Testability = upgrade.injector.get(Testability);
ng2Testability.increasePendingRequestCount();
let ng2Stable = false;
let ng1Stable = false;
angular.getTestability(element).whenStable(() => { ng1Stable = true; });
setTimeout(() => {
ng2Stable = true;
ng2Testability.decreasePendingRequestCount();
}, 100);
expect(ng1Stable).toEqual(false);
expect(ng2Stable).toEqual(false);
tick(100);
expect(ng1Stable).toEqual(true);
expect(ng2Stable).toEqual(true);
});
}));
it('should not wait for $interval', fakeAsync(() => {
const ng1Module = angular.module('ng1', []);
const element = html('<div></div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
const ng2Testability: Testability = upgrade.injector.get(Testability);
const $interval: angular.IIntervalService = upgrade.$injector.get('$interval');
let ng2Stable = false;
let intervalDone = false;
const id = $interval((arg: string) => {
// should only be called once
expect(intervalDone).toEqual(false);
intervalDone = true;
expect(NgZone.isInAngularZone()).toEqual(true);
expect(arg).toEqual('passed argument');
}, 200, 0, true, 'passed argument');
ng2Testability.whenStable(() => { ng2Stable = true; });
tick(100);
expect(intervalDone).toEqual(false);
expect(ng2Stable).toEqual(true);
tick(200);
expect(intervalDone).toEqual(true);
expect($interval.cancel(id)).toEqual(true);
// Interval should not fire after cancel
tick(200);
});
}));
});
});

File diff suppressed because it is too large Load Diff