gravitonian

Adding Navigation, Menu, Toolbar, and Logout to your ADF 2.0 App

Blog Post created by gravitonian Employee on Dec 15, 2017

   

Introduction

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.

 

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).

 

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.

 

Article Series

This article is part of series of articles covering ADF 2.0:

 

 

Prerequisites

This articles assumes that you are starting with an Angular app that has been prepared for ADF 2.0.0 development. You can either follow the "Generating an app with Angular CLI and preparing it for use with ADF 2.0.0" article, or clone the source code as follows:

 

Martins-Macbook-Pro:ADF mbergljung$ git clone https://github.com/gravitonian/adf-workbench20.git adf-workbench-nav20

 

This clones the starter project within a new directory called adf-workbench-nav20. Install all the packages for the project like this:

 

Martins-Macbook-Pro:adf-workbench-nav20 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 1.5.x already installed. If you don't, then resort to the linked article for information about how to install these tools.

 

Source Code

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-nav20.git adf-workbench-nav20-src

 

Adding Side Navigation and Toolbar

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:

 

 

Setting up the layout in the root app component

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>:

ADF App Layout

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:

 

<mat-sidenav-container>
  <mat-sidenav #mainSideNav mode="side" opened>
    <mat-toolbar>
      <img src="../assets/images/alfresco-logo.png" style="height:60%">
      <span fxFlex></span>
      {{appName}}
    </mat-toolbar>
    <mat-nav-list>
      <a *ngFor="let menuItem of mainMenuItems"
         mat-list-item
         mat-ripple
         [style.position]="'relative'"
         routerLinkActive="selected"
         [routerLink]="[menuItem.path]">

        <mat-icon mat-list-icon *ngIf="menuItem.icon">{{menuItem.icon}}</mat-icon>
        <span>{{menuItem.title}}</span>
      </a>
    </mat-nav-list>
  </mat-sidenav>
  <mat-toolbar color="primary">
    <button mat-icon-button (click)="mainSideNav.toggle()">
      <mat-icon *ngIf="mainSideNav.opened">chevron_left</mat-icon>
      <mat-icon *ngIf="!mainSideNav.opened">menu</mat-icon>
    </button>
    {{(activeMenuItem$ | async)?.title}}
    <span fxFlex></span>
    <button mat-icon-button [matMenuTriggerFor]="dropdownMenu">
      <mat-icon>more_vert</mat-icon>
    </button>
  </mat-toolbar>
  <router-outlet></router-outlet>
</mat-sidenav-container>
<mat-menu #dropdownMenu x-position="before">
  <a mat-menu-item href="" (click)="onLogout($event)">
    <mat-icon>exit_to_app</mat-icon>
    <span>Logout</span>
  </a>
  <a mat-menu-item href="" routerLink="/about">
    <mat-icon>info_outline</mat-icon>
    <span>About</span>
  </a>
</mat-menu>

 

All the Angular selectors (i.e. those starting with mat-) 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 using the associated components throughout the framework implementation.

 

