Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kutec/d5d714e585ff90a4fad17b649d41d25a to your computer and use it in GitHub Desktop.
Save kutec/d5d714e585ff90a4fad17b649d41d25a to your computer and use it in GitHub Desktop.
Angular2 in Liferay Portal 7

Angular2 in Liferay Portal 7

The following are current outstanding issues with this process that can make it difficult when using Angular2 inside Portal.

##Pain points:

  • SennaJS does not work correctly when navigating away from a page containing an Angular2 component. This is due to the way ZoneJS wraps the XMLHttpRequest.send method. See https://github.com/angular/zone.js/blob/master/dist/zone.js#L113. I'm not sure if there is a way around this.
  • Currently unable to use the liferay-amd-loader to load an angular project (at least not yet). Must use SystemJS module loader.
    • This should be possible, but I've not yet figured out exactly how yet.
  • Node modules must be copied into /src to allow for proper typescript compilation.
    • This should also be possible by configuring the TypeScript compiler in some way to look in node_modules.
  • Multiple instances/portlets using angular will break. I think this can be resolve by importing angular outside of the portlet, and bootstrapping Angular applications as they become available. Similar to this blog post: https://web.liferay.com/es/web/sampsa.sohlman/blog/-/blogs/trying-the-angularjs-with-liferay

##Steps for creating a Portlet that uses Angular2

  1. Create a Portlet skeleton using the Blade CLI. More information on this can be found here: https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/creating-modules-with-blade-cli. For this tutorial I chose to create a module from the 'portlet' template. Your command should look something like the following blade create -d path/to/angular-portlet portlet

  2. Once you have a portlet skeleton, we can start creating the extra infrastructure we will need for Angular2. Lets create the following files in our angular-portlet/portlet directory. * package.json * gulpfile.js

  3. We need to create a task in our gulpfile that will compile our typescript into javascript. There are many ways to do this, but I used the gulp-typescript node package.

  4. Now that we have a task to compile our code, we need to make sure it gets called when we deploy the module. Inside our build.gradle we create a task of type ExecuteGulpTask, that depends on the npmInstall task, and calls our task we created in our gulp file.

  5. Now, lets import angular's dependencies in our init.jsp `<script src="https://npmcdn.com/zone.js@0.6.12?main=browser"></script>

<script src="https://npmcdn.com/reflect-metadata@0.1.3"></script>`
  1. Now lets create a simple Angular2 application that integrates with a service in Liferay. Please see the attached files.

  2. We can now create a way to load our application inside of your portlet. Most Angular2 applications are loaded with SystemJS, which is what we'll use here. There should be a way to use the liferay-amd-loader for this, but I haven't quire figured out how that will work yet.

So first, lets import SystemJS like this <script src="https://npmcdn.com/systemjs@0.19.27/dist/system.src.js"></script>. And we also have to create and import a SystemJS configuration file, like so <script src="/o/angular-portlet/js/system.config.js"></script>.

The next step is to load our application. This can be done from many places, but for my purposes I put it in our view.jsp like so: ``` aui:script System.import('app').then(function(module) { module.main(Liferay); }).catch(function(err){ console.error(err); }); </aui:script>


What's going on in this code is, SystemJS is trying to load 'app', which we have defined in our system.config.js to load our main.js file first.  the code `module.main(Liferay)` is calling our main function in main.js, and passing in the Liferay global variable.

In the end, our portlet should resemble this directory structure:

angular-portlet/ └─portlet ├── build.gradle ├── package.json ├── gulpfile.js ├── bnd.bnd ├── src/main/java/portlet/portlet/ │ │── AngularPortlet.java │ └── route/ | └── AngularPortletFriendlyURLMapper.java └── resources/META-INF/ ├── friendly-url-routes/ │ └── routes.xml └── resources/ ├── init.jsp ├── view.jsp └── js/ ├── system.config.js └── app/ ├── main.ts ├── app.component.ts ├── app.routes.ts ├── country.ts └── country-detail.component.ts

