Guide - Intershop Progressive Web App 0.14

Table of Contents

Product Version

0.14

Product To Version

0.14
Status

final

1 Introduction

Info

Previous versions of this guide have been archived. To find them, activate the Archived documents (e.g., for Enfinity Suite 6) check box under the search functionality of our Knowledge Base. 

The Intershop Progressive Web App Guide is supposed to provide the information needed to work/develop with the Angular based storefront project. It should answer the main questions and explains our decisions and patterns. And it should help to get started.


2 Development Project

Before working with this project, download and install Node.js with the included npm package manager. Check the project's README.md or the system requirements of the public release Note for the right version.

2.1.1 Quick Start

For external usage of the Intershop Progressive Web App sources use the released Git repository from Artifactory as described in Reference - CI Dependencies of Intershop Commerce Management.

Once the artifact is available in your local repository, you can download the zip file containing the Git repository from there.

quick start setup
~/work/intershop
λ unzip ~/Downloads/intershop-pwa-0.1.1.zip -d intershop-pwa.git

~/work/intershop
λ git daemon --verbose --detach --export-all --base-path=. --strict-paths ./intershop-pwa.git

~/work/intershop
λ git clone git://localhost/intershop-pwa.git
Cloning into 'intershop-pwa'...
remote: Counting objects: 1298, done.
remote: Compressing objects: 100% (880/880), done.
remote: Total 1298 (delta 359), reused 1298 (delta 359)
Receiving objects: 100% (1298/1298), 1.97 MiB | 28.85 MiB/s, done.
Resolving deltas: 100% (359/359), done.

~/work/intershop
λ cd intershop-pwa

~/work/intershop/intershop-pwa
λ npm install

~/work/intershop/intershop-pwa
λ ng serve --open

The example above describes a quick start scenario in which you work directly on the contents of the Git repository released by us, still hosted on your local machine. A real project setup would not work like this as your commit objects will be rejected once you try to push them.

~/work/intershop/intershop-pwa
λ git push              
fatal: remote error: access denied or repository not exported: /intershop-pwa.git

The described scenario serves the artifact in a read-only manner, rejecting any changes. This is done by purpose as new releases of Intershop's Progressive Web App would overwrite custom commit objects, discarding any changes done on your side.

git daemon

The git daemon is used in these examples to convey the idea of serving the contents of a git repository in a read-only fashion. You do not have to use that approach. There are numerous examples on how to centrally serve a git repository. Make sure that you do not introduce new commits into the Git repository delivered by Intershop. Otherwise you will not be able to consume updates.

For Windows users: --detach is the switch that tells git daemon to start in the background leaving the command prompt accessible. If this is does not work on Windows, leave --detach out. Then you might have to open another command prompt window to complete this guide.

2.1.2 Custom Project Setup

SetupCustomProjectOverview

  1. Start with an empty Git repository served by whatever Git service you are bound to use if you want to make project-related changes.

    inital project setup
    ~/work/intershop
    λ git init custom-ish-pwa       
    Initialized empty Git repository in /home/XXX/work/intershop/custom-ish-pwa/.git/
    
    ~/work/intershop
    λ cd custom-ish-pwa         
    
    ~/work/intershop/custom-ish-pwa
    λ git remote add origin <CLONE-URL-OF-YOUR-GIT-SERVER-PROJECT>
    
    ~/work/intershop/custom-ish-pwa
    λ git fetch origin
    Fetching origin

    Now you have set up an empty project repository that can host your changes later. 

  2. Add the contents of the Intershop Progressive Web App artifact.
    The artifact is a zip file containing a Git repository released by Intershop. 

    Add artifact as 2nd upstream repository
    ~/work/intershop
    λ unzip ~/Downloads/intershop-pwa-0.1.1.zip -d intershop-pwa.git
    
    ~/work/intershop
    λ git daemon --verbose --detach --export-all --base-path=. --strict-paths ./intershop-pwa.git
    
    ~/work/intershop
    λ cd custom-ish-pwa
    
    ~/work/intershop/custom-ish-pwa
    λ git remote add intershop git://localhost/intershop-pwa.git         
    
    ~/work/intershop/custom-ish-pwa
    λ git fetch --all
    Fetching origin                                                       
    Fetching intershop
    remote: Counting objects: 1298, done.
    remote: Compressing objects: 100% (880/880), done.
    remote: Total 1298 (delta 359), reused 1298 (delta 359)
    Receiving objects: 100% (1298/1298), 1.97 MiB | 38.10 MiB/s, done.
    Resolving deltas: 100% (359/359), done.
    From git://localhost/intershop-pwa
     * [new branch]      master        -> intershop/master
     * [new tag]         RELEASE_0.1.1 -> RELEASE_0.1.1
     * [new tag]         RELEASE_0.1.0 -> RELEASE_0.1.0
    
    ~/work/intershop/custom-ish-pwa
    λ git checkout -b ish-upstream --track intershop/master
    Branch ish-upstream set up to track remote branch master from intershop.
    Switched to a new branch 'ish-upstream'

    This will introduce the project files into your folder. 

  3. Create a branch that points to your Git endpoint before you start changing things. Otherwise you cannot push your changes.

    Finally setup local development branch
    ~/work/intershop/custom-ish-pwa
    λ git checkout -b master                      
    Switched to a new branch 'master'
    
    ~/work/intershop/custom-ish-pwa
    λ git push --set-upstream origin master                         
    Counting objects: 1296, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (878/878), done.
    Writing objects: 100% (1296/1296), 1.97 MiB | 84.14 MiB/s, done.
    Total 1296 (delta 359), reused 1296 (delta 359)
    remote: Resolving deltas: 100% (359/359), done.
    To gitlab.intershop.de:XXX/betatest.git
     * [new branch]      master -> master
    Branch master set up to track remote branch master from origin.

    Now everything is there to start working on your project.

  4. Install the dependencies after cloning the Intershop Progressive Web App and the development server can be started with the following steps.

    initial project setup
    npm install
    ng serve --open
  5. Start the development server.

    Note

    ng serve requires Angular CLI to be installed globally (run npm install -g @angular/cli once), or run the project with npm start.

2.1.3 Keeping Custom Project up to Date

Intershop will release new versions of the Intershop Progressive Web App artifact that you can consume. This chapter explains what you need to do for that.

  1. Download the archive from the binary repository of your organization.
    For more information on that have a look at Reference - CI Dependencies of Intershop Commerce Management
  2. Extract the archive into a new local folder, see the approach of the section Custom Project Setup
  3. Start the Gradle Daemon to serve the repository you have just extracted. 
  4. Make that endpoint available in your custom project.

    ~/work/intershop
    λ unzip ~/Downloads/intershop-pwa-X.Y.Z.zip -d intershop-pwa.git
    
    ~/work/intershop
    λ git daemon --verbose --detach --export-all --base-path=. --strict-paths ./intershop-pwa.git
    
    ~/work/intershop
    λ cd custom-ish-pwa
    
    ~/work/intershop/custom-ish-pwa
    λ git remote add intershop git://localhost/intershop-pwa.git         
    
    ~/work/intershop/custom-ish-pwa
    λ git fetch --all
    Fetching origin 
    Fetching intershop
    remote: Counting objects: 6, done.
    remote: Compressing objects: 100% (5/5), done.
    remote: Total 6 (delta 5), reused 2 (delta 1)
    Unpacking objects: 100% (6/6), done.
    From git://localhost/intershop-pwa
       6c04a49..48e3f11  master                      -> intershop/master

    The last line shows the named commit set (6c04..48e3) that was just introduced into your local Git repository. You can checkout a (new) local branch that uses intershop/master as upstream tracking branch to verify what has been changed. If you want to consume the changes, you can merge that local branch into your master.

    This should merely be seen as an example on how Intershop plans to introduce new features (e.g., new releases of Intershop Progressive Web App) into your projects. Using git daemon to serve the repository is only one example on how to deliver the contents of Intershop's artifact. You can adapt it according to your needs.

3 Development Tools

Visual Studio Code = VS Code = VSC - https://code.visualstudio.com/

  • IDE with good TypeScript support from Microsoft

Angular CLI - https://cli.angular.io/

  • A command line interface for Angular

Browser Extensions:

4 Development Environment Configuration

The used IDE or editor should support the Prettier - Code formatter that is configured to apply a common formatting style on all TypeScript, Javascript, JSON, HTML, SCSS and other files. In addition, especially for the file types that are not handled by Prettier, the editor needs to follow the EditorConfig configuration of the project to help maintain consistent coding styles. In addition to Prettier, TSLint and Stylelint rules unify the coding style too.

With the PWA project we also supply configuration files for VSCode that suggest downloading recommended plugins and apply best-practice settings.

If your editor or IDE provides no support for the formatting and linting, make sure the rules are applied otherwise. Pre-commit hooks for git are also configured to take care of this.

Also check the project's README.md for available npm tasks that handle code style checks as well.

4.1.1 environment.local.ts

It might be helpful to use your own local environment file environment.local.ts for development purposes. Ignoring it from Git opens up possibilities for local editing without accidentally sharing it. As a result you can save the system properties of your installation, e.g.,

environment.local.properties

export const environment = {
 
production: false,

  /* INTERSHOP COMMERCE MANAGEMENT REST API CONFIGURATION */

  icmBaseURL: 'https://localhost:8444',
  icmServer: 'INTERSHOP/rest/WFS',
...

};

Use this environment by starting your server this way:

ng serve --configuration local

5 Debugging

Tips and tools for debugging Angular applications can be found on the net. As Angular runs in the Browser all the development tool functionality provided there can also be used for Angular (Debugging, Call Stacks, Profiling, Storage, Audits, ...).

Furthermore we recommend reading the following articles for specifics about Angular:

A Guide To Debugging Angular Applications

  • Use tap to log output in RxJS streams. We introduced an operator called log for easier use.
  • When inspecting an element in the browser development tools, you can then use ng.probe($0).componentInstance to get access to the Angular component.
  • Use ng.profiler.timeChangeDetection({record:true}) to profile a change detection cycle of the current page.
  • Use the json pipe in Angular to print out data on templates. Easy-to-use snippets are available with ng-debug and ng-debug-async .

Everything you need to know about debugging Angular applications

  • Provides a more in-depth view about internals.

Debug Angular apps in production without revealing source maps

  • If you also generate the source maps for production builds, you can load them in the browser development tools and use them for debugging production setups.

Debugging Angular CLI Applications in Visual Studio Code

  • You can setup VSCode for debugging.


6 Software Architecture

This section introduces some decisions made from an architectural point of view.

The Intershop Progressive Web App is a REST API based storefront client that works on top of the Intershop Commerce Management server version 7.10. This means that the communication between the Angular based storefront and Intershop Commerce Management only functions via REST. Customizations should fit into that REST based pattern as well.

system_architecture_overview

6.1 Overview

Please refer to Angular - Architecture Overview for an in-depth overview of how an Angular application is structured and composed. In short, an Angular application consists of templates, components and services. Templates contain the HTML that is rendered for the browser and displays the UI. Services implement business functionality using TypeScript. Components are small and (mostly) independent bridges between services and templates that prepare data for display in templates and collect input from the user to interact with services. Data binding links the template with methods and properties from the component.

Services should have a single responsibility by encapsulating all functionality required in it. The API to a service should be as narrow as possible because services are used throughout the application. It is also possible to combine functionality of multiple services in another more general service if necessary.

The components handling the templates should only handle view logic and should not implement too much specific functionalities. If a component does more than just providing data for the template, it might be better to transfer this to service instances instead.

Multiple components and their respective templates are then composed to pages.

6.2 Concepts

6.2.1 Mocking REST API Calls

Mocking complete REST responses is configured in environment.ts. The property "mockServerAPI" switches between mocking all calls (true) and only mocking paths that have to be mocked because they do not yet exist in the REST API. The property "mustMockPaths" is an array of regular expressions for paths that have to be mocked. Mocked data is put in the folder assets/mock-data/<path>. The path is the full path to the endpoint of the service. The JSON response is put into a file called get.json in the respective folder. Serving content dependent on query parameters is done by adding underscored values to the file name.

Mocking REST API Calls is handled by the classes ApiService and MockApiService which read all the configuration and act accordingly.

6.2.2 Change Detection

Change detection is on of the core concepts of Angular. Component templates contain data bindings that embed data from the component class into the view. The change detection cycle keeps view and data in sync. To do so, Angular re-evaluates all data expressions from the templates every time the CD runs. If the newly returned value differs from the current value, the corresponding DOM element will be updated in the view. This way, the template stays synchronized with the underlying data.

6.2.3 Zones

Change detection needs to be triggered whenever a potential change happens. Data changes are most likely invoked by async events (timeout/interval or XHR) or user events (click, input, …). Running CD whenever such an event occurs is – in most cases – enough for establishing a solid view update mechanism.

To access those events, Angular uses the concept of zones. In short words, a zone is an asynchronous execution context that keeps track of all events happening and reports their status to our program.
See also: Using Zones in Angular for better performance

Zones wrap asynchronous browser APIs, and notify a consumer when an asynchronous task has started or ended. Angular takes advantage of these APIs to get notified when any asynchronous task is done. This includes things like XHR calls, setTimeout() and pretty much all user events like click, submit, mousedown, … etc.

When async events happen in the application, the zone informs Angular which then triggers change detection.

6.2.3.1 Zone Stability

The zone tracks all ongoing async events and does also know whether there are pending tasks in the queue. If so (e.g., a running timer or XHR) it is likely that some change will happen in near future. This makes the zone unstable. Once all async tasks have been finished, the zone enters the stable status.

Note

The zone will never become stable with an endless interval running in the application.

A zone is stable when all pending async tasks have been finished.

The stability of the Angular zone can be used to make decisions in the code. ApplicationRef.isStable exposes an Observable stream which gives information about whether the zone is stable or not.

import { ApplicationRef } from '@angular/core';

@Component({ /* ... */ })
export class MyComponent {
  constructor(private appRef: ApplicationRef) {
    appRef.isStable.subscribe(stable => {
      if (stable) {
        console.log('Zone is stable');
      }
    });
  }
}

6.2.3.2 Using Zone Stability

Getting to know whether a zone is stable or not is crucial when programmatically accessing the application from the "outside". Having a stable zone means that Angular has finished rendering and that we do not expect any async tasks to finish in near future. The Intershop PWA effectively uses this concept for communication with the CMS Design View. Also, Angular waits for stability in Service Workers and in Universal Rendering (Server-Side Rendering).

6.2.3.2.1 Design View Communication

The Design View, which is part of the ICM backoffice, displays the Intershop PWA in an iframe. It analyzes the component structure of the rendered page and displays it as a tree so that users can edit the structure of the CMS components.

In order to be able to analyze the structure, the application needs to wait for the rendering to be finished – using isStable. After each router navigation, the app waits for the zone to become stable before the component tree will be analyzed. The corresponding code can be found in the SfeAdapterService class.

6.2.3.2.2 Service Workers and Universal

Both @angular/service-worker and @angular/platform-server use zone stability information internally. The Service Worker won't be attached to the page before the zone has become stable. The same applies for Server-Side Rendering: The page will be rendered as soon as the zone is stable. This is necessary because data from HTTP requests need to be resolved to render meaningful content.

6.2.3.2.3 Pitfall: The zone Needs to Become Stable

For all of those aspects – Design View, Service Workers and Universal rendering – it is essential to get a stable zone at some point. If not, those aspects will not work properly, e.g. Universal rendering will never return the rendered HTML and the Design View will never render the component tree view.

Note

Avoid long-running timers and intervals. If this is unavoidable, make sure the async tasks do not start before the zone has become stable once.

If you have any intervals in the application, wait for zone stability first before starting them.

6.3 Best Practices

6.3.1 Data Handling with Mappers

The data (server-side and client-side) have to be separated, because the data sent by the server may change over iterations or may not be in the right format, while the client side shop data handling may be stable for a long time. Therefore, each existing model served by the httpclient has to fit the mapper pattern.
First of all, the raw data (from the server) is defined by an interface (<name>.interface.ts) and mapped to a type used in the Angular application (<name>.model.ts). Both files have to be close together so they share a parent directory in src/core/models. Next to them is a <name>.mapper.ts to map the raw type to the other and back.

category.interface.ts
export interface CategoryData {
  id: string;
  name: string;
  raw: string;
}
category.model.ts
export class Category {
  id: string;
  name: string;
  transformed: number;
}
category.mapper.ts
@Injectable({ providedIn: 'root'})
export class CategoryMapper {


  fromData(categoryData: CategoryData): Category {
    const category: Category = {
      id: categoryData.id,
      name: categoryData.id,
      transformed: CategoryHelper.transform(categoryData.raw)
    }
    return category;
  }

  fromObject(category: Category): CategoryData {
    const categoryData: CategoryData = {
      id: category.id,
      name: category.id,
      raw: CategoryHelper.raw(categoryData.transformed)
    }
    return categoryData;
  }
}

A <name>.helper.ts can be introduced to provide utility functions for the model.

7 Style Guide

The general code style of the Intershop Progressive Web App follows the Angular Style Guide and coding styles of Angular CLI.
If there are differences or exceptions, it will be pointed out in this section in additional guidelines. Intershop-specific additions will be listed here as well.

7.1 File Structure Conventions

7.1.1 General Rules

  • In accordance with the Angular Style Guide and the Angular CLI convention of naming generated elements in the file system, all file and folder names should use a hyphenated, lowercase structure (dash-case or kebab-case). Camel-case should not be used, especially since it can lead to problems when working with different operating systems, where some systems are case indifferent regarding file and folder naming (Windows).

    Wrong Naming

    breadCrumb/breadCrumb.component.ts

    cacheCustom.service.ts

    registrationPage/registrationPage.module.ts

    Correct Naming

    bread-crumb/bread-crumb.component.ts

    cache-custom.service.ts

    installations/installations.component.ts

  • Components or services must never use the same names.

7.1.2 General Folder Structure

Beginning with Version 0.8 we restructured the folder layout to fit our project-specific needs. Basic concerns included defining a set of basic rules where components and other artifacts should be located to ease development, customization and bundling to achieve fast loading. Here we deviate from the general guidelines of Angular CLI, but we provide custom CLI schematics to easily add all artifacts to the project.

Additionally, the custom tslint rules project-structure and ban-specific-imports should be activated to have a feedback when adding files to the project.

The basic structure looks like this:

App folder structure
src
+--app
|  +--core
|     +--models
|     +--store
|     +--utils
|     +--...
|  +--extensions
|     +--foo
|     +--bar
|  +--pages
|  +--shared
|  +--shell
+--assets
+--environments
+--theme
  • The src/app folder contains all TypeScript code (sources and tests) and HTML templates:
    • core contains all configuration and utility code for the main B2C application.
    • shell contains the synchronously loaded application shell (header and footer).
    • pages contains  a flat folder list of page modules and components that are only used on that corresponding page.
    • shared contains code which is shared across multiple modules and pages.
    • core/models contains models for all data entities for the B2C store, see Data Handling with Mappers.
    • core/utils contains all utility functions that are used in multiple cases.
    • extensions contains extension modules for mainly B2B features that have minimal touch points with the B2C store. Each module (foo and bar) contains code which concerns only itself. The connection to the B2C store is implemented via lazy-loaded modules and components.
  • The src/assets folder contains files which are statically served (pictures, mock-data, fonts, localization, ...).
  • The src/environments folder contains environment properties which are switched by Angular CLI, also see Angular 2: Application Settings using the CLI Environment Option.
  • The src/theme contains styling files.
  • Components should only reside in shared, shell and pages following the Container-Presentation-Pattern

7.1.3 Extension Folder Structure

Each extension module can have multiple subfolders:

FolderContentNotes

pages

page modules and components used on this page

shared

components shared among pages for this extension

models

models specific for this extension

services

services specific for this extension

store

ngrx handlingState, Effects, Reducer, Actions, Selectors

Optionally additional subfolders for module-scoped artifacts are allowed:

  • interceptors
  • directives
  • guards
  • validators
  • configurations
  • pipes

7.2 Modules

As Angular Modules are a rather advanced topic, beginning with the restructured project folder format, we want to give certain guidelines for which modules exist and where components are declared. The Angular Modules are mainly used to feed the Angular Dependency Injection and with that Component Factories that populate the Templates. It has little to do with the Bundling of lazy-loaded modules when a production-ready Ahead-of-Time build is executed.

As a general rule of thumb, modules should mainly aggregate deeper lying artifacts. Only some exceptions are allowed.

7.2.1 Extending Modules

As a developer extending and customizing the functionalities of the PWA, you should only consider modifying/adding to the following Modules:

  • src/app/pages/app.routing.module - for registering new globally available routes
  • src/app/core/core.module - for registering core functionality (if the third party library documentation asks to add a SomeModule.forRoot(), this is the place)
  • src/app/shell/shell.module - for declaring and exporting components that should be available on the Application Shell and also on the remaining parts of the application, do not overuse
  • src/app/shared/shared.module - for declaring and exporting components that are used on more than one page, but not in the application shell
  • src/app/pages/<name>/<name>-page.module - for declaring components that are used only on this page

As a developer developing new functionalities for the PWA, you also have to deal with the following modules:

  • src/app/core/X.module - configuration for the main application organized in various modules
  • src/app/utils/<util>.module - utility modules like CMS which is supplying shared components and uses shared.module
  • src/app/shared/<name>/<name>.module - utility modules aggregating functionality exported with shared.module
  • src/app/core/store/**/<name>-store.module - ngrx specific modules which should only be extended when adding B2C functionality, current stores should not be extended, it is better to add additional store modules for custom functionalities.

As a developer adding new extensions:

  • src/app/extensions/<name>/<name>.module - aggregated collection of components used for this extension, including the ngrx store
  • src/app/extensions/<name>/exports/<name>-exports.module - aggregation of lazy components which lazily load the extension module

When using ng generate with our PWA custom schematics, the components should automatically be declared in the right modules.

7.3 Components

7.3.1 Follow the Container-Presentation-Pattern

Wherever components are added to the project they should have the following pattern:

src/app/shared/
        +--product/
           +--components/
              +--product-add-to-basket/
                 +--product-add-to-basket.component.ts
                 +--product-add-to-basket.component.html
           +--containers/
              +--product-add-to-basket/
                 +--product-add-to-basket.container.ts
                 +--product-add-to-basket.container.html

The specific path should contain a general folder (in this case 'product'), which can be named feature or data driven (depending on the scope of that component). After that, follow the pattern of folders for containers (smart components) and components (presentation components). The container handles how the data is coming to that pair of components and the presentation component is only concerned with the display. Specifically the smart component communicates with the ngrx store and does not have any presentation functionality in its template (styling classes, ...). The presentation component gets the data as input and further only displays this data.

7.3.2 Declare Components in the right NgModule

Angular requires you to declare a Component in one and only one NgModule. Find the right one in the following order:

Your Component is used only on one page? - Add it to the declarations of the corresponding page.module.

Your Component is used among multiple pages? - Declare it in the shared.module and also export it there.

Your Component is used in the Application Shell (and maybe again on certain pages)? - Declare it in the shell.module and also export it there.

(advanced) Your Component relates to a specific B2B extension? - Declare it in that extension module and add it as an entryComponent, add a lazy-loaded component and add that to the extension exports, which are then im-/exported in the shared.module.

Again, when using ng generate, the right module should be found automatically.

7.3.3 Delegate Complex Component Logic to Services

Style 05-15: Do limit logic in a component to only that required for the view. All other logic should be delegated to services.
Do move reusable logic to services and keep components simple and focused on their intended purpose.

There should not be any string or URL manipulation, routing mapping or REST endpoint string handling within components. This is supposed to be handled by methods of services

7.3.4 Provide Components Data via Input and Output - Similar to ISML Modules

The average component is supposed to receive its data via defined inputs and according service calls. The components are considered relatively "dumb" and mainly concerned with the view creation. They should not somehow get the data from somewhere themselves, e.g., ask the router for relevant information.

7.3.5 Put as Little Logic Into constructor as Possible - Use ngOnInit

