Click here to Skip to main content
15,881,882 members
Articles
(untagged)

Append "Angular 2" Feature into "Angular 1" Application

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
28 Feb 2017Apache6 min read 7.5K  
How to append "Angular 2" feature into "Angular 1" application

Currently, I have an existing application written in "Angular 1" and wish to implement new features in "Angular 2". Can you propose a plan for me?

There are 2 solutions for this situation, please consider as below:

  • Solution 1: Rebuild the whole application that is suitable for small application or the budget is huge. This is rarely applicable in real software development. As we usually migrate the current feature to "Angular 2" along with implementing new features.
  • Solution 2: Build new features in "Angular 2" concurrently with migrating current feature from "Angular 1" to "Angular 2" one-by-one. This solution is more applicable in real work as the application can be used as normal.

Ok, "Solution 2" seems to be a nice one. Source-code of application is at "https://github.com/techcoaching/MigrateToAngular2.git", Can you show me how?

Just look over the current app. We have:

The list of users:

Image 1

Clicking on "edit" icon will take user to edit page:

Image 2

Where Should I Start From?

Please open your index.html file and add this script tag at the end of body (before </body>):

HTML
<script>
	System.import('src/main').then(null, console.error.bind(console));
</script>

What Did We Do in "src/main.ts" File?

This is typescript file that will load and bootstrap default angular2 module as below:

JavaScript
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import {UpgradeModule} from "@angular/upgrade/static";
import {Angular2AppModule} from "./angular2AppModule";
platformBrowserDynamic().bootstrapModule(Angular2AppModule).then((ref: any) => {
    let upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
    upgrade.bootstrap(document.body, ["angular1"]);
});

In this code, we load "Angular2AppModule" default module.

JavaScript
platformBrowserDynamic().bootstrapModule(Angular2AppModule)

and continue bootstrap default angular 1 module when done:

JavaScript
upgrade.bootstrap(document.body, ["angular1"])

So it means that "Angular 1" module was not bootstrapped directly as before but it was bootstrapped in-directed from "Angular 2" module (named Angular2AppModule module).

Ok, I understand that Angular2 module works as a bridge for bootstrapping angular1 module. So what will we do in Angular2AppModule module?

In Angular2AppModule, we will register default page that contains "ng-view" directive. This is where Angular 1 displays the content of the appropriated route.

In Angular2AppModule class:

JavaScript
@NgModule({
    imports: [...],
    declarations: [DefaultComponent],
    bootstrap: [DefaultComponent]
})
export class Angular2AppModule { }

In this class, we bootstrap "DefaultComponent" as layout of the application.

DefaultComponent includes html:

HTML
<div>
      <div ng-view></div>
</div>

and ts file:

JavaScript
@Component({
    selector:"default",
    templateUrl:"src/defaultComponent.html"
})
export class DefaultComponent{}

In this class, we just declare the component without any logic.

Please be aware that we register DefaultComponent with "default" name. This name will be used from index.html file:

I have ng-view from my index.html file already and we have new ng-view in defaultComponent.html, Just worry the app will not work?

I see, we use ng-view in index.html as the placeholder where the content of page will be displayed. Now we will replace that by "<default></default>" tag.

This "default" tag matches with selector of DefaultComponent above. So at run-time, content of DefaultComponent will be rendered to that location and default "Angular 1" component will be rendered to "ng-view" (this tag was located in defaultComponent.html file) as photo below:

Image 3

At this time, you complete the first step of migrating your application to "Angular 2".

Ok got it, to this point, my "Angular 1" can be started normally but bootstrapped from "Angular 2" module. Can I define new directive in "Angular 2" and use them in my current "Angular 1"?

Let me show you how to inject existing "Angular 2" directive into "Angular 1" application.

I assume that we have already created "Angular 2" directive named "CategoryPreview" as below:

categoryPreview.html

HTML
<div>
    This is content of category preview directive
</div>

and categoryPreview.ts:

JavaScript
@Component({
    selector: "category-preview",
    templateUrl: "src/security/categoryPreview.html"
})
export class CategoryPreview {
}

Please add the following code into "angular2AppModule.ts":

JavaScript
window.angular.module('angular1').directive
('categoryPreview', downgradeComponent({ component: CategoryPreview}));

This code will convert "CategoryPreview" from "Angular 2" to "Angular 1" and register as "categoryPreview" in "angular1" module.

Then, add CategoryPreview into users.html template file (angular 1 component):

HTML
<category-preview></category-preview>

Let's try to build and run the app. We see the output on the browser as below:

Image 4

Congratulations! You have succeeded in using "CategoryPreview" (Angular 2) directive in "Angular 1" application.

How can I pass parameter from "Angular 1" application into "Angular 2" directive?

Ok, let's try to public input parameter from CategoryPreview directive above as below:

JavaScript
export class CategoryPreview {
    @Input() item: any = null;
}

And change HTML file:

HTML
<div *ngIf="item">
    <h3>Summary of {{item.name}}</h3>
    <form class="form-horizontal form-label-left ">
        <div class="form-group ">
            <label>Name</label>
            <span>{{item.name}}</span>
        </div>
        <div class="form-group ">
            <label>Description</label>
            <span>{{item.description}}</span>
        </div>
    </form>
</div>

and update in Angular2AppModule class as below:

JavaScript
window.angular.module('angular1').directive
('categoryPreview', downgradeComponent({ component: CategoryPreview, inputs:["item"]}));

