Pages & Flow Configuration

1. How to create Page Types to be used in the Flow

After the setup of the PFE module, content pages, that can be used in a flow, need to be provided.

Within a NGX-PFE flow, different page types can be used. Each of the page types can be used multiple times throughout a flow with different page configurations.

Every page type consists of the following parts:

  • A page component, which is a standard angular component. The page component can also use the PFE services and get the page configuration injected. The page is responsible to provide the configuration for components on the page and manage the data flows to and from the global state.
  • A configuration data model that is used in the page flow configuration

1.1. Page Components

A page component can use several different injectables to interact with the APP/PFE.

Service Description
PfeBusinessService Allows it to call various PFE functionalities from within the page. This includes the global state, certain subjects, triggering navigations and some hooks.
ActivatedRoute The angular ActivatedRoute is used to inject the current configuration into the page.

A Simple Page Component

The following example shows a simple page component that retrieves the page configuration provided by the PFE.

Generate a new ExamplePageComponent component

npx ng generate @schematics/angular:component --name=ExamplePage --style=scss --no-interactive

and replace the example-page.component.ts content with this:

import { PfeBusinessService } from '@allianz/ngx-pfe';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MyPageConfig } from '../page-config.model';
import { ExamplePageConfig } from './example-page.model';

@Component({
  selector: 'allianz-example-page',
  templateUrl: './example-page.component.html',
  styleUrls: ['./example-page.component.scss'],
})
export class ExamplePageComponent implements OnInit {
  examplePageConfig?: ExamplePageConfig;

  constructor(private activatedRoute: ActivatedRoute, private pfeBusinessService: PfeBusinessService<MyPageConfig>) {
    // Get the page configuration from the activatedRoute:
    const pageConfig: MyPageConfig = this.pfeBusinessService.getPageConfigFromRoute(this.activatedRoute);
    this.examplePageConfig = pageConfig?.examplePageConfig;
  }

  ngOnInit(): void {
    // The pfeBusinessService can be used to interact with the state and other pfe functionalities:
    this.pfeBusinessService.storeValue('$.someAttribute', true);
    this.pfeBusinessService.setPageStatus(true);
  }
}

Add a new example-page.model.ts for the configuration data model and add the following content:

The page also defines a data model for the page configuration:

export interface ExamplePageConfig {
  aConfigurationAttribute: string;
}

This configuration data model is later used in the overall global configuration data model.

Page Status for Navigation to the Next Page

When a page is opened, the status of the page is set to invalid by default. This will prevent the navigation to the next page.

For the navigation to be allowed, the page status has to be set by calling setPageStatus():

pfeBusinessService.setPageStatus(<boolean>);

If a page contains a form with mandatory fields, this status can be tied to the validity of the form to prevent a navigation as long as the form is invalid.

Unit Testing of Pages

Unit tests need to provide the page config within the ActivatedRoute. The pfe provides the MockActivatedRoute to make this easier.

Example:

describe('ExamplePageComponent', () => {
  const examplePageConfig = {
    firstConfigValue: 'test',
    secondConfigValue: 'test2',
  };

  const pageConfigRoute = new MockActivatedRoute<DemoAppPageConfig>({
    pageId: 'mockpage',
    examplePageConfig: examplePageConfig,
  });

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ExamplePageComponent],
      imports: [
        NgxPfeTestingModule,
        BrowserModule,
        NxIconModule,
        NxLinkModule,
        TranslateModule,
      ],
      providers: [
        { provide: ActivatedRoute, useValue: pageConfigRoute },
        { provide: APP_BASE_HREF, useValue: '/' },
        provideHttpClient(withInterceptorsFromDi()), 
        provideHttpClientTesting(),
      ],
    }).compileComponents();
  }));
});

2. Page Configuration

The configuration of a page consists of the attributes of the PageConfig interface, which is extended with the custom pages used in an app.

An additional optional attribute has to be added for every page type within the flow.

Add a new page-config.model.ts file in the app root with the following content:

import { PageConfig } from '@allianz/ngx-pfe';
import { ExamplePageConfig } from './example-page/example-page.model';

export interface MyPageConfig extends PageConfig {
  examplePageConfig?: ExamplePageConfig;
}

This approach allows it to automatically generate a json-schema for the configuration.

2.1. Pages Mapping

