Building Block Development

1. Prerequisites

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.

2. Generating Libraries and Building Blocks

2.1. Adding a New Building Block Library

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'

Options

  • --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.

2.2. Adding a New Building Block and Example with the building-block Generator

This 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'

Options

  • --name: Name of the generated Building Block
  • --project: Project name of the Building Block library

For details please see the generator's schema file.

Scaffolding

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';
}
  1. To enable TALY to properly detect your Building Block it needs a special provider. Use the createBuildingBlockProvider helper function for that.

  2. 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.

  3. 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.

2.3. Add Further Examples to an Existing Building Block with the building-block-example Generator

This 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'

Options

  • --name: Name of the generated Building Block Example
  • --project: project name of the Building Block library
  • --building-block: Existing Building Block name

For details please see the generator's schema file.

3. Setup in Building Block Libraries

3.1. The Journal Executor

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.

3.2. The Introspection Executor

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.

3.3. Usage of TALY Executors in Your Building Block Library

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.

3.4. Building Block Library Assets

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).

4. Building Block Component

4.1. Headline

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.

4.2. Resources and State

Resources: A Building Block's data-dependency

Take a look at the following Building Block:

Shows the Policy Client Information Summary 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);
  }
}

State: What a Building Block delivers

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.

Shows a delivery form Building Block which is using state

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:

  1. Reference (1) : Again we declare that this is an Abstract Building Block for automatic retrieval later (Required)

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

Guardrails for the Building Block State data model

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.

Combining State & Resources

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.

4.3. ACL

Please take a look at the ACL documentation for details on how to enable ACL in your Building Block.

4.4. Lifecycle Methods

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:

Base

  • 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.

4.5. Validation

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 about ChangeDetectorRef, 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.

4.6. Typed Forms

⚠️ 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.

Usage of Typed Form Control

const fromControl = new FormControl('') as FormControlTyped<string>;

Usage of Typed Form Group

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

Usage of Typed Form Array

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);

4.7. Business Events

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 inherited onPageConnection() 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.

4.8. Navigation

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.

4.9. Aquila Channel

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>

4.10. Footnotes

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).

5. Example Data

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.

5.1. How to Manually Add a File for Example Data

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:

  • Filename: The filename must match the other files of your Building Block. It must have an .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.
  • Export: The *.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.

6. The Markdown File of a Building Block

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 Block
  • channel: List of channels (retail and/or expert) where this Building Block applies
  • lob: 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 with
  • deprecated: 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

results matching ""

    No results matching ""