Yet the common practice is put as little logic into constructor as possible. ... It’s a common practice to use ngOnInit to perform initialization logic even if this logic doesn’t depend on DI, DOM or input bindings.

Basically the constructor function should only be used for the dependency injection. Everything else is handled in the ngOnInit.

For a more detailed explanation see The essential difference between Constructor and ngOnInit in Angular. For a shorter explanation for the difference between the two, refer to Angular constructor versus ngOnInit.

7.3.6 Use Property Binding to Bind Dynamic Values to Attributes or Properties

See Explanation of the difference between an HTML attribute and a DOM property.

There are often two ways to bind values dynamically to attributes or properties: interpolation or property binding.
Intershop uses data binding since this covers more cases in the same way. So the code will be more consistent.

There is an exception for direct string value bindings where we use for example routerLink="/logout" instead of [routerLink]="'/logout'".

Pattern to avoid

<div attr.data-testing-id="category-{{category.id}}">

<img src="{{base_url + category.images[0].effectiveUrl}}">

Correct pattern

<div [attr.data-testing-id]="'category-' + category.id">

<img [src]="base_url + category.images[0].effectiveUrl">

7.3.7 Pattern for Conditions (ngif) with Alternative Template (else) in Component Templates

Also for reasons of consistency we want to establish the following pattern for conditions in component templates:

Condition in template

<ng-container *ngIf="show; else elseBlock">
 ... (template code for if-branch)
</ng-container>

<ng-template #elseBlock>
  ... (template code for else-branch)
</ng-template>

This pattern provides the needed flexibility if used together with Handling Observables with NgIf and the Async Pipe.
In this case the condition should look like this:

<ng-container *ngIf="(user$ | async) as user; else loading">

Please do not use other working patterns like

pattern that should be avoided

<ng-template [ngIf]="isLoggedIn" [ngIfElse]="notLoggedIn">
  ...
</ng-template>
<ng-template #notLoggedIn>
  ...
</ng-template>


7.3.8 Do Not Unsubscribe, Use Destroy Observable and takeUntil Instead

Following the ideas of this article: RxJS: Don’t Unsubscribe, the following pattern is used for ending subscriptions to Observables that are not handled via async pipe in the templates.

'unsubscribe' via destroy obsevable

export class RegistrationFormComponent implements OnInit, OnDestroy {

  ...

  destroy$ = new Subject();

  ...

  ngOnInit() {

    ...

    // build and register new address form when country code changed

    this.form

      .get('countryCodeSwitch')

      .valueChanges.pipe(takeUntil(this.destroy$))

      .subscribe(countryCodeSwitch => this.handleCountryChange(countryCodeSwitch));

  }

  ...

  ngOnDestroy() {

    this.destroy$.next();

  }

}

Please do not use the classic way of unsubscribing in the ngOnDestroy lifecycle hook.

Unsubscribe pattern that should be avoided

export class RegistrationFormComponent implements OnInit, OnDestroy {

  ...

  formCountrySwitchSubscription: Subscription;

  ...

  ngOnInit() {

    ...

    // build and register new address form when country code changed

    this.formCountrySwitchSubscription = this.form

      .get('countryCodeSwitch')

      .valueChanges.subscribe(countryCodeSwitch => this.handleCountryChange(countryCodeSwitch));

  }

  ...

  ngOnDestroy() {

    this.formCountrySwitchSubscription.unsubscribe();

  }

}

8 Localization

Intershop Progressive Web App uses a mix of Angular's internationalization tools (i18n) and the internationalization library ngx-translate for localization.

For more information refer to:

8.1 Usage Examples

Although ngx-translate provides pipe and directive to localize texts, we want to use a pipe-only approach.

8.1.1 Localization of Simple Text

To localize simple texts, just apply the translate pipe to the key:

en.json
{ ...
  "header.wishlists.text": "Wishlist",
  ...
}
*.component.html
<span class="hidden-xs">{{ 'header.wishlists.text' | translate }}</span>

8.1.2 Localization with Parameters

ngx-translate uses named parameters. A map of parameters can be supplied as input to the translate pipe.

Localization file:

en.json
{ ...
  "product.items.label": "{{0}} list items"
  ...
}

Parameter setting in HTML:

*.component.html
<div>{{ 'product.items.label' | translate:{'0': '8'} }}</div>

Parameter setting in component and usage in HTML:

*component.ts
export class Component {
  param = '8';
  ...
*.component.html
<div>{{ 'product.items.label' | translate:{'0': param} }}</div>

8.1.3 Localization with Pluralization

For more information refer to:

Localization file:

en.json
{ ...
  "product.items.label": {
    "=0":"0 items",
    "=1": "1 item", 
    "other": "# items"},
  ...
}

Parameter setting in HTML:

*.component.html
<div>{{ 8 | i18nPlural: ( 'product.items.label' | translate ) }}</div>

Parameter setting in component and usage in HTML:

*component.ts
export class Component {
  products = ['product1','product2','product3'];
  ...
*.component.html
<div>
	{{ products.length | i18nPlural: {'=0': 'product.items.label.none','=1': 'product.items.label.singular','other': 'product.items.label.plural'} | translate:{'0': products.length} }}
</div>

8.1.4 Localization of Text with HTML Tags

To skip the cleansing of the translated text (i.e., to insert HTML), the innerHTML binding has to be used.

Localization file:

en.json
{ ...
  "common.header.contact_no.text": "<small>1300</small>  032 032",
  ...
}

Usage in HTML:

*.component.html
<span [innerHTML]="'common.header.contact_no.text' | translate"></span>

8.1.5 Localization in the component(.ts) File

If you want to get the translation for a key within a component file, you have to:

  • Inject TranslationService in the component
  • Use the get method of the translation service, e.g., translate.get('ID')
  • Use subscribe to assign the translation to the data array

*.component.ts
export class ProductTileComponent implements OnDestroy {
  ...
  destroy$ = new Subject();
  constructor(protected translate: TranslateService) {}
  ...
  toggleCompare() {
    this.compareToggle.emit();
    this.translate
      .get('compare.message.add.text', { 0: this.product.name })
      .pipe(takeUntil(this.destroy$))
      .subscribe((message: string) => {
        this.toastr.success(message);
      });
  }
  ... 
  ngOnDestroy() {
    this.destroy$.next();
  }
}

See also: https://github.com/ngx-translate/core/issues/835

8.2 Localization Files Generation

The idea is to use the existing localization properties files of the current Responsive Starter Store cartridges (or the localization files of a project) and convert them into the proper JSON files that can be used by ngx-translate. For this purpose a Gradle plugin was implemented that can handle this conversion process.

Plugin source as zip file: ngx-translate-plugin-master.zip

In the current state of the Intershop Progressive Web App the converted localization properties from a_responsive (without app_sf_responsive_b2b and app_sf_responsive_costcenter) were added and should be used within the HTML templates.

9 State Management

This section describes how ngrx is integrated into the Intershop Progressive Web App for the application wide state management.

9.1 ngrx Architecture

ngrx-store-effects

ngrx is a Framework for handling state information in Angular applications following the Redux pattern. It consist of a few basic parts:

9.1.1 State

The State is seen as the single source of truth for getting information of the current application state. All information should be held in the store. There is only one immutable state per application, which is composed of sub-states. To get information out of the state, selectors have to be used. Changing the state can only be done by dispatching actions.

9.1.2 Selectors

Selectors are used to retrieve information about the current state from the store. The selectors are grouped in a separate file. They always start the query from the root of the state tree and navigate to the required information. Selectors return Observables which can be held in containers and be bound to in templates.

9.1.3 Actions

Actions are used to alter the current state via reducers or trigger effects. Action creators are held in a separate file. The action class contains a type of the action and an optional payload. To alter the state synchronously, reducers have to be composed. To alter the state asynchronously, effects are used.

9.1.4 Reducers

Reducers alter the state synchronously. They take the previous state and an incoming action to compose a new state. This state is then published and all listening components react automatically to the new state information. Reducers should be simple operations which are easily testable.

9.1.5 Effects

Effects use incoming actions to trigger asynchronous tasks like querying REST resources. After successful or erroneous completion an effect might trigger another action as a way to alter the current state of the application.

9.2 File Structure and Naming

After trying out various file structures and naming patterns we decided the following:

9.2.1 File Structure

[src/app/foobar]
         \[store]
            \[foo]
               \_foo.actions.ts
               \_foo.effects.ts
               \_foo.reducer.ts
               \_foo.selectors.ts
               \_index.ts
            \[bar]
               \_...
            \_foobar.state.ts
            \_foobar.system.ts

An application module named foobar with sub-states named foo and bar serves as an example. The files handling ngrx store should then be contained in the folder src/app/foobar/store. Each sub-state should aggregate its store components in separate subfolders correspondingly named foo and bar:

  • foo.actions.ts: This file contains all action creators for the foo state. Additionally, a bundle type aggregating all action creators and an enum type with all action types is contained here.

  • foo.effects.ts: This file defines an effect class with all its containing effect implementations for the FooState.

  • foo.reducer.ts: This file exports a reducer function which modifies the state of foo. Additionally, the FooState and its initialState is contained here.

  • foo.selectors.ts: This file exports all selectors working on the state of foo.

  • index.ts: This file exports the public API for the state of the foo sub-state. In here all specific selectors and actions are exported.

Furthermore, the state of foobar is aggregated in two files:

  • foobar.state.ts: Contains the FooBarState as an aggregate of the foo and bar states.
  • foobar.system.ts: Contains aggregations for fooBarReducers and fooBarEffects of the corresponding sub-states to be used in modules and TestBed declarations.

9.2.2 Naming

Related to the example in the previous paragraph we want to establish a particular naming scheme.

9.2.2.1 Action Types

Action types should be aggregated in an enum type. The enum should be composed of the sub-state name and 'ActionTypes'. The key of the type should be written in PascalCase. The string value of the type should contain the feature in brackets and a readable action description. The description should give hints about the dispatcher of the said action, i.e., actions dispatched due to a HTTP service response should have 'API' in their name, actions dispatched by other actions should have 'Internal' in their description.

export enum FooActionTypes {
  LoadFoo = '[Foo Internal] Load Foo',
  InsertFoo = '[Foo] Insert Foo',
  LoadFooSuccess = '[Foo API] Load Foo Success',
  ...
}

9.2.2.2 Action Creators

The action creator is a class with an optional payload member. Its PascalCase name should correspond to an action type. The name should not contain 'Action' as the action is always dispatched via the store and it is therefor implicitly correctly named.

export class LoadFoo implements Action {
  readonly type = FooActionTypes.LoadFoo;
  constructor(public payload: string) { }
}

9.2.2.3 Action Bundle

The file actions.ts should also contain an action bundle type with the name of the sub-state + 'Action', which is to be used in the reducer and tests.

export type FooAction = LoadFoo | SaveFoo | ...

9.2.2.4 Reducer

The exported function for the reducer should be named like the sub-state + 'Reducer' in camelCase.

export function fooReducer(state = initialState, action: FooAction): FooState {

9.2.2.5 State

State interfaces should have the state name followed by 'State' in PascalCase.

export interface FooState {
...

9.2.2.6 Selectors

Selectors should always be camelCase and start with 'get'.

export const getSelectedFoo = createSelector( ...


9.3 Smart Containers and Dumb Components

See Dan Abramovs article on Presentational and Container Components.

We want to establish a pattern where markup is only allowed in dumb Components and ngrx usage is only allowed in Containers. We added a tslint rule to enforce this.

This concept also encourages us to use ChangeDetectionStrategy.OnPush whenever possible. Another tslint rule advises you to declare OnPush if possible. If you are using forms or change values of the component, you might run into issues with OnPush change detection. Explicitly declare the default strategy instead.

9.4 Entity State Adapter for Managing Record Collections: @ngrx/entity

@ngrx/entity provides an API to manipulate and query entity collections.

  • Reduces boilerplate for creating reducers that manage a collection of models.
  • Provides performant CRUD operations for managing entity collections.
  • Extensible type-safe adapters for selecting entity information.

See: https://github.com/ngrx/platform/tree/master/docs/entity

9.5 Using async Pipe in Templates

see: https://toddmotto.com/angular-ngif-async-pipe

9.6 Normalized State

It is important to have a normalized state when working with ngrx. To give an example, only the product's state should save products. Every other slice of the state that also uses products must only save identifiers (in this case SKUs) for products. In selectors the data can be linked into views to be easily usable by components.

see: https://medium.com/@timdeschryver/ngrx-normalizing-state-d3960a86a3aa

9.7 ngrx Pitfalls

9.7.1 Using Services and catchError

When interacting with Services in an Effect and handling Errors, a specific pattern should be applied. This can be seen when testing the Effect with marbles.

when(service.method()).thenReturn(_throw(ERROR))

hot('-a-a-a', { a: new SomeTriggerAction() })


wrong

@Effect()
effect = this.actions$.pipe(
  switchMap(this.service.method().pipe(
    map(x => new Action(x))
  ),
  catchError(err => of(new ActionFail(err)))
)

produces: cold('-(b|)----', { b: new ActionFail() })

So the error is caught once and mapped, but the original Effect is completed and dies.

right

@Effect()
effect = this.actions$.pipe(
  switchMap(this.service.method().pipe(
    map(x => new Action(x)),
    catchError(err => of(new ActionFail(err)))
  )
)

produces: cold('-b-b-b', { b: new ActionFail() })

See also: Handling Errors in NgRx Effects

9.7.2 Unsafe switchMap

See: Avoiding switchMap-Related Bugs

10 Forms

10.1 File and Naming Conventions

10.1.1 Reusable Form Components

  • File location: forms\<object>\components\<form-name>
  • Name: <form-name>-form.component.ts
  • These forms can be used as (sub)forms on arbitrary pages, e.g., there are address forms on registration page, checkout and my account pages. The search form is placed in the page header and in the search page.

10.1.2 Page Specific Form Components

  • File location: app\<module>\<components>\<form-name>
  • Name: <form-name>-form.component.ts
  • These forms are only valid for a specific page. They are not reusable.
  • Example: The credentials form on the registration page.

10.1.3 Data Models

  • File location for models and related classes: app\models\<object>
  • Model name: <object>.model.ts
  • Mapper file name: <object>.mapper.ts
  • Data (interface) file name: <object>.interface.ts

10.1.4 Services

  • File location for global services: core\services\<object>
  • File location for module specific services: <module>\services\<object>
  • Name: <object>.service.ts

Usually, there should be no form specific data models. If forms are related to persistent data, use/create generic data models for your forms, e.g., there should be only one data model for addresses. Each model has its own service class(es). In this class there are methods concerning the data model, e.g., updateAddress (address: Address)

10.2 Form Behavior

  • Labels of required form controls have to be marked with a red asterisk.
  • After a form control is validated:
    • Its label gets green and a checked icon is displayed at the end of the control in case the input value is valid.
    • Its label gets red, an error icon is displayed at the end of the control and an error message is displayed below the control in case the input value is invalid.
  • Form validation
    • If a form is shown, there should not be any validation error messages.
    • If a user starts to enter data in an input field, this field will be validated immediately.
    • If the user presses the submit button, all form controls of the form are validated; the submit button gets disabled in case there is an error until all form errors are handled by the user.

10.3 General Rules

10.3.1 Usage of Template Driven Forms vs Reactive Forms

In general, you should use reactive forms for creating your forms. If a form is very simple (only a few form input fields without any special validation rules), it is also possible to use template driven forms.

10.3.2 Validators

For the validation of the form input fields you can use Angular's Build-in Validators.

Additionally, the package ng2-validation is available. It provides further validators.

If there is a need for special custom validators, use class app/shared/validators/special-validators to write your own customs validators.

10.3.3 Keep Templates and Type Script Code Simple

Whenever possible, move logic from the template to the type script.

Use predefined form control components and directives to get general functionality like displaying control validation status, validation error messages and so on, see the following section.

10.4 How to Build a Form?

10.4.1 Build a Form

  • Build a container component (page) which is responsible for getting and sending data using a service.

  • Build a form component which holds the form.
  • Use either predefined form control components (see below) to build your form or ish-form-control-feedback component to display error messages and the validation icons and ishShowFormFeedback directive on the form-group elements in order to color labels and controls according to their validation status.
  • In both cases the parameter errorMessages should be a key value pair of a possible validator that causes the error and its localisation key/localized string, e.g.:
    {'required':'account.login.email.error.required' ,'email':'account.login.email.error.invalid'}
  • Take care of disabling the form submit button in case the user submits an invalid form (see example below).

10.4.2 Example

login-form.html
<form name="LoginUserForm" [formGroup]="loginForm" class="form-horizontal bv-form" (ngSubmit)="onSignin()"> 
  
<ish-input 
  [ form ]= "loginForm" 
  [ controlName ]= "'userName'" 
  [ type ]= "'email'" 
  [ label ]= "'Email'" 
  [ labelClass ]= "'col-sm-3'" 
  [ inputClass ]= "'col-sm-6'" 
  [ markRequiredLabel ]= "'off'" 
  [ errorMessages ]= "{'required':'account.login.email.error.required' ,'email':'account.login.email.error.invalid'}" 
   ></ish-input>
  <ish-input 
  [ form ]= "loginForm" 
  [ controlName ]= "'password'" 
  [ type ]= "'password'" 
  [ label ]= "'Password'" 
  [ labelClass ]= "'col-sm-3'" 
  [ inputClass ]= "'col-sm-6'" 
  [ markRequiredLabel ]= "'off'" 
  [ errorMessages ]= "{'required':'account.login.password.error.required' ,'pattern':'account.login.password.error.invalid'}" 
   ></ish-input>
<button   type = "submit"   value = "Login"   name = "login"   class = "btn btn-primary"  [ disabled ]= "formDisabled" > {{'account.signin.button.label' | translate}} </button>
</form>


login-form.ts
import  { FormBuilder, FormGroup, Validators }  from   '@angular/forms' ;
import  { CustomValidators }  from   'ng2-validation' ;
import { FormUtilsService } from '../../../../core/services/utils/form-utils.service';
...
 
constructor  (
  private  formBuilder :  FormBuilder, 
  private formUtils: FormUtilsService ) {}
...
 


ngOnInit() {
   this .loginForm  =   this .formBuilder.group({ 
       userName: [ '' , [Validators.required, CustomValidators.email]], 
       password: [ '' , Validators.required] 
    });
}
 
onSignin(userCredentials) { 
 if (this.form.invalid) {
   this.submitted = true;
  this.formUtils.markAsDirtyRecursive(this.form);
  return;
 }
this.create.emit(this.form.value);

}


cancelForm() {
   this.cancel.emit();
 }

10.5 Predefined Components / Directives / Services

Component/DirectiveExampleExample CodeDescription

ishShowFormFeedback

(Directive)


<div 
class="form-group has-feedback"
[formGroup]="form"
[ishShowFormFeedback]="formControl">
   ...
</div>

Directive ishShowFormFeedback

  • Can be used to color labels and form controls in dependence of the validation status of the related form control
  • Should be used at the form-group element

ish-input

(Component)

<ish-input
[form]="loginForm"
[controlName]="'userName'"
[type]="'email'"
[label]="'account.login.email.label'"
[labelClass]="'col-sm-3'"
[inputClass]="'col-sm-6'"
[markRequiredLabel]="'off'"
[errorMessages]="
{'required':'account.login.email.error.required' ,
'email':'account.login.email.error.invalid'}"
></ish-input>

Input form control for

  • Text input fields
  • Email input fields
  • Password input fields
  • Number input fields

ish-select

(Component)

<ish-select
[controlName]="'securityQuestion'"
[form]="credentialsForm"
[label]="'Security Question'"
[options]="securityQuestionOptions"
[showEmptyOption]="'true'"
[errorMessages]="
{'required':'account.login.email.error.required'}"
></ish-select>

Select form control

'options' should implement interface

SelectOption

ish-form-control-feedback

(Component)

<ish-form-control-feedback 
[messages]="errorMessages"
[control]="formControl">
</ish-form-control-feedback>

form control feedback component

  • To display error messages
  • To display validation status icon of the related form control

Note

Use this component only if you cannot use one of the components above



form-utils

(Service)

Methods:

  • markAsDirtyRecursive(formGroup: FormGroup)

  • updateValidatorsByDataLength(
    control: AbstractControl,
    array: any[],
    validators: ValidatorFn | ValidatorFn[] = Validators.required,
    async = false
    )

  • Service for form related tasks




10.6 Error Handling of Server Side Error Messages

Server side errors should be saved in the store. Please make sure they are also removed if they are obsolete.

Errors should be read by the container components and passed to the related form components. The error display should be handled there.

registration-page.container.ts
userCreateError$: Observable<HttpErrorResponse>;


ngOnInit() {
 ...
 this.userCreateError$ = this.store.pipe(select(getUserError));
 }
registration-page.container.html
<ish-registration-form
...
 [error]="userCreateError$ | async"
</ish-registration-form>
registration-form.components.ts
@Input() error: HttpErrorResponse;
registration-form.components.html
<div *ngIf="error as error" role="alert" class="alert alert-danger">
 <span>{{error.headers.get('error-key') | translate}}</span>
</div>

10.7 The Address Form as an Example of a Reusable Form

If you want to embed a reusable form onto your page or if you want to combine several forms into one big form like the registration form, you always have to use reactive forms.

10.7.1 How to Use the address-form Component

The following steps describe how to use the address-form component on your form (see also the example below):

Container component:

  1. Get all necessary data (countries, regions, titles etc.) and pass it to the form component.
  2. React on country changes by getting country specific data like regions and titles.

Form component:

  1. Place the address-form component on the html part of your form component.
  2. onInit: Add a (sub) formGroup for your address to your form using the getFactory method of the AddressFormService.
  3. Implement the onCountryChange behavior to switch the address formGroup according to the country specific form controls and emit this to the container.
  4. React on region changes: Update validator for "state" control according to regions.

10.7.2 How to Create a New Country Specific Form

  1. Create a new country-specific address-form component under shared/forms/address-forms/forms/address-form-<countrycode>.
  2. Create the related factory class under shared/forms/address-forms/forms/address-form-<countrycode>.
  3. Add your new component in shared/forms/address-forms/forms/address-form.html under the ngSwitch statement.
  4. Register your new component in shared/forms/address-forms/forms/index.ts under components and factoryProviders.

11 Styling & Behavior

The visual design (styling) and the interaction design (behavior) of the Intershop Progressive Web App is derived from the Responsive Starter Store with some changes (e.g., the header) to improve and modernize the customer experience and to provide an easy optical distinction between the two Intershop storefronts. While the Responsive Starter Store is based on a customized/themed Bootstrap 3, the Intershop Progressive Web App styling was migrated to build upon the current version of Bootstrap 4. This also means that the Intershop Progressive Web App styling is now based on Sass.

11.1 Bootstrap Integration

The styling integration is configured in the \src\styles.scss of the project where Bootstrap together with the customizations is configured.

Instead of the Bootstrap 3 Glyphicons the current styling uses free solid icons of Font Awesome.

The styling itself is integrated into the project as global style via the styles.scss that is referenced in the angular.json and is compiled automatically. Throughout the whole Intershop Progressive Web App, there are almost no component specific styleUrls or styles properties.

The Javascript part of Bootstrap for the behavior is not directly used from the Bootstrap dependency since this implementation is jQuery based and not really suited to work in an Angular environment. For Bootstrap 4, ng-bootstrap provides Bootstrap widgets the angular way. Using these components works best with the styling taken from the Responsive Starter Store. However, the generation and structure of the HTML for the Angular Bootstrap differs from the HTML working with the original jQuery based bootstrap.js. Adaptions and changes in this area are inevitable.

11.2 Assets

The assets folder is the place for any static resources like fonts, images, colors, etc., that are used by the storefront styling.

Currently the default font families for the Intershop Progressive Web App Roboto and Roboto Condensed are placed here as copies from the Responsive Starter Store. They are not defined as npm dependency since the rendering results and the referenced file sizes of the npm packaged Roboto fonts were not the same as with the font files from the Responsive Starter Store.

12 Code Documentation

For our Intershop Progressive Web App, Compodoc is used as documentation package. For further information refer to Compodoc.

For documentation, the tsconfig.app.json file is used as configuration file. The output folder for the documentation is set to <project-home>\docs\compodoc.

We use an own styling theme based on the theme 'readthedocs' provided by Compodoc. The style.css file of the theme can be found in <project-home>\docs\theme.

Examples for the comment styling pattern can be found here: TypeDoc - DocComments.

12.1 Usage

12.1.1 Generate Code Documentation

Generate Code Documentation
npm run docs

The generated documentation can be called by <project-home>\docs\compodoc\index.html.

12.1.2 Serve Generated Documentation with Compodoc

Serve Generated Documentation with Compodoc
npm run docs:serve 

Documentation is generated at <project-home>\docs\compodoc (output folder). The local HTTP server is launched at http://localhost:8080.

12.1.3 Watch Source Files After Serve and Force Documentation Rebuild

Watch Source Files After Serve and Force Documentation Rebuild
npm run docs:watch

12.2 Comments

12.2.1 General Information

The JSDoc comment format is used for general information.

Use this format to describe components, modules, etc., but also methods, inputs, variables and so on.

Example for General Description
/**
 * The Product Images Component
 */

12.2.2 JSDoc Tags

Currently Compodoc supports the following JSDoc tags :

  • @returns
  • @param
  • @link
  • @example

Example for parameter and return values:

Example for parameter and return values
/**
 * REST API - Get full product data
 * @param productSku  The product SKU for the product of interest.
 * @returns           Product information.
 */
getProduct(productSku: string): Observable<Product> {
 ...
}

Example for links and implementation examples:

Example for Links and Implementation Examples
/**
 * The Product Images Component displays carousel slides for all images of the product and a thumbnails list as carousel indicator.
 * It uses the {@link ProductImageComponent} for the rendering of product images.
 *
 * @example
 * <ish-product-images [product]="product"></ish-product-images>
 */

Indentation Warning

TypeScript has an internal margin for new lines. If you want to keep a level of indentation, put a minimum of 13 space characters as shown in the example:

Example with Indentation Keeping
/** 
 * @example
 * <div class="form-group has-feedback" [formGroup]="form" [ishShowFormFeedback]="formControl">
 *               <input
 *                 [type]="type"
 *                 class="form-control">
 *               <ish-form-control-feedback [messages]="errorMessages" [control]="formControl"></ish-form-control-feedback>
 * </div>
 */

New lines are created inside a comment with a blank line between two lines:

Comments with New Lines
/**
 * First line
 *
 * Second line
 */

/**
 * First line
 * Behind first line, produces only one line
 */ 

12.3 Documentation Rules

12.3.1 General Documentation Rules

  • Use speaking names, especially for methods, effects and actions.
  • We do not need additional documentation if the name describes the behavior.
  • Use comments for documentation in all cases where the name alone cannot describe everything.

12.3.2 Component Documentation

To document a component of the Intershop Progressive Web App:

  • A description of the general behavior of the component exists.
  • All input parameters are described.
  • All output parameters are described.
  • An example of how the component can be used in templates is shown.
  • Relevant components used by the component could be linked in the documentation.

Example for the Documentation of a Component
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Product } from '../../../../models/product/product.model';

/**
 * The Product Images Component displays carousel slides for all images of the product and a thumbnails list as carousel indicator.
 * It uses the {@link ProductImageComponent} for the rendering of product images.
 *
 * @example
 * <ish-product-images [product]="product"></ish-product-images>
 */
@Component({
  selector: 'ish-product-images',
  templateUrl: './product-images.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class ProductImagesComponent {

  /**
   * The product for which the images should be displayed.
   */
  @Input() product: Product;

  activeSlide = 0;

  getImageViewIDsExcludePrimary = ProductHelper.getImageViewIDsExcludePrimary;

  /**
   * Set the active slide via index (used by the thumbnail indicator).
   * @param slideIndex  The slide index number to set the active slide.
   */
  setActiveSlide(slideIndex: number) {
    this.activeSlide = slideIndex;
  }

  /**
   * Check if the given slide index equals the active slide.
   * @param slideIndex  The slide index number to be checked if it is the active slide.
   * @returns           True if the given slide index is the active slide, false otherwise.
   */
  isActiveSlide(slideIndex: number): boolean {
    return this.activeSlide === slideIndex;
  }

}

12.3.3 Service Documentation

To document a service of the Intershop Progressive Web App:

  • A description of the service exists.
  • The methods of the service with the input and output parameters are described.

Example for the Documentation of a Service
import { ApiService } from '../../../core/services/api.service';
import { Product } from '../../../models/product/product.model';
...

/**
 * The Products Service handles the interaction with the 'products' REST API.
 */
@Injectable()
export class ProductsService {

  /**
   * The REST API URI endpoints.
   */
  productsServiceIdentifier = 'products/';
  categoriesServiceIdentifier = 'categories/';

  constructor(
    private apiService: ApiService
  ) { }

  /**
   * Get the full Product data for the given Product SKU.
   * @param sku  The Product SKU for the product of interest.
   * @returns    The Product data.
   */
  getProduct(sku: string): Observable<Product> {
    if (!sku) {
      return ErrorObservable.create('getProduct() called without a sku');
    }
    const params: HttpParams = new HttpParams().set('allImages', 'true');
    return this.apiService.get<ProductData>(this.productsServiceIdentifier + sku, params, null, false, false).pipe(
      map(productData => ProductMapper.fromData(productData))
    );
  }

}

12.3.4 Directive Documentation

To document a directive of the Intershop Progressive Web App:

  • A description of the general behavior of the directive exists.
  • All input properties are described.
  • An example how the directive can be used in templates is shown.

Example for the Documentation of a Directive
import { Directive, HostBinding, Input } from '@angular/core';
import { AbstractControl } from '@angular/forms';

/**
 * An attribute directive that adds CSS classes to a dirty host element related to the validity of a FormControl.
 *
 * @example
 * <div class="form-group has-feedback" [formGroup]="form" [ishShowFormFeedback]="formControl">
 *               <input
 *                 [type]="type"
 *                 class="form-control"
 *                 [formControlName]="controlName">
 * </div>
 */
@Directive({
  selector: '[ishShowFormFeedback]'
})

export class ShowFormFeedbackDirective {

  /**
   * FormControl which validation status is considered.
   */
  // tslint:disable-next-line:no-input-rename
  @Input('ishShowFormFeedback') control: AbstractControl;

  /**
   * If form control is invalid and dirty 'has-error' class is added.
   */
  @HostBinding('class.has-error') get hasErrors() {
    return this.control.invalid && this.control.dirty;
  }

  /**
   * If form control is valid and dirty 'has-success' class is added.
   */
  @HostBinding('class.has-success') get hasSuccess() {
    return this.control.valid && this.control.dirty;
  }

}

12.3.5 Model Documentation

For models/interfaces only the helper methods need a documentation (only in the case that the method name is not self explanatory).

12.3.6 Guard/Resolver/Interceptor Documentation

For guards, resolvers and interceptors the class should be described. The methods need no additional description.

Example for the Documentation of an Interceptor
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators';


let TOKEN: string;

const tokenHeaderKeyName = 'authentication-token';
const authorizationHeaderKey = 'Authorization';

/**
 * Intercepts outgoing HTTP request and sets authentication-token if available.
 * Intercepts incoming HTTP response and updates authentication-token.
 */
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    if (TOKEN && !req.headers.has(authorizationHeaderKey)) {
      req = req.clone({ headers: req.headers.set(tokenHeaderKeyName, TOKEN) });
    }

    return next.handle(req).pipe(tap(this.setTokenFromResponse));
  }

  private setTokenFromResponse(event: HttpEvent<any>) {
    if (event instanceof HttpResponse) {
      const response = <HttpResponse<any>>event;
      const tokenReturned = response.headers.get(tokenHeaderKeyName);
      if (tokenReturned) {
        TOKEN = tokenReturned;
      }
    }
  }
}

export function _setToken(token: string): void {
  TOKEN = token;
}

12.3.7 Configuration/Injection Key Documentation

To document configuration or injection keys of the Intershop Progressive Web App, the stored information of the keys has to be described.

If there are several states possible, describe the states of the key.

Example for the Documentation of a Configuration Key
import { InjectionToken } from '@angular/core';

/**
 * global definition of the endless scrolling page size.
 */
export const ENDLESS_SCROLLING_ITEMS_PER_PAGE = new InjectionToken<boolean>('endlessScrollingItemsPerPage');




13 Testing

13.1 Testing Concept

Testing the PWA follows the basic principle of the test pyramid (https://martinfowler.com/bliki/TestPyramid.html).

Test Pyramid

13.1.1 Unit

Most of the testing should be done in low-level unit tests if possible. These tests concern themselves with the behavior of a single unit of code, be it a function, a class or even an Angular component with HTML rendering. All dependencies of that unit shall be mocked out to keep the scope as small as possible. These tests are written in a Jasmine-like style and executed with Jest. Running these tests can be very time-efficient and they serve as the primal short-circuit response to developers.

13.1.2 Module

Following unit tests we also run module tests which serve as the first layer of integration tests. With these tests more dependencies are instantiated in every single test to check the behavior when interconnecting more components. Examples for these are testing a slice of the ngrx store or checking form validation with multiple Angular components. They are also implemented using Jest.

13.1.3 Integration

The next layer of tests are integration tests which run the application as a whole but mock out ICM rest responses. The test is run using a browser and performing various actions and checks on the application. For this kind of test Cypress is required. The tests are written in a Jasmine-like behavior driven style. For the ease of readability we implemented them using a PageObject pattern, see https://martinfowler.com/bliki/PageObject.html. Testing in this stage is of course more time-consuming as the application has to be compiled and started up as a whole. The fact of mocking server responses also limits the available workflows of the application. For example designing Mock-Data for a complete customer journey through the checkout would be too complex and too brittle. Nevertheless, the tests serve well for a basic overview of some functionality.

13.1.4 End-to-End

The most time-consuming tests are complete end-to-end tests. They do not mock out anything and run the PWA against an ICM with a deployed a_responsive:inspired-b2x. We also use Cypress here. Additionally, all tests from the previous Integration step should be composed in a way that they can also be run with real REST Responses. As a basic rule of thumb we only test happy path functionality or workflows that would be too cumbersome to be mocked in module tests.

13.2 Test File Locations

Unit and module tests are closely located next to the production source code in the src folder.

Integration and end-to-end tests currently reside in cypress/integration/specs. PageObjects are located in cypress/integration/pages. If the filename of a spec contains the string mock, it is run as an integration test with mocked server API. If it (additionally) contains b2c or b2b the test also should run on the PWA set up with the corresponding channel.

13.3 Deviation from Standard Angular Test Frameworks

By default Angular Projects are setup with Jasmine and Karma Runner for unit and module tests, as well as Protractor for end-to-end Testing. We decided to deviate from these frameworks, because there are better alternatives available.

Jest provides a better and faster experience when testing. Jest uses a JavaScript engine comparable to a virtual browser. There is no need to start up a real browser like it is standard with Jasmine+Karma. Also Jest provides an interactive command line interface with many options. Integrations for VSCode are available that will ease developing and running tests. Another big advantage of Jest is the functionality for Snapshot Testing.

We also do not use Protractor for end-to-end testing. Like all Selenium-based testing frameworks, Protractor deals with the same problems. Special Bindings for the programming language communicate via HTTP with a browser driver which then remotely controls the browser over HTTP. This indirect way is very fragile against network latency. Also the functionalities are limited. Protractor, however, is especially designed for Angular, so it automatically waits for background tasks to finish before continuing the test run. This functionality must be implemented when using Cypress.

Cypress uses a different approach for running end-to-end tests. It runs directly in the browser and thus provides access to application internals like XHR-Monitoring, access to LocalStorage and so on. The interface also provides page snapshots for debugging, which in turn ease the experience when writing tests or reconstructing bugs. We use Cypress with a PageObject pattern.

13.4 PageObject Pattern

As mentioned earlier, we divide end-to-end tests in PageObjects and Specs. PageObjects abstract the HTML of the pages and provide a human-readable way of accessing to implement test-describing business processes. In that way PageObjects also make certain routines re-usable over all tests. The Specs use these provided PageObject functions to make assertions and trigger actions. PageObjects themselves should not make assertions. All intended assertions should only be made in Specs. Specs are the main entry point for Developers. All test-related data and intended behavior should only be available in each single file.

No form of abstraction shall be made when developing tests, especially not for re-using code. Prefer Composition and introduce action methods in PageObjects instead.

13.5 Handling Test Data

For unit and module tests test data is instantiated as required. Each test should only set fields actually required for each test to ease readability. If new dependencies are introduced or workflows change, the corresponding test cases have to change, too.

Integration and end-to-end tests are tailored for the inSPIRED (a_responsive) demo store. Used test data can be abstracted at the start of the file but at all times it should only be accumulated here to ease readability of these test cases. Further abstraction would lead to longer development cycles as it is harder to understand functionality of test cases if it is distributed among multiple files.

If projects want to re-use the supplied test cases, they have to adapt them in terms of Test Data.

The end-to-end tests have to be adapted as well. Styling and structural changes have to be handled in the PageObjects, which then are used over all Specs. If behavior of the customization differs from the blueprint store, the Specs have to be adapted as well.

13.6 Testing Guidelines for Jest Tests

13.6.1 Stick to General Unit Testing Rules

13.6.1.1 Single Responsibility

A test should test only one thing. One given behavior is tested in one and only one test.

The tests should be independent from the others, that means no chaining and no run in a specific order is necessary.

13.6.1.2 Test Functionality - Not Implementation

A test is implemented incorrectly or the test scenario is meaningless if changes in the HTML structure of the component destroy the test result.

Example: The test fails if an additional input field is added to the form.

Wrong Test Scenario

it('should check if input fields are rendered on HTML', () => {
   const inputFields = element.getElementsByClassName('form-control');
   expect(inputFields.length).toBe(4);
   expect(inputFields[0]).toBeDefined();
   expect(inputFields[1]).toBeDefined();
   expect(inputFields[2]).toBeDefined();
});

13.6.1.3 Do not Comment out Tests

Instead use the xdescribe or xit feature (just add an 'x' before the method declaration) to exclude tests. This way excluded tests are still visible as skipped and can be repaired later on.

xdescribe("description", function() {
  it("description", function() {
    ...
  });
});

13.6.1.4 Always Test the Initial State of a Service/Component/Module/...

This way the test itself documents the initial behavior of the unit under test. Especially if you test that your action triggers a change: Test for the previous state!

it('should call the cache when data is available', () => {
// precondition
service.getData();
expect(cacheService.getChachedData).not.toHaveBeenCalled();

<< change cacheService mock to data available >>

// test again
service.getData();
expect(cacheService.getChachedData).toHaveBeenCalled();
});

13.6.1.5 Do not Test the Obvious

Testing should not be done for the sake of tests existing:

  • It is not useful to test getter and setter methods and use spy on methods which are directly called later on.
  • Do not use assertions which are logically always true.

13.6.1.6 Make Stronger Assertions

It is easy to always test with toBeTruthy or toBeFalsy when you expect something as a return value, but it is better to make stronger assertions like toBe(true), toBeNull or toEqual(12).

it('should cache data with encryption', () => {
    customCacheService.storeDataToCache('My task is testing', 'task', true);
    expect(customCacheService.cacheKeyExists('task')).toBeTruthy();
});
it('should cache data with encryption', () => {
    customCacheService.storeDataToCache('My task is testing', 'task', true);
    expect(customCacheService.cacheKeyExists('task')).toBe(true);
});

Again, do not rely too much on the implementation. If user customizations can easily break the test code, your assertions are too strong.

Test too Close to Implementation

it('should test if tags with their text are getting rendered on the HTML', () => {
    expect(element.getElementsByTagName('h3')[0].textContent).toContain('We are sorry');
    expect(element.getElementsByTagName('p')[0].textContent).toContain('The page you are looking for is currently not available');
    expect(element.getElementsByTagName('h4')[0].textContent).toContain('Please try one of the following:');
    expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search');
});

Same Test in a more Stable Way

it('should test if tags with their text are getting rendered on the HTML', () => {
    expect(element.getElementsByClassName('error-text')).toBeTruthy();
    expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search');
});

13.6.1.7 Do not Meddle with the Framework

Methods like ngOnInit() are lifecycle-hook methods which are called by Angular  The test should not call it directly. When doing component testing, you most likely use TestBed anyway, so use the detectChanges() method of your available ComponentFixture.

Wrong Test with ngOnInit() Method Calling

it('should call ngOnInit method', () => {
    component.ngOnInit();
    expect(component.loginForm).toBeDefined();
});

Test without ngOnInit() Method Call

it('should contain the login form', () => {
    fixture.detectChanges();
expect(component.loginForm).not.toBeNull();
});

13.6.2 Assure Readability of Tests

13.6.2.1 Stick to Meaningful Naming

The test name describes perfectly what the test is doing.

Wrong Naming

it ('wishlist test', () => {...})

Correct Naming

it ('should add a product to an existing wishlist when the button is clicked', () => {...})

Basically it should read like a documentation for the unit under test, not a documentation about what the test does. Jasmine has named the methods accordingly. Read it like `I am describing <component>, it should <do> when/if/on <condition/trigger> (because/to <reason>)`.

This also applies to assertions. They should be readable like meaningful sentences.

const result = accountService.isAuthorized()
expect(result).toBe(true)
const authorized = accountService.isAuthorized()
expect(authorized).toBe(true)

or directly
expect(accountService.isAuthorized()).toBe(true)

13.6.2.2 Avoid Global Variables

Tests should define Variables only in the scope where they are needed. Do not define Variables before describe or respective it methods.

13.6.2.3 Avoid Code Duplication in Tests

This increases readability of test cases.

  • Common initialization code of constants or sub-elements should be located in beforeEach methods.
  • When using TestBed you can handle injection to variables in a separate beforeEach method.
it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const component = fixture.componentInstance;
    ...
});
it(`should have the title 'app'`, async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const component = fixture.componentInstance;
   ...
});
it('should match the text passed in Header Component', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
});
describe('AppComponent', () => {
    let translate: TranslateService;
    let fixture: ComponentFixture<AppComponent>;
    let component: AppComponent;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ ... ] });
        fixture = TestBed.createComponent(AppComponent);
        component = fixture.componentInstance;
    })
    it('should create the app', () => { ... });
    it(`should have as title 'app'`, () => { ... });
    it('should match the text passed in Header Component', () => { ... });
});

13.6.2.4 Do not Use Features You Do not Need

This increases readability of test cases.

If you do not need the functionality of :

  • ComponentFixture.debugElement
  • TestBed
  • async, fakeAsync
  • inject

... do not use it.

Wrong Test With Useless Features (TestBed, ComponentFixture.debugElement)

it('should create the app', async(() => {
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
}));

Same Test - Works Without These Features

it('should be created', () => {
const app = fixture.componentInstance;
    expect(app).toBeTruthy();
});

13.6.2.5 Structure Long Tests

The describe methods in Jasmine are nestable. You can use this to group various it methods into a nested describe where you can also use an additional beforeEach initialization method.

Nested describe Methods

describe('AccountLogin Component', () => {
    it('should be created', () => { ... });
    it('should check if controls are rendered on Login page', () => { ... });
    ....
    describe('Username Field', () => {
        it('should be valid when a correct email is assigned', () => { ... });
       ....
    });
});

13.6.2.6 Avoid Having Dead Code

Always only declare what you need. Unused variables, classes and imports reduce the readability of unit tests.

13.6.2.7 Use a Mocking Framework Instead of Dealing with Stubbed Classes

This way less code needs to be implemented which again increases readability of unit tests. Also mocks can be stubbed on time, depending on the current method. We decided to use ts-mockito as the Test Mocking Framework.