When using the Angular Material side navigation component (mat-sidenav), it needs to be defined inside a side navigation container (mat-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:

 

<mat-toolbar color="primary">
    <button mat-icon-button (click)="mainSideNav.toggle()">
      <mat-icon *ngIf="mainSideNav.opened">chevron_left</mat-icon>
      <mat-icon *ngIf="!mainSideNav.opened">menu</mat-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-nav20/src/assets/images directory, you can grab it from the source code for this article.

 

The navigation list (mat-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:

 

  • path: URL path to invoke when menu item link is clicked.
  • title: The page/component title to show in the side navigation and in toolbar 2 when page is displayed.
  • icon: an optional icon to show together with title. This is the Material Design Icon ID as can be found Material Design Icons.

 

The menu item data is used as follows when creating side navigation links:

 

<mat-nav-list>
  <a *ngFor="let menuItem of mainMenuItems"
     mat-list-item
     mat-ripple
     [style.position]="'relative'"
     routerLinkActive="selected"
     [routerLink]="[menuItem.path]">

     <mat-icon mat-list-icon *ngIf="menuItem.icon">{{menuItem.icon}}</mat-icon>
     <span>{{menuItem.title}}</span>
  </a>
</mat-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:

 

<mat-toolbar color="primary">
  <button mat-icon-button (click)="mainSideNav.toggle()">
    <mat-icon *ngIf="mainSideNav.opened">chevron_left</mat-icon>
    <mat-icon *ngIf="!mainSideNav.opened">menu</mat-icon>
  </button>
  {{(activeMenuItem$ | async)?.title}}
  <span fxFlex></span>
  <button mat-icon-button [matMenuTriggerFor]="dropdownMenu">
    <mat-icon>more_vert</mat-icon>
  </button>
</mat-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.

 

Adding a Common App Component

As you can imagine, we will be using ADF components in almost all of our custom components. And it is also very likely that we will use the Angular Material components extensively. We need to import these components somewhere so they are available. We will use a module for this that imports and exports all the necessary ADF Components and Angular Material components.

 

Create the module with the Angular CLI tool as follows standing in the main application directory:

 

Martins-Macbook-Pro:adf-workbench-nav20 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 actually be imported implicitly by other ADF module imports as we will see below.

 

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';

import { FlexLayoutModule } from '@angular/flex-layout';

import { CoreModule, TRANSLATION_PROVIDER } from '@alfresco/adf-core';
import { ContentModule } from '@alfresco/adf-content-services';
import { ProcessModule } from '@alfresco/adf-process-services';
import { InsightsModule } from '@alfresco/adf-insights';

export function modules() {
  return [
    /* Angular Core */
    CommonModule,

    /* Flex Layout */
    FlexLayoutModule,

    /* Alfresco ADF + Angular Material Components */
    CoreModule,
    ContentModule,
    ProcessModule,
    InsightsModule
  ];
}

@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 import the Angular Core and the Flex Layout so we have it available everywhere when importing this module. We don’t bring in the Angular Material components directly as they are imported indirectly via the ADF CoreModule. The ADF Core module brings in all the basic components and services that we need, including the Login components.

 

Then we import the ADF ContentModule and ADF ProcessModule, which will in turn make most of the content related and process related components available to us without any extra imports. Looking at the source code for one of the modules we can see that in turn brings in all the necessary modules that we might need when building our app:

 

...
imports: [
        CoreModule,
        SocialModule,
        TagModule,
        CommonModule,
        WebScriptModule,
        FormsModule,
        ReactiveFormsModule,
        SearchModule,
        BrowserAnimationsModule,
        DocumentListModule,
        UploadModule,
        MaterialModule,
        SitesDropdownModule,
        BreadcrumbModule,
        VersionManagerModule,
        ContentNodeSelectorModule,
        ContentMetadataModule,
        DialogModule,
        FolderDirectiveModule
    ],
    providers: [
        {
            provide: TRANSLATION_PROVIDER,
            multi: true,
            useValue: {
                name: 'adf-content-services',
                source: 'assets/adf-content-services'
            }
        }
    ],
...

 

We can see also that the translation provider for i18n resources related to the adf-content-services package is defined here. So we really need to import the ContentModule at this point for the i18n resources to be loaded properly. The same goes for the ProcessModule.

 

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:

 

i18n resourcesHere 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. 

 

Adding a Login module and component

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 own separate module. This is also how the ADF Components are structured.

 

Generating the Login page

Create the module with the routing table standing in the main application directory as follows:

 

Martins-Macbook-Pro:adf-workbench-nav20 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-nav20 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 { }

 

Implementing the Login page

The first thing we need to do is to make our new App Login Module aware of ADF. What is missing in the AppLoginModule is the import of our new AppCommonModule. Which in turn imports the ADF CoreModule. And it implicitly imports the LoginModule that we need:

 

...
    exports: [
        AppConfigModule,
        BrowserAnimationsModule,
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        TranslateModule,
        ContextMenuModule,
        CardViewModule,
        CollapsableModule,
        PaginationModule,
        ToolbarModule,
        LoginModule,
        UserInfoModule,
        LanguageMenuModule,
        InfoDrawerModule,
        DataColumnModule,
        DataTableModule,
        HostSettingsModule,
        ServiceModule,
        ViewerModule,
        PipeModule,
        DirectiveModule,
        FormModule,
        MaterialModule
    ]
})
export class CoreModule {
...

 

Import the AppCommonModule as follows:

 

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';

@NgModule({
  imports: [
    CommonModule,
    AppLoginRoutingModule,

    /* Common App imports (Angular Core and Material, ADF Core, Content, and Process */
    AppCommonModule
  ],
  declarations: [AppLoginPageComponent]
})
export class AppLoginModule { }

 

Importing the AppCommonModule will also give access to all the Angular Material components, such as mat-icon

 

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'"
             (success)="onLoginSuccess($event)"
             (error)="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 mat-card, it is already defined inside a card. Further on, we have also customised the <adf-login component with a 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';

@Component({
  selector: 'app-app-login-page',
  templateUrl: './app-login-page.component.html',
  styleUrls: ['./app-login-page.component.scss']
})
export class AppLoginPageComponent implements OnInit {