/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.calendar.web.internal.portlet.route;
import com.liferay.portal.kernel.portlet.DefaultFriendlyURLMapper;
import com.liferay.portal.kernel.portlet.FriendlyURLMapper;
import org.osgi.service.component.annotations.Component;
@Component(
immediate = true,
property = {
"com.liferay.portlet.friendly-url-routes=META-INF/friendly-url-routes/routes.xml",
"javax.portlet.name=AngularPortlet"
},
service = FriendlyURLMapper.class
)
public class AngularPortletFriendlyURLMapper extends DefaultFriendlyURLMapper {
}
import { Component, Inject } from '../node_modules/@angular/core';
import { Router, ROUTER_DIRECTIVES } from '../node_modules/@angular/router';
import { Country } from './country';
import { CountryDetailComponent } from './country-detail.component';
import { LocationService } from './location.service';
@Component({
selector: 'my-app',
styleUrls: [`/o/angular-portlet/styles/app.component.css`],
templateUrl: '/o/angular-portlet/templates/app.component.html',
directives: [ROUTER_DIRECTIVES, CountryDetailComponent],
providers: [LocationService]
})
export class AppComponent {
componentName: 'AppComponent';
title = 'This is an Angular Portlet inside Liferay Portal 7!';
label = 'Select a country to get more information about it.';
countries: Country[];
constructor(
@Inject(Router)private router: Router,
@Inject(LocationService) private locationService: LocationService,
@Inject('Liferay') private Liferay: any) {
this.getCountries();
}
getCountries() {
this.locationService.getCountries().then((countries) => {
this.countries = countries;
});
}
onChange(countryId:number) {
this.locationService.getRegions(countryId).then(() => {
this.router.navigate(['-/angular/country', countryId]);
});
}
}
import { provideRouter, RouterConfig } from '../node_modules/@angular/router';
import { CountryDetailComponent } from './country-detail.component';
import { RegionDetailComponent } from './region-detail.component';
import { AppComponent } from './app.component';
// Route Configuration
export const routes: RouterConfig = [
{ path: '-/angular/country', component: CountryDetailComponent },
{ path: '-/angular/country/:countryId', component: CountryDetailComponent },
{ path: '-/angular/country/:countryId/:regionId', component: RegionDetailComponent },
{ path: '', redirectTo: '-/angular/country', pathMatch: 'full' },
{ path: '**', redirectTo: '' },
];
// Export routes
export const APP_ROUTER_PROVIDERS = [
provideRouter(routes, {enableTracing: true})
];
dependencies {
provided group: "com.liferay", name: "com.liferay.gradle.plugins", version: "latest.release"
provided group: "com.liferay", name: "com.liferay.portal.upgrade", version: "2.0.0"
provided group: "com.liferay.portal", name: "com.liferay.portal.impl", version: "2.0.0"
provided group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.4.0"
provided group: "javax.portlet", name: "portlet-api", version: "2.0"
provided group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
provided group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"
}
apply plugin: "com.liferay.gulp"
apply plugin: "com.liferay.cache"
import com.liferay.gradle.plugins.gulp.ExecuteGulpTask
task compileTypeScript(type: ExecuteGulpTask)
compileTypeScript {
dependsOn npmInstall
gulpCommand = 'default'
}
classes {
dependsOn compileTypeScript
}
import { Component, Inject, Input, OnInit, SimpleChange } from '../node_modules/@angular/core';
import { Country } from './country';
import { LocationService } from './location.service';
import { RegionDetailComponent } from './region-detail.component';
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '../node_modules/@angular/router';
@Component({
selector: 'location-content',
templateUrl: '/o/angular-portlet/templates/country-detail.component.html',
directives: [ROUTER_DIRECTIVES, RegionDetailComponent]
})
export class CountryDetailComponent implements OnInit {
country: Country;
sub: any;
constructor(
@Inject(Router)private router: Router,
@Inject(ActivatedRoute)private route: ActivatedRoute,
@Inject(LocationService) private locationService: LocationService) { }
ngOnInit() {
this.locationService.getCountries().then(() => {
this.sub = this.route.params.subscribe(params => {
let countryId = +params['countryId'];
if (countryId) {
this.country = this.locationService.getCountry(countryId);
}
});
});
}
onChange(regionId:number) {
this.router.navigate(['-/angular/country', this.country.countryId, regionId]);
}
}
import { Region } from './region';
export class Country {
countryId: number;
name: string;
a3: string;
regions: Region[];
constructor(config:any) {
this.countryId = config.countryId;
this.name = config.name;
this.a3 = config.a3;
}
}
var gulp = require('gulp');
var ts = require('gulp-typescript');
gulp.task('default', function () {
return new Promise(
function(resolve, reject) {
gulp.src([
'node_modules/core-js/**/*',
'node_modules/zone.js/**/*',
'node_modules/reflect-metadata/**/*',
'node_modules/systemjs/**/*',
'node_modules/@angular/**/*',
'node_modules/angular2-in-memory-web-api/**/*',
'node_modules/rxjs/**/*'
],
{
base: './'
})
.pipe(gulp.dest('src/main/resources/META-INF/resources/js'))
.on('end', resolve);
}).then(
function() {
gulp.src(['src/main/resources/**/*.ts','!**/node_modules/**'])
.pipe(
ts(
{
noImplicitAny: true,
experimentalDecorators: true,
module: 'amd',
moduleResolution: 'node'
}
)
).pipe(gulp.dest('classes'));
}
);
});
import { Injectable, Inject } from '@angular/core';
import { Country } from './country';
import { Region } from './region';
@Injectable()
export class LocationService {
cache:Country[];
constructor(@Inject('Liferay') private Liferay: any) { }
getCountries() {
var instance = this;
return new Promise<Country[]>((resolve) => {
if (!instance.cache) {
this.Liferay.Service(
'/country/get-countries',
{
active: true
},
function(response:any) {
instance.cache = response;
resolve(instance.cache);
}
);
}
else {
resolve(instance.cache);
}
});
}
getRegions(countryId:number) {
var instance = this;
var country = this.getCountry(countryId);
return new Promise<Region[]>((resolve) => {
if (!country.regions) {
this.Liferay.Service(
'/region/get-regions',
{
active: true,
countryId: countryId
},
function(response:any) {
country.regions = response;
resolve(country.regions);
}
);
}
else {
resolve(country.regions);
}
});
}
getCountry(countryId:number):Country {
if (!this.cache) {
return null;
}
for (var i = 0; i < this.cache.length; i++) {
var c = this.cache[i];
if (c.countryId == countryId) {
return c;
}
}
}
getRegion(countryId:number, regionId:number) {
var country = this.getCountry(countryId);
if (!country.regions) {
return null;
}
for (var i = 0; i < country.regions.length; i++) {
var r = country.regions[i];
if (r.regionId == regionId) {
return r;
}
}
}
}
///<reference path="../../../../../../../node_modules/typescript/lib/lib.es6.d.ts"/>
import { bootstrap } from '../node_modules/@angular/platform-browser-dynamic';
import { APP_ROUTER_PROVIDERS } from './app.routes';
import { AppComponent } from './app.component';
import { provide } from '@angular/core';
export function main(Liferay:any, A:any, baseRenderUrl:String) {
bootstrap(AppComponent, [
provide('Liferay', {useValue: Liferay}),
provide('baseRenderUrl', {useValue: baseRenderUrl}),
APP_ROUTER_PROVIDERS
]);
}
{
"name": "hello-angular",
"version": "1.0.0",
"scripts": {
"start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
"lite": "lite-server",
"postinstall": "typings install",
"tsc": "tsc",
"tsc:w": "tsc -w",
"typings": "typings"
},
"license": "ISC",
"dependencies": {
"@angular/common": "2.0.0-rc.4",
"@angular/compiler": "2.0.0-rc.4",
"@angular/core": "2.0.0-rc.4",
"@angular/forms": "0.2.0",
"@angular/http": "2.0.0-rc.4",
"@angular/platform-browser": "2.0.0-rc.4",
"@angular/platform-browser-dynamic": "2.0.0-rc.4",
"@angular/router": "3.0.0-beta.2",
"@angular/router-deprecated": "2.0.0-rc.2",
"@angular/upgrade": "2.0.0-rc.4",
"core-js": "^2.4.0",
"reflect-metadata": "^0.1.3",
"rxjs": "5.0.0-beta.6",
"zone.js": "^0.6.12",
"angular2-in-memory-web-api": "0.0.14",
"bootstrap": "^3.3.6"
},
"devDependencies": {
"concurrently": "^2.0.0",
"lite-server": "^2.2.0",
"gulp": "^3.9.0",
"gulp-typescript": "^2.13.6",
"typings":"^1.0.4"
}
}
<?xml version="1.0"?>
<!DOCTYPE routes PUBLIC "-//Liferay//DTD Friendly URL Routes 7.0.0//EN" "http://www.liferay.com/dtd/liferay-friendly-url-routes_7_0_0.dtd">
<routes>
<route>
<pattern>/country/{countryId}</pattern>
<implicit-parameter name="mvcPath">/view.jsp</implicit-parameter>
</route>
</routes>
(function(global) {
var basePath = '/o/hello-angular/js';
// map tells the System loader where to look for things
var map = {
'app': basePath + '/app', // 'dist',
'@angular': basePath + '/node_modules/@angular',
'angular2-in-memory-web-api': basePath + '/node_modules/angular2-in-memory-web-api',
'rxjs': basePath + '/node_modules/rxjs'
};
// packages tells the System loader how to load when no filename and/or no extension
var packages = {
'app': { main: 'main.js', defaultExtension: 'js' },
'rxjs': { defaultExtension: 'js' },
'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
};
var ngPackageNames = [
'common',
'compiler',
'core',
'forms',
'http',
'platform-browser',
'platform-browser-dynamic',
'router',
'router-deprecated',
'upgrade',
];
// Individual files (~300 requests):
function packIndex(pkgName) {
packages['@angular/'+pkgName] = { main: 'index.js', defaultExtension: 'js' };
}
// Bundled (~40 requests):
function packUmd(pkgName) {
packages['@angular/'+pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
}
// Most environments should use UMD; some (Karma) need the individual index files
var setPackageConfig = System.packageWithIndex ? packIndex : packUmd;
// Add package entries for angular packages
ngPackageNames.forEach(setPackageConfig);
var config = {
map: map,
packages: packages
};
System.config(config);
})(this);
@yallen011
Copy link

what happens when you have 2+ portliest you want to use Angular 2 for? do you have a gist for how you deal with have multiple portlets in your application that you want to handle, let's say portlet 1 and portlet 2 that are independent of each other but you want to use Angular 2 for both?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment