16. Angular

16.1. Accessibility

Multiple studies suggest that around 15-20% of the population are living with a disability of some kind. In comparison, that number is higher than any single browser demographic currently, other than Chrome2. Not considering those users when developing an application means excluding a large number of people from being able to use it comfortable or at all.

Some people are unable to use the mouse, view a screen, see low contrast text, Hear dialogue or music and some people having difficulty to understanding the complex language.This kind of people needed the support like Keyboard support, screen reader support, high contrast text, captions and transcripts and Plain language support. This disability may change the from permanent to the situation.

16.1.1. Key Concerns of Accessible Web Applications

  • Semantic Markup - Allows the application to be understood on a more general level rather than just details of whats being rendered

  • Keyboard Accessibility - Applications must still be usable when using only a keyboard

  • Visual Assistance - color contrast, focus of elements and text representations of audio and events

Semantic Markup

If you’re creating custom element directives, Web Components or HTML in general, use native elements wherever possible to utilize built-in events and properties. Alternatively, use ARIA to communicate semantic meaning.

HTML tags have attributes that providers extra context on what’s being displayed on the browser. For example, the img tag’s alt attribute lets the reader know what is being shown using a short description.However, native tags don’t cover all cases. This is where ARIA fits in. ARIA attributes can provide context on what roles specific elements have in the application or on how elements within the document relate to each other.

A modal component can be given the role of dialog or alertdialog to let the browser know that that component is acting as a modal. The modal component template can use the ARIA attributes aria-labelledby and aria-described to describe to readers what the title and purpose of the modal is.

@Component({
    selector: 'ngc2-app',
    template: `
      <ngc2-notification-button
        message="Hello!"
        label="Greeting"
        role="button">
      </ngc2-notification-button>
      <ngc2-modal
        [title]="modal.title"
        [description]="modal.description"
        [visible]="modal.visible"
        (close)="modal.close()">
      </ngc2-modal>
    `
})
export class AppComponent {
  constructor(private modal: ModalService) { }
}

notification-button.component.ts

@Component({
  selector: 'ngc2-modal',
  template: `
    <div
      role="dialog"
      aria-labelledby="modal-title"
      aria-describedby="modal-description">
      <div id="modal-title">{{title}}</div>
      <p id="modal-description">{{description}}</p>
      <button (click)="close.emit()">OK</button>
    </div>
  `
})
export class ModalComponent {
  ...
}
Keyboard Accessibility

Keyboard accessibility is the ability of your application to be interacted with using just a keyboard. The more streamlined the site can be used this way, the more keyboard accessible it is. Keyboard accessibility is one of the largest aspects of web accessibility since it targets:

  • those with motor disabilities who can’t use a mouse

  • users who rely on screen readers and other assistive technology, which require keyboard navigation

  • those who prefer not to use a mouse

Focus

Keyboard interaction is driven by something called focus. In web applications, only one element on a document has focus at a time, and keypresses will activate whatever function is bound to that element. Focus element border can be styled with CSS using the outline property, but it should not be removed. Elements can also be styled using the :focus psuedo-selector.

Tabbing

The most common way of moving focus along the page is through the tab key. Elements will be traversed in the order they appear in the document outline - so that order must be carefully considered during development. There is way change the default behaviour or tab order. This can be done through the tabindex attribute. The tabindex can be given the values: * less than zero - to let readers know that an element should be focusable but not keyboard accessible * 0 - to let readers know that that element should be accessible by keyboard * greater than zero - to let readers know the order in which the focusable element should be reached using the keyboard. Order is calculated from lowest to highest.

Transitions

The majority of transitions that happen in an Angular application will not involve a page reload. This means that developers will need to carefully manage what happens to focus in these cases.

For example:

@Component({
  selector: 'ngc2-modal',
  template: `
    <div
      role="dialog"
      aria-labelledby="modal-title"
      aria-describedby="modal-description">
      <div id="modal-title">{{title}}</div>
      <p id="modal-description">{{description}}</p>
      <button (click)="close.emit()">OK</button>
    </div>
  `,
})
export class ModalComponent {
  constructor(private modal: ModalService, private element: ElementRef) { }

  ngOnInit() {
    this.modal.visible$.subscribe(visible => {
      if(visible) {
        setTimeout(() => {
          this.element.nativeElement.querySelector('button').focus();
        }, 0);
      }
    })
  }
}

16.1.2. Visual Assistance

One large category of disability is visual impairment. This includes not just the blind, but those who are color blind or partially sighted, and require some additional consideration.

Color Contrast

When choosing colors for text or elements on a website, the contrast between them needs to be considered. For WCAG 2.0 AA, this means that the contrast ratio for text or visual representations of text needs to be at least 4.5:1. There are tools online to measure the contrast ratio such as this color contrast checker from WebAIM or be checked with using automation tests.

Visual Information

Color can help a user’s understanding of information, but it should never be the only way to convey information to a user. For example, a user with red/green color-blindness may have trouble discerning at a glance if an alert is informing them of success or failure.

Audiovisual Media

Audiovisual elements in the application such as video, sound effects or audio (ie. podcasts) need related textual representations such as transcripts, captions or descriptions. They also should never auto-play and playback controls should be provided to the user.

16.1.3. Accessibility with Angular Material

The a11y package provides a number of tools to improve accessibility. Import

import { A11yModule } from '@angular/cdk/a11y';
ListKeyManager

ListKeyManager manages the active option in a list of items based on keyboard interaction. Intended to be used with components that correspond to a role="menu" or role="listbox" pattern . Any component that uses a ListKeyManager will generally do three things:

  • Create a @ViewChildren query for the options being managed.

  • Initialize the ListKeyManager, passing in the options.

  • Forward keyboard events from the managed component to the ListKeyManager.

Each option should implement the ListKeyManagerOption interface:

interface ListKeyManagerOption {
  disabled?: boolean;
  getLabel?(): string;
}
Types of ListKeyManager

There are two varieties of ListKeyManager, FocusKeyManager and ActiveDescendantKeyManager.

FocusKeyManager

Used when options will directly receive browser focus. Each item managed must implement the FocusableOption interface:

interface FocusableOption extends ListKeyManagerOption {
  focus(): void;
}
ActiveDescendantKeyManager

Used when options will be marked as active via aria-activedescendant. Each item managed must implement the Highlightable interface:

interface Highlightable extends ListKeyManagerOption {
  setActiveStyles(): void;
  setInactiveStyles(): void;
}

Each item must also have an ID bound to the listbox’s or menu’s aria-activedescendant.

FocusTrap

The cdkTrapFocus directive traps Tab key focus within an element. This is intended to be used to create accessible experience for components like modal dialogs, where focus must be constrained. This directive is declared in A11yModule.

This directive will not prevent focus from moving out of the trapped region due to mouse interaction.

For example:

<div class="my-inner-dialog-content" cdkTrapFocus>
  <!-- Tab and Shift + Tab will not leave this element. -->
</div>
Regions

Regions can be declared explicitly with an initial focus element by using the cdkFocusRegionStart, cdkFocusRegionEnd and cdkFocusInitial DOM attributes. When using the tab key, focus will move through this region and wrap around on either end.

For example:

<a mat-list-item routerLink cdkFocusRegionStart>Focus region start</a>
<a mat-list-item routerLink>Link</a>
<a mat-list-item routerLink cdkFocusInitial>Initially focused</a>
<a mat-list-item routerLink cdkFocusRegionEnd>Focus region end</a>
InteractivityChecker

InteractivityChecker is used to check the interactivity of an element, capturing disabled, visible, tabbable, and focusable states for accessibility purposes.

LiveAnnouncer

LiveAnnouncer is used to announce messages for screen-reader users using an aria-live region.

For example:

@Component({...})
export class MyComponent {

 constructor(liveAnnouncer: LiveAnnouncer) {
   liveAnnouncer.announce("Hey Google");
 }
}
API reference for Angular CDK a11y

16.2. Angular Elements

16.2.1. What are Angular Elements?

Angular elements are Angular components packaged as custom elements, a web standard for defining new HTML elements in a framework-agnostic way.

Custom elements are a Web Platform feature currently supported by Chrome, Firefox, Opera, and Safari, and available in other browsers through Polyfills. A custom element extends HTML by allowing you to define a tag whose content is created and controlled by JavaScript code. The browser maintains a CustomElementRegistry of defined custom elements (also called Web Components), which maps an instantiable JavaScript class to an HTML tag.

16.2.2. Why use Angular Elements?

Angular Elements allows Angular to work with different frameworks by using input and output elements. This allows Angular to work with many different frameworks if needed. This is an ideal situation if a slow transformation of an application to Angular is needed or some Angular needs to be added in other web applications(For example. ASP.net, JSP etc )

16.2.3. Negative points about Elements

Angular Elements is really powerful but since, the transition between views between views is going to be handled by another framework or html/javascript, using Angular Router is not possible. the view transitions have to be handled manually. This fact also eliminates the possibility of just porting an application completely.

16.2.4. How to use Angular Elements?

In a generalized way, a simple Angular component could be transformed to an Angular Element with this steps:

Installing Angular Elements

The first step is going to be install the library using our prefered packet manager:

NPM
npm install @angular/elements
YARN
yarn add @angular/elements
Preparing the components in the modules

Inside the app.module.ts, in addition to the normal declaration of the components inside declarations, the modules inside imports and the services inside providers, the components need to added in entryComponents. If there are components that have their own module, the same logic is going to be applied for them, only adding in the app.module.ts the components that dont have their own module. Here is an example of this:

....
@NgModule({
  declarations: [
    DishFormComponent,
    DishviewComponent
  ],
  imports: [
    CoreModule,  // Module containing Angular Materials
    FormsModule
  ],
  entryComponents: [
    DishFormComponent,
    DishviewComponent
  ],
  providers: [DishShareService]
})
....

After that is done, the constructor of the module is going to be modified to use injector and boostrap the application defining the components. This is going to allow the Angular Element to get the injections and to define a component tag that will be used later:

....
})
export class AppModule {
  constructor(private injector: Injector) {

  }

  ngDoBootstrap() {
    const el = createCustomElement(DishFormComponent, {injector: this.injector});
    customElements.define('dish-form', el);

    const elView = createCustomElement(DishviewComponent, {injector: this.injector});
    customElements.define('dish-view', elView);
  }
}
....
A component example

In order to be able to use a component, @Input() and @Output() variables are used. These variables are going to be the ones that will allow the Angular Element to communicate with the framework/javascript:

Component html

<mat-card>
    <mat-grid-list cols="1" rowHeight="100px" rowWidth="50%">
				<mat-grid-tile colspan="1" rowspan="1">
					<span>{{ platename }}</span>
				</mat-grid-tile>
				<form (ngSubmit)="onSubmit(dishForm)" #dishForm="ngForm">
					<mat-grid-tile colspan="1" rowspan="1">
						<mat-form-field>
							<input matInput placeholder="Name" name="name" [(ngModel)]="dish.name">
						</mat-form-field>
					</mat-grid-tile>
					<mat-grid-tile colspan="1" rowspan="1">
						<mat-form-field>
							<textarea matInput placeholder="Description" name="description" [(ngModel)]="dish.description"></textarea>
						</mat-form-field>
					</mat-grid-tile>
					<mat-grid-tile colspan="1" rowspan="1">
						<button mat-raised-button color="primary" type="submit">Submit</button>
					</mat-grid-tile>
				</form>
		</mat-grid-list>
</mat-card>

Component ts

@Component({
  templateUrl: './dish-form.component.html',
  styleUrls: ['./dish-form.component.scss']
})
export class DishFormComponent implements OnInit {

  @Input() platename;

  @Input() platedescription;

  @Output()
  submitDishEvent = new EventEmitter();

  submitted = false;
  dish = {name: '', description: ''};

  constructor(public dishShareService: DishShareService) { }

  ngOnInit() {
    this.dish.name = this.platename;
    this.dish.description = this.platedescription;
  }

  onSubmit(dishForm: NgForm): void {
    console.log('SUBMIT');
    console.log(dishForm.value);
    this.dishShareService.createDish(dishForm.value.name, dishForm.value.description);
    this.submitDishEvent.emit('dishSubmited');
  }

}

In this file there are definitions of multiple variables that will be used as input and output. Since the input variables are going to be used directly by html, only lowercase and underscore strategies can be used for them. On the onSubmit(dishForm: NgForm) a service is used to pass this variables to another component. Finally, as a last thing, the selector inside @Component has been removed since a tag that will be used dynamically was already defined in the last step.

Solving the error

In order to be able to use this Angular Element a Polyfills/Browser support related error needs to solved. This error can be solved in two ways:

Changing the target

