Skip to content

Custom Components with Angular Elements

Travis Tidwell edited this page Nov 8, 2023 · 9 revisions

IMPORTANT NOTE: This feature is only available in the 5.x version of the Angular renderer and below. As of version 6.0, this feature will be officially deprecated. If you wish to still use this concept, you will need to copy the source code provided @ https://github.com/formio/angular/tree/5.5.x/projects/angular-formio/src/custom-component into your own application where it can be integrated within your own application.

NOTE: This is a community contributed feature to this module. Form.io company does not provide support for this feature.

If you have any questions, create a new issue in this repo by choosing "Custom Components Support Request".

Starting from angular-formio v4.3.0 you can register an Angular component as a formio field. It uses the @angular/elements in the background, so that package is required as peer dependency.

Example

Live: https://angular-formio-custom-demo.netlify.com/

Setup: https://github.com/merobal/angular-formio-custom-demo/commit/e1522a6dae006b8994ec79deddcaf010c2b11fd2

Setup

The following setup is tested with desktop Angular app created via Angular CLI. If you use different environment (e.g. Ionic) you may need to setup things differently.

Polyfills

@webcomponents/custom-elements package is required.

After installing it via npm or yarn, add the following line to the angular.json file to the scripts array:

"node_modules/@webcomponents/custom-elements/src/native-shim.js"

And the following line to the app's polyfills.ts file:

import '@webcomponents/custom-elements/custom-elements.min';

Your Component

You should implement the FormioCustomComponent interface and define the required variables. The Component class may look like the following:

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormioCustomComponent } from 'angular-formio';

@Component({
  selector: 'app-rating-wrapper',
  templateUrl: './rating-wrapper.component.html',
  styleUrls: ['./rating-wrapper.component.scss']
})
export class RatingWrapperComponent implements FormioCustomComponent<number> {
  @Input()
  value: number;

  @Output()
  valueChange = new EventEmitter<number>();

  @Input()
  disabled: boolean;
}

The value input stores the value of the field. The valueChange output should be called upon value update, but please note that's only a change triggering event, the formio framework reads the value from the value field. So a value update may look like the following:

updateValue(payload: number) {
  this.value = payload; // Should be updated first
  this.valueChange.emit(payload); // Should be called after this.value update
}

The change event (output) is named as valueChange to keep compatibility with the Angular principles if you want to use the same component at other places inside your app.

NgModule entryComponents

You should register your Angular component in the entryComponents array. This won't be required after Angular v9 with Ivy.

Registration definition

Define the options

Add a new file next to your component. E.g. if you have rating-wrapper.component.ts place a new file next to it, e.g. rating-wrapper.formio.ts

Add the following content:

import { Injector } from '@angular/core';
import { FormioCustomComponentInfo, registerCustomFormioComponent } from 'angular-formio';
import { RatingWrapperComponent } from './rating-wrapper.component';

const COMPONENT_OPTIONS: FormioCustomComponentInfo = {
  type: 'myrating', // custom type. Formio will identify the field with this type.
  selector: 'my-rating', // custom selector. Angular Elements will create a custom html tag with this selector
  title: 'Rating', // Title of the component
  group: 'basic', // Build Group
  icon: 'fa fa-star', // Icon
//  template: 'input', // Optional: define a template for the element. Default: input
//  changeEvent: 'valueChange', // Optional: define the changeEvent when the formio updates the value in the state. Default: 'valueChange',
//  editForm: Components.components.textfield.editForm, // Optional: define the editForm of the field. Default: the editForm of a textfield
//  documentation: '', // Optional: define the documentation of the field
//  weight: 0, // Optional: define the weight in the builder group
//  schema: {}, // Optional: define extra default schema for the field
//  extraValidators: [], // Optional: define extra validators  for the field
//  emptyValue: null, // Optional: the emptyValue of the field
//  fieldOptions: ['values'], // Optional: explicit field options to get as `Input` from the schema (may edited by the editForm)
};

export function registerRatingComponent(injector: Injector) {
  registerCustomFormioComponent(COMPONENT_OPTIONS, RatingWrapperComponent, injector);
}

Register in your AppModule

Call the registration in the constructor of your NgModule like this:

export class AppModule {
  constructor(injector: Injector) {
    registerRatingComponent(injector);
  }
}

Options

You may want to customize your custom field via the editForm. You can reach the options defined there via @Input() as the following:

Default Options

The standard options defined for inputs (e.g. the placeholder) are bound as attributes so you can reach those in the component thanks to Angular Elements (e.g. @Input() placeholder: string).

Explicit Options

Due to performance reasons not all the options are bound to the component. If you want to reach fields from the schema, define in the fieldOptions array at the field registration.

Implicit Custom Options

If you want to define a custom option in the editForm, you can do as the following:

{ key: 'customOptions.myOption', [rest of the field definition] }

And the customOptions defined there will be bound flattened, e.g. @Input() myOption: string.

For Custom Options you may need to create your own editForm (from scratch or extend an existing one) and define in the COMPONENT_OPTIONS described above. You can define fields there following the default schema. :Please remember to put customOptions in the key of the fields.:

E.g.

export function minimalEditForm() {
  return {
    components: [
      { key: 'type', type: 'hidden' },
      {
        weight: 0,
        type: 'textfield',
        input: true,
        key: 'label',
        label: 'Label',
        placeholder: 'Label',
        validate: {
          required: true,
        },
      },
      {
        weight: 10,
        type: 'textfield',
        input: true,
        key: 'key',
        label: 'Field Code',
        placeholder: 'Field Code',
        tooltip: 'The code/key/ID/name of the field.',
        validate: {
          required: true,
          maxLength: 128,
          pattern: '[A-Za-z]\\w*',
          patternMessage:
            'The property name must only contain alphanumeric characters, underscores and should only be started by any letter character.',
        },
      },
      {
        weight: 20,
        type: 'textfield',
        input: true,
        key: 'customOptions.myOption',
        label: 'My Custom Option',
        placeholder: 'My Custom Option',
        validate: {
          required: true,
        },
      },
    ],
  };
}

and

const COMPONENT_OPTIONS: FormioCustomComponentInfo = {
  [...]
  editForm: minimalEditForm,
  [...]
};

Validations

Validations are bound to the component as well, similar to the Custom Options. E.g. if you define validate.required and validate.min you can reach those as @Input() required: boolean; and @Input() min: number;.

Please note that if you define more validations on top of the default ones, and you want formio to process those, you need to define those in the COMPONENT_OPTIONS like: extraValidators: ['min'] as well.

Lifecycle of the component

As the Angular Lifecycle Hooks (e.g. ngOnInit()) take care of the internal rendering state, you can't expect all inputs being populated with the proper value during the init hooks as the Elements are existing inside an "external" library - inside formio which may assign the options including the value asynchronously.

If you need to process the value of the input, put the logic inside a setter. E.g. if you want to coerce the input value as number:

import { coerceNumberProperty } from '@angular/cdk/coercion';

private _value: number;
@Input()
public set value(v: number | string) {
  this._value = coerceNumberProperty(v, undefined);
}
public get value(): number | string {
  return this._value;
}

Register a single class as Custom Component

If you don't want to register an Angular Component as you want to implement the logic in a simple JavaScript-based class, take a look at the CheckMatrix example: https://github.com/formio/angular-demo/blob/master/src/app/components/CheckMatrix.js

Please remember that the Formio.registerComponent method is not available in TypeScript environment due to typings limitations, call the Components.addComponent method instead.

Example of registering a component with custom class:

import { Components } from 'angular-formio';

Components.setComponent('separator', SeparatorComponent);

FAQ

How can I emit an event (e.g. trigger submission) inside from the Custom Component?

Inside the Custom Angular Component, implement as the following:

@Output()
formioEvent = new EventEmitter<FormioEvent>();

And emit an event like:

this.formioEvent.emit({ eventName: 'submitButton', data: { state: 'submitted' } });

See details: https://github.com/formio/angular-formio/pull/443

How can I create a new Template for Custom Components?

See details: https://github.com/formio/angular-formio/issues/442#issuecomment-574263404

Support

This feature is contributed by Merobal with support of CodingSans.

If you have any questions, create a new issue in this repo by choosing "Custom Components Support Request".