The page mapping defines, which page type, and it's page specific configuration, is connected to which attribute in the global configuration.

The following example shows a possible way to create the page mapping configuration within an app. Add a pages-mapping.ts file in the root with the following content:

import { PageDefinition, PfePageMappingService } from '@allianz/ngx-pfe';
import { examplePageRoute } from './example-page/example-page.route';
import { MyPageConfig } from './page-config.model';

// This is used, to get a typesafe mapping from the page config attributes to the page type.
const pageConfigModel =
  <TObj>() =>
  (name: keyof TObj) =>
    name;
const getConfigPropertyName = pageConfigModel<MyPageConfig>();

// This list contains all the page type mappings
export const pages = new Map<string, PageDefinition>();

// The page mapping maps the property name in the configuration
// to the corresponding page component. See below for more details.
// For lazy loaded pages:
PfePageMappingService.addLazyLoadedPageToMap(pages, getConfigPropertyName('examplePageConfig'), examplePageRoute);

// And also for non-lazy loaded pages:
// PfePageMappingService.addPageToMap(pages, getConfigPropertyName('examplePageConfig'), ExamplePageComponent);

2.2. Register a Page Type with the PFE

Lazy Loading of Pages

As the PFE is based on the angular routing, the lazy loading mechanism can also be used for pages.

This is especially useful for larger pages, that are not used in all configurations.

An example for a lazy loaded page can be found in the viewer demo application: ngx-pfe/viewer/src/app/documentation/demos/pfe-basic/app/pages/lazy-loaded-page.

Lazy loaded pages can either be entire modules with their own routing configuration (using RouterModule.forChild(...)) or it can be single (standalone) components.

We'll first take a look at how to create a page module that can be lazy loaded.

Create a Page Module

💡 Skip this step if you plan to lazy-load a standalone component instead of an entire module.

This module has to contain all parts from the page. (components, services...)
The page can also use components, services etc... from the main application or from libraries.

It is very important, that this module or any parts of the page are not imported anywhere else in the application. The angular compiler will package everything into the main bundle, that is reachable by the dependency tree starting at the main app module.

Generate an ExamplePageModule

npx ng generate @schematics/angular:module --name=ExamplePage --no-interactive

and add the following content in the app\example-page\example-page.module.ts:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ExamplePageComponent } from './example-page.component';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    RouterModule.forChild([
      {
        path: '',
        component: ExamplePageComponent,
      },
    ]),
  ],
})
export class ExamplePageModule {}

Tell the angular compiler, that this page should be lazy loaded

The angular compiler needs to know, that this page should be packaged into a separate lazy loaded chunk.

To load a module, add a new example-page.route.ts file with the following content:

export const examplePageRoute = {
  loadChildren: () => import('./example-page.module').then((m) => m.ExamplePageModule),
};

To load a standalone component, use the following route configuration:

export const examplePageRoute = {
  loadComponent: () => import('./example-page.component').then((m) => m.ExamplePageComponent),
};

Warning: The Angular 8 compiler is very picky about the format of the route module import. It has to have the exact same structure like the example above.

Another warning for Angular 9: If the route definition, is part of the same file as the module it references, it will end up in the main bundle! The solution is to put it in a separate file

This has the following purpose: To tell the compiler, that the module referenced here should be prepared for lazy loading. This means, the compiler will package it up in a separate file.

If a page/module from a library should lazy loaded, that is not prepared for this, a lazy loaded wrapper module can be created.

Additional Background information about the chunk handling in Angular 9

Ivy contains a new handling of side effects. See also: https://angular.io/guide/ivy-compatibility#payload-size-debugging and https://github.com/angular/angular-cli/issues/16799#issuecomment-580912090

The previous view engine builds assumed, that the code of the app is side effects free. With ivy this is turned around and the code is assumed to have side effects. (The webpack default is now simply active: https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free)

This means the build will follow all imports and add that code to the bundle. If there are additional imports, those will end up in the bundle.

If the code of the app is side effect free, a package.json file can be added to the app, that marks the code as such.

For example: src/app/package.json

With this content:

{
  "sideEffects": false
}

It is important that this file is not put on the same level as the main.ts, as this one actually does have side effects that are required for the app to run.

Important

The difference between side effect free builds and non free ones can only be observed when the optimization is turned on in the build settings.

