Angular App
Before you begin
Prior to starting this tutorial, you should have already completed the headless tutorial. This tutorial builds upon the concepts you learned in that tutorial as well as the web project and content type definitions discussed previously.
This tutorial aims to show how an Angular developer might use Jahia’s GraphQL API in an Angular application. Therefore, while some elements and concepts of Angular will be touched on and explained, it is assumed that you already have basic experience with Angular.
What You’ll Need
- Jahia 8
- Active LTS version of Node with NPM
- Angular CLI
- Your favorite code editor (Visual Studio Code, Sublime Text, vi/vim, etc.)
What You’ll Build
We will be building a single-page application (SPA) that displays a list of tutorial items created and published in Jahia.
The main screen will show the list of tutorial items:
Clicking on the card will show the details of the tutorial item:
Source Code
The source code for this tutorial can be found here. Also note that for each subsequent section in this tutorial, there is a corresponding branch in the repo. Each branch contains the source code that you should have at the completion of that section. For example, the 01-getting-started branch contains the state of the source through the end of the Getting Started section, before any changes to the code were made as part of the next section.
Getting Started
To get started, we’ll use the Angular CLI to bootstrap a new Angular project for us.
Open your terminal or command prompt and navigate to a directory where you want to save your project. Type in the command:
ng new jahia-angular-headless-tutorial
When the tool prompts you for the following options, choose the following:
- Would you like to add Angular routing?: Yes
- Which stylesheet format would you like to use?: CSS
This will create a new directory named jahia-angular-headless-tutorial with the initial project structure and files and install various dependencies.
To run the SPA, navigate to in jahia-angular-headless-tutorial your terminal and issue the command:
ng serve --open
This will start the app and make it available on port 4200 (assuming no other app is currently listening on port 4200). You should be able to see this app at http://localhost:4200 or the same port on your free cloud trial system.
Once you’re done looking at the initial application, you can stop the app (ctrl+c in the terminal) and open the jahia-angular-headless-tutorial directory in your code editor.
We are now ready to build our app.
Using Mock Data
Before working with real data, it is often easier to start with mock data and build out the components with the mock data before using the real data.
Create a folder named data under the src directory and add a file called items.ts to that directory. Copy the contents of this file to your items.ts file.
Displaying a List of Tutorial Items
The next step is to build a component that will display a list of tutorial items. These items will be retrieved from Jahia later in the tutorial but for now, we’ll use the mock items.
Initial Application Level Changes
To make the list and the app look a little better, we will use Material Design to style our components. Luckily, there is already a library of Material Design-style Angular components ready for us to use in our project. To add them to our project, open your terminal to the project directory (jahia-angular-headless-tutorial) and execute the following command:
ng add @angular/material
Select the following options from the tool:
Choose a prebuilt theme name, or "custom" for a custom theme | Indigo/Pink |
Set up global Angular Material typography styles? | Yes |
Set up browser animations for Angular Material? | Yes |
We also want to install a flex/responsive layout system so we’ll use Flex Layout. To add this to the project, run:
npm i @angular/flex-layout @angular/cdk
In order to use Material and Flex Layout for this part of the tutorial, we need to import the components and modules we plan to use in the project’s src/app/app.module.ts file. Add the following import statements right below the import for
NgModule:
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatCardModule } from '@angular/material/card';
We then need to add the two modules to our app module’s imports list:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
FlexLayoutModule,
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
Next, we want to replace our project’s App component that was initially created when we bootstrapped our app using Angular CLI with the contents of our own app.
Open up src/app/app.component.html and delete all the contents and replace it with the following:
<div class="container" fxLayout="row" >
<router-outlet></router-outlet>
</div>
Open up src/app/app.component.css and delete all the contents and replace it with the following:
.container
{
max-width: 1280px;
margin-left: auto;
margin-right: auto;
}
Now we’re ready to create our components.
Create TutorialItemComponent
In the terminal, navigate to this project’s root directory and issue the following command:
ng generate component tutorial-item --skip-tests
This will create the following directory and component files under /src/app:
$ tree src/app/tutorial-item
src/app/tutorial-item
├── tutorial-item.component.css
├── tutorial-item.component.html
└── tutorial-item.component.ts
Open the src/app/tutorial-item/tutorial-item.component.ts file. We need this component to accept an input that provides it the tutorial data to render. First, add the following import at the top:
import { Input } from '@angular/core';
Now, in the class itself, add a new input property called tutorial:
export class TutorialItemComponent implements OnInit {
@Input() tutorial;
constructor() { }
ngOnInit(): void {
}
}
Now open up the template file (src/app/tutorial-item/tutorial-item.component.html) and replace its contents with the following:
<mat-card class="tutorial-card">
<img mat-card-image class="media" src="{{tutorial.image}}" />
<h4 class="mat-h4">{{tutorial.title}}</h4>
</mat-card>
As you can see, we are simply outputting a Material card and outputting data (title and image) from the tutorial item that’s passed to this component.
The last thing we need to do for this component is to add a little styling. Open its stylesheet file (src/app/tutorial-item/tutorial-item.components.css) and add the following:
.tutorial-card {
margin: 20px 10px 0;
}
.media {
height: 250px;
}
Create TutorialsListComponent
Now that we have a component that can render an individual tutorial item, we need to create a component that is capable of showing a list of tutorials.
In the terminal, navigate to this project’s root directory and issue the following command:
ng generate component tutorial-list --skip-tests
This will create the following directory and component files under /src/app:
$ tree src/app/tutorial-list
src/app/tutorial-list
├── tutorial-list.component.css
├── tutorial-list.component.html
└── tutorial-list.component.ts
Open the src/app/tutorial-list/tutorial-list.component.ts file. We want to use the mock data we created previously as the list of tutorials for this component to render. To do so, we need to add an import statement for it at the top of this file:
import { items } from '../../data/items';
We then need to have a property in our class that will be the list of tutorials the component will render. We’ll call this property tutorials and for now, we will assign it to our mock tutorial items:
export class TutorialListComponent implements OnInit
{
tutorials = items;
constructor() { }
ngOnInit(): void {
}
}
Now we need to update the template to render the list of tutorials. Open up the src/app/tutorial-list/tutorial-list.component.html file and replace its contents with the following:
<div
fxLayout="row wrap"
fxLayout.lt-md="column">
<div
fxFlex="25"
fxFlex.lt-md="100"
*ngFor="let tutorial of tutorials">
<app-tutorial-item [tutorial]=tutorial></app-tutorial-item>
</div>
</div>
As you can see in the code in bold above, we loop through our tutorials array and for each item in the array, we pass in the current tutorial to TutorialItemComponent to render it.
Update App Routing
The last thing we need to do is to update our app’s routing so that, if the user is on the app’s home page, they see the list of tutorials.
Open up the src/app/app-routing.module.ts file and import the TutorialsListComponent:
import { TutorialListComponent } from './tutorial-list/tutorial-list.component';
Then add the following route to the Routes array:
const routes: Routes = [
{ path: '', pathMatch: 'full', component: TutorialListComponent}
];
At this point, if you run your app with `ng serve` and navigate to http://localhost:4200, your app might look something similar to this:
Tutorial Item Detail Page
The last component we want to design and build is the detail page that will display the rest of the tutorial item content when the user clicks on a tutorial from the TutorialsListComponent.
Update TutorialItemComponent
First, we want to update TutorialItemComponent so when a user clicks on the card, they will be redirected to the right tutorial detail page.
Open up the TutorialItemComponent template file and wrap the entire card with the following:
<a [routerLink]="['/tutorial', tutorial.id]">
<mat-card class="tutorial-card">
<img mat-card-image class="media" src="{{tutorial.image}}" />
<h4 class="mat-h4">{{tutorial.title}}</h4>
</mat-card>
</a>
This will create a link around the card with a URL to ‘/tutorial/’ + whatever the id of the tutorial is.
Create TutorialDetailPageComponent
Next create a new TutorialDetailPage component by issuing the following command in the terminal:
ng generate component tutorial-detail-page --skip-tests
In the src/app/tutorial-detail-page/tutorial-detail-page.components.ts file, we need to import our mock tutorial items as well as ActivatedRoute, which we need in order to get the tutorial id that is part of the url:
import { ActivatedRoute } from '@angular/router';
import { items } from '../../data/items';
Next, update the class by adding a tutorial property to the class, update the constructor to accept an ActivatedRoute parameter, updating the ngOnInit method to find the tutorial in the list of items based on the tutorial id found in the URL, and add a method that returns the tutorial’s published date in a locale-specific format:
export class TutorialDetailPageComponent implements OnInit {
tutorial;
constructor(
private route: ActivatedRoute
) { }
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
const tutorialId = params.get('tutorialId');
this.tutorial = items.find(tutorial => tutorial.id === tutorialId);
});
}
getFormattedDate(): string {
return this.tutorial && this.tutorial.publishedDate
? new Date(this.tutorial.publishedDate).toLocaleDateString()
: '';
}
}
The template code is pretty straightforward. We just output the various tutorial properties and use the getFormattedDate method to output a nicely formatted date.
<mat-card class="paper">
<div class="media" style="background-image: url({{tutorial.image}})"></div>
<div class="tutorial-content">
<h2 class="mat-h2 title">{{tutorial.title}}</h2>
<p variant="mat-body2">by: {{tutorial.publishedBy}}</p>
<p variant="mat-body2" >on: {{getFormattedDate()}}</p>
<div [innerHTML]=tutorial.body></div>
</div>
</mat-card>
And here’s the accompanying styles in src/app/tutorial-detail-page/tutorial-detail-page.component.css:
.paper {
margin: 20px 0;
border: solid 1px #eee;
padding: 0;
}
.tutorial-content {
padding: 30px;
}
.title {
font-size: 3.75rem;
font-weight: 300;
margin: 20px 0;
line-height: normal;
}
.media {
height: 0;
padding-top: 50%;
display: block;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
Update App Routing
The last thing we need to do is to update the App routing to display TutorialDetailPageComponent when a user clicks on a card from the tutorial items list.
In src/app/app-routing.module.ts, import TutorialDetailPageComponent:
import { TutorialDetailPageComponent } from './tutorial-detail-page/tutorial-detail-page.component';
Add the following route to our list of routes:
const routes: Routes = [
{ path: '', pathMatch: 'full', component: TutorialListComponent},
{ path: 'tutorial/:tutorialId', component: TutorialDetailPageComponent}
];
Now, when the user tries to navigate to any path like `tutorial/{tutorial-id}`, they will see the new TutorialDetailPageComponent that renders the tutorial contents:
Retrieving Contents from Jahia
Now that we have the app looking the way we want it to look, the final thing we want to do is to replace the mock data with the real content published from Jahia.
As covered previously, content from Jahia is made available via its GraphQL API. While there are different client libraries you can use to interact with a GraphQL API, we’ll use graphql-request. Add this to the project by issuing the following command in your terminal (inside your project root directory):
npm i graphql-request
Working with Different Environments and Paths
Next, before we start refactoring the app to work against the Jahia API, we should consider that this app might be used against multiple different instances of Jahia and therefore, we should make the Jahia URL configurable through an environment variable. Angular CLI has already taken care of adding environment files for us in the src/environments directory:
$ tree src/environments
src/environments
├── environment.prod.ts
└── environment.ts
Open the src/environments/environment.ts file and make the following changes:
export const environment = {
production: false,
jahiaHost: "http://localhost:8080" //or whatever your host url is
};
Create a Tutorial Class
Next, we’ll create a Tutorial class. Run the following in the terminal to generate our new class:
ng generate class Tutorial --skip-tests
In the generated src/app/tutorial.ts file, add the following properties:
export class Tutorial {
public id: sString;
public title: sString;
public body: sString;
public publishedBy: sString;
public publishedDate: Date;
public image: sString;
}
Create JahiaContentService
The next thing we’ll need is a service that can be used by the app to retrieve the tutorial content from Jahia.
To create the service, run the following command in your terminal:
ng generate service jahia-content --skip-tests
This will create a new file called src/app/jahia-content.service.ts. Open that file and add the following import statements:
import { environment } from '../environments/environment';
import { Tutorial } from './tutorial';
import { request } from 'graphql-request';
We are going to use the imported environment to make sure we’re making API calls to the right Jahia environment, and the request object will be used to make the calls to the API and the items returned by the API will be used to construct instances of a Tutorial object.
After the import statements, add the following lines of code:
const API_ENDPOINT = `${environment.jahiaHost}/modules/graphql`;
const MEDIA_BASE_PATH = `${environment.jahiaHost}/files/live`;
These URLs and paths will be used in the JahiaContentService methods we’ll add in a moment.
Now, for the class code, we need to add a private tutorials property to the class, and two new methods, getTutorials() and getTutorial(string):
export class JahiaContentService {
private tutorials: Tutorial[] = [];
getTutorials(): Promise<Tutorial[]> {
const query = `{
jcr (workspace: LIVE) {
queryResults: nodesByCriteria(criteria: {
nodeType: "jntuto:tutorialItem"
}) {
nodes {
uuid
title: property(name: "jcr:title") { value }
body: property(name: "body") { value }
image: property(name: "image") { refNode { path } }
publishedDate: property(name: "jcr:lastModified") { value }
publishedBy: property(name: "jcr:lastModifiedBy") { value }
}
}
}
}`;
let promise = new Promise<Tutorial[]>((resolve, reject) => {
request(API_ENDPOINT, query).then(results => {
this.tutorials = [];
results.jcr.queryResults.nodes.forEach(node => {
let tutorial = new Tutorial();
tutorial = {
id: node.uuid,
body: node.body.value,
title: node.title.value,
image: `${MEDIA_BASE_PATH}${node.image.refNode.path}`,
publishedBy: node.publishedBy.value,
publishedDate: new Date(node.publishedDate.value)
}
this.tutorials.push(tutorial);
});
resolve(this.tutorials);
})
.catch(err => {
console.log(err);
reject(err);
});
});
return promise;
}
getTutorial(tutorialId: string): Tutorial {
return this.tutorials.find(tutorial => tutorialId === tutorial.id);
}
constructor() { }
}
The getTutorials() method does the heavy lifting in our service. It takes care of creating the query that will be sent to Jahia’s GraphQL API to get the list of tutorials. As you can see by the following lines, the query that we use takes advantage of the nodesByCriteria query field provided by Jahia’s GraphQL API:
jcr (workspace: LIVE) {
queryResults: nodesByCriteria(criteria: {
nodeType: "jntuto:tutorialItem"
}) {
…
This allows us to create a query that will only return back content items that fit a certain criteria. In this case, we are looking for all content in the system of a certain content type (jntuto:tutorialItem). Also notice that we are targeting the LIVE workspace, which is the workspace that contains the published content. The rest of the query asks for the tutorial item’s uuid, title, body, image, and (aliased) lastModified and lastModifiedBy fields.
The rest of the method just constructs a promise object that is returned by the method. We use request to send the query to Jahia:
request(API_ENDPOINT, query).then(results => {
this.tutorials = [];
...
Then when the results are returned, we iterate through the results and create a new Tutorial object that is filled with the returned data and then add the tutorial object to our list of tutorials:
results.jcr.queryResults.nodes.forEach(node => {
let tutorial = new Tutorial();
tutorial = {
id: node.uuid,
body: node.body.value,
title: node.title.value,
image: `${MEDIA_BASE_PATH}${node.image.refNode.path}`,
publishedBy: node.publishedBy.value,
publishedDate: new Date(node.publishedDate.value)
}
this.tutorials.push(tutorial);
});
Lastly, once all the tutorials have been collected into our list, we resolve the promise with the list of tutorials:
resolve(this.tutorials);
The getTutorial method is really simple:
getTutorial(tutorialId: string) : Tutorial {
return this.tutorials.find(tutorial => tutorialId === tutorial.id);
}
A tutorialId is passed to this method. That value is used to see if any tutorial in the tutorials list matches the id. Once that tutorial is found, it is returned to the caller.
Refactoring the TutorialListComponent
Now that we have a service that will get the data from Jahia, we need to update our TutorialListComponent to use the real data instead of our mock data.
In your imports, you can remove the items import statement and add the following import statement instead:
import { JahiaContentService } from '../jahia-content.service';
Next, add or make the following changes to the class:
export class TutorialListComponent implements OnInit {
tutorials;
constructor(
private contentService: JahiaContentService
) { }
ngOnInit(): void {
this.contentService.getTutorials().then(tutorials => {
this.tutorials = tutorials;
});
}
}
The important changes we’ve made are, we inject JahiaContentService in this object and then we use that new service in ngOnInit to get the list of tutorials from Jahia, which we then assign to the object’s tutorials property.
At this point, running your app will show you a list of your published tutorials:
Refactoring the TutorialDetailPageComponent
The last part of our application we need to change is our TutorialDetailPageComponent. The changes to this component are very minor and are highlighted below:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { JahiaContentService } from '../jahia-content.service';
@Component({
selector: 'app-tutorial-detail-page',
templateUrl: './tutorial-detail-page.component.html',
styleUrls: ['./tutorial-detail-page.component.css']
})
export class TutorialDetailPageComponent implements OnInit {
tutorial;
constructor(
private route: ActivatedRoute,
private contentService: JahiaContentService
) { }
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
const tutorialId = params.get('tutorialId');
this.tutorial = this.contentService.getTutorial(tutorialId);
})
}
getFormattedDate(): string {
return (this.tutorial && this.tutorial.publishedDate ? new Date(this.tutorial.publishedDate).toLocaleDateString() : '');
}
}
First we replace the mock data with by importing JahiaContentService. We then inject this service to this class. Lastly, in ngOnInit, we use the service’s getTutorialId(tutorialId:string) method to get the tutorial this component needs to render.
Once these changes are made, the component should now render real data from Jahia similar to this: