The PfeBusinessService
is used to store and retrieve data in the application PfeStateService
It also provides observables for keys/expressions in the state.
The state can be automatically stored in the backend on every page switch. There is a spring boot starter plugin available that stores it in a redis database.
There is also a version of the state service that stores the states in a Dynamodb
By default the remote state is only updated with a specific Action. The enableAutomaticStateStorage
flag in the module config allows it to enable an automatic update after every page navigation.
It is also possible to disable the state storage after a certain page in the flow. This can be used to exclude sensitive data from the backend storage.
If an application has a need for a more complex state handling, it is possible to provide a custom implementation of these services.
To access values in the PFE state or any other JSON structure (like Service Activator responses) you will be using "expressions". These expressions are built on JSONPath, a querying language for JSON similar to XPath for XML. The expressions you can use in the PFE configuration exactly follow the rules of JSONPath expressions so please take a look at the documentation available at the previous link or the corresponding npm package.
If the result of your query is a single value then the PFE will return that value. This is not surprising when you imagine queries that access a single property in the state like $.myCustomKey
.
In case you have a more generic query that includes more advanced operators like "array slice" or "recursive descendant" than you will typically expect to get an array back. However, in this case the mentioned logic applies as well: If the result is a single value than you will receive that single value.
Let's look at an example to see why this matters:
Imagine our state contains an array of numbers at $.numbers
. The expression $.numbers[::2]
can be used to query every second number (take 1, skip 1, take 1, skip 1, ...). If we only have 2 numbers in the state this query will only yield a single number (the first one). By default all arrays that contain only a single element will be unwrapped by the PFE so as a result you would receive a single number (i.e. a return value of type number
) - even though you asked for an array of numbers. If, however, the state contains 3 numbers that same query would yield an array with 2 elements. That means that, depending on the PFE state, you would receive a different type of data (number
vs. Array<number>
) which might add complexity to the receiving end of that query.
As shown there are certain expressions where it's reasonable to always expect an array. To overcome the default unwrapping-behavior you can instruct the PFE to keep the result as an array. To do so use the special array root []$
in your expression. For the above example: The query []$.numbers[::2]
will always result in an array.
If the result of a query is already an array than this syntax won't wrap it inside another array, so you don't need to fear nested arrays.
💡 This affects all queries. So even the return value for a very specific query like
$.myCustomKey
can be turned into an array using the array syntax:[]$.myCustomKey
.
There is a possibility to update the state storage from the config, for that purpose you can define a list of updates that can be performed. It is also possible to apply those only with certain conditions.
It is possible to update the the states values as part of the nextOptionList & backOptionList, as you can see in the example:
{
pageId: 'secondPageID',
nextOptionList: [
{
nextPageId: 'thirdPageID',
updateStateValues: [
{
key: `$.user`,
value: 'Tobias',
}
]
}
],
backOptionList: [
{
nextPageId: 'welcomePage',
updateStateValues: [
{
key: `$.user`,
operation: StateOperations.REMOVE
conditions: [
{
value1Expression: '$.inputFieldID',
operator: '===',
value2Expression: 'reset'
}
],
}
]
}
]
}
The same behavior is available as part of the page config.
const examplePageConfig: DemoAppPageConfig = {
pageId: 'welcomePage',
hideNextButton: false,
hideBackButton: true,
updateStateValues: [
{
key: `$.welcomePageReached`,
value: true,
},
],
examplePageConfig: {
firstConfigValue: 'This is the Page Title from the Configuration',
secondConfigValue: 'Another text from configuration',
},
};
Further examples of more complex configurations can be found in the viewer app of the pfe.
this.pfeBusinessService.storeValueByExpression('$.mykey', 'value');
this.pfeBusinessService.getValueByExpression('$.myKey');
this.pfeBusinessService
.getObservableForExpressionKey('$.mykey').subscribe(data => ...)
Service activators can be used to do http calls to the backend via pure configuration.
Service Activators can also be triggered via PFE Actions.
Documentation of the configuration
The configuration of service activators is also supported by the config editor:
{
"pageId": "summary",
"onPageLeaveServiceActivators": [
{
"path": "mo-bff/contract/process",
"responseDataMapping": [
{
"responseDataExpression": "$.contractNumber",
"stateKeyExpression": "$.contractNumber"
}
]
}
]
}
By default, a failure in a service activator call results in a redirect onto the global error page of the app. A failure is defined as any HTTP error response.
In general, the error handling can be configured in these locations:
The service activator configuration supports the definition of certain errorCases
and how they should be handled.
This is done in the errorHandling attribute within the service activator configuration. The configuration itself defines an array of errorCases.
Every ErrorCase handling can define conditions that determine if it should become active or not.
The first valid errorCase
out of all configured ones is then executed.
The execution of an error case follows these steps:
errorResponseDataMapping
configured in the errorCase
. This allows it to save data from the payload of the response into the state. It works the same as the responseDataMapping
for successful service activators.actions
that are configured in the errorCase
. This makes it possible to handle more specific behavior that is not covered by the standard configuration. The result of the action, directly translates to the behavior of the error handling. If the action returns true, the behavior is the same as with the ignoreError
flag. If it returns false, the behavior is the same as with the disableErrorPageNavigation
flag. An action can also trigger a custom navigation in this case. Alternatively, it is possible to ignore the action result with the ignoreActionsResult
flag.ignoreError
flag). In this case, no further error handling is done and the service activator behaves the same as a successful one.disableErrorPageNavigation
flag). For example: If the disableErrorPageNavigation
is set for the error case of a service activator, that was triggered in the onPageLeave*
, the navigation would be aborted and the user would stay on the current page.errorPageNavigation
of the service activator is used to determine the error page to go to. This navigation works similar to a standard navigation, but is able to access the payload of the error response.errorPageNavigation
configuration, a fallback to the global error page is done.A simple example configuration for a service activator with one error case could look like this:
{
"onPageLeaveServiceActivators": [
{
"path": "errorServiceActivator",
"errorHandling": {
"errorCases": [
{
"conditions": [
{
"value1Expression": "$.response.status",
"operator": "==",
"value2Expression": "404"
}
],
"disableErrorPageNavigation": true
}
]
}
}
]
}
In this example, the service activator runs into a 404
response.
It is triggered when the page is left (onPageLeaveServiceActivators
)
As the disableErrorPageNavigation
flag is set, the user will stay on the page and the navigation will be aborted in case of the 404
response.
errorCase
The configuration of the errorCase conditions are the same as the standard navigation conditions. They do however have a few extra gimmicks.
The state is only available under $.state
in these conditions. The error response is available under $.response
,
which means that the response body for error responses is available under $.response.error
.
The response headers are available under $.responseHeaders
The data model for this can be found here: ErrorCaseConditionsState.
They are converted into this structure: [index: string]: string[]
The background for this is, that response headers can have duplicates. So the same header could have multiple values.
An example could look like this:
Response headers:
status: 400,
statusText:'error'
error: "Bad Request",
type: "REJECTION",
type: "this is a duplicate value of the type header"
message: "Http failure response for /path: 400 error",
These response headers are then converted to the following data model and provided to the conditions:
{
"type": ["REJECTION", "this is a duplicate value of the type header"],
"anotherHeader": ["something"]
}
In this example, we only want the error case to become active, if the type
header is set, with the value REJECTION
.
If this value is not available, the error case should not become active.
This can be done with the following condition/expression:
{
"value1Expression": "$.responseHeaders.type[?(@==\"REJECTION\")]"
}
This expression checks the values of the type
header, if the value REJECTION
is present.
To clarify, the following condition would lead to the same result. The extracted valued from the expression is REJECTION
. This value is seen as truthy in the conditions.
{
"value1Expression": "$.responseHeaders.type[?(@==\"REJECTION\")]",
"operator": "==",
"value2Expression": "REJECTION"
}
A full example of a service activator call with a more complex error handling of errors could look like this:
{
"onPageLeaveServiceActivators": [
{
"path": "endpoint",
"errorCases": [
{
"conditions": [
{
"value1Expression": "$.response.status",
"operator": "==",
"value2Expression": "400"
},
{
"value1Expression": "$.responseHeaders.type[?(@==\"REJECTION\")]",
"operator": "==",
"value2Expression": "REJECTION"
}
],
"errorPageNavigation": [
{
"nextPageId": "errorPage"
}
]
},
{
"conditions": [
{
"value1Expression": "$.response.status",
"operator": "==",
"value2Expression": "400"
}
],
"disableErrorPageNavigation": true
}
]
}
]
}
This example configuration has the effect, that the user will only be navigated to the errorPage
if the 400
response also contains the type
header with the value REJECTION
.
In all other cases, the navigation is stopped and the user stays on the page.
Actions that run within an error case can also influence the result of the error handling. If one of the actions returns a negative result (false
), the error is thrown up. This has the same effect as the disableErrorPageNavigation
flag.
An example for a custom errorPageNavigation
could look like this:
{
"onPageLeaveServiceActivators": [
{
"path": "endpoint",
"errorCases": [
{
"conditions": [
{
"value1Expression": "$.response.status",
"operator": "==",
"value2Expression": "500"
}
],
"errorPageNavigation": [
{
"nextPageId": "myCustomErrorPage",
"conditions": [
{
"value1Expression": "$.response.error.thisServer",
"operator": "==",
"value2Expression": "isCrashed"
}
]
},
{
"nextPageId": "fallbackErrorPage"
}
]
}
]
}
]
}
The navigation to the myCustomErrorPage
only happens if the response body contains the text isCrashed
in the field thisServer
for the response with the status code 500.
If that is not the case, the default fallbackErrorPage
is used.
The errorPageNavigation configuration can be used to define conditions that determine the error page that should be displayed.
Example for such a configuration:
{
"navConfiguration": {
"errorPageNavigation": [
{
"nextPageId": "teapotError",
"conditions": [
{
"value1Expression": "$.response.status",
"operator": "===",
"value2Expression": "418"
}
]
},
{
"nextPageId": "defaultErrorPage",
}
]
}
}
The conditions to determine if a navigation to an error page should happen are similar to the NavOptionConfig conditions. But in difference to those, the PFE state is only available under $.state in the error page navigation conditions.
Additionally the response data is available:
$.response
$.response.status
$.response.error
$.responseHeaders
The errorResponseDataMapping
in the errorCases
configuration of a specific service activator can be used to write the error response data to the state before the error page navigation is triggered.
The user will only be able to navigate, once the service returned a non-error response
{
"path": "some-service/validate",
"serviceActivatorMethod": "POST",
"preventNavigationOnError": true,
"requestDataMapping": [
{
"requestDataExpression": "$.fieldToValidate",
"stateKeyExpression": "$.userdata.fieldToValidate"
},
{
"requestDataExpression": "$.anotherFieldToValidate",
"stateKeyExpression": "$.userdata.anotherFieldToValidate"
}
],
"responseDataMapping": [
{
"responseDataExpression": "$",
"stateKeyExpression": "$.someService.validationStatus"
}
],
"serviceActivatorErrorHandlingExcludeStatusCodes": [404, 400],
"serviceActivatorResponseStatusHandlers": [
{
"statusCode": 400,
"stateKeyExpression": "$.validationErrorData",
"stateValue": "common.validation.formatError"
},
{
"statusCode": 404,
"stateKeyExpression": "$.validationErrorData",
"stateValue": "common.validation.notFoundError"
}
]
}
While it is recommended to rely on the trigger points of the navigation flow, it is also possible to trigger service activators directly in the code.
The PfeBusinessService
provides the triggerSingleServiceActivator() for this.
It can be used like this:
this.pfeBusinessService
.triggerSingleServiceActivator('serviceActivatorID')
.then((response) => {
// The full HTTP response of the service activator is available here
})
.catch((error) => {
// The full HTTP error response of the service activator is available here
});
By default, the PFE error handling is active for every service activator. That means an automatic navigation to the error page is triggered.
It might be desirable to stay on the current page, for example if the error is supposed to be handled manually in the code of the catch block. This can be done, by disabling the navigation to the error page for the service activator in question. For example:
{
"errorServiceActivator": {
"path": "http://example.com/this-is-a-404",
"serviceActivatorMethod": "GET",
"errorHandling": {
"errorCases": [
{
"disableErrorPageNavigation": true
}
]
}
}
}
The PFE Actions are a set of actions that the PFE can execute on certain events, like a user entering or leaving, a page. The actions can then trigger Service Activators, state values updates and more. The benefits is, that everything can be mixed and it is also possible to add custom actions in an app using the PFE.
A custom action can be any function within an app.
Actions can also be triggered directly in the code via the PfeActionsService
For example, if a service activator configuration looks like this:
{
"pageId": "examplePage",
"onPageEnterServiceActivators": [
{
"globalConfigID": "exampleServiceActivator"
}
]
}
It will look like this, with the pfeActions:
{
"pageId": "examplePage",
"onPageEnterActions": [
{
"type": "TRIGGER_SERVICE_ACTIVATORS",
"serviceActivators": [
{
"globalConfigID": "myID"
}
]
}
]
}
An updateStateValues
configuration can also be handled with an action.
This configuration:
{
"pageId": "examplePage",
"updateStateValues": [
{
"key": "$.foo",
"value": "bar"
}
],
"pageConfig": {}
}
Would turn into this action:
{
"pageId": "examplePage",
"onPageEnterActions": [
{
"type": "UPDATE_STATE_VALUES",
"stateUpdates": [
{
"key": "$.foo",
"value": "bar"
}
]
}
]
}
onNavigationStartActions can use conditions to determine the target page of a navigation. Dependent on this, they can be executed or not.
The necessary information is available with the $._pfe.navigationState.navigatingTo
expression in the state.
See also the Order of Execution
chapter for more information
{
"pageId": "navigationActionsPage",
"onNavigationStartActions": [
{
"type": "BLOCKING_ACTION",
"message": "Action: Shall we continue to the dead end?",
"conditions": [
{
"value1Expression": "$._pfe.navigationState.navigatingTo",
"operator": "==",
"value2Expression": "deadEndPage"
}
]
}
]
}
onPageEnterActions can check against the previous page in their conditions.
{
"pageId": "onPageEnterActionsPage",
"onPageEnterActions": [
{
"type": "BLOCKING_ACTION",
"message": "Welcome back from the dead end!",
"conditions": [
{
"value1Expression": "$._pfe.navigationState.navigatingFrom",
"operator": "==",
"value2Expression": "deadEndPage"
}
]
}
]
}
This information is not available (undefined
) when the previous page is outside the pfe flow.
The pfe provides a few default actions:
UPDATE_STATE_VALUES
: This action provides access to the state updates features as an action. See also State Storage
section in the documentation.TRIGGER_SERVICE_ACTIVATORS
: This action can be used to trigger service activators and works exactly the samePFE_UPDATE_STATE_ON_BACKEND
: Trigger the push of the state towards the backend. This is identical to the to the backend stateService functionalityPFE_REWIND_HISTORY
: Allows it to rewind the history. Documentation of the parameters and examples on the usage can be found herePFE_RESET_STATE
: This action makes it possible to completely reset the statePFE_RELOAD_PAGE
: Makes it possible to reload the whole pageThere are also 2 optional actions that are not registered by default, but can be imported in an app:
PFE_NESTED_ACTIONS
: Allows it to nest actions within other actions, within other actions...PFE_NAVIGATE
: Trigger a navigation to a pageIdAn action simply triggers a function in the code.
Follow these steps, to add a custom action:
Every action defines a custom configuration data model.
The data model consists of two parts:
The type is used for the mapping of the configuration to the actual action.
The configuration data model is also integrated into the json-schema. This makes it possible to validate the configuration and to provide autocomplete for available attributes in the configuration.
The model extends the PfeBaseActionConfig
.
servus.model.ts
import { PfeBaseActionConfig } from '@allianz/ngx-pfe';
export const ServusActionType = 'SERVUS';
export interface PfeServusActionConfig extends PfeBaseActionConfig {
type: typeof ServusActionType;
/**
* Comments in the configuration model will also be a part of the json-schema.
*/
name: string;
}
An action is simply a function that is triggered by the pfe.
While not required, it is common practice to locate the function within an Angular service, which also makes it possible to access other dependencies.
An action function needs to implement the PfeActionFunction
, which looks like this:
export type PfeActionFunction = (actionConfig: PfeBaseActionConfig) => Promise<void | boolean>;
It needs to return a Promise. The pfe will wait for this promise to be resolved. Actions are ran one after each other in the configured order.
The promise itself can either be empty (void
) or contain a boolean. If it contains a boolean, the result will be used to determine
if the navigation should be continued. If it is false, the navigation is aborted.
A void
response is assumed to be true
, which will allow the navigation to continue.
The mechanism and behavior of this is similar to Angular route guards.
This means, if an onPageLeave
stops the navigation and doesn't trigger anything else, the user will just stay on the current page and can try it again.
It is also possible to trigger another navigation from within the action.
Actions have one parameter, the actionConfig
. It contains the configuration of the action, which follows the defined data model.
servus.service.ts
import { Injectable } from '@angular/core';
import { PfeServusActionConfig } from './servus.model';
@Injectable({
providedIn: 'root',
})
export class PfeServusService {
constructor() {}
public executeServus = async (actionConfig: PfeServusActionConfig): Promise<void> => {
console.log(`Servus ${actionConfig.name}`);
};
}
After that, the action needs to be registered with the PfeActionsService.
This can happen at any time before the action is triggered. For example it could be done in the app.module
constructor.
import { PfeActionsService } from '@allianz/ngx-pfe';
import { PfeServusService } from './servus.service';
import { ServusActionType } from './servus.model';
...
constructor(
private pfeActionsService: PfeActionsService,
private pfeServusService: PfeServusService
) {
this.pfeActionsService.registerAction(ServusActionType, this.pfeServusService.executeServus)
}
The action can now be used in the navigation.json
:
{
"onPageEnterActions": [
{
"type": "SERVUS",
"name": "Alvaro"
},
{
"type": "SERVUS",
"name": "Tobi"
}
]
}
Its possible to use global actions on a very similar way as the global service activators.
It is recommended to use the the ConfigAggregator, which works like this:
Example of global action on file /actions/servus.json
{
"actionId": "servus",
"type": "SERVUS",
"name": "Alexander"
}
And in the navigation json:
{
"onPageLeaveActions": [
{
"globalConfigId": "servus"
},
{
"globalConfigId": "servus",
"name": "Hamilton"
}
]
}
The custom actions can also be reflected in the navigation.json
json-schema.
To do that, we need to create a new file, and then create the json schema from that ``xxx-navigation.model.ts`
import { PageNavigationConfiguration, PfeNavigationConfiguration } from '@allianz/ngx-pfe/models/navigation-config.model';
export type XXXActionConfig = PfeActionConfig | ServusAction;
export interface XXXNavigationConfiguration extends PfeNavigationConfiguration<XXXActionConfig> {}
export interface XXXPageNavigationConfiguration extends PageNavigationConfiguration<XXXActionConfig> {}
TL;DR: Use PFE Actions that return a Promise<boolean>
to intercept a navigation.
There are multiple ways to intercept a navigation:
Actions can be configured in the navigation, whereas route guards are configured on a page level.
âš Careful! âš
If an action stops the navigation from within the firstPage
mechanism, it will automatically fallback to the error page.
Known Issues: There's some known Angular issues around route guards/pfe action guards, if the navigation was triggered with the browser buttons. Dependent on the user interaction, the navigation history might get corrupted. This is especially the case when the forward browser button is used.
Further information:
The pfe already contains workarounds for the backward navigation and these might be extended at some point to cover more cases.
When leaving a page the execution order is:
When entering a page, the execution order is:
The PFE allows to create custom Angular route guards that can be applied to each page in the configuration.
Route guards can trigger on activation or deactivation of a route.
âš Careful! âš The deactivation of a route happens after leave actions like the onPageLeaveAction or onPageLeaveServiceActivator are executed! If these should also be intercepted a pfeAction can be used.
An activation route guard however, runs before the pfe code and can therefore also intercept those actions.
export enum GUARD_KEYS {
PFE_EXAMPLE_CAN_LOAD = 'PFE_EXAMPLE_CAN_LOAD',
PFE_EXAMPLE_CAN_ACTIVATE = 'PFE_EXAMPLE_CAN_ACTIVATE',
}
export interface DemoAppPageConfig extends PageConfig<GUARD_KEYS> {}
This will make it possible to get autocomplete for the guards in the json-schema.
PFE_GUARDS_CONFIG
Injection Token provided by the PFE.import { PFE_GUARDS_CONFIG } from '@allianz/ngx-pfe';
providers: [
{
provide: PFE_GUARDS_CONFIG,
useValue: {
PFE_EXAMPLE_CAN_LOAD: LoadTestGuard,
PFE_EXAMPLE_CAN_ACTIVATE: ActivateTestGuard,
},
},
];
{
"pageId": "pageWithRouteGuard",
"hideNextButton": true,
"myPageType": {
"myConfig": "value"
},
"guards": {
"canActivate": ["PFE_EXAMPLE_CAN_ACTIVATE"],
"canLoad": ["PFE_EXAMPLE_CAN_LOAD"]
}
}
It is possible, to display generic error messages above the currently displayed page.
The master pages checks the state for a configurable key.
If no key is configured, the fallback validationErrorData
is used.
If a value is added to this state key, the message gets displayed. To remove the message, set the key to an undefined/empty value. All values from these keys are translated with ngx-translate.
In combination with the service activators, a response can be written to the state and displayed as a validation message.
Use the serviceActivatorErrorHandlingExcludeStatusCodes configuration in the AppConfiguration PfeConfig to define http status codes that should not be treated as an error that redirects to the error page.
When a page is opened, the status of the page is set to invalid by default. This will also 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.
The PFE supports multiple different URL parameters.
These can be used to switch the tenant/applicationID, to supply a state ID or to transfer parameters into the state.
The configuration is located in the pfeConfig
within the application config (usually application.json
or pfe.json
).
For example:
"pfeConfig": {
"urlParametersInState": [
{
"key": "myCustomURLParameter",
"stateKeyExpression": "$.myCustomURLParameterInTheState"
},
{
"key": "anotherParameter",
"stateKeyExpression": "$.savedInTheState"
}
]
},
Additionally, the library contains the pfe-dev-tools which contain functionality to simplify the creation of a page flow configuration.
These can be integrated in the following way:
<pfe-dev-tools></pfe-dev-tools>
The import PfeDevToolsModule
needs to imported from a secondary entry point:
import { PfeDevToolsModule } from '@allianz/ngx-pfe/pfe-dev-tools';
By default, the development tools have to be activated with the devtools=true
URL parameter.
It is also possible to always activate them:
<!-- prettier-ignore-start -->
<pfe-dev-tools [devToolsActive]="true"></pfe-dev-tools>
<!-- prettier-ignore-end -->
It is recommended, to restrict them to the local/dev/staging environment. This could be done, in the following way:
// prettier-ignore
<div *ngIf="(environment?.env === 'local') || (environment?.env === 'dev') || (environment?.env === 'staging')">
<pfe-dev-tools></pfe-dev-tools>
</div>
The current state can be logged with the following hotkey: [ctrl]+[alt]+[l] or [cmd]+[alt]+[l]
Since version v11.0.9 PFE changes the console.log for the new @allianz/ngx-logger It is possible to supply a custom logging config.