Sometimes the side effect optimization goes overboard, so it is highly recommended to check the functionality after activating this.

It is also recommended to double check that lazy modules are actually built as a separate chunk.

2.3. How the Page Mapping Works

All page types need to be registered with the PFE by adding them to the page mapping.

Lazy loaded pages can be added like this to the page mapping:

PfePageMappingService.addLazyLoadedPageToMap(
  pages,
  // The lazyLoadedPage configuration attribute name comes from the global page configuration data model
  getConfigPropertyName('examplePageConfig'),
  examplePageRoute
);

The examplePageRoute is imported from the separate file. Usually this is put next to the module of the page.

2.4. Adding non-lazy-loaded pages

Alternatively to the lazy loading of pages, it is also possible to add them directly. This is not recommended, as it doesn't provide any advantages over lazy loading.

  1. Create the new page component. Page components are usually located in a pages/ folder structure
  2. Add the new page to the pages-mapping
PfePageMappingService.addPageToMap(
  pages,
  getConfigPropertyName('examplePageConfig'),
  ExamplePageComponent
);

3. Interaction with the PFE API

The PfeBusinessService is the single point of contact for interaction with the PFE in code. Usually, it is not necessary to use any other services from the PFE directly.

It provides access to the global state, data like the current page ID and allows it to trigger functionalities like a service activator or a navigation.

4. Page Flow Configuration Model

The NGX-PFE configuration consists of two parts:

  • The configuration of the page content within the flow
  • The navigation configuration, that defines the order of the pages in a flow. There is also a graphical editor available for the navigation configuration.

4.1. Configuration of a page flow

The PfeNavigationConfiguration interface defines the configuration that is used, to determine the next page.

This configuration is stored as the navigation.json file. It can also be edited with the graphical editor.

The Configuration Structure

Let's take a closer look at the configuration structure of the navigation.

{
  // The errorPageNavigation can be used for a global error handling.
  // It can catch all errors that are not handled by more local error handling.
  "errorPageNavigation": [
    {
      "nextPageId": "globalErrorPage"
    }
  ],
  "pages": [
    {
      // Every page has to have a unique ID. It is used in the navigation by other pages and to
      // link this navigation to a page configuration, which defines the content.
      "pageId": "welcomePage",
      // The nextOptionList contains a list of pages this page can navigate to
      // when the user clicks on next in the journey.
      // The entries of the list are checked in the defined order. The first one with matching conditions is
      // then selected as the one to navigate to.
      "nextOptionList": [
        {
          // The ID of the next page to navigate to
          "nextPageId": "additionalInformationPage",
          // Navigation conditions can be used to only navigate to a certain page
          // if the conditions are fulfilled.
          // They can access the state of the application. This makes it possible
          // to react to user input.
          "conditions": [
            {
              "type": "PFE_EXPRESSION",
              "value1Expression": "$.additionalSupport"
            }
          ]
        },
        {
          // This entry in the nextOptionList does not define any conditions.
          // If none of the previous entries' conditions are met then this entry will be chosen.
          "nextPageId": "personalDetailsPage"
        }
      ]
    },

    {
      "pageId": "additionalInformationPage",
      "nextOptionList": [
        {
          "nextPageId": "personalDetailsPage"
        }
      ]
    },

    {
      "pageId": "personalDetailsPage",
      // If the nextOptionList is empty, or omitted, the user won't be able to navigate forward.
      // This does not influence the visibility of the next navigation button, which can
      // be done via the hideNextButton flag of a page configuration.
      "nextOptionList": []
    }
  ]
}

Backwards Navigation

The navigation back through the flow, can either be configured with conditions or the automatic history can be used. The automatic history simply navigates back through the visited pages. It is possible to omit certain pages from the history, which would cause them to be skipped on the back navigation.

  • An empty configuration (backOptionList: []) will prevent a navigation
  • A non existing backOptionList, will use the history
  • If there is a backOptionList configured and none of the options matches, no navigation will be done. The backOptionListFallBackToHistory flag can be set in the application configuration to activate a fallback to the history in this case.

One navigation configuration can also contain multiple separate or interconnected flows.

4.2. How the First Page of a Flow is Determined

The pfe follows this process during startup of an application to determine the first page to be displayed:

Oder of execution in the flow

An example for a firstPage configuration could look like this:

