Hi everyone,
We're kicking off a new AEM project from scratch, and the requirement is to use Angular for the frontend. By default, the Angular version is set to 9 when scaffolding the project, but we needed a more recent version — so we upgraded Angular successfully, and the build completes without errors.
However, we're facing a few issues:
The Angular components are not rendering on the pages.
Component mapping doesn’t seem to be working.
The clientlibs-angular folder is being generated, but the js.txt file inside is empty.
I have limited experience with Angular, so I’d really appreciate it if anyone who has gone through a similar upgrade process or encountered these issues could share some guidance or best practices.
Thanks in advance!
Solved! Go to Solution.
Views
Replies
Total Likes
Hi @HARPREETSI5,
In main.ts
, make sure this exists:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
In your AEM component HTL file (.html
<div class="my-angular-wrapper">
<app-root></app-root> <!-- or whatever your Angular root component selector is -->
</div>
Make sure this selector matches what is defined in your AppComponent
:
@Component({
selector: 'app-root',
...
})
In the AEM page template HTL (e.g., structure/page.html
<cq:includeClientLib categories="yourproject.angular" />
Replace yourproject.angular
with the category defined in your clientlibs-angular/.content.xml
.
sling:resourceType
in the model JSONIn your Sling Model (Java):
@ValueMapValue
@Optional
private String slingResourceType;
public String getSlingResourceType() {
return slingResourceType;
}
In the model exporter (using @Exporter
), make sure the sling:resourceType
is included in the output.
In Angular (component-mapping.ts
import { HeroComponent } from './components/hero/hero.component';
import { TeaserComponent } from './components/teaser/teaser.component';
export const COMPONENT_MAPPING: any = {
'myproject/components/content/hero': HeroComponent,
'myproject/components/content/teaser': TeaserComponent
};
In your container Angular component (e.g., DynamicContainerComponent
import { Component, ComponentFactoryResolver, Input, ViewChild, ViewContainerRef } from '@angular/core';
import { COMPONENT_MAPPING } from './component-mapping';
@Component({
selector: 'dynamic-container',
template: `<ng-container #container></ng-container>`
})
export class DynamicContainerComponent {
@Input() componentType: string;
@ViewChild('container', { read: ViewContainerRef, static: true }) container: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) {}
ngOnInit() {
const component = COMPONENT_MAPPING[this.componentType];
if (component) {
const factory = this.resolver.resolveComponentFactory(component);
this.container.createComponent(factory);
}
}
}
Then use this like:
<dynamic-container [componentType]="model['sling:resourceType']"></dynamic-container>
clientlibs-angular/js.txt
is EmptyIn angular.json
:
"outputPath": "dist/angular-app"
clientlibs-angular
manually or via scriptYou can use a postbuild
script in package.json
:
"scripts": {
"build": "ng build",
"postbuild": "node scripts/copy-angular-assets.js"
}
scripts/copy-angular-assets.js
:
const fs = require('fs');
const path = require('path');
const sourceDir = path.resolve(__dirname, '../dist/angular-app');
const targetDir = path.resolve(__dirname, '../../ui.apps/src/main/content/jcr_root/apps/yourproject/clientlibs/clientlibs-angular');
const jsFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.js'));
const jsTxtContent = jsFiles.join('\n');
// Copy JS files
jsFiles.forEach(file => {
fs.copyFileSync(path.join(sourceDir, file), path.join(targetDir, file));
});
// Write js.txt
fs.writeFileSync(path.join(targetDir, 'js.txt'), jsTxtContent);
console.log('Copied JS files and updated js.txt');
.content.xml
defines the clientlib correctlyclientlibs-angular/.content.xml
:
<jcr:root
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[yourproject.angular]"
dependencies="[yourproject.base]"
allowProxy="true"/>
To avoid bootstrapping issues, wrap your Angular components as Web Components.
import { createCustomElement } from '@angular/elements';
const el = createCustomElement(MyComponent, { injector });
customElements.define('my-component', el);
Then you can use <my-component>
directly in AEM HTL without worrying about Angular bootstrapping logic.
Hope that helps!
Hi @HARPREETSI5 ,
Before suggesting a solution, could you please confirm:
- Are you using AEM SPA Editor or traditional components?
- How are you embedding Angular ClientLibs, iframe, or something else?
- Are you using Angular Universal or just client-side rendering?
- What's your build + deployment setup (manual copy, Maven, etc.)?
- Do you need full Angular routing, or just embed components?
This will help me provide a solution based on your exact setup.
Thanks!
Views
Replies
Total Likes
Hi Amit
-We will be using AEM SPA Editor
-We will be using Angular Universal
-We are using Maven for building + deployment
For the other questions, I currently don't know. If possible, can we have a small meet?
Views
Replies
Total Likes
Hi @HARPREETSI5 ,
Thanks for confirming the setup.
Try with below solution:
<app-root></app-root>
Ensure Angular bootstraps correctly via main.ts and is loading in the AEM page.
2. Component Mapping Issue
To get mapping working with SPA Editor:
- Add sling:resourceType in your Sling Models and expose it in the exported JSON.
- In Angular, use that value to dynamically load components:
const component = COMPONENT_MAP[model['sling:resourceType']];
3. Empty js.txt in clientlibs-angular
Angular 15+ doesn’t generate classic JS bundles automatically like older versions.
Copy dist/browser/*.js into your AEM clientlibs-angular folder after every build.
Auto-generate js.txt using a simple Node script or Maven copy task to keep it updated.
Regards,
Amit
Views
Replies
Total Likes
Hi @HARPREETSI5,
In main.ts
, make sure this exists:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
In your AEM component HTL file (.html
<div class="my-angular-wrapper">
<app-root></app-root> <!-- or whatever your Angular root component selector is -->
</div>
Make sure this selector matches what is defined in your AppComponent
:
@Component({
selector: 'app-root',
...
})
In the AEM page template HTL (e.g., structure/page.html
<cq:includeClientLib categories="yourproject.angular" />
Replace yourproject.angular
with the category defined in your clientlibs-angular/.content.xml
.
sling:resourceType
in the model JSONIn your Sling Model (Java):
@ValueMapValue
@Optional
private String slingResourceType;
public String getSlingResourceType() {
return slingResourceType;
}
In the model exporter (using @Exporter
), make sure the sling:resourceType
is included in the output.
In Angular (component-mapping.ts
import { HeroComponent } from './components/hero/hero.component';
import { TeaserComponent } from './components/teaser/teaser.component';
export const COMPONENT_MAPPING: any = {
'myproject/components/content/hero': HeroComponent,
'myproject/components/content/teaser': TeaserComponent
};
In your container Angular component (e.g., DynamicContainerComponent
import { Component, ComponentFactoryResolver, Input, ViewChild, ViewContainerRef } from '@angular/core';
import { COMPONENT_MAPPING } from './component-mapping';
@Component({
selector: 'dynamic-container',
template: `<ng-container #container></ng-container>`
})
export class DynamicContainerComponent {
@Input() componentType: string;
@ViewChild('container', { read: ViewContainerRef, static: true }) container: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) {}
ngOnInit() {
const component = COMPONENT_MAPPING[this.componentType];
if (component) {
const factory = this.resolver.resolveComponentFactory(component);
this.container.createComponent(factory);
}
}
}
Then use this like:
<dynamic-container [componentType]="model['sling:resourceType']"></dynamic-container>
clientlibs-angular/js.txt
is EmptyIn angular.json
:
"outputPath": "dist/angular-app"
clientlibs-angular
manually or via scriptYou can use a postbuild
script in package.json
:
"scripts": {
"build": "ng build",
"postbuild": "node scripts/copy-angular-assets.js"
}
scripts/copy-angular-assets.js
:
const fs = require('fs');
const path = require('path');
const sourceDir = path.resolve(__dirname, '../dist/angular-app');
const targetDir = path.resolve(__dirname, '../../ui.apps/src/main/content/jcr_root/apps/yourproject/clientlibs/clientlibs-angular');
const jsFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.js'));
const jsTxtContent = jsFiles.join('\n');
// Copy JS files
jsFiles.forEach(file => {
fs.copyFileSync(path.join(sourceDir, file), path.join(targetDir, file));
});
// Write js.txt
fs.writeFileSync(path.join(targetDir, 'js.txt'), jsTxtContent);
console.log('Copied JS files and updated js.txt');
.content.xml
defines the clientlib correctlyclientlibs-angular/.content.xml
:
<jcr:root
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[yourproject.angular]"
dependencies="[yourproject.base]"
allowProxy="true"/>
To avoid bootstrapping issues, wrap your Angular components as Web Components.
import { createCustomElement } from '@angular/elements';
const el = createCustomElement(MyComponent, { injector });
customElements.define('my-component', el);
Then you can use <my-component>
directly in AEM HTL without worrying about Angular bootstrapping logic.
Hope that helps!
Hi Santosh
Thanks for you help.
I have fixed the JS issue and I also found out the template issue. In my page component the clientlibs was not included.
Now I am facing an issue with the runtime.js file. Whenever I add the runtime.js file to the js.txt. I get errors in the console. These errors are Angular-related errors. Due to which the responsive grid and other components are not loading in the template
Also, my page component is using the " spa-project-core " page component.
<div id="spa-root"></div>
In which file I need to mention spa-root?
Thanks for all the help!!
Views
Replies
Total Likes