18. Angular
18.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.
18.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: `

{{title}}

{{description}}

<button (click)="close.emit()">OK</button>

`
})
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: `

{{title}}

{{description}}

<button (click)="close.emit()">OK</button>

`,
})
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);
}
})
}
}
18.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.
18.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:

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:
Focus region start
Link
Initially focused
Focus region end
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
18.2. Angular Elements
18.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.
18.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 )
18.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.
18.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 install @angular/elements
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">
{{ platename }}
</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 {
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:
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.
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<EMAIL_ADDRESS>....
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.
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:
-
The main js bundle
-
The script js
-
The css
These files will be used later instead of the single js generated in the last step.
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 ourAngular Elements
project with the name of different libraries. Usingwebpack
these libraries will not be included in theAngular Element
. This is useful to lower the size of ourAngular 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:

<link rel="stylesheet" href="styles.css">

<link rel="stylesheet" href="styles.css">

In this html, the css generated in the last step is going to be imported inside the
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:
....

....
and one dynamically inside the 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.
18.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:
In order to add the component in a dynamic way, first adding a container is necessary:
app.component.html
....

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

<dish-form></dish-form>

18.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.
18.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.
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.
<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'.
...
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.
....
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).
...
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.
...
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.
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.
If we click on [Go to right module] a second level module opens, but there is no 'second-right' document.
But, typing the url directly will load 'second-right' but no 'first', even if we click on [Go back]
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).
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:
-
No component attribute.
-
No import of FirstComponent.
-
FirstModule import has to be removed from the imports array at app.module.ts.
-
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.
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'.
When we compile an app with lazy loaded modules, files containing them will be generated
And if we go to developer tools → network, we can find those modules loaded (if they are needed).
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.
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.
...
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.
Clicking on [Go to left module] triggers the load of SecondLeftModule.
18.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.
18.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.
18.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.
18.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.
npm pack
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 runnpm login
and introduce your credentials. -
run
npm publish
insidedist/"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
:

Welcome to {{ title }}!

<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">

Here is my library service being used: {{toUpper}}

<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:
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.
18.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, …
18.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.
In angular material, a palette is represented as a scss map.
|
Tip
|
Some components can be forced to use primary, accent or warn palettes using the attribute color, for example: <mat-toolbar color="primary">. |
18.5.2. Prebuilt themes
Available prebuilt themes:
-
deeppurple-amber.css
-
indigo-pink.css
-
pink-bluegrey.css
-
purple-green.css
The prebuilt themes can be added using @import.
@import<EMAIL_ADDRESS>18.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.
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.
$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:
$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.
-
mat-dark-theme: Similar to mat-light-theme but returns a theme whose foreground is basically white and background black.
For this example we are going to use mat-dark-theme and save its result in $custom-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.
...
@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.
...
@import 'styles-custom-dark.scss';
.custom-dark-theme {
@include angular-material-theme($custom-dark-theme);
}
...
<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:
@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:
@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:
// 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.
// 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.
...
@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.
import {OverlayContainer} from '@angular/cdk/overlay';
@NgModule({
// ...
})
export class AppModule {
constructor(overlayContainer: OverlayContainer) {
overlayContainer.getContainerElement().classList.add('custom-cap-theme');
}
}
18.5.4. Useful resources
18.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.
18.6.1. Assumptions
This guide assumes that you already have installed:
-
Node.js
-
npm package manager
-
Angular CLI
18.6.2. Sample Application
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 and install Angular PWA package
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.
Install Angular PWA package with ng add @angular/pwa --project <name>
. As before substitute name
with basic-ng-pwa.
The above command completes the following actions:
-
Adds the @angular/service-worker package to your project.
-
Enables service worker build support in the CLI.
-
Imports and registers the service worker in the app module.
-
Updates the
index.html
file:-
Includes a link to add the
manifest.json
file. -
Adds meta tags for theme-color.
-
Installs icon files to support the installed Progressive Web App (PWA).
-
Creates the service worker configuration file called
ngsw-config.json
, which specifies the caching behaviors and other settings.
-
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.
In another console instance run ng serve
to open the common app (not built).
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.
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.
Finally, browser extensions like Lighthouse can be used to test whether an application is progressive or not.
18.7. APP_INITIALIZER
18.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.
18.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 |
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 };
18.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.
18.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"
]
....
18.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",
....
18.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.
18.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.

Welcome to {{ title }}!

Here is the configuration version that is using angular right now: {{configService.getValues().version}}

18.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.
18.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.
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.
This leads to the following 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.

