To use any of the generators or executors explained in the following sections you need to be in an Nx workspace with an Nx version that is used by the taly-nx
package. To find out the version and set up a new workspace you can follow the getting started guide.
The library
generator generates a new library for Building Blocks and Plugins in your workspace.
npx nx generate @allianz/taly-nx:library \
--name='@allianz/building-blocks-new-business' \
--category='New Business' \
--prefix='nb'
--name
: Project name of the new Building Block library. We strongly recommend to use the @allianz/
scope and kebab-case for your project name, e.g. @allianz/building-blocks-claim-common
--category
: Name of category you would like future Building Blocks in the library to belong to. It cannot be empty--prefix
: Prefix for the Building Blocks in the library. It must be at least one character without a space.For more information please read the the generator's schema file.
building-block
GeneratorThis generator generates a new Building Block for the given project with the specified name as well as an example for that new Building Block.
npx nx generate @allianz/taly-nx:building-block \
--name='New Generated Building Block' \
--project='@allianz/building-blocks-new-business'
--name
: Name of the generated Building Block--project
: Project name of the Building Block libraryFor details please see the generator's schema file.
The minimal shell of any new Building Block currently looks like this:
import { Component, forwardRef } from '@angular/core';
import { AbstractBuildingBlock, createBuildingBlockProvider } from '@allianz/taly-core';
@Component({
selector: 'bb-kitchen-planner',
template: 'my building block',
providers: [createBuildingBlockProvider(forwardRef(() => KitchenPlannerComponent))]
})
export class KitchenPlannerComponent extends AbstractBuildingBlock<null> {
public id = 'kitchen-planner';
}
To enable TALY to properly detect your Building Block it needs a special provider. Use the createBuildingBlockProvider
helper function for that.
You need to tell Angular that this Component is an actual variant of AbstractBuildingBlock
(extends AbstractBuildingBlock<null>
). Otherwise, your Building Block can't be detected by a Building Block Page
like PFEBuildingBlockPage
.
The ID is actually an @Input()
defined in AbstractBuildingBlock
. It's good to provide an explicit ID, otherwise, there is one automatically generated from the class name (kitchen-planner-component
). The ID is used by a facade to identify the configuration for this Building Block. The Facade also uses it to store the state (if any) in the PFE Storage.
Besides that there is actually nothing more required to create a Building Block.
building-block-example
GeneratorThis generator will add a new example for an existing Building Block.
npx nx generate @allianz/taly-nx:building-block-example \
--name='New Generated Building Block example' \
--project='@allianz/building-blocks-new-business' \
--building-block='New Generated Building Block'
--name
: Name of the generated Building Block Example--project
: project name of the Building Block library--building-block
: Existing Building Block nameFor details please see the generator's schema file.
A TALY executor that will traverse a given library and collect all folders
that contain a Building Block. From that list metadata is then extracted and stored in
a journal.json
file.
The information contains id
, acl
resources, validation
and much more. The collected data is
only emitted but not checked for correctness. We have prepared a journal file schema
which we plan to match against the generated data so we can ensure a valid state of the file.
Use this executor by specifying @allianz/taly-nx:journal
as the executor for your target.
A TALY executor that will extract Resource (Input) and State (Output) metadata from all Building Blocks in a given library.
The resulting introspection.json
is meant to be consumed by the AJL UI editor.
Use this executor by specifying @allianz/taly-nx:introspection
as the executor for your target.
The executors provided by @allianz/taly-nx
package can be used in the project configuration to create custom targets like so:
"targets": {
"...": "...",
"journal": {
"executor": "@allianz/taly-nx:journal",
"options": {
"project": "path/to/your/bb-library",
"outputFile": "path/to/your/journal.json"
}
},
}
You can then call the executor from supported projects:
# single target
npx nx journal @allianz/building-blocks
# or all
npx nx run-many --target=journal --all
🧩 An example setup of TALY executors in a Building Block library can be found here.
When a Building Block library ships its own assets, those assets need to be configured properly in the project configuration (angular.json
/project.json
) of generated applications. To handle this, the TALY generator expects the Building Block libraries to provide a specific file named library.json
in the root of the library. In this file, the root of the library asset folder should be mentioned.
Example:
{
"assets": "assets/fantasy-lib-assets"
}
💡 We recommend to keep all the library assets inside a unique folder like
libs/products/my-fantasy-library/assets/fantasy-lib-assets
. In this way, when these assets are used in the generated application, there won't be any collision between different library assets.
When the generator finds the library.json
file in an installed Building Block library, then it will add these assets to the project that uses this library in the project's configuration (angular.json
or project.json
).
TALY provides the TalyHeadlineComponent
, which can automatically adjust itself to the desired heading level based on a section configuration. It can be either an h2
element when a Building Block is not configured within a section or an h3
element when a Building Block is wrapped within a section.
This headline gives a Building Block the freedom to be placed anywhere on a page as a section or subsection. We highly recommend using this component in combination with the section title for the most flexibility.
The TalyHeadlineComponent
is an Angular standalone component, so it can be imported directly into a Building Block's module.
import { TalyHeadlineComponent } from '@allianz/taly-common/headline';
@NgModule({
imports: [
TalyHeadlineComponent
]
})
Afterwards, it can be used in the template of a Building Block.
<taly-headline i18n>Building Block Headline</taly-headline>
⚠ The
TalyHeadlineComponent
does not support mixing different heading types within a single Building Block. If used multiple times, all instances will be rendered with the same heading type. If your Building Block requires multiple types of headlines, it's likely that your Building Block should only be used as a section. Therefore, please use the NDBX headline instead.
Take a look at the following Building Block:
This is called a Summary and displays values of a previous form based on Resources. Resources are the data that a Building Block needs to function properly. Consider them the read-only "input" of the Building Block. These can be static values like strings or numbers as well as dynamic values based on other Building Blocks' states or other values from the PFE state. The Building Block will receive the configured resources when it is first rendered on the page and then again every time a dynamic resource changes (if there is one).
If you need to modify that data, you can override the method setResources(data)
.
@Component({ ... })
export class KitchenPlannerComponent extends AbstractBuildingBlock {
public id = 'kitchen-planner';
public setResources(data) {}
}
You can now make the assumption what data
should contain. If your Building Block is placed on a PFE Page you will retrieve the resources
value from the Building Block configuration in your journey configuration. If you have declared any PFE Queries, you will receive the normalized/actual values from the PFE state under the given key.
You can put some typing in there to get a proper type for the data
value. Following an example from our Policy Client Information Summary. AbstractBuildingBlock
can receive two generics. One for the State type and a second for the Resources type (The data contract for state and resource needs to be an interface. Otherwise, the introspection will have empty values). We skip the state here and pass a locally defined interface. That way everyone using this component will know what setResources
is actually expected to receive.
You can see that your Building Block always passes the entire resource object. You will never receive partial updates.
interface BuildingBlockResources {
policyClientDetails: PolicyHolder;
}
export class PolicyClientInformationSummaryComponent extends AbstractBuildingBlock<
null,
BuildingBlockResources
> {
setResources({ policyClientDetails }: BuildingBlockResources): void {
console.log('received my data', policyClientDetails);
}
}
In contrast to displaying Building Blocks you also have Building Blocks that generate some data. In many, many cases this will be a form. The form is displayed to a user, the user fills the according fields and the Building Block can then deliver the data provided by the user to the rest of the application. That's the data delivering character of a Building Block. We call it State and it's highly local to the Building Block. You can't have two Building Blocks sharing a single state for example.
Reference: Delivery Method Building Block in the TALY Tutorials Training repository.
This is a Building Block with a form. When used you want two properties: Whatever is entered in the form should be stored in the application state (PFE) for further usage. The storing can happen after every change or before leaving the page. This is something the developer of the Building Block decides. When the user is coming back to this Building Block TALY will restore any previous state of it.
Here is how you would create such a form. We use an interface (KitchenPlannerState
) directly for the state to have a proper typing.
import { Component, OnInit, forwardRef, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup } from '@angular/forms';
import { distinctUntilChanged } from 'rxjs/operators';
import { AbstractBuildingBlock, createBuildingBlockProvider } from '@allianz/taly-core';
// Reference (2)
export interface KitchenPlannerState {
firstName: string;
lastName: string;
}
@Component({
selector: 'bb-kitchen-planner',
template: '<form [formGroup]="formGroup"></form>',
styleUrls: ['kitchen-planner.component.scss'],
providers: [createBuildingBlockProvider(forwardRef(() => KitchenPlannerComponent))] // Reference (1)
})
//Reference (2)
export class KitchenPlannerComponent
extends AbstractBuildingBlock<KitchenPlannerState>
implements OnInit
{
form = new FormGroup({
person: new FormGroup({
firstName: new FormControl('', { nonNullable: true }),
lastName: new FormControl('')
})
});
private destroyRef = inject(DestroyRef);
override ngOnInit(): void {
this.setupFormChangesSubscriptions();
}
private setupFormChangesSubscriptions(): void {
// Reference (6)
this.form.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.stateChanged());
// Reference (7)
this.form.statusChanges
.pipe(takeUntilDestroyed(this.destroyRef), distinctUntilChanged())
.subscribe(() => this.checkCompletion());
}
// Reference (7)
private checkCompletion(): void {
if (this.form.valid || this.form.disabled) {
this.commitCompletion();
} else {
this.revertCompletion();
}
}
// Reference (4)
override setState(data: KitchenPlannerState): void {
this.form.patchValue(data);
console.log('State: ', data);
}
// Reference (3)
override getState(): KitchenPlannerState {
return this.form.getRawValue();
}
// Reference (5)
override getForm() {
return this.form;
}
override onPageConnection(): void {
console.log('The Building Block KitchenPlanner got connected to the page');
this.checkCompletion();
}
}
So what's happening here:
Reference (1) : Again we declare that this is an Abstract Building Block for automatic retrieval later (Required)
Reference (2) : We define our state type as an interface: KitchenPlannerState
. This is optional but highly recommended to make sure you know what data is being delivered.
Reference (3) : This is the state that our Building Block produces. That's what's being saved in the PFE state under the key of the ID of the Building Block. We just return our form value as the field names match our Business Model. But you could do any transformation you want at this point. Just make sure you do the inverse transformation in the setState
too. This method will be called automatically after the stateChanged
(Reference (6)) is triggered.
Reference (4) : That's called exactly once after the Building Block appeared on a page. We will receive any previous value and ensure that our form data is synced with it. The Reactive Form will take care to populate all actual html fields accordingly.
Reference (5) : If Building Blocks have a state then the getForm
should be overridden to ensure that several TALY features can function properly. This includes automatic scrolling to the first erroneous control, generating an introspection file, and setting up automatic form tracking.
In addition to fulfilling the interface by overriding the setState
& getState
we also need to call some lifecycle methods to inform any underlying facade about what's happening:
Reference (6) : We listen for value changes on the form to update the state whenever the user changes the data. this.stateChanged();
is crucial ⚠️. You need to call stateChanged
. We won't automatically call this method. If it's not called the state is never stored. Not even on page leave. For a form it's best to use the value changes stream.
Reference (7) : In addition to value changes we also want to know when a form is valid. A Building Block can mark itself as complete or incomplete. Completion is an important concept of a Building Block as we can't rely on explicit button clicks for every single Building Block. You can mark a Building Block complete with the inherited method commitCompletion
. You can make it incomplete again by calling revertCompletion
. Navigation to the next page is only possible if all Building Blocks on the current page are complete.
The interface that describes the data model of the Building Block state has to be an "object". It cannot be an Array or any other "non-object" type.
It also cannot be a typescript "type" as that would interfere with the introspection. (See also the example above)
Additionally to objects, it is possible to use undefined
.
Sometimes you need to combine resources and state of course. It's the case when you have a form and you want to have a Dropdown list that you want to configure. In this case You would define setResources
& setState
/getState
as seen before.
Please take a look at the ACL documentation for details on how to enable ACL in your Building Block.
setResources
and setState
& getState
are the important parts of the interface. You can use them to call code depending on what's happening with your Building Block.
💡 The Building Block lifecycle methods are not connected to the Angular lifecycle. That means your Building Block might not be initialized by Angular when a lifecycle method is called. You need to make sure that everything that is accessed in your lifecycle methods is readily available. In a real-life scenario that means to not instantiate things like forms in
ngOnInit
but instead create them in the Building Block's constructor or directly in the class body - see the above "KitchenPlanner" example.
Here is the list of current lifecycle methods:
onPageConnection()
: Overwrite if you want to execute code on connection.onPageDisconnected()
: Overwrite if you want to execute code on disconnection.stateChanged()
: Invoke whenever you want to save a Building Block's state to the PFE state.setState(value)
: Overwrite to handle incoming states.getState()
: Define the outgoing state.setResources(value)
: Overwrite to handle incoming resource data.Some Building Blocks need a mechanism to validate their form via configuration without explicitly declaring the validators when creating the formGroup
. Note that not all form validation needs to be configurable. You can still add validators directly to the form controls if they are relevant for all use cases of your Building Block.
In order to enable such a validation, you need to provide a validation configuration and empower the Building Block to react accordingly.
The steps to create a validation configuration, and the available validation types are described in the validators section of the journey configuration guide.
To apply the configured validators to your Building Block's form, the setValidationConfiguration
method has to be overridden.
override setValidationConfiguration(data: ValidationConfigItem[]): void {
this.validationMap = applyValidationConfig(this.form, data);
this.cdr.markForCheck();
}
Inside the method, you simply have to call the applyValidationConfig
function. The return value from this method is called the validationMap
. It can be forwarded to the Validation Errors component to display the error message dynamically. Here is an example of how the markup of this component could look:
<taly-validation-errors
nxFormfieldError
[errorMessages]="validationMap?.get('person.lastName')"
[controlErrors]="form.get('person.lastName')?.errors"
>
</taly-validation-errors>
💡 To avoid
ExpressionChangedAfterItHasBeenCheckedError
, you need to trigger the change detection manually as shown in the above snippet. Here's a detailed information about this error and possible ways to resolve them. To learn more aboutChangeDetectorRef
, refer to the angular documentation here.
To prevent moving to the next page with an invalid form, you have to check the validity of the Building Block's formGroup
before committing completion.
private checkCompletion(): void {
if (this.form.valid || this.form.disabled) {
this.commitCompletion();
} else {
this.revertCompletion();
}
}
This method should be called whenever the value of the formGroup
changes.
⚠️ Angular 14 introduced native Typed Forms which made the TALY Typed Forms obsolete. All interfaces and types that are mentioned below are deprecated. Please don't use them. If you are already using them, please migrate to the Angular Typed Forms.
Typed forms are the type-safe Angular reactive forms provided by @allianz/taly-common
including FormControlTyped
, FormGroupTyped
and FormArrayTyped
.
const fromControl = new FormControl('') as FormControlTyped<string>;
Declare an interface, that describes the form
interface Profile {
firstName: string;
lastName: string;
skills: string[];
address: {
street: string;
city: string;
};
}
Create typed form group
const profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
skills: new FormArray([]),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl('')
})
}) as FormGroupTyped<Profile>;
Usage
The biggest benefit from using FormGroupTyped
is that patchValue
, setValue
, reset
, and valueChanges
are type-safe. The IDEs can offer a code-completion for the form properties and can detect the errors immediately if a specified property does not match the interface. However, the get()
method does not return typed control, hence it's required to be manually typed using type assertions.
profileForm.setValue({
firstName: '',
lastName: '',
skills: ['1'],
address: { street: '', city: '' }
});
profileForm.patchValue({ firstName: '' });
profileForm.valueChanges.subscribe((v) => v.firstName);
const requiredErrors: boolean = profileForm.hasError('required', ['address', 'city']);
const city = profileForm.get('address.city') as FormControlTyped<string>;
const address = profileForm.get(['address']) as FormControlTyped<Profile['address']>;
const skills = profileForm.get('skills') as FormArrayTyped<string>;
profileForm.reset({ lastName: '' });
// Do something with those variables
Declare an interface, that describes the form
interface User {
id: number;
name: string;
}
Create typed form array
const users = new FormArray([]) as FormArrayTyped<User>;
Usage
Similar to FormGroupTyped
, the various setter functions of FormArrayTyped
and its valueChanges
are typed. Therefore, the developers can get a full benefit from IDE intellisense and can implement a less error-prone code. The type assertions are also required by getters of FormArrayTyped
since they do not return typed controls.
users.setValue([{ id: 123, name: 'John Doe' }]);
const first = users.at(0) as FormGroupTyped<User>;
first.setValue({ id: 1, name: 'Jane Doe' });
const newUser = new FormGroup({ id: new FormControl(), name: new FormControl('') });
users.push(newUser);
A Building Block can trigger a so called "Business Event". This is a mechanism for the Building Block to signal to the outside world that something specific happened.
When configuring a Building Block in a TALY journey, it is possible to provide a configuration for a Business Event being triggered from that Building Block. For more information about that, please read here.
Building Blocks can trigger a Business Event by running their callBusinessEvent
method, provided by TALY as part of the AbstractBuildingBlock
class. For example:
this.callBusinessEvent('countries-loaded')
.then(() => {
console.log(`Some external logic ran successfully. Awesome!`);
})
.catch(() => {
console.log(`Oh... something went wrong);
});
⚠️ Business Events may only be called if the Building Block is connected to a page. Calling a Business Event from inside your Building Block's constructor or the
ngOnInit()
method will not work. Please use the inheritedonPageConnection()
lifecycle hook of your Building Block if you need to call a Business Event upon initialization of your Building Block.
Based on the TALY journey configuration for the Building Block, the journey will execute a specific logic when processing the Business Event triggered by the Building Block. When that is done, the journey returns the control to the Building Block by resolving or rejecting a promise, depending on the result of the logic executed. If the journey configuration does not provide a setup for a Business Event being triggered by a Building Block, that promise will also be rejected.
Depending on your configuration you may receive the data through setResources
as a result. You can't pass any parameters to the Business Event. This is not an expected behavior as Service Activators are driven by configuration.
Every Building Block can request the navigation to different pages of a journey. By inheriting from AbstractBuildingBlock
, every Building Block has a navigate
method for that purpose. There are 4 types of navigation as shown in this code snippet:
import { BUILDING_BLOCK_NAVIGATION_TYPE } from '@allianz/taly-core';
// ...
@Component({ ... })
export class PgrSimpleComponent extends AbstractBuildingBlock {
navigationExamples() {
// navigate to the first page of the journey
this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Home);
// navigate to the previous page
this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Back);
// navigate to the next page
this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Next);
// navigate to a page that is known to this Building Block as "payment"
this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Page, 'payment');
}
}
💡 The Building Block needs to be configured properly in a journey for the navigation type
BUILDING_BLOCK_NAVIGATION_TYPE.Page
to work. Please take a look at Navigation Configuration for details if you are working on a journey configuration. If a requested page is not configured in the journey a warning will be output to the console and no navigation will happen.
Every Building Block component extends the AbstractBuildingBlock
class. This class provides two properties that can be used to identify the current Aquila channel: isExpertChannel
and isRetailChannel
. Both properties are booleans.
These properties can be used directly in a template to apply different styles to your Building Block based on the active channel.
Here is an example of how to use the isExpertChannel
.
<div [ngClass]="isExpertChannel ? 'expert-header' : 'retail-header'" `>
<!-- header -->
</div>
It's possible to deliver footnotes with a Building Block. These footnotes will be shown at the bottom of the page in the Small Print section of the Frame. We highly recommend using the bc-cc-footnote
component from @allianz/common-building-block-components
to add footnotes to your Building Block. Take a look at the documentation for this common component here (username: itmp
, password: allianz
).
Each Building Block should provide example data for its state and resources. The default
property of the example data is used in "Showroom Journeys". In the future, this file should also contain the data for the Building Block's example(s). Those examples are eventually consumed by the corresponding documentation application.
A Building Block's example data must be provided in a file called [building-block-name].example-data.ts
in this format:
import { BuildingBlockExampleData } from '@allianz/taly-core/showroom';
import { MyBuildingBlockResources, MyBuildingBlockState } from './my-building-block.model';
export const myBuildingBlockExampleData: BuildingBlockExampleData<
MyBuildingBlockState,
MyBuildingBlockResources
> = {
// mandatory: the "default" property holds the default example data for this Building Block.
default: {
state: { '...': '...' }, // optional: object of type MyBuildingBlockState
resources: { '...': '...' } // optional: object of type MyBuildingBlockResources
},
// optional: data for an example with name "anExample".
anExample: {
state: { '...': '...' }, // optional: object of type MyBuildingBlockState
resources: { '...': '...' } // optional: object of type MyBuildingBlockResources
},
// optional: data for another example with name "another-example"
'another-example': { '...': '...' }
};
💡 If you generated your Building Block with the
library
TALY generator then you already have this file set up and can start adding/editing your example data right away.
If you don't yet have a file to provide example data for your Building Block and you want to add it, please comply with these naming requirements:
.example-data .ts
file extension. Example: If you have a file called my-building-block.acl.yml
, then this new file must be called my-building-block.example-data.ts
.*.example-data.ts
file must export an object of type BuildingBlockExampleData<YourStateType, YourResourcesType>
. The name of this export must be the name of your Building Block in camelCase with the ending ShowroomData
. Example: myBuildingBlockExampleData
.You can add a markdown file with documentation and structured metadata to each Building Block. The documentation that you provide here is usually shown alongside your Building Block in your workspace's documentation application. The metadata (in the form of YAML frontmatter) provides meaningful insights in a machine-readable format and is extracted to the journal.json
file using the journal executor.
This markdown file has to have the same file name as your other Building Block files, but without any suffix like .model
or .component
, like in this example:
.
├── ...
├── ctv-claim-progress.component.html
├── ctv-claim-progress.component.ts
└── ctv-claim-progress.md
Take a look at this example markdown file to understand what values can be set:
---
title: My Building Block
channel:
- expert
- retail
lob:
- any
validations:
- required:
- first.control
- second.control
- maxLength:
- first.controls
deprecated: true
design-approved: true
business-events:
- id: my-business-event
description: Description of the event
---
...
title
: The title of a Building Blockchannel
: List of channels (retail
and/or expert
) where this Building Block applieslob
: List of LOB (Line of Business) where this Building Block applies (e.g. motor
)validations
: List of built-in validations that this Building Block ships withdeprecated
: Whether this Building Block is deprecated. When the flag is set in the markdown frontmatter, the deprecated
field in journal.json
is set to true
. Additionally, the text content is added to the deprecationMessage
in journal.json
.design-approved
: Whether the design of this Building Block is approved. The flag is set to true
in journal.json
only if it is defined in markdown and has the value true
.business-events
: List of available business events dispatched by this Building Block