One solution is to change the target in tsconfig.json to es2015. This might not be doable for every application since maybe a specific target is required.

Installing Polyfaces

Another solution is to use AutoPollyfill. In order to do so, the library is going to be installed with a packet manager:

Yarn

yarn add @webcomponents/webcomponentsjs

Npm

npm install @webcomponents/webcomponentsjs

After the packet manager has finished, inside the src folder a new file polyfills.ts is found. To solve the error, importing the corresponding adapter (custom-elements-es5-adapter.js) is necessary:

....
/***************************************************************************************************
 * APPLICATION IMPORTS
 */

import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
....

If you want to learn more about polyfills in angular you can do it here

Building the Angular Element

First, before building the Angular Element, every element inside that app component except the module need to be removed. After that, a bash script is created in the root folder,. This script will allow to put every necessary file into a js.

ng build "projectName" --prod --output-hashing=none && cat dist/"projectName"/runtime.js dist/"projectName"/polyfills.js dist/"projectName"/scripts.js dist/"projectName"/main.js > ./dist/"projectName"/"nameWantedAngularElement".js

After executing the bash script, it will generate inside the path dist/"projectName" a js file named "nameWantedAngularElement".js and a css file.

Building with ngx-build-plus (Recommended)

The library ngx-build-plus allows to add different options when building. In addition, it solves some errors that will occur when trying to use multiple angular elements in an application. In order to use it, yarn or npm can be used:

Yarn

yarn add ngx-build-plus

Npm

npm install ngx-build-plus

If you want to add it to a specific sub project in your projects folder, use the --project:

.... ngx-build-plus --project "project-name"

Using this library and the following command, an isolated Angular Element which won’t have conflict with others can be generated. This Angular Element will not have a polyfill so, the project where we use them will need to include a poliyfill with the Angular Element requirements.

ng build "projectName" --output-hashing none --single-bundle true --prod --bundle-styles false

This command will generate three things:

  1. The main js bundle

  2. The script js

  3. The css

These files will be used later instead of the single js generated in the last step.

Extra parameters

Here are some extra useful parameters that ngx-build-plus provides:

  • --keep-polyfills: This paremeter is going to allow us to keep the polyfills. This needs to be used with caution, avoiding using multiple different polyfills that could cause an error is necessary.

  • --extraWebpackConfig webpack.extra.js: This parameter allows us to create a javascript file inside our Angular Elements project with the name of different libraries. Using webpack these libraries will not be included in the Angular Element. This is useful to lower the size of our Angular Element by removing libraries shared. Example:

const webpack = require('webpack');

module.exports = {
    "externals": {
        "rxjs": "rxjs",
        "@angular/core": "ng.core",
        "@angular/common": "ng.common",
        "@angular/common/http": "ng.common.http",
        "@angular/platform-browser": "ng.platformBrowser",
        "@angular/platform-browser-dynamic": "ng.platformBrowserDynamic",
        "@angular/compiler": "ng.compiler",
        "@angular/elements": "ng.elements",
        "@angular/router": "ng.router",
        "@angular/forms": "ng.forms"
    }
}
Note
If some libraries are excluded from the `Angular Element` you will need to add the bundled umd files of those libraries manually.
Using the Angular Element

The Angular Element that got generated in the last step can be used in almost every framework. In this case, the Angular Element is going to be used in html:

Listing 14. Sample index.html version without ngx-build-plus
<html>
    <head>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>
        <div id="container">

        </div>
        <!--Use of the element non dynamically-->
        <!--<plate-form platename="test" platedescription="test"></plate-form>-->
        <script src="./devon4ngAngularElements.js"> </script>
        <script>
                var elContainer = document.getElementById('container');
                var el= document.createElement('dish-form');
                el.setAttribute('platename','test');
                el.setAttribute('platedescription','test');
                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });
                elContainer.appendChild(el);
        </script>
    </body>
</html>
Listing 15. Sample index.html version with ngx-build-plus
<html>
    <head>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>
        <div id="container">

        </div>
        <!--Use of the element non dynamically-->
        <!--<plate-form platename="test" platedescription="test"></plate-form>-->
         <script src="./polyfills.js"> </script> <!-- Created using --keep-polyfills options -->
        <script src="./scripts.js"> </script>
         <script src="./main.js"> </script>
        <script>
                var elContainer = document.getElementById('container');
                var el= document.createElement('dish-form');
                el.setAttribute('platename','test');
                el.setAttribute('platedescription','test');
                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });
                elContainer.appendChild(el);
        </script>
    </body>
</html>

In this html, the css generated in the last step is going to be imported inside the <head> and then, the javascript element is going to be imported at the end of the body. After that is done, There is two uses of Angular Elements in the html, one directly whith use of the @input() variables as parameters commented in the html:

....
        <!--Use of the element non dynamically-->
        <!--<plate-form platename="test" platedescription="test"></plate-form>-->
....

and one dynamically inside the script:

....
        <script>
                var elContainer = document.getElementById('container');
                var el= document.createElement('dish-form');
                el.setAttribute('platename','test');
                el.setAttribute('platedescription','test');
                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });
                elContainer.appendChild(el);
        </script>
....

This javascript is an example of how to create dynamically an Angular Element inserting attributed to fill our @Input() variables and listen to the @Output() that was defined earlier. This is done with:

                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });

This allows javascript to hook with the @Output() event emitter that was defined. When this event gets called, another component that was defined gets inserted dynamically.

16.2.5. Angular Element within another Angular project

In order to use an Angular Element within another Angular project the following steps need to be followed:

Copy bundled script and css to resources

First copy the generated .js and .css inside assets in the corresponding folder.

Add bundled script to angular.json

Inside angular.json both of the files that were copied in the last step are going to be included. This will be done both, in test and in build. Including it on the test, will allow to perform unitary tests.

{
....
  "architect": {
    ....
    "build": {
      ....
      "styles": [
        ....
          "src/assets/css/devon4ngAngularElements.css"
        ....
      ]
      ....
      "scripts": [
        "src/assets/js/devon4ngAngularElements.js"
      ]
      ....
    }
    ....
    "test": {
      ....
      "styles": [
        ....
          "src/assets/css/devon4ngAngularElements.css"
        ....
      ]
      ....
      "scripts": [
        "src/assets/js/devon4ngAngularElements.js"
      ]
      ....
    }
  }
}

By declaring the files in the angular.json angular will take care of including them in a proper way.

Using Angular Element

There are two ways that Angular Element can be used:

Create component dynamicly

In order to add the component in a dynamic way, first adding a container is necessary:

app.component.html

....
<div id="container">
</div>
....

With this container created, inside the app.component.ts a method is going to be created. This method is going to find the container, create the dynamic element and append it into the container.

app.component.ts

