Liferay Remote Application with Angular Custom Elements

Angular Clock

Introduction

Liferay DXP has traditionally offered a wide variety of extension mechanisms to extend and customize the platform. Most of these mechanisms require the deployment of custom-written code to be activated.

As we move towards SaaS solutions and Cloud offerings, we need to provide other extension mechanisms that don't require customers to deliver and run arbitrary code on the company servers.

In this blog we will be tackling HTML Custom Components which have been built with angular as a new extension mechanism.

Constructing A Custom Component Using Angular

Create  Angular Sample Application

As a starting point we need to create a normal angular application, which will be converting it into a custom html component.

Please follow the below instructions to create the application:

  1. Install Angular CLI
    npm install -g @angular/cli

  2. Create new angular project
    ng new ng-clock

  3. Install the required node modules
    npm install 

Hint

This example has been developed using the following framework / tools versions

Liferay 7.4

NPM 8.1.2

Node 16.13.1

Angular Cli 12.2.4

Designing The Application

In order to sense the output of our example, let's add a custom behavior to the application, in this example we will make it very simple, we will assume that we are building a clock.

  1. Replace the code inside app.component.ts with the following code:

import { AfterViewInit, Component } from '@angular/core';

 @Component({

 selector: 'ng-clock',

 templateUrl: './app.component.html',

 styleUrls: ['./app.component.css']

})

export class AngularClock implements AfterViewInit {

timerId: any;

 ngAfterViewInit() {

   this.generateDialLines();

   this.timerId = this.getTime();

 }

 getTime() {

   setInterval(()=>{

     this.clock();

   }, 100);

 }

clock() {

   var weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],

       d = new Date(),

       h = d.getHours(),

       m = d.getMinutes(),

       s = d.getSeconds(),

       date = d.getDate(),

       month = d.getMonth() + 1,

       year = d.getFullYear(),

       hDeg = h * 30 + m * (360/720),

       mDeg = m * 6 + s * (360/3600),

       sDeg = s * 6;

       var day = weekday[d.getDay()];

var monthtext = month+"";

   if(month < 9) {

     monthtext = "0" + month;

   }

   this.h_transform = "rotate("+hDeg+"deg)";

   this.m_transform = "rotate("+mDeg+"deg)";

   this.s_transform = "rotate("+sDeg+"deg)";

   this.date_text = date+"/"+monthtext+"/"+year;

   this.day_text = day;

 }

 generateDialLines()

 {

   for (var i = 1; i < 60; i++) {

     var temp = "rotate(" + 6 * i + "deg)";

     this.diallines.push(temp);

   }

 }

 public h_transform:any;

 public m_transform:any;

 public s_transform:any;

 public date_text :any;

 public day_text : any;

 public diallines = new Array();

 }

 

 

  1. Replace the code inside app.component.html with the following code:

<div class="clock">

   <div>

       <div class="info date">{{date_text}}</div>

       <div class="info day">{{day_text}}</div>

   </div>

   <div class="dot"></div>

   <div>

       <div class="hour-hand" style="transform:{{h_transform}};"></div>

       <div class="minute-hand" style="transform:{{m_transform}};"></div>

       <div class="second-hand" style="transform:{{s_transform}};"></div>

   </div>

   <div>

       <span class="h3">3</span>

       <span class="h6">6</span>

       <span class="h9">9</span>

       <span class="h12">12</span>

   </div>

   <div class="diallines"></div>

   <div class="diallines" *ngFor="let line of diallines" style="transform: {{line}};"></div>

</div>

 

  1. Replace the code inside app.component.css with the following code:

 

  .clock {

       background: #ececec;

       width: 300px;

       height: 300px;

       margin: auto;

       border-radius: 50%;

       position: relative;

       box-shadow: 0 2vw 4vw -1vw rgba(0, 0, 0, 0.8);

   }

  

   .dot {

       width: 14px;

       height: 14px;

       border-radius: 50%;

       background: #ccc;

       top: 0;

       left: 0;

       right: 0;

       bottom: 0;

       margin: auto;

       position: absolute;

       z-index: 10;

       box-shadow: 0 2px 4px -1px black;

   }

  

   .hour-hand {

       position: absolute;

       z-index: 5;

       width: 4px;

       height: 65px;

       background: #333;

       top: 79px;

       transform-origin: 50% 72px;

       left: 50%;

       margin-left: -2px;

       border-top-left-radius: 50%;

       border-top-right-radius: 50%;

   }

  

   .minute-hand {

       position: absolute;

       z-index: 6;

       width: 4px;

       height: 100px;

       background: #666;

       top: 46px;

       left: 50%;

       margin-left: -2px;

       border-top-left-radius: 50%;

       border-top-right-radius: 50%;

       transform-origin: 50% 105px;

   }

   .second-hand {

       position: absolute;

       z-index: 7;

       width: 2px;

       height: 120px;

       background: gold;

       top: 26px;

       lefT: 50%;

       margin-left: -1px;

       border-top-left-radius: 50%;

       border-top-right-radius: 50%;

       transform-origin: 50% 125px;

   }

   span {

       display: inline-block;

       position: absolute;

       color: #333;

       font-size: 22px;

       font-family: 'Poiret One';

       font-weight: 700;

       z-index: 4;

   }

   .h12 {

       top: 30px;

       left: 50%;

       margin-left: -9px;

   }

   .h3 {

       top: 140px;

       right: 30px;

   }

   .h6 {

       bottom: 30px;

       left: 50%;

       margin-left: -5px;

   }

   .h9 {

       left: 32px;

       top: 140px;

   }

   .diallines {

       position: absolute;

       z-index: 2;

       width: 2px;

       height: 15px;

       background: #666;

       left: 50%;

       margin-left: -1px;

       transform-origin: 50% 150px;

   }

   .diallines:nth-of-type(5n) {

       position: absolute;

       z-index: 2;

       width: 4px;

       height: 25px;

       background: #666;

       left: 50%;

       margin-left: -1px;

       transform-origin: 50% 150px;

   }

   .info {

       position: absolute;

       width: 120px;

       height: 20px;

       border-radius: 7px;

       background: #ccc;

       text-align: center;

       line-height: 20px;

       color: #000;

       font-size: 11px;

       top: 200px;

       left: 50%;

       margin-left: -60px;

       font-family: "Poiret One";

       font-weight: 700;

       z-index: 3;

       letter-spacing: 3px;

       margin-left: -60px;

       left: 50%;

   }

   .date {

       top: 80px;

   }

   .day {

       top: 200px;

   }

 

  1. Replace the code inside app.module.ts with the following code:

 