13.6.3 Do not Change Implementation to Satisfy Tests

13.6.3.1 DOM Element Selection

Use only IDs or definite class names to select DOM elements in tests. Try to avoid general class names.

Wrong Selector

const selectedLanguage = element.getElementsByClassName('hidden-xs');

Correct Selector

// by id

const selectedLanguage = element.querySelector('#language-switch');

// by class

const selectedLanguage = element.getElementsByClassName('language-switch');

13.6.3.2 DOM Changes for Tests

Use data-testing-id via attribute binding to implement an identifier used for testing purpose only.

Correct Testing ID

*.component.html

< ol class = "viewer-nav" >< li * ngFor = "let section of sections" [ attr . data - testing-id ]= "section.value" >{{ section . text }}</ li > </ ol >

*.spec.ts

element.querySelectorAll("[data-testing-id]")[0].innerHTML

element.querySelectorAll("[data-testing-id='en']").length

Note

Do not overuse this feature!

13.6.4 Stick to Intershop Conventions Regarding Angular Tests

13.6.4.1 Every Component Should Have a 'should be created' Test

Every component should have a 'should be created' test like the one Angular CLI auto-generates. This test handles runtime initialization Errors.

it('should be created', () => {

  expect(component).toBeTruthy();

  expect(element).toBeTruthy();

  expect(() => fixture.detectChanges()).not.toThrow();

});

13.6.4.2 Choose the Right Level of Abstraction

  • When working mainly with stubs for specific services which mock dependencies of services under test, you should mainly use spies to check whether the right methods of the stub are called.
  • When working mainly with fully configured services, it is best to check return values.
  • When testing complex scenarios (e.g., when the test has to handle multiple pages), it might be better to implement a Geb+Spock end to end test.

See Three Ways to Test Angular Components for more information.

13.6.5 Be Aware of Common Pitfalls

13.6.5.1 Be Careful When Using toBeDefined

Be careful when using toBeDefined, because a dynamic language like JavaScript has another meaning of defined (see: Is It Defined? toBeDefined, toBeUndefined).

Do not use toBeDefined if you really want to check for not null because technically 'null' is defined. Use toBeTruthy instead.

13.6.5.2 Be Careful With Variable Initialization

Jasmine does not automatically reset all your variables for each test like other test frameworks do. If you initialize directly under describe, the variable is initialized only once.

Warning

Since tests should be independent of each other, do not do this.


describe(... () => {
    let varA = true;     // if changed once, value is not initialized again
    const varB = true;   // immutable value
    let varC;            // initialized in beforeEach for every test

    beforeEach({
        varC = true;
    });

    it( 'test1' () => {
        varA = false;
		// varB = false; not possible
        varC = false;
    });

    it( 'test2' () => {
        // varA is still false
        // varB is still true
        // varC is back to true
    })
});

As shown in the above example, varA shows the wrong way of initializing variables in tests.

If you do not need to change the value, use a const declaration like variable varB. If you need to change the value in some tests, assure it is reinitialized each time in the beforeEach method like varC.

13.6.5.3 Use the right way to test EventEmitter

Testing EventEmitter firing can be done in multiple ways that have advantages and disadvantages. Consider the following example:

import { EventEmitter } from '@angular/core';
import { anything, capture, deepEqual, spy, verify } from 'ts-mockito';

describe('Emitter', () => {
  class Component {
    valueChange = new EventEmitter<{ val: number }>();

    do() {
      this.valueChange.emit({ val: 0 });
    }
  }

  let component: Component;

  beforeEach(() => {
    component = new Component();
  });

  it('should detect errors using spy with extract', () => { // *1*
    const emitter = spy(component.valueChange);

    component.do();

    verify(emitter.emit(anything())).once();
    const [arg] = capture(emitter.emit).last();
    expect(arg).toEqual({ val: 0 });
  });

  it('should detect errors using spy with deepEqual', () => { // *2*
    const emitter = spy(component.valueChange);

    component.do();

    verify(emitter.emit(deepEqual({ val: 0 }))).once();
  });

  it('should detect errors using subscribe', done => { // *3*
    component.valueChange.subscribe(data => {
      expect(data).toEqual({ val: 0 });
      done();
    });

    component.do();
  });
});

As EventEmitter is Observable, subscribing to it might be the most logical way of testing it. We, however, would recommend using ts-mockito to increase readability. The ways 1 and 2 portrait two options, we would recommend using the first one.