So the SharedModule
is the place for it to be defined.
18.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.
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.
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
});
}
}
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.
18.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.
18.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.
18.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.
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:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { FlightTo } from './flight-to';
@Injectable({
providedIn: 'root',
})
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.
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({
providedIn: 'root',
})
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
}
}
18.9.2. Token management
In most cases the access to backend API is secured using well konwn mechanisms as CSRF, JWT or both. In these cases the frontend application must manage the tokens that are generated when the user authenticates. More concretely it must store them to include them in every request automatically. Obviously, when user logs out these tokens must be removed from localStorage, memory, etc.
Store security token
In order to make this guide simple we are going to store the token in memory. Therefore, if we consider that we already have a login mechanism implemented we would like to store the token using a auth.service.ts
:
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private loggedIn = false;
private token: string;
constructor(public router: Router) {}
public isLogged(): boolean {
return this.loggedIn || false;
}
public setLogged(login: boolean): void {
this.loggedIn = login;
}
public getToken(): string {
return this.token;
}
public setToken(token: string): void {
this.token = token;
}
}
Using the previous service we will be able to store the token obtained in the login request using the method setToken(token)
. Please consider that, if you want a more sofisticated approach using localStorage API, you will need to modify this service accordingly.
Include token in every request
Now that the token is available in the application it is necessary to include it in every request to a protected API endpoint. Instead of modifying all the http requests in our application, Angular provides a class to intercept every request (and every response if we need to) called HttpInterceptor
. Let’s create a service called http-interceptor.service.ts
to implement the intercept
method of this class:
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { AuthService } from './auth.service';
@Injectable()
export class HttpRequestInterceptorService implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(
req: HttpRequest<any>,
next: HttpHandler,
): Observable<HttpEvent<any>> {
// Get the auth header from the service.
const authHeader: string = this.auth.getToken();
if (authHeader) {
let authReq: HttpRequest<any>;
// CSRF
if (environment.security === 'csrf') {
authReq = req.clone({
withCredentials: true,
setHeaders: { 'x-csrf-token': authHeader },
});
}
// JWT
if (environment.security === 'jwt') {
authReq = req.clone({
setHeaders: { Authorization: authHeader },
});
}
return next.handle(authReq);
} else {
return next.handle(req);
}
}
}
As you may notice, this service is making use of an environment field environment.security
to determine if we are using JWT or CSRF in order to inject the token accordingly. In your application you can combine both if necessary.
Configure environment.ts file to use the CSRF/JWT.
security: 'csrf'
The authHeader
used is obtained using the injected service AuthService
already presented above.
In order to activate the interceptor we need to provide it in our app.module.ts
or core.module.ts
depending on the application structure. Let’s assume that we are using the latter and the interceptor file is inside a security
folder:
...
import { HttpRequestInterceptorService } from './security/http-request-interceptor.service';
...
@NgModule({
imports: [...],
exports: [...],
declarations: [],
providers: [
...
{
provide: HTTP_INTERCEPTORS,
useClass: HttpRequestInterceptorService,
multi: true,
},
],
})
export class CoreModule {}
Angular automatically will now modify every request and include in the header the token if it is convenient.
18.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.
18.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.
18.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:
-
With
delay(500)
do a delay to allow some time in between requests -
With
take(5)
retry five times. -
With
concatMap
if the index thattake()
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,
....
18.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 withHttpClient
. -
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.
18.11. File Structure
18.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.
/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.
18.11.2. Feature Modules
A feature module contains the modules definition and two folders representing 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).
18.11.3. Components Layer
The component layer reflects the distinction between Smart Components and Dumb Components.
/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.
/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.
/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!
18.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.
More info at Angular i18n official documentation
18.12.1. devon4ng i18n approach
The official approach could be a bit complicated, therefore the recommended one is to use the recommended library Transloco from https://netbasal.gitbook.io/transloco/.
Install and configure Transloco
In order to include this library in your devon4ng Angular >= 7.2 project you will need to execute in a terminal:
$ ng add @ngneat/transloco
As part of the installation process you’ll be presented with questions; Once you answer them, everything you need will automatically be created for you.
-
First, Transloco creates boilerplate files for the requested translations.
-
Next, it will create a new file,
transloco-root.module.ts
which exposes an Angular’smodule
with a default configuration, and inject it into theAppModule
.
import { HttpClient } from '@angular/common/http';
import {
TRANSLOCO_LOADER,
Translation,
TranslocoLoader,
TRANSLOCO_CONFIG,
translocoConfig,
TranslocoModule
} from '@ngneat/transloco';
import { Injectable, NgModule } from '@angular/core';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
constructor(private http: HttpClient) {}
getTranslation(lang: string) {
return this.http.get<Translation>(`/assets/i18n/${lang}.json`);
}
}
@NgModule({
exports: [ TranslocoModule ],
providers: [
{
provide: TRANSLOCO_CONFIG,
useValue: translocoConfig({
availableLangs: ['en', 'es'],
defaultLang: 'en',
// Remove this option if your application doesn't support changing language in runtime.
reRenderOnLangChange: true,
prodMode: environment.production,
})
},
{ provide: TRANSLOCO_LOADER, useClass: TranslocoHttpLoader }
]
})
export class TranslocoRootModule {}
|
Note
|
As you might have noticed it also set an HttpLoader into the module’s providers. The HttpLoader is a class that implements the TranslocoLoader interface. It’s responsible for instructing Transloco how to load the translation files. It uses Angular HTTP client to fetch the files, based on the given path.
|
Usage
In order to translate any label in any HTML template you will need to use the transloco
pipe available:
{{ 'HELLO' | transloco }}
An optional parameter from the component TypeScript class could be included as follows:
{{ 'HELLO' | transloco: { value: dynamic } }}
It is possible to use with inputs
:
Attribute
Property
In order to change the language used you will need to create a button or selector that calls the this.translocoService.use(language: string)
method from TranslocoService
. For example:
export class AppComponent {
constructor(private translocoService: TranslocoService) {}
changeLanguage(lang) {
this.translocoService.setActiveLang(lang);
}
}
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}}"
}
Transloco 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
Structural Directive
Using a structural directive is the recommended approach. It’s DRY and efficient, as it creates one subscription per template:
<ng-container *transloco="let t">

