TU Tran

Technologies should serve for business purpose.

NAVIGATION - SEARCH

Append "Angualr 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 solution for situation, please consider as below:

  • Solution 1: Rebuild the whole application is suitable for small application or the budget is huge. this rarely applicable in real software development. As we usually migrate the current feature to "Angular 2" along with implement 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:

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

Where should I start from?

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

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.

platformBrowserDynamic().bootstrapModule(Angular2AppModule)


and continue bootstrap default angular 1 module when done:

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 work as a bridge for bootstrapping angular1 module. So what will we do in Angular2AppModule module?

in Angular2AppModule, We will register default page that contain "ng-view" directive. this is where angular 1 display the content of appropriated route.

in Angular2AppModule class:

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

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

DefaultComponent include, html:

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

and ts file:

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

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

Please 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 inex.html as the place holder 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 "Angualr 1" component will be rendered to "ng-view" (this tag was located in defaultComponent.html file) as photo below:

At this time, you complete the first step of migrate 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"?

For more information how to create "Angular 2" directive, Please have a look at "Angular2 - Directives" (Part 1) and "Angular2 - Directives" (Part 2)

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:

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

and categoryPreview.ts:

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

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

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):

<category-preview></category-preview>

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

 

Congratulation, You success to use "CategoryPreview" (Angular 2) directive in "Angular 1" application.

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

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

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

And change html file:

<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:

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:

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

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

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 try to publish GroupService (Angular 2) to "Angular 1" application.

The content of GroupService.ts as below:

export class GroupService {
    private static groups: Array<any> = [
        { id: 1, name: "category 1", description: "Desription"},
        { id: 2, name: "category 2", description: "Desription 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":

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:

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:

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

Let build and run the app again, the output:

I see that we can passing 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:

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

And modify CategoryPreview.ts as below:

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 click on "Delete" button from CategoryPreview. this function will notify to listener by onDeleteClicked.emit call.

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

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:

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

Please 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 add this function into UsersCtrl as below:

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

now, build and refresh the browser:

I have tried as 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?

In traditional, all controller, directive and angular app were loaded by including directly into index.html file as below:

<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 can use angular with requirejs for lazy loading dependency automaticly.

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 improvement as above as below:

Wrap System.import into boostrapAngular2App function:

<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:

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

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

to:

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

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

In 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.

For learning Angular2, I think you should follow the list of articles as below (click on link to see detail page):

  • Overview: Introduce about Angular2
  • Routing: Understand how Angular2 navigate between pages/ components
  • Component: Learn about components/ pages in Angular2.
  • Binding: Learn how Angular2 show data on UI and receive input data from user.
  • Directive: Learn how to create re-usable component/ control that can be re-used across the application.
  • Integrate together: using what we learn about Angular for building the sample demo. 
  • Append "angular 2" into current "Angular 1" application.

Thank for reading.

Note: Please like and share to your friends if you think this is usefull article, I really appreciate

Add comment