export class AppComponent implements OnInit {
  ....
  ngOnInit(): void {
    this.createComponent();
  }
  ....
  createComponent(): void {
    const container = document.getElementById('container');
    const component = document.createElement('dish-form');
    container.appendChild(component);
  }
  ....
Using it directly

In order to use it directly on the templates, in the app.module.ts the CUSTOM_ELEMENTS_SCHEMA needs to be added:

....
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
....
@NgModule({
  ....
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ],

This is going to allow the use of the Angular Element in the templates directly:

app.component.html

....
<div id="container">
  <dish-form></dish-form>
</div>

16.3. Angular Lazy loading

When the development of an application starts, it just contains a small set of features so the app usually loads fast. However, as new features are added, the overall application size grows up and its loading speed decreases, is in this context where Lazy loading finds its place. Lazy loading is a dessign pattern that defers initialization of objects until it is needed so, for example, Users that just access to a website’s home page do not need to have other areas loaded. Angular handles lazy loading through the routing module which redirect to requested pages. Those pages can be loaded at start or on demand.

16.3.1. An example with Angular

To explain how lazy loading is implemented using angular, a basic sample app is going to be developed. This app will consist in a window named "level 1" that contains two buttons that redirects to other windows in a "second level". It is a simple example, but useful to understand the relation between angular modules and lazy loading.

Levels app structure
Figure 31. Levels app structure.

This graphic shows that modules acts as gates to access components "inside" them.

Because the objective of this guide is related mainly with logic, the html structure and scss styles are less relevant, but the complete code can be found as a sample here.

Implementation

First write in a console ng new level-app --routing, to generate a new project called level-app including an app-routing.module.ts file (--routing flag).

In the file app.component.html delete all the content except the router-outlet tag.

Listing 16. File app.component.html
<router-outlet></router-outlet>

The next steps consists on creating features modules.

run ng generate module first --routing to generate a module named first.

  • run ng generate module first/second-left --routing to generate a module named second-left under first.

  • run ng generate module first/second-right --routing to generate a module second-right under first.

  • run ng generate component first/first to generate a component named first inside the module first.

  • run ng generate component first/second-left/content to generate a component content inside the module second-left.

  • run ng generate component first/second-right/content to generate a component content inside the module second-right.

To move between components we have to configure the routes used:

In app-routing.module.ts add a path 'first' to FirstComponent and a redirection from '' to 'first'.

Listing 17. File app-routing.module.ts.
...
import { FirstComponent } from './first/first/first.component';

const routes: Routes = [
  {
    path: 'first',
    component: FirstComponent
  },
  {
    path: '',
    redirectTo: 'first',
    pathMatch: 'full',
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

In app.module.ts import the module which includes FirstComponent.

Listing 18. File app.module.ts
....
import { FirstModule } from './first/first.module';

@NgModule({
  ...
  imports: [
    ....
    FirstModule
  ],
  ...
})
export class AppModule { }

In first-routing.module.ts add routes that direct to the content of SecondRightModule and SecondLeftModule. The content of both modules have the same name so, in order to avoid conflicts the name of the components are going to be changed using as ( original-name as new-name).

Listing 19. File first-routing.module.ts
...
import { ContentComponent as ContentLeft} from './second-left/content/content.component';
import { ContentComponent as ContentRight} from './second-right/content/content.component';
import { FirstComponent } from './first/first.component';

const routes: Routes = [
  {
    path: '',
    component: FirstComponent
  },
  {
    path: 'first/second-left',
    component: ContentLeft
  },
  {
    path: 'first/second-right',
    component: ContentRight
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class FirstRoutingModule { }

In first.module.ts import SecondLeftModule and SecondRightModule.

Listing 20. File first.module.ts
...
import { SecondLeftModule } from './second-left/second-left.module';
import { SecondRightModule } from './second-right/second-right.module';

@NgModule({
  ...
  imports: [
    ...
    SecondLeftModule,
    SecondRightModule,
  ]
})
export class FirstModule { }

Using the current configuration, we have a project that loads all the modules in a eager way. Run ng serve to see what happens.

First, during the compilation we can see that just a main file is built.

Compile eager
Figure 32. Compile eager.

If we go to http//localhost:4200/first and open developer options (F12 on Chrome), it is found that a document named "first" is loaded.

First level eager
Figure 33. First level eager.

If we click on [Go to right module] a second level module opens, but there is no 'second-right' document.

Second level right eager
Figure 34. Second level right eager.

But, typing the url directly will load 'second-right' but no 'first', even if we click on [Go back]

Second level right eager
Figure 35. Second level right eager direct url.

Modifying an angular application to load its modules lazily is easy, you have to change the routing configuration of the desired module (for example FirstModule).

Listing 21. File app-routing.module.ts.
const routes: Routes = [
  {
    path: 'first',
    loadChildren: () => import('./first/first.module').then(m => m.FirstModule),
  },
  {
    path: '',
    redirectTo: 'first',
    pathMatch: 'full',
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Notice that instead of loading a component, you dynamically import it in a loadChildren attribute because modules acts as gates to access components "inside" them. Updating the app to load lazily has four consecuences:

  1. No component attribute.

  2. No import of FirstComponent.

  3. FirstModule import has to be removed from the imports array at app.module.ts.

  4. Change of context.

If we check first-routing.module.ts again, the can see that the path for ContentLeft and ContentRight is set to 'first/second-left' and 'first/second-right' respectively, so writing 'http//localhost:4200/first/second-left' will redirect us to ContentLeft. However, after loading a module with loadChildren setting the path to 'second-left' and 'second-right' is enough because it adquires the context set by AppRoutingModule.

Listing 22. File first-routing.module.ts
const routes: Routes = [
  {
    path: '',
    component: FirstComponent
  },
  {
    path: 'second-left',
    component: ContentLeft
  },
  {
    path: 'second-right',
    component: ContentRight
  }
];

If we go to 'first' then FirstModule is situated in '/first' but also its children ContentLeft and ContentRight, so it is not necessary to write in their path 'first/second-left' and 'first/second-right', because that will situate the components on 'first/first/second-left' and 'first/first/second-right'.

First level wrong path
Figure 36. First level lazy wrong path.

When we compile an app with lazy loaded modules, files containing them will be generated

First level lazy compilation
Figure 37. First level lazy compilation.

And if we go to developer tools → network, we can find those modules loaded (if they are needed).

First level lazy
Figure 38. First level lazy.

To load the component ContentComponent of SecondLeftModule lazily, we have to load SecondLeftModule as a children of FirstModule:

  • Change component to loadChildren and reference SecondLeftModule.

Listing 23. File first-routing.module.ts.
const routes: Routes = [
  {
    path: '',
    component: FirstComponent
  },
  {
    path: 'second-left',
    loadChildren: () => import('./second-left/second-left.module').then(m => m.SecondLeftModule),
  },
  {
    path: 'second-right',
    component: ContentRight
  }
];
  • Remove SecondLeftModule at first.component.ts

  • Route the components inside SecondLeftModule. Without this step nothing would be displayed.

Listing 24. File second-left-routing.module.ts.
...
import { ContentComponent } from './content/content.component';

const routes: Routes = [
  {
    path: '',
    component: ContentComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class SecondLeftRoutingModule { }
  • run ng serve to generate files containing the lazy modules.

Second level lazy
Figure 39. Second level lazy loading compilation.

Clicking on [Go to left module] triggers the load of SecondLeftModule.

Second level lazy network
Figure 40. Second level lazy loading network.

16.3.2. Conclusion

Lazy loading is a pattern useful when new features are added, these features are usually identified as modules which can be loaded only if needed as shown in this document, reducing the time spent loading an application.

16.4. Angular Library

Angular CLI provides us with methods that allow the creation of a library. After that, using either packet manager (npm or yarn) the library can be build and packed which will allow later to install/publish it.

16.4.1. Whats a library?

From wikipedia: a library is a collection of non-volatile resources used by computer programs, often for software development. These may include configuration data, documentation, help data, message templates, pre-written code and subroutines, classes, values or type specifications.

16.4.2. How to build a library

In this section, a library is going to be build step by step.

Creating an empty application

First, using Angular CLI we are going to generate a empty application which will be later filled with the generated library. In order to do so, Angular CLI allows us to add to ng new "application-name" an option (--create-application). This option is going to tell Angular CLI not to create the initial app project. This is convenient since a library is going to be generated in later steps. Using this command ng new "application-name" --create-application=false an empty project with the name wanted is created.

ng new "application-name" --create-application=false
Generating a library

After generating an empty application, a library is going to be generated. Inside the folder of the project, the Angular CLI command ng generate library "library-name" is going to generate the library as a project (projects/"library-name"). As an addition, the option --prefix="library-prefix-wanted" allows us to switch the default prefix that Angular generated with (lib). Using the option to change the prefix the command will look like this ng generate library "library-name" --prefix="library-prefix-wanted".

ng generate library "library-name" --prefix="library-prefix-wanted"
Generating/Modifying in our library

In the last step we generated a library. This generates automaticly a module,service and component inside (projects/"library-name") that we can modify adding new methods, components etc that we want to use in other projects. We can generate other elements, using the usual Angular CLI generate commands adding the option --project="library-name" is going to allow to generate elements within our project . An example of this is: ng generate service "name" --project="library-name".

ng generate "element" "name" --project="library-name"
Exporting the generated things

Inside the library (projects/"library-name) theres a public_api.ts which is the file that exports the elements inside the library. In case we generated other things, that file needs to be modified adding the extra exports with the generated elements. In addition, changing the library version is possible in the file package.json.

Building our library

Once we added the necessary exports, in order to use the library in other applications, we need to build the library. The command ng build "library-name" is going to build the library, generating in "project-name"/dist/"library-name" the necessary files.

ng build "library-name"
Packing the library

In this step we are going to pack the build library. In order to do so, we need to go inside dist/"library-name" and then run either npm pack or yarn pack to generate a "library-name-version.tgz" file.

Listing 25. Packing using npm
npm pack
Listing 26. Packing using yarn
yarn pack
Publishing to npm repository (optional)
  • Add a README.md and LICENSE file. The text inside README.md will be used in you npm package web page as documentation.

  • run npm adduser if you do not have a npm account to create it, otherwise run npm login and introduce your credentials.

  • run npm publish inside dist/"library-name" folder.

  • Check that the library is published: https://npmjs.com/package/library-name

Installing our library in other projects

In this step we are going to install/add the library on other projects.

npm

In order to add the library in other applications, there are two ways:

  • Option 1: From inside the application where the library is going to get used, using the command npm install "path-to-tgz"/"library-name-version.tgz" allows us to install the .tgz generated in Packing the library.

  • Option 2: run npm install "library-name" to install it from npm repository.

yarn

To add the package using yarn:

  • Option 1: From inside the application where the library is going to get used, using the command yarn add "path-to-tgz"/"library-name-version.tgz" allows us to install the .tgz generated in Packing the library.

  • Option 2: run yarn add "library-name" to install it from npm repository.

Using the library

Finally, once the library was installed with either packet manager, you can start using the elements from inside like they would be used in a normal element inside the application. Example app.component.ts:

import { Component, OnInit } from '@angular/core';
import { MyLibraryService } from 'my-library';

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

  toUpper: string;

  constructor(private myLibraryService: MyLibraryService) {}
  title = 'devon4ng library test';
  ngOnInit(): void {
    this.toUpper = this.myLibraryService.firstLetterToUpper('test');
  }
}

Example app.component.html:

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<h2>Here is my library service being used: {{toUpper}}</h2>
<lib-my-library></lib-my-library>

Example app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { MyLibraryModule } from 'my-library';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    MyLibraryModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

The result from using the library:

result
devon4ng libraries

In devonfw/devon4ng-library you can find some useful libraries:

  • Authorization module: This devon4ng Angular module adds rights-based authorization to your Angular app.

  • Cache module: Use this devon4ng Angular module when you want to cache requests to server. You may configure it to store in cache only the requests you need and to set the duration you want.

16.5. Angular Material Theming

Angular Material library offers UI components for developers, those components follows Google Material desing baselines but characteristics like colors can be modified in order to adapt them to the needs of the client: corporative colors, corporative identity, dark themes, …​

16.5.1. Theming basics

In Angular Material, a theme is created mixing multiple colors. Colors and its light and dark variants conform a palette. In general, a theme consists of the following palettes:

  • primary: Most used across screens and componets.

  • accent: Floating action button and interactive elements.

  • warn: Error state.

  • foreground: Text and icons.

  • background: Element backgrounds.

Theme palette
Figure 41. Palettes and variants.

In angular material, a palette is represented as a scss map.

Scss map
Figure 42. Scss map and palettes.
Tip
Some components can be forced to use primary, accent or warn palettes using the attribute color, for example: <mat-toolbar color="primary">.

16.5.2. Prebuilt themes

Available prebuilt themes:

  • deeppurple-amber.css

deeppurple-amber theme
Figure 43. deeppurple-amber theme.
  • indigo-pink.css

indigo-pink theme
Figure 44. indigo-pink theme.
  • pink-bluegrey.css

pink-bluegrey theme
Figure 45. ink-bluegrey theme.
  • purple-green.css

purple-green theme
Figure 46. purple-green theme.

The prebuilt themes can be added using @import.

@import '@angular/material/prebuilt-themes/deeppurple-amber.css';

16.5.3. Custom themes

Somethimes prebuild themes do not meet the needs of a project, because color schemas are too specific or do not incorporate branding colors, in those situations custom themes can be built to offer a better solution to the client.

For this topic, we are going to use a basic layout project that can be found in devon4ng repository.

Basics

Before starting writing custom themes, there are some necessary things that have to be mentioned:

  • Add a default theme: The project mentioned before has just one global scss stylesheet styles.scss that includes indigo-pink.scss which will be the default theme.

  • Add @import '~@angular/material/theming'; at the begining of the every stylesheet to be able to use angular material prebuilt color palettes and functions.

  • Add @include mat-core(); once per project, so if you are writing multiple themes in multiple files you could import those files from a 'central' one (for example styles.scss). This includes all common styles that are used by multiple components.

Theme files structure
Figure 47. Theme files structure.
Basic custom theme

To create a new custom theme, the .scss file containing it has to have imported the angular _theming.scss file (angular/material/theming) file and mat-core included. _theming.scss includes multiple color palettes and some functions that we are going to see below. The file for this basic theme is going to be named styles-custom-dark.scss.

First, declare new variables for primary, accent and warn palettes. Those variables are going to store the result of the function mat-palette.

mat-palette accepts four arguments: base color palette, main, lighter and darker variants (See [id_palette_variants]) and returns a new palette including some additional map values: default, lighter and darker ([id_scss_map]). Only the first argument is mandatory.

Listing 27. File styles-custom-dark.scss.
$custom-dark-theme-primary: mat-palette($mat-pink);
$custom-dark-theme-accent: mat-palette($mat-blue);
$custom-dark-theme-warn: mat-palette($mat-red);
);

In this example we are using colors available in _theming.scss: mat-pink, mat-blue, mat-red. If you want to use a custom color you need to define a new map, for instance:

Listing 28. File styles-custom-dark.scss custom pink.
$my-pink: (
    50 : #fcf3f3,
    100 : #f9e0e0,
    200 : #f5cccc,
    300 : #f0b8b8,
    500 : #ea9999,
    900 : #db6b6b,
    A100 : #ffffff,
    A200 : #ffffff,
    A400 : #ffeaea,
    A700 : #ffd0d0,
    contrast: (
        50 : #000000,
        100 : #000000,
        200 : #000000,
        300 : #000000,
        900 : #000000,
        A100 : #000000,
        A200 : #000000,
        A400 : #000000,
        A700 : #000000,
    )
);

$custom-dark-theme-primary: mat-palette($my-pink);
...
Tip
Some pages allows to create these palettes easily, for instance: http://mcg.mbitson.com

Until now, we just have defined primary, accent and warn palettes but what about foreground and background? Angular material has two functions to change both:

  • mat-light-theme: Receives as arguments primary, accent and warn palettes and return a theme whose foreground is basically black (texts, icons, …​), the background is white and the other palettes are the received ones.

deeppurple-amber theme
Figure 48. Custom light theme.
  • mat-dark-theme: Similar to mat-light-theme but returns a theme whose foreground is basically white and background black.

deeppurple-amber theme
Figure 49. Custom dark theme.

For this example we are going to use mat-dark-theme and save its result in $custom-dark-theme.

Listing 29. File styles-custom-dark.scss updated with mat-dark-theme.
...

$custom-dark-theme: mat-dark-theme(
  $custom-dark-theme-primary,
  $custom-dark-theme-accent,
  $custom-dark-theme-warn
);

To apply the saved theme, we have to go to styles.scss and import our styles-custom-dark.scss and include a function called angular-material-theme using the theme variable as argument.

Listing 30. File styles.scss.
...
@import 'styles-custom-dark.scss';
@include angular-material-theme($custom-dark-theme);

If we have multiple themes it is necessary to add the include statement inside a css class and use it in src/index.html → app-root component.

Listing 31. File styles.scss updated with custom-dark-theme class.
...
@import 'styles-custom-dark.scss';

.custom-dark-theme {
  @include angular-material-theme($custom-dark-theme);
}
Listing 32. File src/index.html.
...
<app-root class="custom-dark-theme"></app-root>
...

This will apply $custom-dark-theme theme for the entire application.

Full custom theme

Sometimes it is needed to custom different elementsw from background and foreground, in those situations we have to create a new function similar to mat-light-theme and mat-dark-theme. Let’s focus con mat-light-theme:

Listing 33. Source code of mat-light-theme
@function mat-light-theme($primary, $accent, $warn: mat-palette($mat-red)) {
  @return (
    primary: $primary,
    accent: $accent,
    warn: $warn,
    is-dark: false,
    foreground: $mat-light-theme-foreground,
    background: $mat-light-theme-background,
  );
}

As we can se, mat-light-theme takes three arguments and returs a map including them as primary, accent and warn color; but there are three more keys in that map: is-dark, foreground and background.

  • is-dark: Boolean true if it is a dark theme, false otherwise.

  • background: Map that stores the color for multiple background elements.

  • foreground: Map that stores the color for multiple foreground elements.

To show which elements can be colored lets create a new theme in a file styles-custom-cap.scss:

Listing 34. File styles-custom-cap.scss: Background and foreground variables.
@import '~@angular/material/theming';

// custom background and foreground palettes
$my-cap-theme-background: (
  status-bar: #0070ad,
  app-bar: map_get($mat-blue, 900),
  background: #12abdb,
  hover: rgba(white, 0.04),
  card: map_get($mat-red, 800),
  dialog: map_get($mat-grey, 800),
  disabled-button: $white-12-opacity,
  raised-button: map-get($mat-grey, 800),
  focused-button: $white-6-opacity,
  selected-button: map_get($mat-grey, 900),
  selected-disabled-button: map_get($mat-grey, 800),
  disabled-button-toggle: black,
  unselected-chip: map_get($mat-grey, 700),
  disabled-list-option: black,
);

$my-cap-theme-foreground: (
  base: yellow,
  divider: $white-12-opacity,
  dividers: $white-12-opacity,
  disabled: rgba(white, 0.3),
  disabled-button: rgba(white, 0.3),
  disabled-text: rgba(white, 0.3),
  hint-text: rgba(white, 0.3),
  secondary-text: rgba(white, 0.7),
  icon: white,
  icons: white,
  text: white,
  slider-min: white,
  slider-off: rgba(white, 0.3),
  slider-off-active: rgba(white, 0.3),
);

Function which uses the variables defined before to create a new theme:

Listing 35. File styles-custom-cap.scss: Creating a new theme function.
// instead of creating a theme with mat-light-theme or mat-dark-theme,
// we will create our own theme-creating function that lets us apply our own foreground and background palettes.
@function create-my-cap-theme($primary, $accent, $warn: mat-palette($mat-red)) {
  @return (
    primary: $primary,
    accent: $accent,
    warn: $warn,
    is-dark: false,
    foreground: $my-cap-theme-foreground,
    background: $my-cap-theme-background
  );
}

Calling the new function and storing its value in $custom-cap-theme.

Listing 36. File styles-custom-cap.scss: Storing the new theme.
// We use create-my-cap-theme instead of mat-light-theme or mat-dark-theme
$custom-cap-theme-primary: mat-palette($mat-green);
$custom-cap-theme-accent: mat-palette($mat-blue);
$custom-cap-theme-warn: mat-palette($mat-red);

$custom-cap-theme: create-my-cap-theme(
  $custom-cap-theme-primary,
  $custom-cap-theme-accent,
  $custom-cap-theme-warn
);

After defining our new theme, we can import it from styles.scss.

Listing 37. File styles.scss updated with custom-cap-theme class.
...
@import 'styles-custom-cap.scss';
.custom-cap-theme {
  @include angular-material-theme($custom-cap-theme);
}
Multiple themes and overlay-based components

Certain components (e.g. menu, select, dialog, etc.) that are inside of a global overlay container,require an additional step to be affected by the theme’s css class selector.

Listing 38. File app.module.ts
import {OverlayContainer} from '@angular/cdk/overlay';

@NgModule({
  // ...
})
export class AppModule {
  constructor(overlayContainer: OverlayContainer) {
    overlayContainer.getContainerElement().classList.add('custom-cap-theme');
  }
}

16.6. Angular Progressive Web App

Progresive web applications (PWAs) are web application that offer better user experience than the traditional ones. In general, they solve problems related with reliability and speed:

  • Reliability: PWAs are stable. In this context stability means than even with slow connections or even with no network at all, the application still works. To achieve this, some basic resources like styles, fonts, requests, …​ are stored; due to this caching, it is not possible to assure that the content is always up-to-date.

  • Speed: When an users opens an application, he or she will expect it to load almost inmediately (almost 53% of users abandon sites that take longer that 3 seconds, source: https://developers.google.com/web/progressive-web-apps/#fast).

PWAs uses a script called service worker, which runs in background and essentially act as proxy between web app and network, intercepting requests and acting depending on the network conditions.

16.6.1. Assumptions

This guide assumes that you already have installed:

  • Node.js

  • npm package manager

  • Angular CLI

16.6.2. Sample Application

My thai star recommendation
Figure 50. Basic angular PWA.

To explain how to build PWAs using angular, a basic application is going to be built. This app will be able to ask for resources and save in the cache in order to work even offline.

Step 1: Create a new project

This step can be completed with one simple command: ng new <name>, where <names> is the name for the app. In this case, the app is going to be named basic-ng-pwa.

Step 2: Create a service

Web applications usually uses external resources, making necessary the addition of services which can get those resources. This application gets a dish from My Thai Star’s back-end and shows it. To do so, a new service is going to be created.

  • go to project folder: cd basic-ng-pwa

  • run ng generate service data

  • Modify data.service.ts, environment.ts, environment.prod.ts

To retrieve data with this service, you have to import the module HttpClient and add it to the service’s contructor. Once added, use it to create a function getDishes() that sends http request to My Thai Start’s back-end. The URL of the back-end can be stored as an environment variable MY_THAI_STAR_DISH.

data.service.ts

  ...
  import { HttpClient } from '@angular/common/http';
  import { MY_THAI_STAR_DISH } from '../environments/environment';
  ...

  export class DataService {
    constructor(private http: HttpClient) {}

    /* Get data from Back-end */
    getDishes() {
      return this.http.get(MY_THAI_STAR_DISH);
    }
    ...
  }

environments.ts

  ...
  export const MY_THAI_STAR_DISH =
  'http://de-mucdevondepl01:8090/api/services/rest/dishmanagement/v1/dish/1';
  ...

environments.prod.ts

  ...
  export const MY_THAI_STAR_DISH =
  'http://de-mucdevondepl01:8090/api/services/rest/dishmanagement/v1/dish/1';
  ...
Step 3: Use the service

The component AppComponent implements the interface OnInit and inside its method ngOnInit() the suscription to the services is done. When a dish arrives, it is saved and shown (app.component.html).

  ...
  import { DataService } from './data.service';
  export class AppComponent implements OnInit {
  dish: { name: string; description: string } = { name: '', description: ''};

  ...
  ngOnInit() {
    this.data
      .getDishes()
      .subscribe(
        (dishToday: { dish: { name: string; description: string } }) => {
          this.dish = {
            name: dishToday.dish.name,
            description: dishToday.dish.description,
          };
        },
      );
  }
}
Step 4: Structures, styles and updates

This step shows code interesting inside the sample app. The complete content can be found in devon4ng samples.

index.html

To use the Montserrat font add the following link inside the tag header.

  <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">

styles.scss

  body {
    ...
    font-family: 'Montserrat', sans-serif;
  }

app.component.ts

This file is also used to reload the app if there are any changes.

  • SwUpdate: This object comes inside the @angular/pwa package and it is used to detect changes and reload the page if needed.

  ...
  import { SwUpdate } from '@angular/service-worker';

  export class AppComponent implements OnInit {

  ...
    constructor(updates: SwUpdate, private data: DataService) {
      updates.available.subscribe((event) => {
        updates.activateUpdate().then(() => document.location.reload());
      });
    }
    ...
  }
Step 5: Make it Progressive.

Turining an angular app into a PWA is pretty easy, just one module has to be added. To do so, run: ng add @angular/pwa. This command also adds two important files, explained below.

 
 

  • manifest.json

manifest.json is a file that allows to control how the app is displayed in places where native apps are displayed.

Fields

name: Name of the web application.

short_name: Short version of name.

theme_color: Default theme color for an application context.

background_color: Expected background color of the web application.

display: Preferred display mode.

scope: Navigation scope of tghis web application’s application context.

start_url: URL loaded when the user launches the web application.

icons: Array of icons that serve as representations of the web app.

Additional information can be found here.

 
 

  • ngsw-config.json

nsgw-config.json specifies which files and data URLs have to be cached and updated by the Angular service worker.

Fields

  • index: File that serves as index page to satisfy navigation requests.

  • assetGroups: Resources that are part of the app version that update along with the app.

    • name: Identifies the group.

    • installMode: How the resources are cached (prefetch or lazy).

    • updateMode: Caching behaviour when a new version of the app is found (prefetch or lazy).

    • resources: Resources to cache. There are three groups.

      • files: Lists patterns that match files in the distribution directory.

      • urls: URL patterns matched at runtime.

  • dataGroups: UsefulIdentifies the group. for API requests.

    • name: Identifies the group.

    • urls: URL patterns matched at runtime.

    • version: Indicates that the resources being cached have been updated in a backwards-incompatible way.

    • cacheConfig: Policy by which matching requests will be cached

      • maxSize: The maximum number of entries, or responses, in the cache.

      • maxAge: How long responses are allowed to remain in the cache.

        • d: days. (5d = 5 days).

        • h: hours

        • m: minutes

        • s: seconds. (5m20s = 5 minutes and 20 seconds).

        • u: milliseconds

      • timeout: How long the Angular service worker will wait for the network to respond before using a cached response. Same dataformat as maxAge.

      • strategy: Caching strategies (performance or freshness).

  • navigationUrls: List of URLs that will be redirected to the index file.

Additional information can be found here.

Step 6: Configure the app

manifest.json

Default configuration.

 
 
ngsw-config.json

At assetGroups → resources → urls: In this field the google fonts api is added in order to use Montserrat font even without network.

  "urls": [
          "https://fonts.googleapis.com/**"
        ]

At the root of the json: A data group to cache API calls.

  {
    ...
    "dataGroups": [{
      "name": "mythaistar-dishes",
      "urls": [
        "http://de-mucdevondepl01:8090/api/services/rest/dishmanagement/v1/dish/1"
      ],
      "cacheConfig": {
        "maxSize": 100,
        "maxAge": "1h",
        "timeout": "10s",
        "strategy": "freshness"
      }
    }]
  }
Step 7: Check that your app is a PWA

To check if an app is a PWA lets compare its normal behaviour against itself but built for production. Run in the project’s root folder the commands below:

ng build --prod to build the app using production settings.

npm install http-server to install an npm module that can serve your built application. Documentation here.

Go to the dist/basic-ng-pwa/ folder running cd dist/basic-ng-pwa.

http-server -o to serve your built app.

Http server running
Figure 51. Http server running on localhost:8081.

 

In another console instance run ng serve to open the common app (not built).

.Angular server running
Figure 52. Angular server running on localhost:4200.

 

The first difference can be found on Developer tools → application, here it is seen that the PWA application (left) has a service worker and the common (right) one does not.

Application comparison
Figure 53. Application service worker comparison.

 

If the "offline" box is checked, it will force a disconnection from network. In situations where users do not have connectivity or have a slow, one the PWA can still be accesed and used.

Online offline apps
Figure 54. Offline application.

 

Finally, browser extensions like Lighthouse can be used to test whether an application is progressive or not.

Lighthouse report
Figure 55. Lighthouse report.

16.7. APP_INITIALIZER

16.7.1. What is the APP_INITIALIZER pattern

The APP_INITIALIZER pattern allows an aplication to choose which configuration is going to be used in the start of the application, this is useful because it allows to setup different configurations, for example, for docker or a remote configuration. This provides benefits since this is done on runtime, so theres no need to recompile the whole application to switch from configuration.

16.7.2. What is APP_INITIALIZER

APP_INITIALIZER allows to provide a service in the initialization of the application in a @NgModule. It also allows to use a factory, allowing to create a singleton in the same service. An example can be found in MyThaiStar /core/config/config.module.ts:

Note

The provider expects the return of a Promise, if it is using Observables, a change with the method toPromise() will allow a switch from Observable to Promise

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { ConfigService } from './config.service';

@NgModule({
  imports: [HttpClientModule],
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: ConfigService.factory,
      deps: [ConfigService],
      multi: true,
    },
  ],
})
export class ConfigModule {}

This is going to allow the creation of a ConfigService where, using a singleton, the service is going to load an external config depending on a route. This dependence with a route, allows to setup diferent configuration for docker etc. This is seen in the ConfigService of MyThaiStar:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Config, config } from './config';

@Injectable()
export class ConfigService {
  constructor(private httpClient: HttpClient) {}

  static factory(appLoadService: ConfigService) {
    return () => appLoadService.loadExternalConfig();
  }

  // this method gets external configuration calling /config endpoint
  //and merges into config object
  loadExternalConfig(): Promise<any> {
    if (!environment.loadExternalConfig) {
      return Promise.resolve({});
    }

    const promise = this.httpClient
      .get('/config')
      .toPromise()
      .then((settings) => {
        Object.keys(settings || {}).forEach((k) => {
          config[k] = settings[k];
        });
        return settings;
      })
      .catch((error) => {
        return 'ok, no external configuration';
      });

    return promise;
  }

  getValues(): Config {
    return config;
  }
}

As it is mentioned earlier, you can see the use of a factory to create a singleton at the start. After that, loadExternalConfig is going to look for a boolean inside the corresponding environment file inside the path src/environments/, this boolean loadExternalConfig is going to easily allow to switch to a external config. If it is true, it generates a promise that overwrites the parameters of the local config, allowing to load the external config. Finally, the last method getValues() is going to allow to return the file config with the values (overwritten or not). The local config file from MyThaiStar can be seen here:

export enum BackendType {
  IN_MEMORY,
  REST,
  GRAPHQL,
}

interface Role {
  name: string;
  permission: number;
}

interface Lang {
  label: string;
  value: string;
}

export interface Config {
  version: string;
  backendType: BackendType;
  restPathRoot: string;
  restServiceRoot: string;
  pageSizes: number[];
  pageSizesDialog: number[];
  roles: Role[];
  langs: Lang[];
}

export const config: Config = {
  version: 'dev',
  backendType: BackendType.REST,
  restPathRoot: 'http://localhost:8081/mythaistar/',
  restServiceRoot: 'http://localhost:8081/mythaistar/services/rest/',
  pageSizes: [8, 16, 24],
  pageSizesDialog: [4, 8, 12],
  roles: [
    { name: 'CUSTOMER', permission: 0 },
    { name: 'WAITER', permission: 1 },
  ],
  langs: [
    { label: 'English', value: 'en' },
    { label: 'Deutsch', value: 'de' },
    { label: 'Español', value: 'es' },
    { label: 'Català', value: 'ca' },
    { label: 'Français', value: 'fr' },
    { label: 'Nederlands', value: 'nl' },
    { label: 'हिन्दी', value: 'hi' },
    { label: 'Polski', value: 'pl' },
    { label: 'Русский', value: 'ru' },
    { label: 'български', value: 'bg' },
  ],
};

Finally, inside a environment file src/environments/environment.ts the use of the boolean loadExternalConfig is seen:

// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.

export const environment: {
  production: boolean;
  loadExternalConfig: boolean;
} = { production: false, loadExternalConfig: false };

16.7.3. Creating a APP_INITIALIZER configuration

This section is going to be used to create a new APP_INITIALIZER basic example. For this, a basic app with angular is going to be generated using ng new "appname" substituting appname for the name of the app choosed.

16.7.4. Setting up the config files

Docker external configuration (Optional)

This section is only done if theres a docker configuration in the app you are setting up this type of configuration.

1.- Create in the root folder /docker-external-config.json. This external config is going to be used when the application is loaded with docker (if the boolean to load the external configuration is set to true). Here you need to add all the config parameter you want to load with docker:

{
    "version": "docker-version"
}

2.- In the root, in the file /Dockerfile angular is going to copy the docker-external-config.json that was created before into the nginx html route:

....
COPY docker-external-config.json /usr/share/nginx/html/docker-external-config.json
....
External json configuration

1.- Create a json file in the route /src/external-config.json. This external config is going to be used when the application is loaded with the start script (if the boolean to load the external configuration is set to true). Here you need to add all the config parameter you want to load:

{
    "version": "external-config"
}

2.- The file named /angular.json located at the root is going to be modified to add the file external-config.json that was just created to both "assets" inside Build and Test:

	....
	"build": {
          ....
            "assets": [
              "src/assets",
              "src/data",
              "src/favicon.ico",
              "src/manifest.json",
              "src/external-config.json"
            ]
	        ....
        "test": {
	  ....
	   "assets": [
              "src/assets",
              "src/data",
              "src/favicon.ico",
              "src/manifest.json",
              "src/external-config.json"
            ]
	  ....

16.7.5. Setting up the proxies

This step is going to setup two proxies. This is going to allow to load the config desired by the context, in case that it is using docker to load the app or in case it loads the app with angular. Loading diferent files is made posible by the fact that the ConfigService method loadExternalConfig() looks for the path /config.

Docker (Optional)

1.- This step is going to be for docker. Add docker-external-config.json to nginx configuration (/nginx.conf) that is in the root of the application:

....
  location  ~ ^/config {
        alias /usr/share/nginx/html/docker-external-config.json;
  }
....
External Configuration

1.- Now the file /proxy.conf.json, needs to be created/modified this file can be found in the root of the application. In this file you can add the route of the external configuration in target and the name of the file in ^/config::

....
  "/config": {
    "target": "http://localhost:4200",
    "secure": false,
    "pathRewrite": {
      "^/config": "/external-config.json"
    }
  }
....

2.- The file package.json found in the root of the application is gonna use the start script to load the proxy config that was just created:

  "scripts": {
....
    "start": "ng serve --proxy-config proxy.conf.json -o",
....

16.7.6. Adding the loadExternalConfig boolean to the environments

In order to load an external config we need to add the loadExternalConfig boolean to the environments. To do so, inside the folder environments/ the files are going to get modified adding this boolean to each environment that is going to be used. In this case, only two environments are going to be modified (environment.ts and environment.prod.ts). Down below theres an example of the modification being done in the environment.prod.ts:

export const environment: {
  production: boolean;
  loadExternalConfig: boolean;
} = { production: false, loadExternalConfig: false };

In the file in first instance theres the declaration of the types of the variables. After that, theres the definition of those variables. This variable loadExternalConfig is going to be used by the service, allowing to setup a external config just by switching the loadExternalConfig to true.

16.7.7. Creating core configuration service

In order to create the whole configuration module three are going to be created:

1.- Create in the core app/core/config/ a config.ts

  export interface Config {
    version: string;
  }

  export const config: Config = {
    version: 'dev'
  };

Taking a look to this file, it creates a interface (Config) that is going to be used by the variable that exports (export const config: Config). This variable config is going to be used by the service that is going to be created.

2.- Create in the core app/core/config/ a config.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Config, config } from './config';

@Injectable()
export class ConfigService {
  constructor(private httpClient: HttpClient) {}

  static factory(appLoadService: ConfigService) {
    return () => appLoadService.loadExternalConfig();
  }

  // this method gets external configuration calling /config endpoint
  // and merges into config object
  loadExternalConfig(): Promise<any> {
    if (!environment.loadExternalConfig) {
      return Promise.resolve({});
    }

    const promise = this.httpClient
      .get('/config')
      .toPromise()
      .then((settings) => {
        Object.keys(settings || {}).forEach((k) => {
          config[k] = settings[k];
        });
        return settings;
      })
      .catch((error) => {
        return 'ok, no external configuration';
      });

    return promise;
  }

  getValues(): Config {
    return config;
  }
}

As it was explained in previous steps, at first, there is a factory that uses the method loadExternalConfig(), this factory is going to be used in later steps in the module. After that, the loadExternalConfig() method checks if the boolean in the environment is false. If it is false it will return the promise resolved with the normal config. Else, it is going to load the external config in the path (/config), and overwrite the values from the external config to the config thats going to be used by the app, this is all returned in a promise.

3.- Create in the core a module for the config app/core/config/ a config.module.ts:

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { ConfigService } from './config.service';

@NgModule({
  imports: [HttpClientModule],
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: ConfigService.factory,
      deps: [ConfigService],
      multi: true,
    },
  ],
})
export class ConfigModule {}

As seen earlier, the ConfigService is added to the module. In this addition, the app is initialized(provide) and it uses the factory that was created in the ConfigService loading the config with or without the external values depending on the boolean in the config.

Using the Config Service

As a first step, in the file /app/app.module.ts the ConfigModule created earlier in the other step is going to be imported:

  imports: [
    ....
    ConfigModule,
    ....
  ]

After that, the ConfigService is going to be injected into the app.component.ts

....
import { ConfigService } from './core/config/config.service';
....
export class AppComponent {
....
  constructor(public configService: ConfigService) { }
....

Finally, for this demonstration app, the component app/app.component.html is going to show the version of the config it is using at that moment.

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<h2>Here is the configuration version that is using angular right now: {{configService.getValues().version}}</h2>
Final steps

The script start that was created earlier in the package.json (npm start) is going to be used to start the application. After that, modifying the boolean loadExternalConfig inside the corresponding environment file inside /app/environments/ should show the different config versions.

loadExternalConfigFalse
loadExternalConfigTrue

16.8. Component Decomposition

When implementing a new requirement there are a few design decisions, which need to be considered. A decomposition in Smart and Dumb Components should be done first. This includes the definition of state and responsibilities. Implementing a new dialog will most likely be done by defining a new Smart Component with multiple Dumb Component children.

In the component tree this would translate to the definition of a new subtree.

Component Tree With Highlighted Sub Tree
Figure 56. Component Tree with highlighted subtree

16.8.1. Defining Components

The following gives an example for component decomposition. Shown is a screenshot from a styleguide to be implemented. It is a widget called Listpicker.

The basic function is an input field accepting direct input. So typing otto puts otto inside the FormControl. With arrow down key or by clicking the icon displayed in the inputs right edge a dropdown is opened. Inside possible values can be selected and filtered beforehand. After pressing arrow down key the focus should move into the filter input field. Up and down arrow keys can be used to select an element from the list. Typing into the filter input field filters the list from which the elements can be selected. The current selected element is highlighted with green background color.

Component Decomposition Example 1v2
Figure 57. Component decomposition example before

What should be done, is to define small reusable Dumb Components. This way the complexity becomes manageable. In the example every colored box describes a component with the purple box being a Smart Component.

Component Decomposition Example 2v2
Figure 58. Component decomposition example after

This leads to the following component tree.

Component Decomposition Example component tree
Figure 59. Component decomposition example component tree

Note the uppermost component is a Dumb Component. It is a wrapper for the label and the component to be displayed inside a form. The Smart Component is Listpicker. This way the widget can be reused without a form needed.

A widgets is a typical Smart Component to be shared across feature modules. So the SharedModule is the place for it to be defined.

16.8.2. Defining state

Every UI has state. There are different kinds of state, for example

  • View State: e.g. is a panel open, a css transition pending, etc.

  • Application State: e.g. is a payment pending, current URL, user info, etc.

  • Business Data: e.g. products loaded from backend

It is good practice to base the component decomposition on the state handled by a component and to define a simplified state model beforehand. Starting with the parent - the Smart Component:

  • What overall state does the dialog have: e.g. loading, error, valid data loaded, valid input, invalid input, etc. Every defined value should correspond to an overall appearance of the whole dialog.

  • What events can occur to the dialog: e.g. submitting a form, changing a filter, pressing buttons, pressing keys, etc.

For every Dumb Component:

  • What data does a component display: e.g. a header text, user information to be displayed, a loading flag, etc.
    This will be a slice of the overall state of the parent Smart Component. In general a Dumb Component presents a slice of its parent Smart Components state to the user.

  • What events can occur: keyboard events, mouse events, etc.
    These events are all handled by its parent Smart Component - every event is passed up the tree to be handled by a Smart Component.

These information should be reflected inside the modeled state. The implementation is a TypeScript type - an interface or a class describing the model.

So there should be a type describing all state relevant for a Smart Component. An instance of that type is send down the component tree at runtime. Not every Dumb Component will need the whole state. For instance a single Dumb Component could only need a single string.

The state model for the previous Listpicker example is shown in the following listing.

Listing 39. Listpicker state model
export class ListpickerState {

  items: {}[]|undefined;
  columns = ['key', 'value'];
  keyColumn = 'key';
  displayValueColumn = 'value';
  filteredItems: {}[]|undefined;
  filter = '';
  placeholder = '';
  caseSensitive = true;
  isDisabled = false;
  isDropdownOpen = false;
  selectedItem: {}|undefined;
  displayValue = '';

}

Listpicker holds an instance of ListpickerState which is passed down the component tree via @Input() bindings in the Dumb Components. Events emitted by children - Dumb Components - create a new instance of ListpickerState based on the current instance and the event and its data. So a state transition is just setting a new instance of ListpickerState. Angular Bindings propagate the value down the tree after exchanging the state.

Listing 40. Listpicker State transition
export class ListpickerComponent {

  // initial default values are set
  state = new ListpickerState();

  /** User changes filter */
  onFilterChange(filter: string): void {
    // apply filter ...
    const filteredList = this.filterService.filter(...);

    // important: A new instance is created, instead of altering the existing one.
    //            This makes change detection easier and prevents hard to find bugs.
    this.state = Object.assing({}, this.state, {
      filteredItems: filteredList,
      filter: filter
    });
  }

}
Note:

It is not always necessary to define the model as independent type. So there would be no state property and just properties for every state defined directly in the component class. When complexity grows and state becomes larger this is usually a good idea. If the state should be shared between Smart Components a store is to be used.

16.8.3. When are Dumb Components needed

Sometimes it is not necessary to perform a full decomposition. The architecture does not enforce it generally. What you should keep in mind is, that there is always a point when it becomes recommendable.

For example a template with 800 loc is:

  • not understandable

  • not maintanable

  • not testable

  • not reusable

So when implementing a template with more than 50 loc you should think about decomposition.

16.9. Consuming REST services

A good introduction to working with Angular HttpClient can be found in Angular Docs

This guide will cover, how to embed Angular HttpClient in the application architecture. For backend request a special service with the suffix Adapter needs to be defined.

16.9.1. Defining Adapters

It is a good practice to have a Angular service whose single responsibility is to call the backend and parse the received value to a transfer data model (e.g. Swagger generated TOs). Those services need to have the suffix Adapter to make them easy to recognize.

Adapters handle backend communication
Figure 60. Adapters handle backend communication

As illustrated in the figure a Use Case service does not use Angular HttpClient directly but uses an adapter. A basic adapter could look like this:

Listing 41. Example adapter
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

import { FlightTo } from './flight-to';

@Injectable()
export class FlightsAdapter {

  constructor(
    private httpClient: HttpClient
  ) {}

  getFlights(): Observable<FlightTo> {
    return this.httpClient.get<FlightTo>('/relative/url/to/flights');
  }

}

The adapters should use a well-defined transfer data model. This could be generated from server endpoints with CobiGen, Swagger, typescript-maven-plugin, etc. If inside the application there is a business model defined, the adapter has to parse to the transfer model. This is illustrated in the following listing.

Listing 42. Example adapter mapping from business model to transfer model
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';

import { FlightTo } from './flight-to';
import { Flight } from '../../../model/flight';

@Injectable()
export class FlightsAdapter {

  constructor(
    private httpClient: HttpClient
  ) {}

  updateFlight(flight: Flight): Observable<Flight> {
    const to = this.mapFlight(flight);

    return this.httpClient.post<FlightTo>('/relative/url/to/flights', to).pipe(
      map(to => this.mapFlightTo(to))
    );
  }

  private mapFlight(flight: Flight): FlightTo {
    // mapping logic
  }

  private mapFlightTo(flightTo: FlightTo): Flight {
    // mapping logic
  }

}

16.9.2. Token management

16.10. Error Handler in angular

Angular allows us to set up a custom error handler that can be used to control the different errors and them in a correct way. Using a global error handler will avoid mistakes and provide a use friendly interface allowing us to indicate the user what problem is happening.

16.10.1. What is ErrorHandler

ErrorHandler is the class that Angular uses by default to control the errors. This means that, even if the application doesnt have a ErrorHandler it is going to use the one setup by default in Angular. This can be tested by trying to find a page not existing in any app, instantly Angular will print the error in the console.

16.10.2. Creating your custom ErrorHandler step by step

In order to create a custom ErrorHandler three steps are going to be needed:

Creating the custom ErrorHandler class

In this first step the custom ErrorHandler class is going to be created inside the folder /app/core/errors/errors-handler.ts:

import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';

@Injectable()
export class ErrorsHandler implements ErrorHandler {

    constructor(private injector: Injector) {}

    handleError(error: Error | HttpErrorResponse) {
      //  To do: Use injector to get the necessary services to redirect or
      // show a message to the user
      const classname  = error.constructor.name;
      switch ( classname )  {
        case 'HttpErrorResponse':
          console.error('HttpError:' + error.message);
          if (!navigator.onLine) {
            console.error('Theres no internet connection');
            // To do: control here in internet what you wanna do if user has no internet
          } else {
            console.error('Server Error:' + error.message);
            // To do: control here if the server gave an error
          }
          break;
        default:
          console.error('Error:' + error.message);
          // To do: control here if the client/other things gave an error
      }
    }
}

This class can be used to control the different type of errors. If wanted, the classname variable could be used to add more switch cases. This would allow control of more specific situations.

Creating a ErrorInterceptor

Inside the same folder created in the last step we are going to create the ErrorInterceptor(errors-handler-interceptor.ts). This ErrorInterceptor is going to retry any failed calls to the server to make sure it is not being found before showing the error:

import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { retry } from 'rxjs/operators';

@Injectable()
export class ErrorsHandlerInterceptor implements HttpInterceptor {

    constructor() {}
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            retryWhen((errors: Observable<any>) => errors.pipe(
                delay(500),
                take(5),
                concatMap((error: any, retryIndex: number) => {
                    if (++retryIndex === 5) {
                        throw error;
                    }
                    return of(error);
                })
            ))
        );
    }
}

This custom made interceptor is implementing the HttpInterceptor and inside the method intercept using the method pipe,retryWhen,delay,take and concatMap from RxJs it is going to do the next things if there is errors:

  1. With delay(500) do a delay to allow some time in between requests

  2. With take(5) retry five times.

  3. With concatMap if the index that take() gives is not 5 it returns the error, else, it throws the error.

Creating a Error Module

Finally, creating a module(errors-handler.module.ts) is necessary to include the interceptor and the custom error handler. In this case, the module is going to be created in the same folder as the last two:

import { NgModule, ErrorHandler } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ErrorsHandler } from './errors-handler';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorsHandlerInterceptor } from './errors-handler-interceptor';

@NgModule({
  declarations: [], // Declare here component if you want to use routing to error component
  imports: [
    CommonModule
  ],
  providers: [
    {
      provide: ErrorHandler,
      useClass: ErrorsHandler,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ErrorsHandlerInterceptor,
      multi: true,
    }
  ]
})
export class ErrorsHandlerModule { }

This module simply is providing the services that are implemented by our custom classes and then telling angular to use our custom made classes instead of the default ones. After doing this, the module has to be included in the app module app.module.ts in order to be used.

....
  imports: [
    ErrorsHandlerModule,
    ....

16.10.3. Handling Errors

As a final step, handling these errors is necessary. Theres different ways that can be used to control the errors, here are a few:

  • Creating a custom page and using with Router to redirect to a page showing an error.

  • Creating a service in the server side or Backend to create a log with the error and calling it with HttpClient.

  • Showing a custom made SnackBar with the error message.

Using SnackBarService and NgZone

If the SnackBar is used directly, some errors can ocurr, this is due to SnackBar being out of the Angular zone. In order to use this service properly, NgZone is necessary. The method run() from NgZone will allow the service to be inside the Angular Zone. An example on how to use it:

import { ErrorHandler, Injectable, Injector, NgZone } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { MatSnackBar } from '@angular/material';

@Injectable()
export class ErrorsHandler implements ErrorHandler {

    constructor(private injector: Injector, private zone: NgZone) {}

    handleError(error: Error | HttpErrorResponse) {
      // Use injector to get the necessary services to redirect or
      const snackBar: MatSnackBar = this.injector.get(MatSnackBar);
      const classname  = error.constructor.name;
      let message: string;
      switch ( classname )  {
        case 'HttpErrorResponse':
          message = !(navigator.onLine) ? 'There is no internet connection' : error.message;
          break;
        default:
          message = error.message;
      }
      this.zone.run(
        () => snackBar.open(message, 'danger', { duration : 4000})
      );
    }
}

Using Injector the MatSnackBar is obtained, then the correct message is obtained inside the switch. Finally, using NgZone and run(), we open the SnackBar passing the message, and the paremeters wanted.

16.11. File Structure

16.11.1. Toplevel

The toplevel file structure is defined by Angular CLI. You might put this "toplevel file structure" into a subdirectory to facilitate your build, but this is not relevant for this guide. So the applications file structure relevant to this guide is the folder /src/app inside the part managed by Angular CLI.

Listing 43. Toplevel file structure shows feature modules
    /src
    └── /app
        ├── /account-management
        ├── /billing
        ├── /booking
        ├── /core
        ├── /shared
        ├── /status
        |
        ├── app.module.ts
        ├── app.component.spec.ts
        ├── app.component.ts
        └── app.routing-module.ts

Besides the definition of app module the app folder has feature modules on toplevel. The special modules shared and core are present as well.

16.11.2. Feature Modules

A feature module contains the modules definition and two folders representing both layers.

Listing 44. Feature module file structure has both layers
    /src
    └── /app
        └── /account-management
            ├── /components
            ├── /services
            |
            ├── account-management.module.ts
            ├── account-management.component.spec.ts
            ├── account-management.component.ts
            └── account-management.routing-module.ts

Additionally an entry component is possible. This would be the case in lazy loading scenarios. So account-management.component.ts would be only present if account-management is lazy loaded. Otherwise, the module’s routes would be defined Component-less (see vsavkin blog post).

16.11.3. Components Layer

The component layer reflects the distinction between Smart Components and Dumb Components.

Listing 45. Components layer file structure shows Smart Components on toplevel
    /src
    └── /app
        └── /account-management
            └── /components
                ├── /account-overview
                ├── /confirm-modal
                ├── /create-account
                ├── /forgot-password
                └── /shared

Every folder inside the /components folder represents a smart component. The only exception is /shared. /shared contains Dumb Components shared across Smart Components inside the components layer.

Listing 46. Smart components contain Dumb components
    /src
    └── /app
        └── /account-management
            └── /components
                └── /account-overview
                    ├── /user-info-panel
                    |   ├── /address-tab
                    |   ├── /last-activities-tab
                    |   |
                    |   ├── user-info-panel.component.html
                    |   ├── user-info-panel.component.scss
                    |   ├── user-info-panel.component.spec.ts
                    |   └── user-info-panel.component.ts
                    |
                    ├── /user-header
                    ├── /user-toolbar
                    |
                    ├── account-overview.component.html
                    ├── account-overview.component.scss
                    ├── account-overview.component.spec.ts
                    └── account-overview.component.ts

Inside the folder of a Smart Component the component is defined. Besides that are folders containing the Dumb Components the Smart Component consists of. This can be recursive - a Dumb Component can consist of other Dumb Components. This is reflected by the file structure as well. This way the structure of a view becomes very readable. As mentioned before, if a Dumb Component is used by multiple Smart Components inside the components layer it is put inside the /shared folder inside the components layer.

With this way of thinking the shared module makes a lot of sense. If a Dumb Component is used by multiple Smart Components from different feature modules, the Dumb Component is placed into the shared module.

Listing 47. The shared module contains Dumb Components shared across Smart Components from different feature modules
    /src
    └── /app
        └── /shared
            └── /user-panel
                |
                ├── user-panel.component.html
                ├── user-panel.component.scss
                ├── user-panel.component.spec.ts
                └── user-panel.component.ts

The layer folder /components is not necessary inside the shared module. The shared module only contains components!

16.12. Internationalization

Nowadays, a common scenario in front-end applications is to have the ability to translate labels and locate numbers, dates, currency and so on when the user clicks over a language selector or similar. devon4ng and specifically Angular has a default mechanism in order to fill the gap of such features, and besides there are some wide used libraries that make even easier to translate applications.

16.12.1. devon4ng i18n approach

The official approach could be a bit complicated, therefore the recommended one is to use the recommended library NGX Translate from http://www.ngx-translate.com/.

Install NGX Translate

In order to include this library in your devon4ng Angular >= 4.3 project you will need to execute in a terminal:

$ npm install @ngx-translate/core @ngx-translate/http-loader --save
# or if you use yarn
$ yarn add @ngx-translate/core @ngx-translate/http-loader
  • @ngx-translate/core is the core library to provide i18n capabilities.

  • @ngx-translate/http-loader is a loader for ngx-translate that loads translations using http.

Configure NGX Translate

Depending on the volume of the devon4ng application we will include the NGX Translate library in the app.module.ts or in the core.module.ts transversal to the application.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

Next, an exported function for factories has to be created:

// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
}

@NgModule({
    imports: [
        BrowserModule,
        HttpClientModule,
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient]
            }
        })
    ],
    bootstrap: [AppComponent]
})
export class AppModule { } // or CoreModule