{
  "firstPage": {
    // The nextOptionList in the firstPage configuration is exactly the same
    //  as in the pages navigation
    "nextOptionList": [
      {
        "nextPageId": "pageA",
        // Conditions can be used to dynamically determine the first page to be displayed
        "conditions": [
          {
            "value1Expression": "$.dataFromAServiceActivator"
          }
        ]
      },
      {
        // An entry without conditions acts as the fallthrough default.
        "nextPageId": "pageB"
      }
    ],
    // The onEnterActions are ran, before the first page is determined.
    // It is also possible to trigger asynchronous service activators during
    // startup of the application. These will then continue to run in the background
    "onEnterActions": [
      {
        "type": "TRIGGER_SERVICE_ACTIVATORS",
        "serviceActivators": [
          {
            "globalConfigId": "aServiceActivator"
          }
        ]
      }
    ]
  }
}

4.3. Navigation Nodes

Usually, an entry in the navigation is always tied to a page with content. Additionally to that, there is support for "navigation nodes". These are entries in the navigation configuration that are not tied to a visible page. They are invisible to the user.

This makes it possible to use them as navigation gateways that immediately redirect the user to another page. The same navigation conditions can be used in navigation nodes as in regular pages. All forward navigation hooks, like onPageEnterActions, onPageEnterServiceActivators or onNavigationStartActions are supported by navigation nodes. Backwards navigation hooks are not supported as navigation nodes are always a forward navigation.

As they can be navigated towards from multiple other pages, they make it possible to re-use a navigation configuration.

The following example shows how pageA and pageB both navigate to the navigationNode page. The navigationNode then redirects the user to pageC or pageD. pageA and pageB do not know anything about pageC and pageD.

Navigation nodes Configuration

{
  "pages": [
    {
      // pageA and pageB both navigate to the navigationNode entry
      // of the configuration. That means they essentially share the nextOptionList
      "pageId": "pageA",
      "nextOptionList": [
        {
          "nextPageId": "navigationNode"
        }
      ]
    },
    {
      "pageId": "pageB",
      "nextOptionList": [
        {
          "nextPageId": "navigationNode"
        }
      ]
    },
    {
      "pageId": "navigationNode",
      // The entry in the navigation is marked as a navigationNode
      // It is not necessary to create an entry in the pagesConfiguration for it.
      // The entry therefore is a pure navigation node/page, which is invisible.
      "navigationNode": true,
      // The nextOptionList works exactly the same as for visible pages.
      // If another page, for example pageA, navigates to the
      // navigationNode, the nextOptionList is evaluated and
      // a navigation to the actual target page is triggered.
      "nextOptionList": [
        {
          "nextPageId": "pageC",
          "conditions": [
            {
              "value1Expression": "$.aValueInTheState"
            }
          ]
        },
        {
          "nextPageId": "pageD"
        }
      ]
    },
    {
      "pageId": "pageC"
    },
    {
      "pageId": "pageD"
    }
  ]
}

5. How to Serve the Configuration

The configuration can either be fetched from a remote config service or be provided with the NGX_PFE_FLOW_CONFIGURATION injection token.

5.1. Injection Token

The NGX_PFE_FLOW_CONFIGURATION injection token can be used to provide the flow configuration.

The configuration data model follows the same one as in the json files. The NgxPfeConfig. The generics of this data model can be used to support custom app/page/action config data models.

It is possible to import the config from a json file. Another way is to directly define it in the code:

const myConfig: NgxPfeConfig<AppConfiguration, MyPageConfig, MyActionConfig> = {
  navConfiguration: <the navigation config>,
  pagesConfiguration: <the page configurations>,
  appConfiguration: {
    pfeConfig: <the pfe config>
  },
};

It can then be used in the providers of the module, where the NgxPfeModule is also imported:

providers: [
  { provide: NGX_PFE_FLOW_CONFIGURATION, useValue: Promise.resolve(exampleConfig) }
],

5.2. Config Service

There are two solutions available to serve the configuration:

The NGX-PFE Config Aggregator

The config aggregator transforms the config into static files that can be served by any webserver/CDN. It also provides the possibility to manage multiple tenants with a multi or single branch model.

The Config Service

The config service is a java based service that fetches the configuration directly from github. It also automatically updates the configuration within a configurable timeframe.

It is recommended to use the Config Aggregator instead as the config service is in a deprecated state.