import { NgModule } from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';

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

import { APP_BASE_HREF } from '@angular/common';

@NgModule({

 declarations: [

   AngularClock

 ],

 imports: [

   BrowserModule,

   AppRoutingModule

 ],

 providers: [{provide: APP_BASE_HREF, useValue: "/"}],

 bootstrap: [AngularClock]

})

export class AppModule { }

 

  1. Replace the code inside  <app-root></app-root> with <angualr-clock></angualr-clock> inside index.html file.

  2. Test the application in the browser, by executing the following command:
    Ng serve

  3. Navigate to http://localhost:4200 to check the result application running, and you should see the following output.

 

Now we have designed and implemented an angular analog clock, what we have done till now is a pure  standard Angular implementation.

 

Build as Custom Web Component / Custom HTML Element

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.

 

Angular elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.

 

In order to build angular as a custom web component, we will need to follow the below steps, please note that the following steps are available in details at https://angular.io/guide/elements.

 

  1. Install @angular/elements by executing the following:
    ng add @angular/elements

  2. Define the component(s) which we want to build as a custom element (web component); for that replace the code in app.module.ts with the following:
     

import { Injector, NgModule } from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

import { createCustomElement } from "@angular/elements";

import { AppRoutingModule } from './app-routing.module';

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

import { APP_BASE_HREF } from '@angular/common';

@NgModule({

 declarations: [AngularClock],

 imports: [BrowserModule,AppRoutingModule],

 providers: [{provide: APP_BASE_HREF, useValue: "/"}],

 entryComponents: [AngularClock],

 bootstrap : [AngularClock]

})

export class AppModule {

 constructor(private injector: Injector) {

   const appElement = createCustomElement(AngularClock, {

     injector: this.injector

   });

   customElements.define("ng-clock", appElement);

 }

}

 

If you look at the following line, you will notice that we have defined our html tag name, so you will be able to use your component by embedding <ng-clock></ng-clock> 

 

customElements.define("ng-clock", appElement);

 

  1. Open angular.json and replace "outputHashing": "all" with "outputHashing": "none"

  2. Create the script to build the custom element, create a file at your project root level and name it “build-custom-element.js”

  3. Place the following code in  “build-custom-element.js”

const fs = require('fs-extra');

const concat = require('concat');

(async function build() {

   const files = [

       './dist/ng-clock/runtime.js',

       './dist/ng-clock/polyfills.js',

       './dist/ng-clock/main.js'

   ];

   await fs.ensureDir('angular-elements-build');

   await concat(files, 'angular-elements-build/angular-elements.js');

   await fs.copy('./dist/ng-clock/styles.css', 'angular-elements-build/styles.css');

   //uncomment the below line if you have assets folder in your project

   //await fs.copy('./dist/ng-clock/assets/', 'angular-elements/assets/');

})();

  1. Install the required NPM modules
    npm i concat fs-extra --save-dev

  2. In your package.json file, add the following line under scripts
    "build:ng-clock": "ng build ng-clock --prod && node build-custom-element.js"

  3. Execute the following command to build your custom element
    npm run build:ng-clock

Congratulations! You have implemented and built your angular web component, this component can be used anywhere by adding the generated javascript file and css, this will allow you to use the custom component in any plain html page or any other places like Liferay.

Create Liferay Remote Application

Liferay 7.4 has been empowered with a new extension mechanism by adding custom web component using Liferay Remote Applications, please follow the below instructions in order to add the Angular Clock Component we have just created:

  1. Navigate to Liferay -> Content and Data -> Documents and Media and create a folder to host our web component files.

  2. Copy the files in your angular project folder -> angular-elements-build

    1. Angular-elements.js

    2. styles.css

Make sure to upload the above mentioned files in the Liferay folder you have just created.

  1. Navigate to Liferay -> Global Menu -> Applications -> Custom Apps -> Remote Apps

  2. Create New Remote App

  3. In the type filed, select “Custom Element”

  4. In the HTML Element Name type “ng-clock”

  5. In the URL enter the Angular-elements.js full url from liferay file information panel.

  6. In the CSS URL enter the styles.css full url from liferay file information panel.

  7. Save the application

Congratulations! You can now use your NG-Clock in any liferay page by drag and drop it from the page editor tool box under Widgets -> Remote Apps.

 

Resources

Angular Application Sample Code “NG Clock” has been published on github:

https://github.com/mahmoudhtayem87/ng-clock