The TranslateHttpLoader also has two optional parameters:

  • prefix: string = "/assets/i18n/"

  • suffix: string = ".json"

By using those default parameters, it will load the translations files for the lang "en" from: /assets/i18n/en.json. In general, any translation file will loaded from the /assets/i18n/ folder.

Those parameters can be changed in the HttpLoaderFactory method just defined. For example if you want to load the "en" translations from /public/lang-files/en-lang.json you would use:

export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http, "/public/lang-files/", "-lang.json");
}

For now this loader only support the json format.

Note
If you’re still on Angular < 4.3, please use Http from @angular/http with http-loader@0.1.0.
Usage

In order to translate any label in any HTML template you will need to use the translate pipe available:

{{ 'HELLO' | translate }}

An optional parameter from the component TypeScript class could be included as follows:

{{ 'HELLO' | translate:param }}

So, param has to be defined in the class. The default language used is defined as follows:

// imports

@Component({
    selector: 'app',
    template: `
        <div>{{ 'HELLO' | translate }}</div>            // Without param
        <div>{{ 'HELLO' | translate:param }}</div>      // With param
    `
})
export class AppComponent {
    // This param will be used in the translation
    param = { value: 'world' };

    constructor(translate: TranslateService) {
        // this language will be used as a fallback when a translation isn't found in the current language
        translate.setDefaultLang('en');

        // the lang to use, if the lang isn't available, it will use the current loader to get them
        translate.use('en');
    }
}