Local Development

The pfe-nodejs-mock is available for local development.

5.3. Add Local Example Config via Token

Create a new file called pfe-config.ts with the following content:

import { AppConfiguration, NgxPfeConfig, PfeActionConfig } from '@allianz/ngx-pfe';
import { MyPageConfig } from './page-config.model';

export const pfeConfig: NgxPfeConfig<AppConfiguration, MyPageConfig, PfeActionConfig> = {
  navConfiguration: {
    pages: [
      {
        pageId: 'examplepage',
      },
    ],
  },
  pagesConfiguration: [
    {
      pageId: 'examplepage',
      examplePageConfig: {
        aConfigurationAttribute: 'example',
      },
      hideBackButton: true,
    },
  ],
  appConfiguration: {
    pfeConfig: {
      nextButtonLabel: 'Next',
      backButtonLabel: 'Back',
    },
  },
};

Now the example app can be run with npm run start

Add Local Example Config via Config API Endpoint

Alternatively, it is also possible to supply the configuration via the config api endpoint.

Uncomment the config api configuration in the pfe-integration.module.ts and remove the NGX_PFE_FLOW_CONFIGURATION token.

Then add a config file under src\assets\config\tenant\app\index.json with the following content:

{
  "navConfiguration": {
    "pages": [
      {
        "pageId": "examplepage"
      }
    ]
  },
  "pagesConfiguration": [
    {
      "pageId": "examplepage",
      "examplePageConfig": {
        "aConfigurationAttribute": "example"
      },
      "hideBackButton": true
    }
  ],
  "appConfiguration": {
    "pfeConfig": {
      "nextButtonLabel": "Next",
      "backButtonLabel": "Back"
    }
  }
}

6. Add a Second Page to the Example App

6.1. Component Setup

As we also want to lazy load the second page, a module has to be generated:

npx ng generate @schematics/angular:module --name=SecondPage --no-interactive

Then, generate a component for the second page:

npx ng generate @schematics/angular:component --name=SecondPage --module=second-page --style=scss --no-interactive

Open the second-page.module.ts and replace the content:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SecondPageComponent } from './second-page.component';

@NgModule({
  declarations: [SecondPageComponent],
  imports: [
    CommonModule,
    RouterModule.forChild([
      {
        path: '',
        component: SecondPageComponent,
      },
    ]),
  ],
})
export class SecondPageModule {}

Add a second-page.route.ts for the routing:

export const secondPageRoute = {
  loadChildren: () => import('./second-page.module').then((m) => m.SecondPageModule),
};

Add a second-page.model.ts file for the config model:

export interface SecondPageConfig {}

6.2. Configuration and Routing Setup

Add the page config data model to the global app configuration in the page-config.model.ts:

import { PageConfig } from '@allianz/ngx-pfe';
import { ExamplePageConfig } from './example-page/example-page.model';
import { SecondPageConfig } from './second-page/second-page.model';

export interface MyPageConfig extends PageConfig {
  examplePageConfig?: ExamplePageConfig;
  secondPageConfig?: SecondPageConfig;
}

Add the routing to the pages-mapping.ts:

PfePageMappingService.addLazyLoadedPageToMap(pages, getConfigPropertyName('secondPageConfig'), secondPageRoute);

Now the page can be used in the pfe-config.ts for a simple navigation configuration:

import { AppConfiguration, NgxPfeConfig, PfeActionConfig } from '@allianz/ngx-pfe';
import { MyPageConfig } from './page-config.model';

export const pfeConfig: NgxPfeConfig<AppConfiguration, MyPageConfig, PfeActionConfig> = {
  navConfiguration: {
    pages: [
      {
        pageId: 'examplepage',
        nextOptionList: [
          {
            nextPageId: 'secondPage',
          },
        ],
      },
      {
        pageId: 'secondPage',
      },
    ],
  },
  pagesConfiguration: [
    {
      pageId: 'examplepage',
      examplePageConfig: {
        aConfigurationAttribute: 'example',
      },
      hideBackButton: true,
    },
    {
      pageId: 'secondPage',
      secondPageConfig: {},
      hideNextButton: true,
    },
  ],
  appConfiguration: {
    pfeConfig: {
      nextButtonLabel: 'Next',
      backButtonLabel: 'Back',
    },
  },
};

results matching ""

    No results matching ""