1 (preferred)23
Description
  • Using ts-mockito spy and then verify it has fired
  • Then check argument for expected value
  • Using ts-mockito spy and then verify it has fired with the expected value
  • Using subscription and asynchronous method safeguard
ReadabilityCapturing arguments with ts-mockito might seem tricky and therefore reduces readability, but the test is done in the right order.(tick) Right order, fewest lines of code(error) Order is reversed.
In case it does not emit(tick) Correct line number and a missing emission is reported.(tick) Correct line number and a missing emission is reported.(error) Test runs into timeout as the asynchronous callback is not called.
In case it emits another value(tick) Correct line number and an incorrect value is reported.(error) Missing emission is reported.(tick) Correct line number and an incorrect value is reported.

14 CMS Integration

14.1 Introduction

Intershops REST API contains resources reflecting the main aspects of CMS, i.e., Pagelets, Pages and Includes. The API is still in beta state as not all business features of the classic (ISML) storefront are supported yet. With this API a client can retrieve a composition of involved CMS objects (page, page variant, slot, component and so on). It is the client's responsibility to interpret and "render" such a composition tree. In the PWA this is done by mapping each element onto an Angular specific render component.

CMS_Overview

14.2 Angular

A render component in Angular has to fulfill the following requirements:

  • It is declared in the CMSModule.
  • The component must have an input for the assigned pagelet.
  • It is added to the CMSModule as an entryComponent (required, so a factory is generated as it is not referenced directly).
  • A mapping has to be provided in the CMSModule to map the definitionQualifiedName of the ICM realm to the PWA render component.
providers: [
   ...
  {
    provide: CMS_COMPONENT,
    useValue: {
      definitionQualifiedName: 'app_sf_customer_cm:component.custom.inventory.pagelet2-Component',
      class: CMSInventoryComponent,
    },
    multi: true,
  }
]

When using ng generate with PWA custom schematics, you can apply all those changes described automatically. For example, the following code block creates a new Angular component named cms-inventory and registers it with the CMSModule.

$ ng generate cms inventory --definitionQualifiedName app_sf_customer_cm:component.custom.inventory.pagelet2-Component
CREATE src/app/cms/components/cms-inventory/cms-inventory.component.ts (386 bytes)
CREATE src/app/cms/components/cms-inventory/cms-inventory.component.html (32 bytes)
CREATE src/app/cms/components/cms-inventory/cms-inventory.component.spec.ts (795 bytes)
UPDATE src/app/cms/cms.module.ts (4956 bytes)

Visual Studio Code integration

For Visual Studio Code there is an extension that offers comfortable usage options for the schematics, see Angular Schematics.

15 Configuration

In a complex application like the Intershop Progressive Web App, there are multiple ways and kinds of configuration. The complexity increases if you consider that the communication with Intershop Commerce Management has to be coordinated as well. In addition, the PWA, when run with Angular Universal, consists of a server-side and a client-side application.

15.1 Ways of Configuring Angular Applications

If available always prefer configuration via system environment variables and running the PWA with Universal Rendering.

15.1.1 Angular CLI Environments

The standard way of configuring an Angular Application can be done by managing multiple environment files that are part of the project's source tree, usually located in src/environments. To choose one configuration, you have to supply the parameter during building the Angular Application. The file angular.json defines how the correct environment file is swapped in for the corresponding environment. See Angular 2: Application Settings using the CLI Environment Option for further information.

Properties supplied with environment files should not be accessed directly in artifacts other than modules. Instead, you need to provide them via InjectionTokens to be used in components, pipes or services. The InjectionToken can be used to access a certain property later on:

Property provider
export const PROPERTY = new InjectionToken<string>('property');


@NgModule({
  providers: [
    { provide: PROPERTY, useValue: environment.property }
  ],
})
export class SomeModule {}


Property consumer
import { Inject } from '@angular/core'
import { PROPERTY } from '../injection-keys'

...

constructor(@Inject(PROPERTY) private property: string)

It is good practice to never write those properties at runtime.

As you can see, only build-time and deploy-time configuration parameters can be supplied this way.

15.1.2 Node.js Environment Variables

When running the application in Angular Universal mode within a Node.js environment, we can additionally access the process environment variables via process.env. This method provides a way to configure the application at deploy-time, e.g., when using docker images. Configuration can then be consumed and passed to the client side via means of state transfer.

15.1.3 NgRx Configuration State

Previous ways were mainly handling deployment or build-time related means to configure an Angular application. All further configuration that has some kind of runtime flexibility, especially configuration that is retrieved via REST calls from the ICM, has to be handled in the NgRx store and to be used throughout the application with selectors. Effects and actions should be used to manipulate those settings.

15.1.4 URL Parameters

We composed a configuration effect (NgRx) for listening to route parameters when initially visiting the page. This provides the most flexible way of configuring the application at runtime.

15.2 Different Levels of Configuration Settings

15.2.1 Build Settings

One example for a build-time configuration is the property serviceWorker, which is managed in the environment.ts and used to activate the ServiceWorker module. Another example of such a build setting is the property production as multiple debug modules are only compiled into the application when running in development mode.

In general, properties available at build time can only be supplied by Angular CLI environments (see above).

15.2.2 Deployment Settings

Deployment settings do not influence the build process and therefore can be set in more flexible manners. The main criteria of this category is the fact that deployment settings do not change during runtime. The most common way of supplying them can be implemented by using Angular CLI environment files and InjectionTokens for distribution throughout the application's code.

An example for this kind of settings are breakpoint settings for the different device classes of the application touchpoints.

15.2.3 Runtime Settings

The most flexible kind of settings, which can also change when the application runs, are runtime settings. Angular CLI environment files cannot provide a way to handle those. Only the NgRx store can do that. Therefore only NgRx means should be used to supply them. Nevertheless, default values can be provided by environment files and can later be overridden by system environment variables.

Everything managed in the NgRx state is accumulated on the server side and sent to the client side with the initial HTML response. The reason for this is that this is the most common deployment scenario of PWAs (see Deployment Scenarios). 

15.3 Multi-Site Handling

Since version 0.9 of the PWA there are means to dynamically configure ICM channel and application to determine the correct REST endpoint for each incoming top level request. Nevertheless, you can still configure it in a static way for each PWA deployment via Angluar CLI environments.

15.3.1 Setting the Base URL

At first, the PWA has to be connected with the corresponding ICM. This can be done by modifying environment files or by setting the environment variable ICM_BASE_URL for the process running the Node.js server. The latter is the preferred way.

Independent of where and how you deploy the Angular Universal application, be it in a docker container or plain, running on Azure, with or without service orchestrator, setting the base URL provides the most flexible way of configuring the PWA. Refer to the documentation for mechanisms of your environment on how to set and pass environment variables.

15.3.2 Static Setting for Channels

Use the properties icmChannel and icmApplication in the Angular CLI environment or the environment variables ICM_CHANNEL and ICM_APPLICATION to statically direct one deployment to a specific REST endpoint of the ICM.

15.3.3 Dynamic Setting of Channels

To set ICM channels and applications dynamically, you have to use URL rewriting in a reverse proxy running in front of the PWA instances. The values have to be provided as URL parameters (not to be confused with query parameters).

nginx URL rewrite snippet
rewrite ^(.*)$ "$1;channel=inSPIRED-inTRONICS_Business-Site;application=-" break;

The above example configuration snippet shows a Nginx rewrite rule on how to map an incoming top level request URL to an internal worker process (e.g., Node.js). It shows both PWA parameters channel, application and their fixed example values. The parameters of each incoming request are then read and transferred to the NgRx store to be used for the composition of the initial HTML response on the server side. Afterwards they are propagated to the client side and re-used for subsequent REST requests.

In the source code of the project we supplied an extended Nginx docker image for easy configuration of multiple channels via sub-domains. Refer to our Gitlab CI configuration (file .gitlab-ci.yml) for a usage example.

15.4 Feature Toggles

To activate additional functionality, we use the concept of feature toggles throughout the application. For instance, there is no general distinction between B2B and B2C applications. Each setup can define specific features any time. Of course, the ICM server must supply appropriate REST resources to leverage functionality.

15.4.1 Configuring Features

The configuration of features can be done statically by the Angular CLI environment property features (string array) or the environment parameter FEATURES (comma-separated string list). To configure it dynamically, use the PWA URL parameter features (comma-separated string list) during URL rewriting in the reverse proxy.

15.4.2 Programmatically Switching Features

We supply various means to activate and deactivate functionality based on feature toggles.

15.4.2.1 Guard