In order to change the language used you will need to create a button or selector that calls the this.translate.use(language: string) method from TranslateService. For example:

toggleLanguage(option) {
    this.translate.use(option);
}

The translations will be included in the en.json, es.json, de.json, etc. files inside the /assets/i18n folder. For example en.json would be (using the previous param):

{
    "HELLO": "hello"
}

Or with an optional param:

{
    "HELLO": "hello {{value}}"
}

The TranslateParser understands nested JSON objects. This means that you can have a translation that looks like this:

{
    "HOME": {
        "HELLO": "hello {{value}}"
    }
}

In order to access access the value, use the dot notation, in this case HOME.HELLO.

Using the service, pipe or directive
Service

If you need to access translations in any component or service you can do it injecting the Translateservice into them:

translate.get('HELLO', {value: 'world'}).subscribe((res: string) => {
    console.log(res);
    //=> 'hello world'
});
Pipe

The use of pipes can be possible too:

template:

<div>{{ 'HELLO' | translate:param }}</div>

component:

param = {value: 'world'};
Directives

Finally, it can also be used with directives:

<div [translate]="'HELLO'" [translateParams]="{value: 'world'}"></div>

or, using the content of your element as a key

<div translate [translateParams]="{value: 'world'}">HELLO</div>
Important
You can find a complete example at https://github.com/devonfw/devon4ng-application-template.

