TALY provides the mechanism to integrate plugins in the generated applications through configuration. Plugins usually cover the invisible aspects of an app, whereas Building Blocks are the visible parts. Plugins can be used to provide additional custom functionality to your application like interceptors, special validators, authentication, registering of PFE actions, etc. Developers can either implement their own plugins or re-use third-party plugins.
forRoot
method through which it can receive options. This is true even for plugin modules that don't accept any options.Plugins are configurable. The schema for the configuration can be found here. An app can be configured to use one or more plugin(s) package(s), and for each plugin package, one or more plugins from that package.
Plugins optionally receive data through a plain options object set in the configuration. In addition to static data, it is possible to use angular environment variables (which are passed as parameters to the generators) by using the prefix @environment.
. For example, to use an environment variable with the key BFF_URL
in your plugin options, you would use a string "@environment.BFF_URL"
in your plugin options, like so:
{
"bffUrl": "@environment.BFF_URL",
"...": "..."
}
In case the environment object path (in the example above that would be BFF_URL
) cannot be found in the actual environment object, the value will be kept as specified in the configuration (@environment.BFF_URL
in the example).
🧩 See the plugins recipe for an actual app using this feature.
The interceptors will be executed in the order in which they are provided in the configuration.
Example in the journey configuration:
{
"...": "...",
"plugins": [
{
"package": "my-first-awesome-plugin-library",
"modules": [
{
"name": "PluginAuthInterceptorModule",
"options": {
"foo": "bar-auth",
"auth": {
"tokenType": "Bearer"
}
}
},
{
"name": "PluginOtherInterceptorModule"
}
]
},
{
"package": "my-second-awesome-plugin-library",
"modules": [
{
"name": "PluginLanguageInterceptorModule",
"options": {
"language": "de"
}
}
]
},
{ "...": "..." }
],
"...": "..."
}
Every validator provided via a plugin needs to be additionally configured in the validators
array of the Building Block configuration, as it is already being done when configuring any of the validators provided by TALY.
Example in the journey configuration:
// Plugins configuration
{
"plugins": [
{
"package": "my-team-plugin-lib",
"modules": [
{ "name": "ThaiIdNumberValidatorModule" },
{ "name": "PostalCodeValidatorModule" },
{ "name": "EscapedMaxLengthValidatorModule" }
]
},
{ "...": "..." }
]
}
// Page configuration
{
"id": "form-with-custom-validator",
"blocks": [
{
"id": "form-with-custom-validator",
"selector": "bb-form-with-custom-validator",
"module": "FormWithCustomValidatorModule",
"package": "plugin-recipe-building-blocks",
"validators": [
{
"id": "personalDetails.idNumber",
"type": "PLUGIN",
"name": "PLUGIN_MYTEAM_THAI_ID_NUMBER",
"errorMessage": "Please enter a valid Thai id number, e.g. 6123922063509"
},
{
"id": "personalDetails.idNumber",
"type": "REQUIRED",
"errorMessage": "This is a 'required' error msg from the journey config"
},
{
"id": "personalDetails.postalCode",
"type": "PLUGIN",
"name": "PLUGIN_MYTEAM_POSTAL_CODE"
},
{
"id": "personalDetails.postalCode",
"type": "REQUIRED"
},
{
"id": "personalDetails.streetName",
"type": "PLUGIN",
"name": "PLUGIN_MYTEAM_ESCAPED_MAX_LENGTH",
"validationParam": 14
}
]
}
]
}
When configuring the validator in the Building Block (form control) level, there are a couple of things to consider:
name
field, you need to specify the value of the type
field in the plugin validator implementation. Read here for additional info.validationParam
. Note that this param will be specific for the validation of a concrete form field. A validator plugin might offer less specific ways to set this validation param, see here for more information.TALY provides a generator to add a new plugin to an existing Building Block library. If it is the first plugin in the library, the generator will automatically create a secondary entry point in the Building Block library, and then create the plugin there.
npx nx generate plugin \
--name='New Generated Plugin' \
--project='@allianz/building-blocks-new-business'
💡 Make sure to modify the version in the generated plugins/package.json file to match the version of your Building Block library.
Creating a plugin library as a secondary entry point of a Building Block library is recommended to reduce the number of publishable packages. However, if there is a need for it, it's also possible to manually create a dedicated library for your plugins.
--name
: Name of the generated Plugin--project
: Project name of the Building Block libraryFor details please see the generator's schema file.
A plugin is essentially an Angular module. As stated above each module can optionally receive options via configuration, which will be received by the plugin module as an input parameter in the static forRoot
method. The recommended way to pass these options to the provided service/component is to use a module-specific injection token. If possible we recommend keeping the options optional to not break the app in case users do not provide options for your plugin.
💡 As a Plugin library author, you need to consider that your Plugin library needs to be integrated in AJL Editor. TALY provides a metadata extractor in the form of an Angular Nx executor. Follow the guidelines provided in this section to make your Plugin library compliant with the metadata extractor.
Example of a plugin module that optionally accepts options:
// auth.module.ts
// other import statements
import { AuthInterceptor } from './auth.interceptor';
export const AUTH_INTERCEPTOR_PLUGIN_OPTIONS = new InjectionToken(
'AUTH_INTERCEPTOR_PLUGIN_OPTIONS'
);
export interface AuthInterceptorPluginOptions {
value: string;
}
const DEFAULT_OPTIONS = {
value: 'default'
};
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AuthInterceptorPluginModule {
static forRoot(
options: AuthInterceptorPluginOptions = DEFAULT_OPTIONS
): ModuleWithProviders<AuthInterceptorPluginModule> {
return {
ngModule: AuthInterceptorPluginModule,
providers: [{ provide: AUTH_INTERCEPTOR_PLUGIN_OPTIONS, useValue: options }]
};
}
}
As already mentioned, plugins may use the regular Angular dependency injection mechanisms. They can use this to get ahold of the plugin options as well as other dependencies. Example:
// auth.interceptor.ts
// other import statements
import { BFF_BASE_URL_TOKEN } from '@allianz/taly-core';
import { AUTH_INTERCEPTOR_PLUGIN_OPTIONS, AuthInterceptorPluginOptions } from './auth.module';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
@Inject(AUTH_INTERCEPTOR_PLUGIN_OPTIONS) private options: AuthInterceptorPluginOptions,
@Inject(BFF_BASE_URL_TOKEN) private bffUrl: string
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Interceptor implementation
}
}
TALY provides validators that cover many use cases for apps to validate their forms. In addition, it is possible to provide custom validators via the TALY plugin system to cover your application's specific needs.
💡 Validator plugins rely on
taly-core
. Therefore, if your library contains a validator plugin, please make sure that it specifiestaly-core
as a peer dependency.
Validator plugins provide validators to generated apps via the PLUGIN_VALIDATORS
injection Token, provided by taly-core
. Example:
// thai-id-number-validator.module
// other import statements
import { PLUGIN_VALIDATORS } from '@allianz/taly-core';
@NgModule({
providers: [
{
provide: PLUGIN_VALIDATORS,
useClass: ThaiIdNumberValidator,
multi: true
}
],
imports: [CommonModule]
})
export class ThaiIdNumberValidatorModule {
static forRoot(): ModuleWithProviders<ThaiIdNumberValidatorModule> {
return { ngModule: ThaiIdNumberValidatorModule };
}
}
Similarly to HTTP interceptor plugins, validator plugins can also receive options via configuration. Please check the section for HTTP interceptor plugins for an example of how this can be done.
💡 Notice that when you provide your validator via the
PLUGIN_VALIDATORS
injection token, you have to usemulti: true
, asPLUGIN_VALIDATORS
is meant to contain an array of plugin validators.
A plugin validator class needs to implement one of the following interfaces provided by taly-core
, depending on the validator being sync or async:
export type PluginValidatorType = `PLUGIN_${string}`;
export interface PluginValidator<VALIDATION_PARAM = ValidationParam> {
type: PluginValidatorType;
defaultErrorMessage?: string;
validate: (validationParam?: VALIDATION_PARAM) => ValidatorFn;
}
export interface AsyncPluginValidator<VALIDATION_PARAM = ValidationParam> {
type: PluginValidatorType;
defaultErrorMessage?: string;
validateAsync: (validationParam?: VALIDATION_PARAM) => AsyncValidatorFn;
}
Here is a short description of each field:
type
: this field must only contain uppercase letters and underscore characters and start with the prefix "PLUGIN_"
, otherwise the plugin will not be usable for app implementors. In your workspace, you could set the convention to add the team name and the validator name after the prefix. Examples: PLUGIN_MYTEAM_ESCAPED_MAX_LENGTH
, PLUGIN_MYTEAM_POSTAL_CODE
, PLUGIN_MYTEAM_ID_NUMBER
, etc. You need to use it as error key in the validator function, to make the validator compatible with the validation errors component.
🧩 See a working example in this Building Block in the plugins recipe
defaultErrorMessage
: used as error message if no error message is provided when configuring the validator in the corresponding Building Block configuration for the journey. We encourage you to set up internationalization (i18n) for this property, to ensure that there is a message to communicate to users if the input value is invalid. The translatable error message can also be provided for specific use cases via the journey configuration.
validate
/ validateAsync
: the validation function for a sync or an async validator, respectively. If necessary, these functions can be implemented to receive almost any value as a validation parameter. The plugin implementors can provide more strict typing for the validation parameter by adding their own generic type to the plugin validator interfaces.
💡 A plugin validator requiring a validation param could also support alternative, less specific ways to set this param by setting an internal default value for the param, and even allow app implementors to use plugin options to override this internal default value in the plugin configuration.
🧩 An example for such a validator plugin can be found here.
This is an implementation example of a validator plugin:
// thai-id-number.validator.ts
// other import statements
import { PluginValidator, PluginValidatorType } from '@allianz/taly-core';
declare var $localize: any;
@Injectable({ providedIn: 'root' })
export class ThaiIdNumberValidator implements PluginValidator {
type: PluginValidatorType = 'PLUGIN_MYTEAM_THAI_ID_NUMBER';
defaultErrorMessage = $localize`:@@validation.error.thaiIdNumber:Please enter a valid Thai id number`;
validate(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
let validLength = false,
validNumericValue = false;
// logic setting the value of "validLength" and "validNumericValue"
if (validLength && validNumericValue) {
return null;
}
return { [this.type]: true };
};
}
}
🧩 For an example of an async validator, please check this plugin in the plugins recipe. Also, you can see in action a validator with a custom validation param in this plugin.
To provide your own custom component(s) for Dynamic Form(s), you need to create a module that provides your component(s) using the CUSTOM_DYNAMIC_FORM_COMPONENT
Injection Token from @allianz/taly-core/dynamic-form
.
This is what a plugin module could look like:
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CUSTOM_DYNAMIC_FORM_COMPONENT } from '@allianz/taly-core/dynamic-form';
@NgModule({
providers: [
{
provide: CUSTOM_DYNAMIC_FORM_COMPONENT,
multi: true,
useValue: {
load: () => import('./my-custom.component'),
componentName: 'MyCustomComponent' // this is a standalone component
}
},
{
provide: CUSTOM_DYNAMIC_FORM_COMPONENT,
multi: true,
useValue: {
load: () => import('./my-other-custom.component'),
componentName: 'MyOtherCustomComponent',
moduleName: 'MyOtherCustomComponentModule' // <- this is only needed if your component is not a standalone component
}
}
]
})
export class MyCustomComponentPluginModule {
static forRoot(): ModuleWithProviders<MyCustomComponentPluginModule> {
return { ngModule: MyCustomComponentPluginModule };
}
}
The custom Dynamic Form component MyCustomComponent
is a standalone Angular component that extends DfCustomComponent
from @allianz/taly-core/dynamic-form
, like so:
import { CommonModule } from '@angular/common';
import { AfterViewInit, Component } from '@angular/core';
import { interval } from 'rxjs';
import { DfCustomComponent } from '@allianz/taly-core/dynamic-form';
import { ValidationErrorsModule } from '@allianz/taly-core/validation-errors';
@Component({
selector: 'app-custom-df-component',
standalone: true,
imports: [CommonModule, ValidationErrorsModule],
template: `<p>This is my custom Dynamic Form component.</p>
<taly-validation-errors
*ngIf="control?.touched"
ngProjectAs="nx-error"
nxFormfieldError
[errorMessages]="validationConfigs"
[controlErrors]="control?.errors"
>
</taly-validation-errors>`
})
export class MyCustomComponent extends DfCustomComponent implements AfterViewInit {
ngAfterViewInit() {
interval(1000).subscribe((value) => this.control?.setValue(value));
}
}
By extending DfCustomComponent
, you get everything you need to start writing your custom Dynamic Form component:
this.config
object that was passed to your componentthis.validationConfigs
object that you can pass to the taly-validation-errors
componentthis.control
instance that you can use to get/set the value of your custom componentand many more... Let your IDE guide you.
To learn how you can use a custom Dynamic Form component in your journey, take a look at the corresponding documentation.
Plugin modules should add some metadata attributes to their markdown file, for the Plugin metadata extractor to write them to the metadata file. These are the supported attributes:
type
: must be set to 'plugin'
for the extractor to recognise the plugin folder as suchtitle
: a human readable title of the Plugindescription
: this field is meant to contain relevant information about the PluginHere an example of how this looks like in the markdown file:
---
type: plugin
title: Thai ID Number Validator
description: A description for Thai ID Number Validator
---
Plugin modules that don't accept any options still need to provide a static forRoot
method. Here is an example of such a module:
// auth.module.ts
// import statements
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class OptionlessAuthPluginModule {
static forRoot(): ModuleWithProviders<OptionlessAuthPluginModule> {
return { ngModule: OptionlessAuthPluginModule };
}
}
For plugins, a recipe is provided in the building-block-platform-recipes repository. This recipe includes:
TALY provides an Angular Nx executor which is responsible for the extraction of metadata from a library containing plugins. This metadata will be written to a plugin-metadata.json
file for the UI editor to consume it and integrate that library.
More information related to this executor can be found in this page.