April 25, 2020
Estimated Post Reading Time ~

AEM + Angular 6



In the last few years, frontend frameworks have grown considerably. Furthuermore, they’re not only being used for building small parts of a website, but even Single Page Applications too. These kind of frameworks can simplify the development process and provide great user experiences.

In this post, we are going to illustrate our road to integrate AEM 6.4 with Angular 6 by leveraging the use of Custom Elements.

Angular 6 was released last May and one of its big features was Angular Elements, which allow us to build Custom Elements from our Angular components.

In order to make this integration, we are going to have two projects:
AEM project

It is a regular AEM project with some clientlibs containing the JavaScript and CSS files generated after building our Angular project.
Angular 6 project

In this project we are going to build Angular components and export them as Custom Elements, which we’ll use later as templates for our AEM components. In this project we use the aem-clientlib-generator npm module to generate the clientlibs mentioned above.


Angular project
Our angular project looks like this:



Now let's see some of the configurations we have done.

First of all, we have set the property “outputHashing”: “none” in our angular.json file. With this property set we prevent Angular from appending hashes to our built files, so we get clear names easily identifiable from our clientlib.config.js file.

After installing the aem-clientlib-generator (by running npm install aem-clientlib-generator –save-dev in the console) we created a clientlib.config.js file, that looks like this:

module.exports = {
// default working directory (can be changed per ‘cwd’ in every asset option)

context: __dirname,

// path to the clientlib root folder (output)

clientLibRoot: “./../AEMAngularProject/ui.apps/src/main/content/jcr_root/apps/AEMAngularProject/clientlibs/angular”,

libs: [
{
name: “main”,
categories: [“AEMAngularProject.angular-main”],
serializationFormat: “xml”,
assets: {
js: [
“dist/main.js”
]
}
},

{
name: “polyfills”,
categories: [“AEMAngularProject.angular-polyfills”],
serializationFormat: “xml”,
assets: {
js: [
“dist/polyfills.js”
]
}
},

{
name: “runtime”,
categories: [“AEMAngularProject.angular-runtime”],
serializationFormat: “xml”,
assets: {
js: [
“dist/runtime.js”
]
}
},

{
name: “styles”,
categories: [“AEMAngularProject.angular-styles”],
serializationFormat: “xml”,
assets: {
css: [
“dist/styles.css”
]
}
},
]
};

And finally, we configure our build command so it generates the clientilibs after building our project. To do so, we set the npm build command like this:

“scripts”: {
“ng”: “ng”,
“start”: “ng serve”,
“build”: “ng build –prod && clientlib“,
“test”: “ng test”,
“lint”: “ng lint”,
“e2e”: “ng e2e”
},


Note: both Angular and AEM projects must be located into the same directory.
Heroes component

For this post we created a Heroes component – a variation of the Heroes component of the Angular’s getting started tutorial. In this case we use Dragula to drag and drop heroes between two main and secondary collections.

The heroes.component.html file looks like this:
<div class=”heroes”>
<h1 style=”text-align: center”>My Heroes</h1>
<div class=’wrapper’>
<div class=”container-title”>Main</div>
<div class=”container-title”>Secondary</div>
</div>


<div class=’wrapper’>
<div class=’container’ [dragula]='”heroes-bag”‘ [dragulaModel]=’mainHeroes’>
<div class=’list-element’ *ngFor=’let hero of mainHeroes’ (click)=”onClicked(hero)” [class.selected]=”hero === selectedHero”>
<span class=”badge”>{{hero.id}}</span> – {{hero.name}}
</div>
</div>


<div class=’container’ [dragula]='”heroes-bag”‘ [dragulaModel]=’secondaryHeroes’>
<div class=’list-element’ *ngFor=’let hero of secondaryHeroes’ (click)=”onClicked(hero)” [class.selected]=”hero === selectedHero”>
<span class=”badge”>{{hero.id}}</span> – {{hero.name}}
</div>
</div>
</div>

<div class=”hero-detail”>
<app-hero-detail [hero]=”selectedHero”></app-hero-detail>
</div>
</div>


while our heroes.component.ts will look as follows:

import {Component, OnInit, SimpleChange, SimpleChanges, Input} from ‘@angular/core’;

import { DragulaService } from ‘ng2-dragula’;
import { Hero } from ‘../hero’;

@Component({
selector: ‘heroes’,
templateUrl: ‘./heroes.component.html’,
styleUrls: [‘./heroes.component.css’]
})

export class HeroesComponent implements OnInit {
@Input() heroes: string;

mainHeroes: Hero[];
secondaryHeroes: Hero[] = [];
selectedHero: Hero;
constructor() {}

ngOnInit() {
if (this.heroes) {
this.mainHeroes = JSON.parse(this.heroes);
}
}

ngOnChanges(changes: SimpleChanges) {
const heroes: SimpleChange = changes.heroes;
this.mainHeroes = JSON.parse(heroes.currentValue);
}

onSelect(hero: Hero): void {
this.selectedHero = hero;
}

onClicked(hero: Hero): void {
this.selectedHero = hero;
}
}


