Angular 2+ Portlets in DXP

So I've been working a lot more with Angular 2+ recently (Angular 4 actually, but that is not so important) and wanted to share some of my findings for those of you whom are interested...

Accessing the Liferay Javascript Object

So TypeScript is sensitive to defined variables, classes, objects, etc.  Which is good when you want to make sure you are building complex apps, type safety and compilation help to ensure that your code starts on a solid foundation.

However, without a corresponding .ts file to support your import of the Liferay javascript object, TypeScript will not be able to compile your code if you try to use the Liferay object.

That said, it is easy to get access to the Liferay object in your TypeScript code.  Near the top of your .ts file, add the following line:

declare var Liferay: any;

Drop it in like after your imports but before your class declaration.

This line basically tells Angular that there is an object, Liferay, out there and it is enough to pass the compile phase.

Alternatively you can use the following syntax:

window['Liferay']

to get to the object, but to me this is not really the cleanest looking line of code.

Supplying Callback Function References to Liferay

So much of the Liferay javascript functions take callback functions.  For example, if you wanted to use the Liferay.fire() and Liferay.on() mechanism for in-browser notification, the Liferay.on() function takes as the second argument a Javascript function.

But, when you're in your TypeScript code, your object methods are not pure javascript functions, plus as an object instance, the method is for a particular object, not a generic method.

But you can pass a bound pointer to an object method and Liferay will call that at the appropriate points.

For example, if you have a method like:

myAlert(event) {
  alert('Received event data ' + event.data);
}

So if you want it to be called on a 'stuff-happened' event, you could wire it up like:

ngOnInit() {
  Liferay.on('stuff-happened', this.myAlert.bind(this));
}

The this.myAlert.bind(this) is used to bind up sort of a proxy to invoke the myAlert() method of the particular instance. If someone does a:

Liferay.fire('stuff-happened', { data: 'Yay!'});

Liferay will end up invoking the myAlert() method, providing the event object, and the method will invoke the alert() to show the details.

Sometimes it is advantageous to have the callback run inside of a zone.  We would change the above ngOnInit() method to:

constructor(private ngZone: NgZone) {}
myZoneAlert(event) {
  this.ngZone.run(() => this.myAlert(event));
}
ngOnInit() {
  Liferay.on('stuff-happened', this.myZoneAlert.bind(this));
}

Using NgRoute w/o APP_BASE_HREF Errors

When using NgRoute, I typically get runtime browser errors complaining Error: No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.

This one is pretty easy to fix.  In your @NgModule declaration, you need to import the APP_BASE_HREF and declare it as a provider. For example:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { APP_BASE_HREF } from '@angular/common';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [{provide: APP_BASE_HREF, useValue : '/' }],
  bootstrap: [AppComponent]
})
export class AppModule { }

The important parts above are (a) the import of APP_BASE_HREF and (b) the declaration of the providers.

NgRoute Without Address Bar Changes

Personally I don't like NgRoute changing the address bar as it gives the really false impression that those URLs can be bookmarked or referenced or manually changed.  The first time you try this, though, you'll see the Liferay error page because Liferay has no idea what that URL is for, it doesn't know that Angular might have taken care of it if the page were rendered.

So I prefer just to block the address bar changing.  This doesn't give false impressions about the URL, plus if you have multiple Angular portlets on a page they are not fighting to change the address bar...

When I'm routing, I always include the skipLocationChange property:

this.router.navigateByUrl('path', { skipLocationChange: true });

Senna and Angular

Senna is Liferay's framework that basically allows for partial-page updates for non-SPA server side portlets. Well, it is actually a lot more than that, but for a JSP or Struts or Spring MVC portlet developer, it is Senna which is allowing your unmodified portlet to do partial page updates in the rendered portal page w/o a full page refresh.

Senna may or may not cause you problems for your Angular apps. I wouldn't disable it out of the gate, but if during testing you find that hokey things are happening, you might try disabling Senna to see if things clear up for you.

Find out how to disable Senna here: https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/automatic-single-page-applications#disabling-spa

I say try your app w/o disabling Senna first because, well, if you disable Senna globally then your non-SPA portlets revert to doing full page refreshes.

Conclusion

So that's really all of the tips and tricks I have at this point.

I guess one final thing I would leave you with is that the solutions presented above really have nothing to do with Liferay. That's kind of important, I found these solutions basically by googling for an answer, but I would leave Liferay out of the search query.

When you think about it, at the end of the day you're left with a browser with some HTML, some CSS, some Angular and non-Angular Javascript. Whatever problems you run into with Angular, if you try to solve them generically that solution will likely also work for fixing the problem under Liferay.

Don't get too hung up on trying to figure out why something in Angular is not working under Liferay, because you are not going to find a great deal of articles which talks about the two of them together.