Please, visit https://github.com/ngx-translate/core for more info.

16.13. Routing

A basic introduction to the Angular Router can be found in Angular Docs.

This guide will show common tasks and best practices.

16.13.1. Defining Routes

For each feature module and the app module all routes should be defined in a seperate module with the suffix RoutingModule. This way the routing modules are the only place where routes are defined. This pattern achieves a clear seperation of concernes. The following figure illustrates this.

Routing module declaration
Figure 61. Routing module declaration

It is important to define routes inside app routing module with .forRoot() and in feature routing modules with .forChild().

Example 1 - No Lazy Loading

In this example two modules need to be configured with routes - AppModule and FlightModule.

The following routes will be configured

  • / will redirect to /search

  • /search displays FlightSearchComponent (FlightModule)

  • /search/print/:flightId/:date displays FlightPrintComponent (FlightModule)

  • /search/details/:flightId/:date displays FlightDetailsComponent (FlightModule)

  • All other routes will display ErrorPage404 (AppModule)

Listing 48. app-routing.module.ts
const routes: Routes = [
  { path: '', redirectTo: 'search', pathMatch: 'full' },
  { path: '**', component: ErrorPage404 }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Listing 49. flight-search-routing.module.ts
const routes: Routes = [
  {
    path: 'search', children: [
      { path: '', component: FlightSearchComponent },
      { path: 'print/:flightId/:date', component: FlightPrintComponent },
      { path: 'details/:flightId/:date', component: FlightDetailsComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class FlightSearchRoutingModule { }
Tip
The import order inside AppModule is important. AppRoutingModule needs to be imported after FlightModule.
Example 2 - Lazy Loading

Lazy Loading is a good practice when the application has multiple feature areas and a user might not visit every dialog. Or at least he might not need every dialog up front.

The following example will configure the same routes as example 1 but will lazy load FlightModule.

Listing 50. app-routing.module.ts
const routes: Routes = [
  { path: '/search', loadChildren: 'app/flight-search/flight-search.module#FlightSearchModule' },
  { path: '**', component: ErrorPage404 }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Listing 51. flight-search-routing.module.ts
const routes: Routes = [
  {
    path: '', children: [
      { path: '', component: FlightSearchComponent },
      { path: 'print/:flightId/:date', component: FlightPrintComponent },
      { path: 'details/:flightId/:date', component: FlightDetailsComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class FlightSearchRoutingModule { }

16.13.2. Triggering Route Changes

With Angular you have two ways of triggering route changes.

  1. Declarative with bindings in component HTML templates

  2. Programmatic with Angular Router service inside component classes

On the one hand, architecture-wise it is a much cleaner solution to trigger route changes in Smart Components. This way you have every UI event that should trigger a navigation handled in one place - in a Smart Component. It becomes very easy to look inside the code for every navigation, that can occure. Refactoring is also much easier, as there are no navigation events "hidden" in the HTML templates

On the other hand, in terms of accessibility and SEO it is a better solution to rely on bindings in the view - e.g. by using Angulars router-link directive. This way screen readers and the Google crawler can move through the page easily.

Tip
If you do not have to support accessibility (screen readers, etc.) and to care about SEO (Google rank, etc.), then you should aim for triggering navigations only in Smart Components.
Triggering navigation
Figure 62. Triggering navigation

16.13.3. Guards

Guards are Angular services implemented on routes which determines whether a user can naviagate to/from the route. There are examples below which will explain things better. We have the following types of Guards:

  • CanActivate: It is used to determine whether a user can visit a route. The most common scenario for this guard is to check if the user is authenticated. For example, if we want only logged in users to be able to go to a particular route, we will implement the CanActivate guard on this route.

  • CanActivateChild: Same as above, only implemented on child routes.

  • CanDeactivate: It is used to determine if a user can naviagate away from a route. Most common example is when a user tries to go to a different page after filling up a form and does not save/submit the changes, we can use this guard to confirm whether the user really wants to leave the page without saving/submiting.

  • Resolve: For resolving dynamic data.

  • CanLoad: It is used to determine whether an Angular module can be loaded lazily. Example below will be helpful to understand it.

Let’s have a look at some examples.

Example 1 - CanActivate and CanActivateChild guards
CanActivate guard

As mentioned earlier, a guard is an Angular service and services are simply TypeScript classes. So we begin by creating a class. This class has to implement the CanActivate interface (imported from angular/router), and therefore, must have a canActivate function. The logic of this function determines whether the requested route can be navigated to or not. It returns either a boolean value or an Observable or a Promise which resolves to a boolean value. If it is true, the route is loaded, else not.

Listing 52. CanActivate example
...
import {CanActivate} from "@angular/router";

@Injectable()
class ExampleAuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(route: ActivatedRouterSnapshot, state: RouterStateSnapshot) {
	if (this.authService.isLoggedIn()) {
      return true;
    } else {
	  window.alert('Please log in first');
      return false;
    }
  }
}

In the above example, let’s assume we have a AuthService which has a isLoggedIn() method which returns a boolean value depending on whether the user is logged in. We use it to return true or false from the canActivate function. The canActivate function accepts two parameters (provided by Angular). The first parameter of type ActivatedRouterSnapshot is the snapshot of the route the user is trying to naviagate to (where the guard is implemented); we can extract the route parameters from this instance. The second parameter of type RouterStateSnapshot is a snapshot of the router state the user is trying to naviagate to; we can fetch the URL from it’s url property.

Tip
We can also redirect the user to another page (maybe a login page) if the authService returns false. To do that, inject Router and use it’s naviagate function to redirect to the appropriate page.

Since it is a service, it needs to be provided in our module:

Listing 53. provide the guard in a module
@NgModule({
  ...
  providers: [
    ...
    ExampleAuthGuard
  ]
})

Now this guard is ready to use on our routes. We implement it where we define our array of routes in the application:

Listing 54. Implementing the guard
...
const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'page1', component: Page1Component, canActivate: [ExampleAuthGuard] }
];

As you can see, the canActivate property accepts an array of guards. So we can implement more than one guard on a route.

CanActivateChild guard

To use the guard on nested (children) routes, we add it to the canActivateChild property like so:

Listing 55. Implementing the guard on child routes
...
const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'page1', component: Page1Component, canActivateChild: [ExampleAuthGuard], children: [
	{path: 'sub-page1', component: SubPageComponent},
    {path: 'sub-page2', component: SubPageComponent}
  ] }
];
Example 2 - CanLoad guard

Similar to CanActivate, to use this guard we implement the CanLoad interface and overwrite it’s canLoad function. Again, this function returns either a boolean value or an Observable or a Promise which resolves to a boolean value. The fundamental difference between CanActivate and CanLoad is that CanLoad is used to determine whether an entire module can be lazily loaded or not. If the guard returns false for a module protected by CanLoad, the entire module is not loaded.

Listing 56. CanLoad example
...
import {CanLoad, Route} from "@angular/router";

@Injectable()
class ExampleCanLoadGuard implements CanLoad {
  constructor(private authService: AuthService) {}

  canLoad(route: Route) {
	if (this.authService.isLoggedIn()) {
      return true;
    } else {
	  window.alert('Please log in first');
      return false;
    }
  }
}

Again, let’s assume we have a AuthService which has a isLoggedIn() method which returns a boolean value depending on whether the user is logged in. The canLoad function accepts a parameter of type Route which we can use to fetch the path a user is trying to navigate to (using the path property of Route).

This guard needs to be provided in our module like any other service.

To implement the guard, we use the canLoad property:

Listing 57. Implementing the guard
...
const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [ExampleCanLoadGuard] }
];

16.14. Testing

This guide will cover the basics of testing logic inside your code with UnitTests. The guide assumes that you are familiar with Angular CLI (see the guide)

For testing your Angular application with UnitTests there are two main strategies:

  1. Isolated UnitTests
    Isolated unit tests examine an instance of a class all by itself without any dependence on Angular or any injected values. The amount of code and effort needed to create such tests in minimal.

  2. Angular Testing Utilities
    Let you test components including their interaction with Angular. The amount of code and effort needed to create such tests is a little higher.

16.14.1. Testing Concept

The following figure shows you an overview of the application architecture devided in testing areas.

Testing Areas
Figure 63. Testing Areas

There are three areas, which need to be covered by different testing strategies.

  1. Components:
    Smart Components need to be tested because they contain view logic. Also the interaction with 3rd party components needs to be tested. When a 3rd party component changes with an upgrade a test will be failing and warn you, that there is something wrong with the new version. Most of the time Dumb Components do not need to be teste because they mainly display data and do not contain any logic. Smart Components are alway tested with Angular Testing Utilities. For example selectors, which select data from the store and transform it further, need to be tested.

  2. Stores:
    A store contains methods representing state transitions. If these methods contain logic, they need to be tested. Stores are always testet using Isolated UnitTests.

  3. Services:
    Services contain Business Logic, which needs to be tested. UseCase Services represent a whole business use case. For instance this could be initializing a store with all the data that is needed for a dialog - loading, transforming, storing. Often Angular Testing Utilities are the optimal solution for testing UseCase Services, because they allow for an easy stubbing of the backend. All other services should be tested with Isolated UnitTests as they are much easier to write and maintain.

16.14.2. Testing Smart Components

Testing Smart Components should assure the following.

  1. Bindings are correct.

  2. Selectors which load data from the store are correct.

  3. Asynchronous behavior is correct (loading state, error state, "normal" state).

  4. Oftentimes through testing one realizes, that important edge cases are forgotten.

  5. Do these test become very complex, it is often an indicator for poor code quality in the component. Then the implementation is to be adjusted / refactored.

  6. When testing values received from the native DOM, you will test also that 3rd party libraries did not change with a version upgrade. A failing test will show you what part of a 3rd party library has changed. This is much better than the users doing this for you. For example a binding might fail because the property name was changed with a newer version of a 3rd party library.

In the function beforeEach() the TestBed imported from Angular Testing Utilities needs to be initialized. The goal should be to define a minimal test-module with TestBed. The following code gives you an example.

Listing 58. Example test setup for Smart Components
describe('PrintFlightComponent', () => {

  let fixture: ComponentFixture<PrintCPrintFlightComponentomponent>;
  let store: FlightStore;
  let printServiceSpy: jasmine.SpyObj<FlightPrintService>;

  beforeEach(() => {
    const urlParam = '1337';
    const activatedRouteStub = { params: of({ id: urlParam }) };
    printServiceSpy = jasmine.createSpyObj('FlightPrintService', ['initializePrintDialog']);
    TestBed.configureTestingModule({
      imports: [
        TranslateModule.forRoot(),
        RouterTestingModule
      ],
      declarations: [
        PrintFlightComponent,
        PrintContentComponent,
        GeneralInformationPrintPanelComponent,
        PassengersPrintPanelComponent
      ],
      providers: [
        FlightStore,
        {provide: FlightPrintService, useValue: printServiceSpy},
        {provide: ActivatedRoute, useValue: activatedRouteStub}
      ]
    });
    fixture = TestBed.createComponent(PrintFlightComponent);
    store = fixture.debugElement.injector.get(FlightStore);
    fixture.detectChanges();
  });

  // ... test cases
})

It is important:

  • Use RouterTestingModule` instead of RouterModule

  • Use TranslateModule.forRoot() without translations This way you can test language-neutral without translation marks.

  • Do not add a whole module from your application - in declarations add the tested Smart Component with all its Dumb Components

  • The store should never be stubbed. If you need a complex test setup, just use the regular methods defined on the store.

  • Stub all services used by the Smart Component. These are mostly UseCase services. They should not be tested by these tests. Only the correct call to their functions should be assured. The logic inside the UseCase services is tested with seperate tests.

  • detectChanges() performance an Angular Change Detection cycle (Angular refreshes all the bindings present in the view)

  • tick() performance a virtual marco task, tick(1000) is equal to the virtual passing of 1s.

The following test cases show the testing strategy in action.

Listing 59. Example
it('calls initializePrintDialog for url parameter 1337', fakeAsync(() => {
  expect(printServiceSpy.initializePrintDialog).toHaveBeenCalledWith(1337);
}));

it('creates correct loading subtitle', fakeAsync(() => {
  store.setPrintStateLoading(123);
  tick();
  fixture.detectChanges();

  const subtitle = fixture.debugElement.query(By.css('app-header-element .print-header-container span:last-child'));
  expect(subtitle.nativeElement.textContent).toBe('PRINT_HEADER.FLIGHT STATE.IS_LOADING');
}));

it('creates correct subtitle for loaded flight', fakeAsync(() => {
  store.setPrintStateLoadedSuccess({
    id: 123,
    description: 'Description',
    iata: 'FRA',
    name: 'Frankfurt',
    // ...
  });
  tick();
  fixture.detectChanges();

  const subtitle = fixture.debugElement.query(By.css('app-header-element .print-header-container span:last-child'));
  expect(subtitle.nativeElement.textContent).toBe('PRINT_HEADER.FLIGHT "FRA (Frankfurt)" (ID: 123)');
}));

The examples show the basic testing method

  • Set the store to a well-defined state

  • check if the component displays the correct values

  • …​ via checking values inside the native DOM.

16.14.3. Testing state transitions performed by stores

Stores are always tested with Isolated UnitTests.

Actions triggered by dispatchAction() calls are asynchronously performed to alter the state. A good solution to test such a state transition is to use the done callback from Jasmine.

Listing 60. Example for testing a store
let sut: FlightStore;

beforeEach(() => {
  sut = new FlightStore();
});

it('setPrintStateLoading sets print state to loading', (done: Function) => {
  sut.setPrintStateLoading(4711);

  sut.state$.pipe(first()).subscribe(result => {
    expect(result.print.isLoading).toBe(true);
    expect(result.print.loadingId).toBe(4711);
    done();
  });
});

it('toggleRowChecked adds flight with given id to selectedValues Property', (done: Function) => {
  const flight: FlightTO = {
    id: 12
    // dummy data
  };
  sut.setRegisterabgleichListe([flight]);
  sut.toggleRowChecked(12);

  sut.state$.pipe(first()).subscribe(result => {
    expect(result.selectedValues).toContain(flight);
    done();
  });
});

16.14.4. Testing services

When testing services both strategies - Isolated UnitTests and Angular Testing Utilities - are valid options.

The goal of such tests are

  • assuring the behavior for valid data.

  • assuring the behavior for invalid data.

  • documenting functionality

  • savely performing refactorings

  • thinking about edge case behavior while testing

For simple services Isolated UnitTests can be written. Writing these tests takes lesser effort and they can be written very fast.

The following listing gives an example of such tests.

Listing 61. Testing a simple services with Isolated UnitTests
let sut: IsyDatePipe;

beforeEach(() => {
  sut = new IsyDatePipe();
});

it('transform should return empty string if input value is empty', () => {
  expect(sut.transform('')).toBe('');
});

it('transform should return empty string if input value is null', () => {
  expect(sut.transform(undefined)).toBe('');
});

// ...more tests

For testing Use Case services the Angular Testing Utilities should be used. The following listing gives an example.

Listing 62. Test setup for testing use case services with Angular Testing Utilities
let sut: FlightPrintService;
let store: FlightStore;
let httpController: HttpTestingController;
let flightCalculationServiceStub: jasmine.SpyObj<FlightCalculationService>;
const flight: FlightTo = {
  // ... valid dummy data
};

beforeEach(() => {
  flightCalculationServiceStub = jasmine.createSpyObj('FlightCalculationService', ['getFlightType']);
  flightCalculationServiceStub.getFlightType.and.callFake((catalog: string, type: string, key: string) => of(`${key}_long`));
  TestBed.configureTestingModule({
    imports: [
      HttpClientTestingModule,
      RouterTestingModule,
    ],
    providers: [
      FlightPrintService,
      FlightStore,
      FlightAdapter,
      {provide: FlightCalculationService, useValue: flightCalculationServiceStub}
    ]
  });

  sut = TestBed.get(FlightPrintService);
  store = TestBed.get(FlightStore);
  httpController = TestBed.get(HttpTestingController);
});

When using TestBed, it is important

  • to import HttpClientTestingModule for stubbing the backend

  • to import RouterTestingModule for stubbing the Angular router

  • not to stub stores, adapters and business services

  • to stub services from libraries like FlightCalculationService - the correct implementation of libraries should not be tested by these tests.

Testing backend communication looks like this:

Listing 63. Testing backend communication with Angular HttpTestingController
it('loads flight if not present in store', fakeAsync(() => {
  sut.initializePrintDialog(1337);
  const processRequest = httpController.expectOne('/path/to/flight');
  processRequest.flush(flight);

  httpController.verify();
}));

it('does not load flight if present in store', fakeAsync(() => {
  const flight = {...flight, id: 4711};
  store.setRegisterabgleich(flight);

  sut.initializePrintDialog(4711);
  httpController.expectNone('/path/to/flight');

  httpController.verify();
}));

The first test assures a correct XHR request is performed if initializePrintDialog() is called and no data is in the store. The second test assures no XHR request ist performed if the needed data is already in the store.

The next steps are checks for the correct implementation of logic.

Listing 64. Example testing a Use Case service
it('creates flight destination for valid key in svz', fakeAsync(() => {
  const flightTo: FlightTo = {
    ...flight,
    id: 4712,
    profile: '77'
  };
  store.setFlight(flightTo);
  let result: FlightPrintContent|undefined;

  sut.initializePrintDialog(4712);
  store.select(s => s.print.content).subscribe(content => result = content);
  tick();

  expect(result!.destination).toBe('77_long (ID: 77)');
}));

16.15. Update Angular CLI

16.15.1. Angular CLI common issues

There are constant updates for the official Angular framework dependencies. These dependencies are directly related with the Angular CLI package. Since this package comes installed by default inside the devonfw distribution folder for Windows OS and the distribution is updated every few months it needs to be updated in order to avoid known issues.

16.15.2. Angular CLI update guide

For Linux users is as easy as updating the global package:

$ npm unistall -g @angular/cli
$ npm install -g @angular/cli

For Windows users the process is only a bit harder. Open the devonfw bundled console and do as follows:

$ cd [devonfw_dist_folder]
$ cd software/nodejs
$ npm uninstall @angular/cli --no-save
$ npm install @angular/cli --no-save

After following these steps you should have the latest Angular CLI version installed in your system. In order to check it run in the distribution console:

Note
At the time of this writing, the Angular CLI is at 1.7.4 version.
λ ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 7.2.3
Node: 10.13.0
OS: win32 x64
Angular:
...

16.16. Upgrade devon4ng Angular and Ionic/Angular applications

Angular CLI provides a powerful tool to upgrade Angular based applications to the current stable release of the core framework.

This tool is ng update. It will not only upgrade dependencies and their related ones but also will perform some fixes in your code if available thanks to the provided schematics. It will check even if the update is not possible as there is another library or libraries that are not compatible with the versions of the upgraded dependencies. In this case it will keep your application untouched.

Note
The repository must be in a clean state before executing a ng update. So, remember to commit your changes first.

16.16.1. Basic usage

In order to perform a basic upgrade we will execute:

$ ng update @angular/cli @angular/core

Important use cases:

  • To update to the next beta or pre-release version, use the --next=true option.

  • To update from one major version to another, use the format ng update @angular/cli@^<major_version> @angular/core@^<major_version>.

  • In case your Angular application uses @angular/material include it in the first command:

    $ ng update @angular/cli @angular/core @angular/material
Ionic/Angular applications

Just following the same procedure we can upgrade Angular applications, but we must take care of important specific Ionic dependencies:

$ ng update @angular/cli @angular/core @ionic/angular @ionic/angular-toolkit [@ionic/...]
Other dependencies

Every application will make use of different dependencies. Angular CLI ng upgrade will also take care of these ones. For example, if you need to upgrade @capacitor you will perform:

$ ng update @capacitor/cli @capacitor/core [@capacitor/...]

Another example could be that you need to upgrade @ngx-translate packages. As always in this case you will execute:

$ ng update @ngx-translate/core @ngx-translate/http-loader

16.17. Working with Angular CLI

Angular CLI provides a facade for building, testing, linting, debugging and generating code. Under the hood Angular CLI uses specific tools to achieve these tasks. The user does no need to maintain them and can rely on Angular to keep them up to date and maybe switch to other tools which come up in the future.

The Angular CLI provides a wiki with common tasks you encounter when working on applications with the Angular CLI. The Angular CLI Wiki can be found here.

In this guide we will go through the most important tasks. To go into more details, please visit the Angular CLI wiki.

16.17.1. Installing Angular CLI

Angular CLI should be added as global and local dependency. The following commands add Angular CLI as global Dependency.

yarn command

yarn global add @angular/cli

npm command

npm install -g @angular/cli

You can check a successful installtion with ng --version. This should print out the version installed.

Printing Angular CLI Version
Figure 64. Printing Angular CLI Version

16.17.2. Running a live development server

The Angular CLI can be used to start a live development server. First your application will be compiled and then the server will be started. If you change the code of a file, the server will reload the displayed page. Run your application with the following command:

ng serve -o

16.17.3. Running Unit Tests

All unit tests can be executed with the command:

ng test

To make a single run and create a code coverage file use the following command:

ng test -sr -cc
Tip
You can configure the output format for code coverage files to match your requirements in the file karma.conf.js which can be found on toplevel of your project folder. For instance, this can be useful for exporting the results to a SonarQube.

16.17.4. Linting the code quality

You can lint your files with the command

ng lint --type-check
Tip
You can adjust the linting rules in the file tslint.json which can be found on toplevel of your project folder.

16.17.5. Generating Code

Creating a new Angular CLI project

For creating a new Angular CLI project the command ng new is used.

The following command creates a new application named my-app.

ng create my-app
Creating a new feature module

A new feature module can be created via ng generate module` command.

The following command generates a new feature module named todo.

ng generate module todo
Generate a module with Angular CLI
Figure 65. Generate a module with Angular CLI
Tip
The created feature module needs to be added to the AppModule by hand. Other option would be to define a lazy route in AppRoutingModule to make this a lazy loaded module.
Creating a new component

To create components the command ng generate component can be used.

The following command will generate the component todo-details inside the components layer of todo module. It will generate a class, a html file, a css file and a test file. Also, it will register this component as declaration inside the nearest module - this ist TodoModule.

ng generate component todo/components/todo-details
Generate a component with Angular CLI
Figure 66. Generate a component with Angular CLI
Tip
If you want to export the component, you have to add the component to exports array of the module. This would be the case if you generate a component inside shared module.

16.17.6. Configuring an Angular CLI project

Inside an Angular CLI project the file .angular-cli.json can be used to configure the Angular CLI.

The following options are very important to understand.

  • The property defaults` can be used to change the default style extension. The following settings will make the Angular CLI generate .less files, when a new component is generated.

"defaults": {
  "styleExt": "less",
  "component": {}
}
  • The property apps contains all applications maintained with Angular CLI. Most of the time you will have only one.

    • assets configures all the static files, that the application needs - this can be images, fonts, json files, etc. When you add them to assets the Angular CLI will put these files to the build target and serve them while debugging. The following will put all files in /i18n to the output folder /i18n

"assets": [
  { "glob": "**/*.json", "input": "./i18n", "output": "./i18n" }
]
  • styles property contains all style files that will be globally available. The Angular CLI will create a styles bundle that goes directly into index.html with it. The following will make all styles in styles.less globally available.

"styles": [
  "styles.less"
]
  • environmentSource and environments are used to configure configuration with the Angular CLI. Inside the code always the file specified in environmentSource will be referenced. You can define different environments - eg. production, staging, etc. - which you list in enviroments. At compile time the Angular CLI will override all values in environmentSource with the values from the matching environment target. The following code will build the application for the environment staging.

ng build --environment=staging
Last updated 2019-12-11 12:58:44 UTC