const routes: Routes = [
  {
    path: 'quote',
    loadChildren: ...,
    canActivate: [FeatureToggleGuard],
    data: { feature: 'quoting' },
  },
...

Add the Guard as CanActivate to the routing definition. Additionally, you have to supply a data field called feature, containing a string that determines for which feature the route should be active. If the feature is deactivated, the user is sent to the error page on accessing.

15.4.2.2 Directive

<ish-product-add-to-compare *ishFeature="'compare'"> ...

15.4.2.3 Service

@Injectable({ providedIn: 'root' })
export class SomeService {
  constructor(private featureToggleService: FeatureToggleService) {}
...
    if (this.featureToggleService.enabled('quoting')) {
...
}

15.5 Setting Default Locale

You can set the default locale statically by modifying the order of the provided locales in the Angular CLI environment files. The first locale is always chosen as the default one. To dynamically set the default locale, use the URL parameter lang when rewriting the URL in the reverse proxy (see Dynamic Setting of Channels).

16 Angular Universal

Angular Universal is set up in this project mainly following https://github.com/angular/universal-starter.

Official documentation can be found at https://angular.io/guide/universal.

We use Universal for pre-rendering complete pages to tackle SEO concerns. An Angular application without Universal support will not respond to web crawlers with complete indexable page responses.

Angular's state transfer mechanism is used to transfer properties to the client side. We use it to de-hydrate the ngrx state in the server application and re-hydrate it on the client side. See Using TransferState API in an Angular v5 Universal App for specifics.

Follow the steps in the README.md to build and run the application in Universal mode.

17 Continuous Integration

This section provides an overview of required Continuous Integration steps to verify the validity of code contributions.

All mentioned tools provide feedback on success or failure via exit code.

17.1 Code Integrity

Since Angular projects are JavaScript-based, even though they use TypeScript-based code, everything is highly dynamic. Parts of the software can still run error free with webpack-dev-server (ng serve), even if other parts were not compiled or have template errors.

To ensure having a consistent code base, the CI system should always perform at least an ahead-of-time compile step (ng build --aot).

Angular in production mode does AoT and applies some more code optimizations that can sometimes clash with definitions or third-party libraries. To catch this, a production build should be performed: ng build --prod.

To check the integrity of the unit tests, the TypeScript Compiler can be used: npx tsc -p src/tsconfig.spec.json.

17.2 Dependencies

When using npm as manager for third-party libraries, all dependencies get pinned down with exact version numbers and archive digests. A CI system should check if the current package.json corresponds to the checked-in package-lock.json.

This can be done with git tooling (check for git changes after npm install) or can be done by hashing package-lock.json before and after the install step and comparing hash values.

17.3 Code Formatting

In larger projects it is beneficial for all users to contribute code in a consistent style. This reduces the number of conflicts when merging code that was developed in parallel.

To ensure that contributed code is properly formatted, run the formatter on the CI server with npm run format and check for changes with git tooling or calculate hashes before and after.

17.4 Unit Testing

Jest is used as a test runner. All tests can and should be run on the CI server with npm test.

Since jest is very flexible in accepting code with compile errors, the code integrity should be checked separately.

17.5 UI Testing

UI testing is done with Cypress. This requires a suitable version of Google Chrome to be installed on the CI worker (or in the Docker image used for the tests).

Run UI tests interactively with npm run e2e. Before that you have to start up a PWA application.

See an example in our .gitlab-ci.yml how to do this automatically.

17.6 Universal Testing

Since Angular Universal is used for server-side pre-rendering of content to tackle SEO concerns, the CI system should also check if server-side rendering is still working. For this purpose, it must be checked whether the server response contains content from lazy-loaded modules, in other words making sure all modules have produced compliant HTML markup.

This can be done by pointing curl to a product detail page and checking if a specific CSS class could be found (via grep) in the HTML. Have a look into .gitlab-ci-test-universal.sh to see how we are doing that.

17.7 Static Code Analysis

SCA tools help improve the code quality to improve maintainability and therefore reduce technical debt. Intershop uses tslint for static code analysis. Run the linting process on the CI with "npm run lint".

If a rule seems too harsh for you, downgrade it to warning level by choosing:

tslint.json
"rule-name": { "severity": "warning" }

Turn it off completely by using "rule-name": false.

18 Deployment Scenarios

18.1 Simple Browser-Side Rendering

This is suitable for Demo Servers to have a fast build chain. We do not recommend this setup for production use.

The application is built completely with Angular CLI:

Angular-BrowserSideApp-Build-Activity

The resulting files can be statically served using any HTTP server that is capable of doing that. The initial page response from the browser is minimal and the application gets composed and rendered on the client side.

Angular-BrowserSideApp-Sequence

Of course, this can have a significant impact on the client side if no efficient rendering power is available. Search engine crawlers might also not be able to execute JavaScript and therefor might only see the initial minimal response.

18.2 Browser-Side Rendering with On-Demand Server-Side Pre-Rendering (Angular Universal)

We recommend using this approach for production use. You can use the supplied Dockerfile to build it.

The application consists of two parts, the server-side and the client-side application:

Angular-ServerSideApp-Build-Activity

The resulting distribution has to be executed in a node environment. The server.js executable handles client requests and pre-renders the content of the page. The resulting response to the browser is mainly prepared and can be displayed quickly on the client side.

Angular-ServerSideRendering-Sequence

19 Microsoft Azure Deployment

19.1 Prerequisites

Make sure you have a deployment user at hand. If this is not the case, create one with Azure CLI: az webapp deployment user set --user-name USERNAME --password PASSWORD.

Note

  • Both USERNAME and PASSWORD settings have nothing to do with your Azure login. Do not mix them.
  • Replace RESOURCE_GROUP_NAME and PLAN_NAME with meaningful values.
  • You need to have a resource group with an Azure App Service plan. If this is not the case, create them with Azure CLI or with the Azure Portal:
    • To create the resource group: az group create --name $RESOURCE_GROUP_NAME --location "West Europe"
    • To create the app service plan: az appservice plan create --name $PLAN_NAME --resource-group $RESOURCE_GROUP_NAME --sku S1 --is-linux
  • Make sure to select Linux as the underlying operating system.

19.2 App Services

Azure App Services delivers PAAS nature to Azure customers. It contains a set of deployable apps for which you can choose basic runtime settings (depending on the app).

The Microsoft Web App allows you to deploy either NodeJS, Python, Java or .NET applications. Which means in whatever computer language your application is written, it must match the Web App's technology stack. As described above, Angular Universal needs a JavaScript runtime environment to fulfill the 'server' role. Consequently, we need to deploy a NodeJS runtime together with the Microsoft Web App. To do so, perform the following steps:

  1. Create a Web App.
  2. Specify a deployment setting (only Azure Portal).
  3. Produce a production ready distribution with npm run build.
  4. Use the git push command to trigger a deployment.

19.2.1 Create a Web App

Depending on your taste, you can create a Web App with either Azure CLI or the Azure Portal. Within the portal just search for "Web App" from vendor Microsoft. You will see a creation dialog like this:

















Specify a name, select Linux as OS, Runtime Stack, Publish (see next step) and select the correct App Service Plan (see the prerequisites chapter above). The same applies for the CLI.

CLI to create a Web App
az webapp create --resource-group $RESOURCE_GROUP_NAME --plan $PLAN_NAME --name NAME --runtime "NODE|8.9" --deployment-local-git

Note

The runtime stack of the Web App must be specified to be higher or equal to Node.js 8.9

19.2.2 Specify a Deployment Setting

If Azure CLI was used in the previous step, you can fast forward to the next step (Produce a Production Ready Distribution).

If the Node.js Web App was created with the Azure Portal, you must specify how to publish the app. If Code was chosen as publish setting, this happens via FTP/ZIP upload or just Git.

To specify a Code deployment:

  1. Go to the created Web App and select Deployment Center.
    Azure Portal will automatically ask you what source you want to configure.
  2. Select Local Git Repository as deployment option.
    This will create an empty git repository connected with your Web App.

Another possibility is to choose Docker Image as publish setting. In this guide we will not elaborate further on this option. All that is important is that the PWA comes with a Dockerfile that can be utilized for Docker-based deployments. The Azure Devops chapter picks this topic up again to ease builds and deployments.

19.2.3 Produce a Production Ready Distribution

To produce a production ready distribution:

  1. Go to the checked out source tree of the Intershop Progressive Web App.

  2. Execute:

    npm run build

    This step takes some time and will produce a new version of the ./dist folder.

19.2.4 Use Git Push to Trigger a Deployment

Use Git Push to trigger a deployment:

  1. Go to a different folder and let Git clone your remote repository. 

    Info

    You can copy the git URL from the Overview tab of your created Web App. 
    Search for Git clone url. Make sure your created deployment user is part of that URL, e.g., https://deploymentuser@webappname.scm.azurewebsites.net:443/webappname.git.

    This will produce another empty repository on your local machine. Go to the folder containing the repository and the work tree (which is empty).

  2. Copy the contents of the dist folder from the previous step into your repository.
  3. Run git add and git push.
Example
λ git clone https://deploymentuser@webappname.scm.azurewebsites.net:443/webappname.git
Cloning into 'webappname'...
Password for 'https://deploymentuser@webappname.scm.azurewebsites.net:443': 
warning: You appear to have cloned an empty repository.

λ rsync -avz proof-of-concept/dist/ webappname
...
sent 6,255,008 bytes  received 3,748 bytes  4,172,504.00 bytes/sec
total size is 21,386,819  speedup is 3.42

λ git add .

λ git commit --message "Whatever it should"
...
 create mode 100644 server/ngsw.json
 create mode 100644 server/styles.bundle.css

λ git push
...
remote: Finished successfully.
remote: Running post deployment command(s)...
remote: Deployment successful.
remote: App container will begin restart within 10 seconds.
To https://webappname.scm.azurewebsites.net:443/webappname.git
 * [new branch]      master -> master

19.2.5 Configure Intershop Commerce Management Backend

To configure Intershop Commerce Management Backend:

  1. Go to the deployed Web App in the Azure Portal and select Application settings. 
  2. Click on Add new setting and provide the Intershop base URL (i.e., the place where ICM is hosted) under the name ICM_BASE_URL. 
  3. Apply with the Save button.
    The Node.js web application will use the value to construct REST URLs.

HTTP and HTTPS

Make sure that you choose a consistent protocol scheme for both applications, the Intershop Commerce Management and the Progressive Web App. Since Web Apps in Azure are using HTTPS automatically, the ICM_BASE_URL has to use HTTPS, too. Otherwise you risk a Mixed Content: error in the browser developer tools console.

SSL handshake

Intershop Commerce Management uses a self-signed certificate by default which does not carry any trust. Unless your ICM deployment configures a correct certificate, you have to manually tell your browser to trust the ICM backend. Otherwise the ssl handshake with the configured ICM_BASE_URL will fail resulting in a PWA error page. Details on this can be found using the browsers developer tooling.

19.3 Azure Devops (Former VSTS)

19.3.1 Introduction

With Azure Devops, it is possible to create a Git-based project that can be task managed (like JIRA), Wiki documented (like Confluence) and also continuously integrated. It also contains release management of previously build artifacts. In the end, such a release is deployed in Azure where it can become publicly available or end-to-end tested.

VSTS-Flow

Angular apps can be easily managed with the aid of Azure Devops. It all begins with a git clone of a given project URL. After successful cloning of the remote repository you can work on the project. Start a new feature branch and once the feature is ready, create a pull request to merge changes into protected branches. All state changes can be configured to trigger a new build. A build artifact is defined by the 'Build Definition' that can be managed after logging into the Azure Devops account successfully (see info box above).

19.3.2 Build Definition

Click on the project | Build and Release | Build to open the available build definitions.

A definition is basically a set of tasks that needs to be configured. Tons of different tasks are available. Command scripts can be executed as part of a build step as well. A build definition also defines where that build is executed later (the so-called agent queue). By registering your own build agents (installed on some machines in your IT infrastructure), you can build artifacts in your own environment and let only releases leave your premises.

The easiest way to build the Intershop Progressive Web App is to use Docker build tasks, since our sources already contain Docker files that produce and test the necessary artifacts. The image below shows the complete implementation of the build process. It all starts with getting the sources with settings like name of the project and Git branch. In Phase 1 (executed on agents) an image is built, which is later being pushed to a docker container registry.

Every Docker build task wants to know about:

  • Container registry
  • Docker File
  • Action to execute (building the image, pushing the image)
  • Image Name
  • Build Arguments
  • Tags

Building a production build out of the PWA uses just npm run build in a Multi-Step Docker build (see Dockerfile).

As an alternative approach, you can also rebuild all the tasks that are implemented in the Docker file used for the Docker-based build. However, this is not the recommended approach.

Let Node NPM build tasks, execute scripts delivered together with the sources. Zip everything together and publish the artifact in the VSTS project store later on. No container registry is needed for that.

Whatever fits best, the artifacts that are coming out of a build definition are not deployed (unless you add a deployment task). They are merely stored in a file system source ready to be picked up by a release process.

19.3.3 Release Definition

The implementation of a release process (in a way continuous delivery) is done via release definition:

  1. Click on the project | Build and Release | Releases to open the available definitions.
    A release consumes artifacts from different sources, be it GitHub, VSTS Build Definition, VSTS Git repo, Azure Container Registry or even Jenkins. It is the starting point of such a release pipeline.
  2. Decide what to do with the artifact (called Drop in the picture above).
    To do so, you need to define an environment. Each environment has a pre-deployment condition that must be satisfied in order to proceed. Things like manual approval or quality gates based on reports that have been published together with the artifact are possible. A specific deployment process that can be tailored just like the build definitions comes with the environment. Once again, there are agents that execute different tasks. These tasks can be found within the marketplace. Tasks to deploy to Microsoft Azure are available.

    The straight forward approach is to deploy an artifact into Azure App Services. You just need one phase and one '' task in that phase. The deploy task asks for:
    • Azure Subscription
    • App type
    • App Service name
    • Image Source (Container Registry or Built-in Image)
    Image Source setting determines whether a Docker image will be pulled from the given container registry or such an image will be created by Azure itself. If Built-in Image is selected, you have to define what kind of runtime stack you need (i.e., Node.js, Ruby, PHP, .NET). Either way docker is the easiest way, but you can also let Azure create the image for you. All you need is a web app that is compatible with the available runtime stacks. For the Intershop Progressive Web App based Storefront, the Node.js environment would be suitable.

20 Third-party Integrations

20.1 Google Tag Manager

To enable user tracking and setting it up with Google Tag Manager the popular library Angulartics2 is used. 

To activate GTM tracking, set the Tag Manager Token either in the used Angular CLI environment file with the property gtmToken or via the environment variable GTM_TOKEN. Additionally, the feature toggle tracking has to be added to the enabled feature list. This feature only works in Universal Rendering Mode. Prefer configuration via system environment variables.

Please refer to the angulartics2 documentation for information on how to enable tracking for additional events.

For GDPR compliance the tracked IPs have to be anonymized. See IP-Anonymisierung in Google Analytics prüfen und aktivieren.

20.2 Sentry

We recommend using Sentry for Browser-side error tracking. It is integrated with the official Angular support dependency.

To activate Sentry in the PWA, set the Sentry DSN URL (Settings | Projects | Your Project | Client Keys (DSN) | DSN) either via Angular CLI environment file with the property sentryDSN or via the environment variable SENTRY_DSN. Additionally, the feature toggle sentry has to be added to the enabled feature list. This feature only works in Universal Rendering Mode. Prefer configuration via system environment variables.


Disclaimer

The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Web site, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.

Customer Support
Knowledge Base
Product Resources
Support Tickets