In this code, we add 'inputs:["item"]' into downgradeComponent call.

In users.html, pass item parameter into "CategoryPreview" directive:

HTML
<category-preview [item]="{name:'name', description:'description'}"></category-preview>

Build and run the app again, we receive the output on browser:

Image 5

This means that, from "Angular 1" application, we can pass parameter into "Angular 2" directive.

Got it! What about "Angular 2" service, can I call it from "Angular 1" application?

Surely yes, we do the same way as publishing directive. Let's try to publish GroupService (Angular 2) to "Angular 1" application.

The content of GroupService.ts is as below:

JavaScript
export class GroupService {
    private static groups: Array<any> = [
        { id: 1, name: "category 1", description: "Description"},
        { id: 2, name: "category 2", description: "Description 2"},
    ];
    public getAllGroups(): Array<any> {
        return GroupService.groups;
    }
}

This is pure typescript (ts) class that uses hard-coded data.

Similar to "categoryPreview" directive. Add this line of code into "angular2AppModule.ts":

JavaScript
window.angular.module('angular1').factory("groupService", downgradeInjectable(GroupService));

This means that we convert GroupService (angular 2) to "Angular 1" and register as factory.

Then, we can inject to other component in "Angular 1" as below:

JavaScript
function UsersCtrl($scope, $location, groupService) {
    $scope.group = groupService.getAllGroups()[0];
}
UsersCtrl.$inject = ["$scope", "$location", "groupService"];

And we also need to update in users.html as below:

HTML
<category-preview [item]="{name:group.name, description:group.description}"></category-preview>

Let's build and run the app again, the output:

Image 6

I see that we can pass input parameter for Angular 2 directive from Angular 1 application. What about @Output parameter?

For output parameter, it is nearly the same as @Input parameter.

Now, we will add new delete button in CategoryPreview directive:

HTML
<button (click)="onButtonClicked()">Delete</button>

And modify CategoryPreview.ts as below:

JavaScript
export class CategoryPreview {
    @Input() item: any = null;
    @Output() onDeleteClicked: EventEmitter<any> = new EventEmitter<any>();
    public onButtonClicked() {
        this.onDeleteClicked.emit(this.item);
    }
}

In this case, we add new "onButtonClicked" function that will be called when user clicks on "Delete" button from CategoryPreview. This function will notify to listener by onDeleteClicked.emit call.

We also need to change in "angular2AppModule.ts":

JavaScript
window.angular.module('angular1').directive('categoryPreview', downgradeComponent
({ component: CategoryPreview, inputs: ["item"], outputs: ["onDeleteClicked"] }));

In this code, we add outputs option for downgradeComponent call.

And also update in users.html:

HTML
<category-preview (on-delete-clicked)="onAngular1DeleteClicked($event)" 
[item]="{name:group.name, description:group.description}"></category-preview>

Please be aware that "onDeleteClicked" event in CategoryPreview will be changed to kebab-case in Angular 1. it means that "OnDeleteClicked" will be changed to "on-delete-clicked" in "Angular 1".

And we map this event to "onAngular1DeleteClicked" in Angular 1 component. This method was populated from "usersCtrl.js" file. Let's add this function into UsersCtrl as below:

JavaScript
$scope.onAngular1DeleteClicked = function () {
	console.log("onDeleteClicked")
}

Now, build and refresh the browser:

Image 7

I have tried as per your instruction, but my code was not be able to run. I used requirejs in my app for bootstrapping angular module. What should I do?

Traditionally, all controller, directive and angular apps were loaded by including directly into index.html file as below:

HTML
<script src="src/app.js"></script>
<script src="src/userPreview.js"></script>
<script src="src/userService.js"></script>
<script src="src/usersCtrl.js"></script>
<script src="src/userDetailCtrl.js"></script>

There are some applications that can use angular with requirejs for lazy loading dependency automatically.

So at the time we run "System.import" (import and run angular 2 code), some angular 1 resources is not ready.

With this case, we make some improvements as shown below:

Wrap System.import into boostrapAngular2App function:

HTML
<script>
	function boostrapAngular2App(){
		System.import('src/main').then(null, console.error.bind(console));
	}
</script>

In main.js file of the "angular 1" application, change it from:

JavaScript
require.config({
	/*setting for require js*/
});

require([
/*List of resource need to be loaded before bootstrapping angular 1 module*/
,], function() {
    angular.bootstrap(document, ['angular1']);
});

to:

JavaScript
require.config({
	/*setting for require js*/
});

require([
/*List of resource need to be loaded before bootstrapping angular 1 module*/
,], function() {
    /*angular.bootstrap(document, ['angular1']);*/
	if(boostrapAngular2App){
		boostrapAngular2App();
	}
});

In the code above, we will not bootstrap default Angular 1 module, but bootstrap Angular 2 code instead.

Please aware that "boostrapAngular2App" was defined from index.html file.

Thanks for reading.

Note: Please like and share with your friends if you think this is a useful article, I would really appreciate that.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Architect
Vietnam Vietnam
I have more than 8 years in web development for multiple types of applications (ERP, Education System, ...).
I usually organize training/ coaching on specified topic (such as: RESTful/ WebApi, Angular2, BEM, LESS, SASS, EF, NodeJs ....). Please contact me on Skype (tranthanhtu83) or email (contact@tranthanhtu.vn) if need.
For more information about me, Please visit http://www.tranthanhtu.vn/page/about-me

Comments and Discussions

 
-- There are no messages in this forum --