diff --git a/modules/@angular/examples/upgrade/static/ts/e2e_test/static_spec.ts b/modules/@angular/examples/upgrade/static/ts/e2e_test/static_spec.ts new file mode 100644 index 0000000000..ed780162e6 --- /dev/null +++ b/modules/@angular/examples/upgrade/static/ts/e2e_test/static_spec.ts @@ -0,0 +1,54 @@ +/** + * @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 {browser, by, element} from 'protractor'; +import {verifyNoBrowserErrors} from '../../../../_common/e2e_util'; + +function loadPage(url: string) { + browser.ng12Hybrid = true; + browser.rootEl = 'example-app'; + browser.get(url); +} + +describe('upgrade(static)', () => { + beforeEach(() => { loadPage('/upgrade/static/ts/'); }); + afterEach(verifyNoBrowserErrors); + + it('should render the `ng2-heroes` component', () => { + expect(element(by.css('h1')).getText()).toEqual('Heroes'); + expect(element.all(by.css('p')).get(0).getText()).toEqual('There are 3 heroes.'); + }); + + it('should render 3 ng1-hero components', () => { + const heroComponents = element.all(by.css('ng1-hero')); + expect(heroComponents.count()).toEqual(3); + }); + + it('should add a new hero when the "Add Hero" button is pressed', () => { + const addHeroButton = element.all(by.css('button')).last(); + expect(addHeroButton.getText()).toEqual('Add Hero'); + addHeroButton.click(); + const heroComponents = element.all(by.css('ng1-hero')); + expect(heroComponents.last().element(by.css('h2')).getText()).toEqual('Kamala Khan'); + }); + + it('should remove a hero when the "Remove" button is pressed', () => { + let firstHero = element.all(by.css('ng1-hero')).get(0); + expect(firstHero.element(by.css('h2')).getText()).toEqual('Superman'); + + const removeHeroButton = firstHero.element(by.css('button')); + expect(removeHeroButton.getText()).toEqual('Remove'); + removeHeroButton.click(); + + const heroComponents = element.all(by.css('ng1-hero')); + expect(heroComponents.count()).toEqual(2); + + firstHero = element.all(by.css('ng1-hero')).get(0); + expect(firstHero.element(by.css('h2')).getText()).toEqual('Wonder Woman'); + }); +}); \ No newline at end of file diff --git a/modules/@angular/examples/upgrade/static/ts/module.ts b/modules/@angular/examples/upgrade/static/ts/module.ts new file mode 100644 index 0000000000..20dd59871e --- /dev/null +++ b/modules/@angular/examples/upgrade/static/ts/module.ts @@ -0,0 +1,184 @@ +/** + * @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, DoCheck, ElementRef, EventEmitter, Inject, Injectable, Injector, Input, NgModule, OnChanges, OnDestroy, OnInit, Output, SimpleChanges} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {UpgradeComponent, UpgradeModule, downgradeComponent, downgradeInjectable} from '@angular/upgrade/static'; + +interface Hero { + name: string; + description: string; +} + +// #docregion Angular 2 Stuff +// #docregion ng2-heroes +// This Angular 2 component will be "downgraded" to be used in Angular 1 +@Component({ + selector: 'ng2-heroes', + // This template uses the upgraded `ng1-hero` component + // Note that because its element is compiled by Angular 2+ we must use camelCased attribute names + template: `

Heroes

+

+
+ Super Hero +
+ `, +}) +class Ng2HeroesComponent { + @Input() heroes: Hero[]; + @Output() addHero = new EventEmitter(); + @Output() removeHero = new EventEmitter(); +} +// #enddocregion + +// #docregion ng2-heroes-service +// This Angular 2 service will be "downgraded" to be used in Angular 1 +@Injectable() +class HeroesService { + heroes: Hero[] = [ + {name: 'superman', description: 'The man of steel'}, + {name: 'wonder woman', description: 'Princess of the Amazons'}, + {name: 'thor', description: 'The hammer-wielding god'} + ]; + + // #docregion use-ng1-upgraded-service + constructor(@Inject('titleCase') titleCase: (v: string) => string) { + // Change all the hero names to title case, using the "upgraded" Angular 1 service + this.heroes.forEach((hero: Hero) => hero.name = titleCase(hero.name)); + } + // #enddocregion + + addHero() { + this.heroes = + this.heroes.concat([{name: 'Kamala Khan', description: 'Epic shape-shifting healer'}]); + } + + removeHero(hero: Hero) { this.heroes = this.heroes.filter((item: Hero) => item !== hero); } +} +// #enddocregion + +// #docregion ng1-hero-wrapper +// This Angular 2 directive will act as an interface to the "upgraded" Angular 1 component +@Directive({selector: 'ng1-hero'}) +class Ng1HeroComponentWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, + OnDestroy { + // The names of the input and output properties here must match the names of the + // `<` and `&` bindings in the Angular 1 component that is being wrapped + @Input() hero: Hero; + @Output() onRemove: EventEmitter; + constructor(@Inject(ElementRef) elementRef: ElementRef, @Inject(Injector) injector: Injector) { + // We must pass the name of the directive as used by Angular 1 to the super + super('ng1Hero', elementRef, injector); + } + + // For this class to work when compiled with AoT, we must implement these lifecycle hooks + // because the AoT compiler will not realise that the super class implements them + ngOnInit() { super.ngOnInit(); } + + ngOnChanges(changes: SimpleChanges) { super.ngOnChanges(changes); } + + ngDoCheck() { super.ngDoCheck(); } + + ngOnDestroy() { super.ngOnDestroy(); } +} +// #enddocregion + +// #docregion ng2-module +// This NgModule represents the Angular 2 pieces of the application +@NgModule({ + declarations: [Ng2HeroesComponent, Ng1HeroComponentWrapper], + providers: [ + HeroesService, + // #docregion upgrade-ng1-service + // Register an Angular 2+ provider whose value is the "upgraded" Angular 1 service + {provide: 'titleCase', useFactory: (i: any) => i.get('titleCase'), deps: ['$injector']} + // #enddocregion + ], + // All components that are to be "downgraded" must be declared as `entryComponents` + entryComponents: [Ng2HeroesComponent], + // We must import `UpgradeModule` to get access to the Angular 1 core services + imports: [BrowserModule, UpgradeModule] +}) +class Ng2AppModule { + ngDoBootstrap() { /* this is a placeholder to stop the boostrapper from complaining */ + } +} +// #enddocregion +// #enddocregion + + +// #docregion Angular 1 Stuff +// #docregion ng1-module +// This Angular 1 module represents the Angular 1 pieces of the application +const ng1AppModule = angular.module('ng1AppModule', []); +// #enddocregion + +// #docregion ng1-hero +// This Angular 1 component will be "upgraded" to be used in Angular 2+ +ng1AppModule.component('ng1Hero', { + bindings: {hero: '<', onRemove: '&'}, + transclude: true, + template: `
+

