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 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. |
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.
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 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();
}));
});
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.
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);
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.
💡 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 {}
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.
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.
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.
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.
pages/
folder structurePfePageMappingService.addPageToMap(
pages,
getConfigPropertyName('examplePageConfig'),
ExamplePageComponent
);
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.
The NGX-PFE configuration consists of two parts:
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.
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": []
}
]
}
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.
backOptionList
: []
) will prevent a navigationbackOptionList
, will use the historybackOptionList
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.
The pfe follows this process during startup of an application to determine the first page to be displayed:
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"
}
]
}
]
}
}
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
.
{
"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"
}
]
}
The configuration can either be fetched from a remote config service or be provided with the NGX_PFE_FLOW_CONFIGURATION
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) }
],
There are two solutions available to serve the configuration:
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 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.
The pfe-nodejs-mock is available for local development.
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
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"
}
}
}
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 {}
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',
},
},
};