Plugins

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.

1. Concepts

  • Plugins are defined as Angular modules. To fulfill the concept of a plugin, every module should have a single responsibility.
  • Since our plugins are implemented as Angular modules, they can be used to provide (almost) anything in the generated app that uses them (e.g., interceptors, sync/async custom validators).
  • Plugin modules are encapsulated in packages. A plugin package can contain one or more plugins.
  • Each plugin module needs to provide a static forRoot method through which it can receive options. This is true even for plugin modules that don't accept any options.

2. Configuring Plugins in Journeys

2.1. Introduction

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.

2.2. HTTP interceptors

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"
          }
        }
      ]
    },
    { "...": "..." }
  ],
  "...": "..."
}

2.3. Validators

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:

  • For the name field, you need to specify the value of the type field in the plugin validator implementation. Read here for additional info.
  • If the validation function of the plugin validator uses a configurable parameter to perform the validation, you can specify it in the field 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.

3. Authoring Plugins

3.1. Create a New Plugin and Plugin Library

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.

Options

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

For details please see the generator's schema file.

3.2. Enable the Options Configuration for Your Plugin

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.

3.3. HTTP Interceptor Plugins

Plugin module definition

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 }]
    };
  }
}

Plugin implementation

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
  }
}

3.4. Validator Plugins

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 specifies taly-core as a peer dependency.

Plugin module definition

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 use multi: true, as PLUGIN_VALIDATORS is meant to contain an array of plugin validators.

Plugin implementation

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.

3.5. Dynamic Form Custom Component Plugins

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:

  • the this.config object that was passed to your component
  • a this.validationConfigs object that you can pass to the taly-validation-errors component
  • the this.control instance that you can use to get/set the value of your custom component

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

3.6. Common Plugin Authoring Notes

Metadata attributes in Plugin markdown file

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 such
  • title: a human readable title of the Plugin
  • description: this field is meant to contain relevant information about the Plugin

Here 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 module definition without options

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

4. See plugins in action

For plugins, a recipe is provided in the building-block-platform-recipes repository. This recipe includes:

5. Executor to Extract Plugin Metadata

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.

results matching ""

    No results matching ""