{{ $ctrl.hero.name }}

+

{{ $ctrl.hero.description }}

+ ` +}); +// #enddocregion + +// #docregion ng1-title-case-service +// This Angular 1 service will be "upgraded" to be used in Angular 2+ +ng1AppModule.factory( + 'titleCase', + () => (value: string) => value.replace(/((^|\s)[a-z])/g, (_, c) => c.toUpperCase())); +// #enddocregion + +// #docregion downgrade-ng2-heroes-service +// Register an Angular 1 service, whose value is the "downgraded" Angular 2+ injectable. +ng1AppModule.factory('heroesService', downgradeInjectable(HeroesService)); +// #enddocregion + +// #docregion ng2-heroes-wrapper +// This is directive will act as the interface to the "downgraded" Angular 2+ component +ng1AppModule.directive( + 'ng2Heroes', + downgradeComponent( + // The inputs and outputs here must match the relevant names of the properties on the + // "downgraded" component + {component: Ng2HeroesComponent, inputs: ['heroes'], outputs: ['addHero', 'removeHero']})); +// #enddocregion + +// #docregion example-app +// This is our top level application component +ng1AppModule.component('exampleApp', { + // We inject the "downgraded" HeroesService into this Angular 1 component + // (We don't need the `HeroesService` type for Angular 1 DI - it just helps with TypeScript + // compilation) + controller: [ + 'heroesService', function(heroesService: HeroesService) { this.heroesService = heroesService; } + ], + // This template make use of the downgraded `ng2-heroes` component + // Note that because its element is compiled by Angular 1 we must use kebab-case attributes for + // inputs and outputs + template: ` + + There are {{ $ctrl.heroesService.heroes.length }} heroes. + ` +}); +// #enddocregion +// #enddocregion + + +// #docregion bootstrap +// First we bootstrap the Angular 2 HybridModule +// (We are using the dynamic browser platform as this example has not been compiled AoT) +platformBrowserDynamic().bootstrapModule(Ng2AppModule).then(ref => { + // Once Angular 2 bootstrap is complete then we bootstrap the Angular 1 module + const upgrade = ref.injector.get(UpgradeModule) as UpgradeModule; + upgrade.bootstrap(document.body, [ng1AppModule.name]); +}); +// #enddocregion diff --git a/modules/@angular/examples/upgrade/static/ts/styles.css b/modules/@angular/examples/upgrade/static/ts/styles.css new file mode 100644 index 0000000000..f3785c1c22 --- /dev/null +++ b/modules/@angular/examples/upgrade/static/ts/styles.css @@ -0,0 +1,17 @@ +ng2-heroes { + border: solid black 2px; + display: block; + padding: 5px; +} + +ng1-hero { + border: solid green 2px; + margin-top: 5px; + padding: 5px; + display: block; +} + +.title { + background-color: blue; + color: white; +}