Most apps will have a side navigation where you can access different areas of the application. It will also have a toolbar that changes depending on where you are in the application (application state). In a content management application the toolbar usually also contains a search field. Other things that go into the toolbar are, for example, a drop down menu with things such as logout, about, and help.
There is newer version of this article covering ADF 2.0: https://community.alfresco.com/community/application-development-framework/blog/2017/12/15/adding-na...
We want our new application interface to look something like this:
The application interface should be easily extendable. So when adding a new page (i.e. component) it should appear in the side navigation menu without the need for too much configuration/coding. The title in the toolbar should be linked to the side navigation and update automatically when you navigate around between the pages (i.e. components).
This articles assumes that you are starting with an Angular app that has been prepared for ADF 1.9.0 development. You can either follow the "Generating an app with Angular CLI and preparing it for use with ADF 1.9.0" article, or clone the source code as follows:
Martins-Macbook-Pro:ADF mbergljung$ git clone https://github.com/gravitonian/adf-workbench.git adf-workbench-nav
This clones the starter project within a new directory called adf-workbench-nav. Install all the packages for the project like this:
Martins-Macbook-Pro:adf-workbench-nav mbergljung$ npm install
If you are just cloning the source code from the article, please remember that you must have Node.js 8 and Angular CLI already installed. If you don't, then resort to the linked article for information about how to install these tools.
While walking through this article it is a good idea to have the source code available. You can clone the source as follows:
Martins-Macbook-Pro:ADF mbergljung$ git clone https://github.com/gravitonian/adf-workbench-nav.git my-project-name
This application will be built up of a left column with an App Icon and an App Title with the Side Navigation under it. The right column contains the toolbar at the top and the main content area with the router-outlet below it.
When you access http://localhost:4200 it takes you to the login page, which is displayed in the the main content area. You can collapse the left column with the side navigation by clicking on the chevron (<) icon to the left of the toolbar title (i.e. Login). It then looks like this:
Clicking the three stripes (hamburger menu) button to the left brings back the sidenav again. Clicking the right upper corner “Three dots” menu brings out the drop down menu:
As you can see, the ADF Login component have also been customized a bit to fit better into the app, and to give some extra information.
Let’s start by setting up the main layout of this new application interface in the application's root component. It will contain toolbars, side nav, and the main content area with the <router-outlet>:
When the user clicks a link in the Side Navigation the Toolbar 2 is updated to show page title and the Content Area is updated to show page content.
Open up the src/app/app.component.html file and change it so it no longer contains the login markup but instead this:
<md-sidenav-container>
<md-sidenav #mainSideNav mode="side" opened>
<md-toolbar>
<img src="../assets/images/alfresco-logo.png" style="height:60%">
<span fxFlex></span>
{{appName}}
</md-toolbar>
<md-nav-list>
<a *ngFor="let menuItem of mainMenuItems"
md-list-item
md-ripple
[style.position]="'relative'"
routerLinkActive="selected"
[routerLink]="[menuItem.path]">
<md-icon md-list-icon *ngIf="menuItem.icon">{{menuItem.icon}}</md-icon>
<span>{{menuItem.title}}</span>
</a>
</md-nav-list>
</md-sidenav>
<md-toolbar color="primary">
<button md-icon-button (click)="mainSideNav.toggle()">
<md-icon *ngIf="mainSideNav.opened">chevron_left</md-icon>
<md-icon *ngIf="!mainSideNav.opened">menu</md-icon>
</button>
{{(activeMenuItem$ | async)?.title}}
<span fxFlex></span>
<button md-icon-button [mdMenuTriggerFor]="dropdownMenu">
<md-icon>more_vert</md-icon>
</button>
</md-toolbar>
<router-outlet></router-outlet>
</md-sidenav-container>
<md-menu #dropdownMenu x-position="before">
<a md-menu-item href="" (click)="onLogout($event)">
<md-icon>exit_to_app</md-icon>
<span>Logout</span>
</a>
<a md-menu-item href="" routerLink="/about">
<md-icon>info_outline</md-icon>
<span>About</span>
</a>
</md-menu>
All the Angular selectors (i.e. those starting with md-) are associated with Angular Material components, which we can use as we have already installed the Angular Material library when setting up the starter project, or when cloning the project in the beginning. ADF depends on Angular Material and is actively moving towards a complete rewrite to use Angular Material components.
If you want to use the Angular Material side navigation component (md-sidenav), then it needs to be defined inside a side navigation container (md-sidenav-container). We give the sidenav component an id of mainSideNav, and it is open by default so you can see it.
The id is used to show and hide (toggle) the side navigation including toolbar 1, and we can see this further down in the markup as part of toolbar 2:
<md-toolbar color="primary">
<button md-icon-button (click)="mainSideNav.toggle()">
<md-icon *ngIf="mainSideNav.opened">chevron_left</md-icon>
<md-icon *ngIf="!mainSideNav.opened">menu</md-icon>
</button>
The side navigation has toolbar 1 above it with the application name and the application icon. An application icon with the file name alfresco-logo.png needs to be copied into the adf-workbench-nav/src/assets/images directory, you can grab it from the source code for this article.
The navigation list (md-nav-list) is displayed under toolbar 1. The navigation list is dynamic and is read from the mainMenuItems variable. Each item in the mainMenuItems array will be an object looking like this:
export class MenuItem {
path: string;
title: string;
icon?: string;
}
The menu item fields have the following meaning:
The menu item data is used as follows when creating side navigation links:
<md-nav-list>
<a *ngFor="let menuItem of mainMenuItems"
md-list-item
md-ripple
[style.position]="'relative'"
routerLinkActive="selected"
[routerLink]="[menuItem.path]">
<md-icon md-list-icon *ngIf="menuItem.icon">{{menuItem.icon}}</md-icon>
<span>{{menuItem.title}}</span>
</a>
</md-nav-list>
The mainMenuItems list will be managed by a new service and populated based on the application routes that we have configured. So when a new route and component is added the menu system will be automatically updated. Unless we have configured the route to be hidden, or not relevant, which will also be possible.
So for each menu item we create an HTML anchor tag with a router link (URL) based on the menu item path variable value (menuItem.path). The anchor tag content will be the title of the page (menuItem.title) and an optional page icon (menuItem.icon).
The second part of the layout is for toolbar 2, which will contain the side navigation toggle as talked about previously, title of the active page (i.e. component), and the drop down menu. Under toolbar 2 is the main content area where the <router-outlet> will insert each page’s (i.e. component's) content:
<md-toolbar color="primary">
<button md-icon-button (click)="mainSideNav.toggle()">
<md-icon *ngIf="mainSideNav.opened">chevron_left</md-icon>
<md-icon *ngIf="!mainSideNav.opened">menu</md-icon>
</button>
{{(activeMenuItem$ | async)?.title}}
<span fxFlex></span>
<button md-icon-button [mdMenuTriggerFor]="dropdownMenu">
<md-icon>more_vert</md-icon>
</button>
</md-toolbar>
<router-outlet></router-outlet>
The toolbar will show the title of the active menu item (activeMenuItem$), which is the one that was last selected in the side navigation menu. The variable is specified with a dollar sign, which means it is an Observable that will update anytime the router switches to another active route. The update happens because the variable is piped into the async pipe.
Note that the two toolbars will appear as one toolbar but the second one to the right will have a different color (color="primary").
Now when we got the layout of the application sorted we can continue and create the necessary components and services.
As you have seen, we use a lot of Angular Material components in the layout. We need to import these components somewhere so they are available. It is very likely that Angular Material components will be used in most components that we create now and in the future, so it make sense to add them in a common place that can be imported where it is needed. We will use a module for this that imports and exports all the necessary Angular Material components.
Create the module with the Angular CLI tool as follows standing in the main application directory:
Martins-Macbook-Pro:adf-workbench-nav mbergljung$ ng g module app-common --flat false
create src/app/app-common/app-common.module.ts (193 bytes)
This module is not yet provided anywhere so we need to add it to the AppModule, open up the src/app/app.module.ts file and add it as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppCommonModule } from './app-common/app-common.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AppCommonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Note that we have removed the ADF Core module import as it will be done from the AppCommonModule instead. We have also removed the ADF Login module import as it will be part of a new AppLoginModule.
This makes the common module available everywhere. Now, open up the src/app/app-common/app-common.module.ts file and implement the AppCommonModule as follows:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
/** Angular Material imports, only import the portions you will use to optimize build
(MaterialModule to include all is deprecated) */
import {
MdAutocompleteModule,
MdButtonModule,
MdButtonToggleModule,
MdCardModule,
MdCheckboxModule,
MdIconModule,
MdInputModule,
MdListModule,
MdMenuModule,
MdProgressSpinnerModule,
MdRadioModule,
MdRippleModule,
MdSelectModule,
MdSidenavModule,
MdSlideToggleModule,
MdSnackBarModule,
MdTabsModule,
MdToolbarModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { CoreModule, TRANSLATION_PROVIDER } from 'ng2-alfresco-core';
export function modules() {
return [
/** Angular Core */
CommonModule,
/** Angular Material */
MdAutocompleteModule,
MdButtonModule,
MdButtonToggleModule,
MdCardModule,
MdCheckboxModule,
MdIconModule,
MdInputModule,
MdListModule,
MdMenuModule,
MdProgressSpinnerModule,
MdRadioModule,
MdRippleModule,
MdSelectModule,
MdSidenavModule,
MdSlideToggleModule,
MdSnackBarModule,
MdToolbarModule,
MdTabsModule,
FlexLayoutModule,
/* Alfresco ADF */
CoreModule
];
}
@NgModule({
imports: modules(),
declarations: [],
providers: [
{
provide: TRANSLATION_PROVIDER,
multi: true,
useValue: {
name: 'app',
source: 'assets'
}
}
],
exports: modules()
})
export class AppCommonModule { }
A couple of things to note here. We don’t bring in every available Angular Material component, just those that we think we need for the application. It used to be possible to import the MaterialModule and you would have all components. This is no longer best practice and is deprecated. We also bring in the ADF Core module as it is used by pretty much all ADF-based pages.
The layout that we have makes use of the Angular Flex Layout library. It is not yet defined as a dependency in package.json. Fix this by the following npm install command:
Martins-MacBook-Pro:adf-workbench-nav mbergljung$ npm install --save-exact @angular/flex-layout@2.0.0-beta.9
+ @angular/flex-layout@2.0.0-beta.9
We have also added a new translation provider (i.e. we import the TRANSLATION_PROVIDER from ADF Core) for our custom user interface labels. It will provide all the labels in different languages (if we provide it, just english for now) to the application. The translation provider configuration expects the i18n resource files to be located in the src/assets/i18n directory. We will provide English translation as default in the en.json file. Create the directory and file so it looks like this:
Here I have just put some sample resource strings into the en.json file. These i18n resources can then be used in your component templates as follows:
title="{{'SOME_COMPONENT.FIELDS.NAME' | translate}}"
The actual translation is done by the translate pipe from the ngx-translate/core library.
The login functionality that uses the ADF Login component will be kept in a separate component with its own routing table. When applications grow it is best practice to keep each feature area, consisting of one or more components, in its separate module. This is also how the ADF Components are structured.
Create the module with the routing table standing in the main application directory as follows:
Martins-Macbook-Pro:adf-workbench-nav mbergljung$ ng g module app-login --flat false --routing
create src/app/app-login/app-login-routing.module.ts (251 bytes)
create src/app/app-login/app-login.module.ts (288 bytes)
This new module is not yet provided anywhere so we need to add it to the AppModule. In fact we need to add two modules as components and routing are kept in separate modules. This make sense as the routing definitions could become quite large, and you might also want to decide where you want to import the routing list and in what order.
Open up the src/app/app.module.ts file and add as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppCommonModule } from './app-common/app-common.module';
import { AppLoginRoutingModule } from './app-login/app-login-routing.module';
import { AppLoginModule } from './app-login/app-login.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AppCommonModule,
AppLoginModule,
AppLoginRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Before we can define any routes for the Login module we have to define at least one component that represent the login page. The easiest way to do this is again to use the Angular CLI tool. This time stand in the app-login directory when executing the following command:
Martins-MacBook-Pro:adf-workbench-nav mbergljung$ cd src/app/app-login/
Martins-MacBook-Pro:app-login mbergljung$ ng g component app-login-page
create src/app/app-login/app-login-page/app-login-page.component.css (0 bytes)
create src/app/app-login/app-login-page/app-login-page.component.html (33 bytes)
create src/app/app-login/app-login-page/app-login-page.component.spec.ts (672 bytes)
create src/app/app-login/app-login-page/app-login-page.component.ts (299 bytes)
update src/app/app-login/app-login.module.ts (392 bytes)
Note how the app login page component is automatically declared in the src/app/app-login/app-login.module.ts:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppLoginRoutingModule } from './app-login-routing.module';
import { AppLoginPageComponent } from './app-login-page/app-login-page.component';
@NgModule({
imports: [
CommonModule,
AppLoginRoutingModule
],
declarations: [AppLoginPageComponent]
})
export class AppLoginModule { }
What is missing in the module is the import of the ADF Login module. Remember, we removed it from the AppModule. Import it again here in the AppLoginModule so it now looks like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppLoginRoutingModule } from './app-login-routing.module';
import { AppLoginPageComponent } from './app-login-page/app-login-page.component';
import { AppCommonModule } from '../app-common/app-common.module';
import { LoginModule } from 'ng2-alfresco-login';
@NgModule({
imports: [
CommonModule,
AppLoginRoutingModule,
/* Common App imports (Angular Core and Material, ADF Core */
AppCommonModule,
/* ADF libs specific to this module */
LoginModule
],
declarations: [AppLoginPageComponent]
})
export class AppLoginModule { }
We also import the AppCommonModule, so we have access to all the Angular Material components, such as md-icon. The app common module also brings in ADF Core with lots of things that are used frequently.
Now we can define at least one route for this module pointing to the new Login page. Open up the src/app/app-login/app-login-routing.module.ts file, you should see something like this:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AppLoginRoutingModule { }
We can see that there are no routes defined yet for a new module. Add the login page (i.e. component) route as follows:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppLoginPageComponent } from './app-login-page/app-login-page.component';
const routes: Routes = [{
path: 'login',
component: AppLoginPageComponent,
data: {
title: 'Login',
icon: 'forward',
hidden: false,
isLogin: true
}
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AppLoginRoutingModule { }
We start by importing the component that we want to create a route for, in this case the AppLoginPageComponent. Then we define the new route in the Routes object and give it the a path of /login (the full path to the login page is then http://localhost:4200/login).
Note how we also support the necessary extra data for the MenuItem object, the title and the icon. The icon is an identifier from the Angular Material list of icons. There are also some other fields that are used to specify if the route should always be hidden (i.e. hidden), useful for child routes that you don't want to display in the main navigation menu, and if the route is the login route (i.e. isLogin).
The next thing we need to do is move the login template and callback method implementations over to the new Login page component implementation (they were previously in the app component). Starting with the template, open the src/app/app-login/app-login-page/app-login-page.component.html:
<div fxFlex="100">
<adf-login class="app-logo"
[providers]="'ALL'"
[copyrightText]="'© 2017 Alfresco Training.'"
[showRememberMe]="false"
[showLoginActions]="false"
[logoImageUrl]="'assets/images/adf-workbench-logo.png'"
[backgroundImageUrl]="'assets/images/adf-workbench-background.jpg'"
(onSuccess)="onLoginSuccess($event)"
(onError)="onLoginError($event)">
<!--
You cannot have both logo image and header,
you have to choose one or the other,
if both are defined then the header wins.
<login-header><ng-template>Some custom HTML for the header</ng-template></login-header>-->
<login-footer>
<ng-template>
This will log you into both Alfresco Content Services (ACS) and Alfresco Process Services (APS).
</ng-template>
</login-footer>
</adf-login>
</div>
In fact, we have not just copied the template, we have defined it inside a Flex Layout. If you use a card view then you don't need to define the login page inside a md-card, it is already defined inside a card. Further on, we have also customised the <adf-login component with new logo, background, copyright text, button config, and footer. You can get the images from the project source code.
If you are just using one of the backend services, meaning ACS or APS, then update the [providers] property accordingly. For ACS use "'ECM'" and for APS use "'BPM'".
The login component class implementation in src/app/app-login/app-login-page/app-login-page.component.ts looks like this:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-app-login-page',
templateUrl: './app-login-page.component.html',
styleUrls: ['./app-login-page.component.css']
})
export class AppLoginPageComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit() {
}
onLoginSuccess($event) {
console.log('Successful login: ' + $event.value);
// Now, navigate somewhere...
// this.router.navigate(['/some-page']);
}
onLoginError($event) {
console.log('Failed login: ' + $event.value);
}
}
Make sure to remove the onLoginSuccess and onLoginError functions from the src/app/app.component.ts class. When you start building your Content Management or Process Management application it's likely that you will navigate to some other page (i.e. component) directly after a successful login. The code for this is there now but commented out.
When we defined the login page template we used the <adf-login component and we customised it using quite a few properties (Input) and event handlers (Output). So how do we know exactly what input and output properties that are available for an ADF component and ADF version? We can obviously consult the documentation. However, there is a quicker way if you are in an IDE that indexes all packages that are used and that provides stepping into the source code.
In the WebStorm IDE you can step into an ADF component via the component template as follows:
This takes you to the ADF Login component source code:
When you are looking at the ADF component source code every property with the @Input annotation can be used when customising the component and every @Output annotation means an event handler function can be defined to do stuff when associated event happens.
When customising an ADF component you sometimes see input variables defined as follows:
[providers]="'ALL'"
and sometimes you will see the property definition like this:
providers="ALL"
So what is that all about and which way is the correct way. The first version with the brackets ([...]) is the preferred way as that will support change management in Angular and the property will be properly bound to the class member:
@Input()
providers: string;
When you bind the property with the bracket format it is also assuming that the value is an expression, so to get it to a string we need to enclose it in single quotes (i.e. 'ALL').
Now to the confusing part, in this case it will also work to define the property as a normal HTML attribute, it will be set once. But if you had code inside your component that would need to update providers, then that would not work unless the property is properly bound.
So might be a good idea to always use brackets ([...]).
The login logo as it stands now is quite small, the page looks like this:
We would like it to be a bit bigger, so how can we do that when the styling is enclosed in the ADF login component? All ADF components change the default view encapsulation Emulated to view encapsulation None:
@Component({
selector: 'adf-login, alfresco-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
host: {'(blur)': 'onBlur($event)'},
encapsulation: ViewEncapsulation.None
})
export class LoginComponent implements OnInit
This means that a Shadow DOM is not used for view encapsulation. The same is true for all Angular Material components.
What this means is that we can override the styles that are set in login.component.scss. Using an IDE such as WebStorm we can again step into the login.component.html and inspect what styles that are set on the component view:
The only thing we need to do now then is to override the adf-img-logo style and change the hight. Open up the src/app/app-login/app-login-page/app-login-page.component.css file and add the following:
.adf-login-content .adf-login-card-wide .adf-alfresco-logo .adf-img-logo {
max-height: 120px;
}
This will still not work as our component still got the default view encapsulation using a Shadow DOM, so the style will not trickle through to child components, such as the ADF Login component. We can fix this by setting view encapsulation to None in our component. Open the src/app/app-login/app-login-page/app-login-page.component.ts file and add it as follows:
@Component({
selector: 'app-app-login-page',
templateUrl: './app-login-page.component.html',
styleUrls: ['./app-login-page.component.css'],
/* We need to turn off view encapsulation so our component styles affects the child ADF components */
encapsulation: ViewEncapsulation.None
})
export class AppLoginPageComponent implements OnInit
If you are not very happy about turning off view encapsulation for your components, then you can instead add this style to the global src/styles.css.
The logo should now be a bit bigger and the login page looking a bit better:
The menu service will handle the list of available menu items and define the MenuItem class. The service will have one method called getMenItems that can be called to get an array of MenuItem objects to display in the side navigation list. We can easily create this service with the Angular CLI tool. Stand in the main directory of the app and execute the following command:
Martins-MacBook-Pro:adf-workbench-nav mbergljung$ ng g service app-menu --flat false
create src/app/app-menu/app-menu.service.spec.ts (381 bytes)
create src/app/app-menu/app-menu.service.ts (113 bytes)
The new service is not yet provided. We know we need the service in the AppComponent template where we have defined the main app layout. So we need to edit the AppModule and provide the service there. The toolbars are hosted by the AppComponent so it makes sense to add the service to it. If we were going to use this service directly in one of the other components, then we would provide it there.
Open up the src/app/app.module.ts file and add the following:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppCommonModule } from './app-common/app-common.module';
import { AppLoginRoutingModule } from './app-login/app-login-routing.module';
import { AppLoginModule } from './app-login/app-login.module';
import { AppMenuService } from './app-menu/app-menu.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AppCommonModule,
AppLoginModule,
AppLoginRoutingModule
],
providers: [AppMenuService],
bootstrap: [AppComponent]
})
export class AppModule { }
Now, implement the AppMenuService as follows in the src/app/app-menu/app-menu.service.ts file:
import { Injectable } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { AuthenticationService } from 'ng2-alfresco-core';
/* Data for a menu item */
export class MenuItem {
path: string; /* The URL path to the page */
title: string; /* The Title of the page */
icon?: string; /* An optional icon for the page title */
}
@Injectable()
export class AppMenuService {
// Make it possible to send an event about menu changed, so we can talk between components
menuChanged = new Subject<any>();
/* Keep track of which menu item is currently being active/selected */
activeMenuItem$: Observable<MenuItem>;
constructor(private router: Router,
private titleService: Title,
private authService: AuthenticationService) {
this.activeMenuItem$ = this.router.events
.filter(e => e instanceof NavigationEnd)
.map(_ => this.router.routerState.root)
.map(route => {
const active = this.lastRouteWithMenuItem(route.root);
this.titleService.setTitle(active.title);
return active;
});
}
/**
* Get the MenuItem array that should be displayed.
* @returns {MenuItem[]}
*/
getMenuItems(): MenuItem[] {
return this.router.config
.filter(route =>
route.data &&
route.data.title &&
!route.data.hidden &&
this.isNotLoginRouteOrLoginRouteAndNotLoggedIn(route.data) &&
this.isNotEcmRouteOrEcmRouteAndNoAuth(route.data) &&
this.isNotBpmRouteOrBpmRouteAndNoAuth(route.data))
.map(route => {
if (!route.data.title) {
throw new Error('Missing title for toolbar menu route ' + route.path);
}
return {
path: route.path,
title: route.data.title,
icon: route.data.icon
};
});
}
fireMenuChanged() {
this.menuChanged.next(null);
}
private isNotLoginRouteOrLoginRouteAndNotLoggedIn(data: any): boolean {
return !data.isLogin || data.isLogin && !this.authService.isLoggedIn();
}
private isNotEcmRouteOrEcmRouteAndNoAuth(data: any): boolean {
return !data.needEcmAuth || data.needEcmAuth && this.authService.isEcmLoggedIn();
}
private isNotBpmRouteOrBpmRouteAndNoAuth(data: any): boolean {
return !data.needBpmAuth || data.needBpmAuth && this.authService.isBpmLoggedIn();
}
private lastRouteWithMenuItem(route: ActivatedRoute): MenuItem {
let lastMenu;
do {
lastMenu = this.extractMenu(route) || lastMenu;
}
while ((route = route.firstChild));
return lastMenu;
}
private extractMenu(route: ActivatedRoute): MenuItem {
const cfg = route.routeConfig;
return cfg && cfg.data && cfg.data.title
? {path: cfg.path, title: cfg.data.title, icon: cfg.data.icon}
: undefined;
}
}
The idea with the application menu service is that it should expose the menu items from the router and keep track of the active menu item as it changes when the user navigates. The menu items that are available from the Angular Router, currently only the Login route, are kept in MenuItem objects. The Active Menu item logic is handled via the Observable variable defined at the start of the class.
In the getMenuItems method the routes are filtered based on the properties in the data object and if the route needs ECM or BPM authentication. When we continue building on this app we will define new routes specific to content management and process management. They will use some extra properties as in the following examples.
Here is an ECM route example that uses the needEcmAuth data property:
const routes: Routes = [
{
path: 'repository',
component: RepositoryPageComponent,
canActivate: [AuthGuardEcm],
data: {
title: 'Repository',
icon: 'repo',
isLogin: false,
hidden: false,
needEcmAuth: true
}
And here is an example of a BPM route that uses the needBpmAuth property:
const routes: Routes = [ {
path: 'my-tasks',
component: MyTasksPageComponent,
canActivate: [AuthGuardBpm],
data: {
title: 'My Tasks',
icon: 'mytasks',
isLogin: false,
hidden: false,
needBpmAuth: true
},
What we also do in the Menu Service is to emit an event when the menu changes, so any component can listen in and do stuff. The variable for this is defined like this:
menuChanged = new Subject<any>();
And when the menu changes we can fire the event:
fireMenuChanged() {
this.menuChanged.next(null);
}
We are going to update the login page and fire this event after a successful login, open up src/app/app-login/app-login-page/app-login-page.component.ts and update it to look as follows:
import {Component, OnInit, ViewEncapsulation} from '@angular/core';
import { Router } from '@angular/router';
import { AppMenuService } from '../../app-menu/app-menu.service';
@Component({
selector: 'app-app-login-page',
templateUrl: './app-login-page.component.html',
styleUrls: ['./app-login-page.component.css'],
/* We need to turn off view encapsulation so our component styles affects the child ADF components */
encapsulation: ViewEncapsulation.None
})
export class AppLoginPageComponent implements OnInit {
constructor(private router: Router,
private menuService: AppMenuService) { }
ngOnInit() {
}
onLoginSuccess($event) {
console.log('Successful login: ' + $event.value);
// Tell parent component that successful login has happened and menu should change
this.menuService.fireMenuChanged();
// Now, navigate somewhere...
// this.router.navigate(['/some-page']);
}
onLoginError($event) {
console.log('Failed login: ' + $event.value);
}
}
So we import the menu service and then fire the event in the onLoginSuccess function. We will see in the next section how we can listen to this event and update the menu.
The code for this class is based on an article by Todd Motto, read the full background and explanation here.
The new application layout template uses some variables that we need to make available in the AppComponent class before we can test the refactored application.
More specifically the appName variable, the mainMenuItems variable:
{{appName}}
</md-toolbar>
<md-nav-list>
<a *ngFor="let menuItem of mainMenuItems"
And the activeMenuItem variable:
{{(activeMenuItem$ | async)?.title}}
Open up the src/app/app.component.ts file and update it to look like this:
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AppMenuService, MenuItem } from './app-menu/app-menu.service';
import { AuthenticationService } from 'ng2-alfresco-core';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
appName = 'ADF Workbench';
mainMenuItems;
activeMenuItem$: Observable<MenuItem>;
constructor(private router: Router,
private menuService: AppMenuService,
private authService: AuthenticationService) {
this.updateMenu();
this.menuService.menuChanged.subscribe((any) => {
this.updateMenu();
});
}
onLogout(event) {
event.preventDefault();
this.authService.logout().subscribe(() => {
this.navigateToLogin();
},
(error: any) => {
if (error && error.response && error.response.status === 401) {
this.navigateToLogin();
} else {
console.log('An unknown error occurred while logging out', error);
this.navigateToLogin();
}
});
}
navigateToLogin() {
this.updateMenu();
this.router.navigate(['/login']);
}
private updateMenu() {
this.mainMenuItems = this.menuService.getMenuItems();
this.activeMenuItem$ = this.menuService.activeMenuItem$;
}
}
We define the three variables that the template expects and then we use the AppMenuService to fetch the menu items that should be displayed and what menu item that should be displayed as currently active.
In the constructor we set up a handler for when the successful login event happens:
constructor(private router: Router,
private menuService: AppMenuService,
private authService: AuthenticationService) {
this.updateMenu();
this.menuService.menuChanged.subscribe((any) => {
this.updateMenu();
});
}
We then update the menu, which means the Login menu item will disappear.
We also take the opportunity to implement the onLogout button handler. We use the ADF Authentication Service to logout and show the login page directly after.
Now is also the time to set up the default route for the application. We will have it display the Login page by default when http://localhost:4200 is accessed. Open up the src/app/app-routing.module.ts and update it with the default route:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [{
path: '',
redirectTo: '/login',
pathMatch: 'full'
}];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Finally we want to enable the flex layout and also put in some general margins for content. Open up the application wide styles file called src/styles.css and add the following:
html, body {
display: flex;
flex-direction: column;
margin: 0;
height: 100%;
}
md-sidenav {
width: 250px;
}
.content {
padding: 12px;
}
Testing the Refactored Application
We should now be ready to test all the refactoring and additions to the application. Standing in the adf-workbench directory, execute the npm start command as follows:
Martins-MacBook-Pro:workbench-x martin$ npm start
Access http://localhost:4200 and it should redirect you to the login page. Make sure you can login to the backend services that you have configured, you should see the following login dialog if login worked:
The Login navigation item to the left should now be hidden. Logging out via the upper right corner drop down menu should make the Login navigation menu item appear again.
Cool, so we are ready to move on and add more pages.
In this article we have built an application layout that will make it easy to build content and process applications in the future. We don't have to think about how to implement navigation, toolbars, menus etc. We just need to add new modules and pages for the specific app we are building.
Next step now would be to implement a content management app and a process management app based on this application layout:
Building a Content Management App with ADF 1.9.0
Building a Process Management App with ADF 1.9.0 and APS 1.6.4