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:
Clicking 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 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:
@NgModule({
imports: [...],
declarations: [DefaultComponent],
bootstrap: [DefaultComponent]
})
export class Angular2AppModule { }
In this class, we bootstrap "DefaultComponent
" as layout of the application.
DefaultComponent
includes 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 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:
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
<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's try to build and run the app. We see the output on the browser as below:
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:
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's try to publish GroupService
(Angular 2) to "Angular 1" application.
The content of GroupService.ts is as below:
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":
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's build and run the app again, the output:
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:
<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 clicks 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 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:
$scope.onAngular1DeleteClicked = function () {
console.log("onDeleteClicked")
}
Now, build and refresh the browser:
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:
<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:
<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({
});
require([
,], function() {
angular.bootstrap(document, ['angular1']);
});
to:
require.config({
});
require([
,], function() {
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.
CodeProject
Note: Please like and share with your friends if you think this is a useful article, I would really appreciate that.