Basically, this class keeps two lists of Heroes that will be bound to the main and secondary containers in the HTML. This component receives a JSON string as an attribute with all heroes and sets them to the main container by default.

It also handles click events on the heroes, displaying a HeroDetails component where we can edit the name of the clicked hero. For this, selectedHero will be passed as an attribute to the HeroDetails component.

Note: You could do whatever you want with those two lists of heroes, like submitting them with an ajax request, pretty format them as JSON, etc. In this case, we are only showing the lists.

This is how our Heroes component looks like:





Up to now, we have our Heroes component created, but as we mentioned at the beginning of this post, we want to use our Angular components as custom elements into our AEM project.

To do this, we are going to do some things in our AppModule. Ours looks like this:

import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule, Injector, CUSTOM_ELEMENTS_SCHEMA } from ‘@angular/core’;
import { FormsModule } from ‘@angular/forms’;
import { createCustomElement } from ‘@angular/elements’;
import { DragulaModule } from ‘ng2-dragula’;

import { HeroesComponent } from ‘./heroes/heroes.component’;
import { HeroDetailComponent } from ‘./hero-detail/hero-detail.component’;

@NgModule({
declarations: [
HeroesComponent,
HeroDetailComponent
],

imports: [
BrowserModule,
FormsModule,
DragulaModule
],

providers: [],

bootstrap: [],

entryComponents: [
HeroesComponent,
HeroDetailComponent
],

schemas : [
CUSTOM_ELEMENTS_SCHEMA
]
})

export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
this.registerCustomElements();
}

registerCustomElements() {
const HeroesElement = createCustomElement(HeroesComponent, {injector: this.injector});
customElements.define(‘heroes-element’, HeroesElement);
}
}


The things we have to do in order to define our component as a custom element are highlighted.

We added our components to the entryComponents array. This makes Angular not to bootstrap that component at the beginning, but to be prepared to bootstrap them any time we add the component to the DOM.
We set the CUSTOM_ELEMENTS_SCHEMA in order to allow any HTML tag and attributes names.

We pass the HeroesComponent class to the createCustomElement function in order to generate a custom element from our Angular component. After this, we assign the tag heroes-element to the custom element.

Having done this, we’ll be able to add an <heroes-element> HTML tag to the DOM anytime we want and Angular will render our Heroes component.

Building the project
The last thing we need to do at the frontend is to build our Angular project.

Placed in the project’s folder, we are going to run npm run build. With this, as we configured above, Angular will generate some .js files with our project compiled and the aem-clientlib-generator library will generate the clientlibs with those files into our AEM project.

Backend
On the other hand, we have an AEM project where we are going to use our <heroes-element> as the template of a heroes AEM component.

The above-generated clientlibs should look like this in our crx:


Now, in order to use the heroes-element in our AEM project, we created a heroes component. To author the heroes’ id and the name we created a dialog with a multifield as follows:


We also have a Sling Model in the backend that is in charge of getting the nodes of our heroes and build a JSON string with the data (another possible implementation is to use Sling Model Exporter). That Sling Model is defined as follows:

@Model(adaptables=Resource.class)
public class HeroesModel {

@Inject @Named(“heroesList”) @Optional
protected Node heroes;

protected JSONArray heroesJSON = new JSONArray();

@PostConstruct
protected void init() throws Exception {
if (heroes != null) {
NodeIterator heroesIterator = heroes.getNodes();

Node hero;
JSONObject heroJSON;

while (heroesIterator.hasNext()) {
hero = heroesIterator.nextNode();
heroJSON = new JSONObject();

heroJSON.put(“name”, hero.getProperty(“name”).getString());
heroJSON.put(“id”, hero.getProperty(“id”).getString());

heroesJSON.put(heroJSON);
}
}
}

public String getHeroes() {
return heroesJSON.toString();
}
}


And this is an example of a JSON string returned by this model:

[
{ “name”: “Aquaman”, “id”: “1” },
{ “name”: “Ironman”, “id”: “2” },
{ “name”: “Superman”, “id”: “3” },
{ “name”: “Spiderman”, “id”: “4” }
]

And finally, we have the template of the heroes component. With sightly we pass the above JSON string to our heroes-element as a heroes attribute:

<sly data-sly-use.heroes=”com.aem.community.core.models.HeroesModel”/>
<heroes-element heroes=’${heroes.heroes}’></heroes-element>

With all this done, we should be able to have our component working on a page.



Conclusion
In a short development time and with not much effort we have built an AEM component with rich user experience. This would be much more difficult to do without the help of a frontend framework/library like Angular.

This process also helps separate backend and frontend development teams to integrate their solutions with little conventions about how backend and frontend will interact.


By aem4beginner

No comments:

Post a Comment

If you have any doubts or questions, please let us know.