{{ t('title') }}

<comp [title]="t('title')"></comp>
</ng-container>
Note that the t
function is memoized. It means that given the same key
it will return the result directly from the cache.
We can pass a params
object as the second parameter:
<ng-container *transloco="let t">

{{ t('name', { name: 'Transloco' }) }}

</ng-container>
We can instruct the directive to use a different language in our template:
<ng-container *transloco="let t; lang: 'es'">

{{ t('title') }}

</ng-container>
Pipe
The use of pipes can be possible too:
template:

{{ 'HELLO' | transloco:param }}

component:
param = {value: 'world'};
Attribute Directive
The last option available with transloco
is the attribute directive:

Service
If you need to access translations in any component or service you can do it injecting the TranslocoService
into them:
// Sync translation
translocoService.translate('HELLO', {value: 'world'});
// Async translation
translocoService.selectTranslate('HELLO', { value: 'world' }).subscribe(res => {
console.log(res);
//=> 'hello world'
});
|
Important
|
You can find a complete example at https://github.com/devonfw/devon4ng-application-template. |
Please, visit https://netbasal.gitbook.io/transloco for more info.
18.13. Routing
A basic introduction to the Angular Router can be found in Angular Docs.
This guide will show common tasks and best practices.
18.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 pattern achieves a clear seperation of concernes.
The following figure illustrates this.
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)
const routes: Routes = [
{ path: '', redirectTo: 'search', pathMatch: 'full' },
{ path: '**', component: ErrorPage404 }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
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.
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 { }
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 { }
18.13.2. Triggering Route Changes
With Angular you have two ways of triggering route changes.
-
Declarative with bindings in component HTML templates
-
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. |
18.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.
...
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:
@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:
...
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:
...
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.
...
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:
...
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [ExampleCanLoadGuard] }
];
18.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:
-
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. -
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.
18.14.1. Testing Concept
The following figure shows you an overview of the application architecture devided in testing areas.
There are three areas, which need to be covered by different testing strategies.
-
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. -
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. -
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.
18.14.2. Testing Smart Components
Testing Smart Components should assure the following.
-
Bindings are correct.
-
Selectors which load data from the store are correct.
-
Asynchronous behavior is correct (loading state, error state, "normal" state).
-
Oftentimes through testing one realizes, that important edge cases are forgotten.
-
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.
-
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.
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 ofRouterModule
-
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.
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.
18.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.
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();
});
});
18.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.
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.
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:
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.
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)');
}));
18.15. Update Angular CLI
18.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.
18.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.  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.
|
18.16.1. Basic usage
In order to perform a basic upgrade we will execute:
$ ng update @angular/cli @angular/core
Upgrade to new Angular version
The process will be the same, but first we need to make sure that our devon4ng application is in the lates version of Angular 8, so the ng update
command can perform the upgrade not only in the dependencies but also making code changes to reflect the new features and fixes.
-
First, upgrade to latest Angular 9 version:
$ ng update @angular/cli@9 @angular/core@9
Optionally the flag -C
can be added to previous command to make a commit automatically. This is also valid for the next steps.
-
Then, upgrade Angular:
$ ng update @angular/cli @angular/core
-
In case you use Angular Material:
$ ng update @angular/material
-
If the application depends on third party libraries, the new tool
ngcc
can be run to make them compatible with the new Ivy compiler. In this case it is recommended to include apostinstall
script in thepackage.json
:
{
"scripts": {
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
}
}
More information at https://angular.io/guide/updating-to-version-9 and https://angular.io/guide/ivy.
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<EMAIL_ADDRESS>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<EMAIL_ADDRESS>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
Angular Update Guide online tool
It is recommended to use the Angular Update Guide tool at https://update.angular.io/ that will provide the necessary steps to upgrade any Angular application depending on multiple criteria.
18.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.
18.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.
18.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
18.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.
|
18.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. |
18.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
|
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
|
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. |
18.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 instyles.less
globally available.
"styles": [
"styles.less"
]
-
environmentSource
andenvironments
are used to configure configuration with the Angular CLI. Inside the code always the file specified inenvironmentSource
will be referenced. You can define different environments - eg. production, staging, etc. - which you list inenviroments
. At compile time the Angular CLI will override all values inenvironmentSource
with the values from the matching environment target. The following code will build the application for the environment staging.
ng build --environment=staging