  constructor() { }

  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.

 

Redirecting to the Login Page after Login

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 { }

 

Test the App

Now let's see how the application looks like. Run it as follows:

 

adf-workbench-nav20 mbergljung$ npm start

 

The login logo looks a bit small at the moment:

 

 

But we don't have that much left to fix before we got a nice login and navigation system going.

 

Styling your Login page

As it stands now, the Alfresco logo is quite small. We would like it to be a bit bigger. So how can we do that when the styling is enclosed in the ADF LoginComponent? All ADF components change the default view encapsulation from Emulated to None:

 

@Component({
    selector: 'adf-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 and do some custom padding. 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;
  padding-top: 0px;
  padding-right: 30px;
  padding-bottom: 0px;
  padding-left: 60px;
}

This will still not work as our component 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.scss'],
/* 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.scss. Speaking about global styles, we want to set the width of the side navigation/menu globally, open the styles.scss file and add the following class:

 

...

/* The side navigation/menu width */
mat-sidenav {
  width: 250px;
}

The logo should now be a bit bigger and the login page looking a bit better:

 

 

So what properties are available for an ADF component?

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:

 

ADF Component link

 

This takes you to the ADF Login component typings files (i.e. .d.ts files). Lookup the propDecorators to find out available Input and Output properties:

 

 

When you are looking at the ADF Login 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. 

 

Input property definition explained

When customising an ADF component you sometimes see input properties 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 ([...]).

 

Adding a Menu service

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-nav20 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 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';

import { AuthenticationService } from '@alfresco/adf-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);

        if (active && active.title) {
          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 (i.e. login to ACS) or BPM (i.e. login to APS) 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: 'folder',

      hidden: false,
      needEcmAuth: true,
      isLogin: false
    },
...

 

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: 'assignment',

    hidden: false,
    needBpmAuth: true,
    isLogin: false
  },
...

 

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 { AppMenuService } from '../../app-menu/app-menu.service';

@Component({
  selector: 'app-app-login-page',
  templateUrl: './app-login-page.component.html',
  styleUrls: ['./app-login-page.component.scss'],
  /* 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 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.

 

Update the App root component to support the new template

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}}
    </mat-toolbar>
    <mat-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';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  appName = 'ADF Workbench';
  mainMenuItems;
  activeMenuItem$: Observable<MenuItem>;

  constructor(private menuService: AppMenuService) {
    this.updateMenu();

    this.menuService.menuChanged.subscribe((any) => {
      this.updateMenu();
    });
  }

  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.

 

Implement Logout

We also need to implement the logout menu item that is available in the upper right corner drop down menu. This means implementing the onLogout button handler. We use the ADF Authentication Service to logout and show the login page directly after.

 

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 '@alfresco/adf-core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
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$;
  }
}

Displaying currently logged in user in the Toolbar

It would be nice to have the currently logged in username displayed in the upper right corner of Toobar 2, just to the left of the drop down menu. Open up the src/app/app.component.html template and add the {{ getCurrentUser() }} property injection as follows:

 

<mat-sidenav-container>
...
  <mat-toolbar color="primary">
    <button mat-icon-button (click)="mainSideNav.toggle()">
      <mat-icon *ngIf="mainSideNav.opened">chevron_left</mat-icon>
      <mat-icon *ngIf="!mainSideNav.opened">menu</mat-icon>
    </button>
    {{ (activeMenuItem$ | async)?.title }}
    <span fxFlex></span>
    {{ getCurrentUser() }}
    <button mat-icon-button [matMenuTriggerFor]="dropdownMenu">
      <mat-icon>more_vert</mat-icon>
    </button>
  </mat-toolbar>
  <router-outlet></router-outlet>
</mat-sidenav-container>
...

Then implement the function as follows in the src/app/app.component.ts class:

 

getCurrentUser() {
  return this.authService.getEcmUsername();
}

We can choose either the Ecm or the Bpm username, they should be the same.

 

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:adf-workbench-nav20 martin$ npm start

 

Access http://localhost:4200 and it should redirect you to the login page:

 

 

If you login successfully you should see the following screen:

 

 

The Login navigation item to the left should now be hidden and you should see a username in the toolbar. 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.

 

Summary

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 2.0.0

Building a Process Management App with ADF 2.0.0

Outcomes