Skip to main content
Version: 8.3

Implement a Freedom UI Designer setup area for a custom UI component

Level: advanced
note

This functionality is available for Creatio 8.3.3 and later.

To implement the example:

  1. Create an Angular project. Read more >>>
  2. Create a custom UI component. Read more >>>
  3. Create a custom setup area. Read more >>>
  4. Add the custom UI component to the Freedom UI page. Read more >>>
Example

Implement a custom Rating component to rate contacts directly on the contact page.

The component must have its own setup area to configure the following parameters directly in the Freedom UI Designer:

  • Star count — the maximum number of stars to display.
  • Star color (active) — the color of filled stars.
  • Data source — the object that implements rating functionality.
  • Data source attribute — the column of the object that implements the data source. The column stores rating value.

Implement the custom component and its setup area using a remote module created in the Angular framework.

1. Create an Angular project

To create an Angular project, follow the instructions: Create an Angular project to develop a custom UI component using remote module via the Clio utility.

For this example:

  • Create an Angular project whose name is sdkCustomRatingComponent.
  • Use usr as the vendor prefix.
  • Update the @creatio-devkit/common library to the latest version.
  • Install the class-transformer and the @creatio/interface-designer libraries.

As a result, an Angular project to develop a custom UI component using a remote module will be added.

2. Create a custom UI component

  1. Run the ng g c view-elements/rating --view-encapsulation=ShadowDom command at the Visual Studio Code terminal to create an Angular component in the project.

    As a result, the RatingComponent files will be added to the "src/app/view-elements/rating" project directory.

  2. Define the component constants.

    1. Go to the "src/app/" directory.
    2. Create the "constants" directory → "rating.constants.ts" file.
    3. Open the "rating.constants.ts" file.
    4. Export constants that define the component type and the component icon.
    5. Save the file.
    "rating.constants.ts" file
    /* Define the component type identifier. */
    export const COMPONENT_TYPE = 'usr.CustomRating';

    /* Define the component icon as an inline SVG. */
    export const COMPONENT_ICON =
    `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" ` +
    `viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">` +
    `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 ` +
    `12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`;
  3. Specify that the RatingComponent is a view element.

    1. Open the "rating.component.ts" file.
    2. Flag the component using the @CrtViewElement decorator.
    3. Import the required functionality from the libraries into the component.
    4. Save the file.
    "rating.component.ts" file
    /* Import the required functionality from the libraries. */
    import {
    Component,
    OnInit,
    ViewEncapsulation
    } from '@angular/core';
    import {
    CrtViewElement
    } from '@creatio-devkit/common';

    @Component({
    selector: 'usr-rating-internal',
    templateUrl: './rating.component.html',
    styleUrls: ['./rating.component.scss'],
    encapsulation: ViewEncapsulation.ShadowDom
    })

    /* Register the component as a Freedom UI view element. */
    @CrtViewElement({
    selector: 'usr-rating',
    type: COMPONENT_TYPE
    })

    export class RatingComponent implements OnInit {
    constructor() { }

    ngOnInit(): void {
    }
    }
  4. Register the RatingComponent view element as a component.

    1. Open the "app.module.ts" file.
    2. Add the RatingComponent view element to the @CrtModule decorator.
    3. Register the RatingComponent view element as a component, i.e., Angular Element, in the module constructor.
    4. Save the file.
    "app.module.ts" file
    /* Import the required functionality from the libraries. */
    import {
    DoBootstrap,
    Injector,
    NgModule,
    ProviderToken
    } from '@angular/core';
    import {
    createCustomElement
    } from '@angular/elements';
    import {
    BrowserModule
    } from '@angular/platform-browser';
    import {
    bootstrapCrtModule,
    CrtModule
    } from '@creatio-devkit/common';
    import {
    RatingComponent
    } from './view-elements/rating/rating.component';

    @CrtModule({
    /* Specify that RatingComponent is a view element. */
    viewElements: [RatingComponent]
    })
    @NgModule({
    declarations: [RatingComponent],
    imports: [BrowserModule],
    providers: [],
    })
    export class AppModule implements DoBootstrap {
    constructor(private _injector: Injector) {}

    ngDoBootstrap(): void {
    /* Register RatingComponent as an Angular Element. */
    const ratingElement = createCustomElement(RatingComponent, {
    injector: this._injector,
    });
    customElements.define('usr-rating', ratingElement);

    /* Bootstrap CrtModule definitions. */
    bootstrapCrtModule('sdkCustomRatingComponent', AppModule, {
    resolveDependency: (token) => this._injector.get(<ProviderToken<unknown>>token)
    });
    }
    }
  5. Implement the business logic of the component.

    1. Open the "rating.component.ts" file.
    2. Add the rating (the current rating value), maxStars (the maximum number of stars), and filledColor (the color of filled stars) properties to the RatingComponent class.
    3. Flag the properties using the @Input and @CrtInput decorators.
    4. Add the ratingChange event to the RatingComponent class. The event emits the new rating value when a star is clicked.
    5. Flag the ratingChange event using the @Output and @CrtOutput decorators.
    6. Register the component in the Freedom UI Designer element library using the @CrtInterfaceDesignerItem decorator.
    7. Import the required functionality from the libraries into the component.
    8. Save the file.
    "rating.component.ts" file
    /* Import the required functionality from the libraries. */
    import {
    ChangeDetectionStrategy,
    Component,
    EventEmitter,
    Input,
    OnInit,
    Output,
    ViewEncapsulation
    } from '@angular/core';
    import {
    CrtInput,
    CrtInterfaceDesignerItem,
    CrtOutput,
    CrtViewElement
    } from '@creatio-devkit/common';
    import {
    COMPONENT_ICON,
    COMPONENT_TYPE
    } from '../../constants/rating.constants';

    /* Register the component in the Freedom UI Designer element library. */
    @CrtInterfaceDesignerItem({
    toolbarConfig: {
    caption: 'Rating',
    icon: COMPONENT_ICON,
    },
    })
    /* Register the component as a Freedom UI view element. */
    @CrtViewElement({
    selector: 'usr-rating',
    type: COMPONENT_TYPE,
    })
    @Component({
    selector: 'usr-rating-internal',
    templateUrl: './rating.component.html',
    styleUrls: ['./rating.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.ShadowDom,
    })
    export class RatingComponent implements OnInit {

    /* The current rating value. */
    @Input()
    @CrtInput()
    public rating: number = 0;

    /* The maximum number of stars. */
    @Input()
    @CrtInput()
    public maxStars: number = 5;

    /* The color of filled stars. */
    @Input()
    @CrtInput()
    public filledColor: string = '#ffc107';

    /* Emits the new rating value when a star is clicked. */
    @Output()
    @CrtOutput()
    public readonly ratingChange = new EventEmitter<number>();

    constructor() { }

    ngOnInit(): void { }

    protected get stars(): number[] {
    return Array(this.maxStars).fill(0).map((_, i) => i + 1);
    }

    protected isStarFilled(starIndex: number): boolean {
    return starIndex <= this.rating;
    }

    protected onStarClick(starIndex: number): void {
    this.ratingChange.emit(starIndex);
    }
    }
  6. Add the markup of the custom component.

    1. Open the "rating.component.html" file.
    2. Add the markup.
    3. Save the file.
    "rating.component.html" file
    <div class="custom-rating" data-testid="custom-rating">
    <div class="stars-container">
    @for (star of stars; track star) {
    <button
    type="button"
    class="star"
    [class.star--filled]="isStarFilled(star)"
    [style.color]="isStarFilled(star) ? filledColor : null"
    [attr.aria-label]="'Rate ' + star + ' stars'"
    (click)="onStarClick(star)"
    >
    @if (isStarFilled(star)) {
    <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="currentColor"
    >
    <polygon
    points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02
    12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
    />
    </svg>
    } @else {
    <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    >
    <polygon
    points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02
    12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
    />
    </svg>
    }
    </button>
    }
    </div>
    <div class="rating-value">{{ rating }} / {{ maxStars }}</div>
    </div>
  7. Add the styles of the custom component.

    1. Open the "rating.component.scss" file.
    2. Add the styles.
    3. Save the file.
    "rating.component.scss" file
    :host {
    display: block;
    }

    .custom-rating {
    display: flex;
    flex-direction: column;
    gap: 4px;
    }

    .stars-container {
    display: flex;
    gap: 4px;
    }

    .star {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
    color: #ccc;
    line-height: 0;

    &:hover {
    color: #ffc107;
    }
    }

    .star--filled {
    color: #ffc107;
    }

    .rating-value {
    font-size: 12px;
    color: #666;
    }

3. Create a custom Freedom UI Designer setup area

  1. Run the ng g c view-elements/rating-setup-area command at the Visual Studio Code terminal to create an Angular component in the project.

    As a result, the RatingSetupAreaComponent files will be added to the "src/app/view-elements/rating-setup-area" project directory.

  2. Define the setup area constants.

    1. Go to the "src/app/constants" directory.
    2. Open the "rating.constants.ts" file.
    3. Export constants that define the setup area type, property codes, and default property values.
    4. Save the file.
    "rating.constants.ts" file
    /* Define the identifier of the setup area type. */
    export const SETUP_AREA_TYPE = 'usr.RatingSetupArea';

    /* Define the property codes used in the Freedom UI page schema. */
    export const PROPERTY_RATING = 'rating';
    export const PROPERTY_MAX_STARS = 'maxStars';
    export const PROPERTY_FILLED_COLOR = 'filledColor';

    /* Define the default property values. */
    export const DEFAULT_MAX_STARS = 5;
    export const DEFAULT_FILLED_COLOR = '#ffc107';
  3. Specify that the RatingSetupAreaComponent is a view element.

    1. Open the "rating-setup-area.component.ts" file.
    2. Flag the component using the @CrtViewElement decorator.
    3. Import the required functionality from the libraries into the component.
    4. Save the file.
    "rating-setup-area.component.ts" file
    /* Import the required functionality from the libraries. */
    import {
    Component,
    ViewEncapsulation
    } from '@angular/core';
    import {
    CrtViewElement
    } from '@creatio-devkit/common';
    import {
    SETUP_AREA_TYPE
    } from 'src/app/constants/rating.constants';

    @Component({
    selector: 'usr-rating-setup-area-internal',
    templateUrl: './rating-setup-area.component.html',
    styleUrls: ['./rating-setup-area.component.scss'],
    encapsulation: ViewEncapsulation.None
    })

    /* Register the component as a Freedom UI view element. */
    @CrtViewElement({
    selector: 'usr-rating-setup-area',
    type: SETUP_AREA_TYPE
    })

    export class RatingSetupAreaComponent {

    }
  4. Implement the business logic of the setup area.

    1. Open the "rating-setup-area.component.ts" file.
    2. Add the viewNodeEditor setter to implement the PropertyPanel interface. The PropertyPanel interface defines the contract for the setup area implemented using a remote module. Freedom UI Designer calls the viewNodeEditor setter when a user selects the component on the canvas.
    3. Initialize InterfaceDesignerSchemaService to access view model attributes and data sources.
    4. Implement _loadElementData() to read the current property values from the schema asynchronously on setup area initialization.
    5. Implement _applyChanges() to write the updated property values back to the schema when the user changes a field.
    6. Import the required functionality from the libraries into the component.
    7. Save the file.
    "rating-setup-area.component.ts" file
    /* Import the required functionality from the libraries. */
    import {
    ChangeDetectionStrategy,
    Component,
    Input,
    signal,
    ViewEncapsulation,
    WritableSignal
    } from '@angular/core';
    import {
    InterfaceDesignerSchemaService,
    PropertyPanel,
    ViewModelAttributeType,
    ViewNodeEditor,
    ViewNodePropertyValueType,
    } from '@creatio/interface-designer';
    import {
    CrtInput,
    CrtViewElement
    } from '@creatio-devkit/common';
    import {
    DEFAULT_MAX_STARS,
    DEFAULT_FILLED_COLOR,
    SETUP_AREA_TYPE,
    PROPERTY_MAX_STARS,
    PROPERTY_RATING,
    PROPERTY_FILLED_COLOR,
    } from 'src/app/constants/rating.constants';

    /* Describe the schema state read from Freedom UI Designer. */
    interface RatingSetupAreaSchemaState {
    maxStars?: number;
    filledColor?: string;
    viewModelAttributeName?: string;
    dataSourceName?: string;
    dataSourceAttributeName?: string;
    }

    /* Register the setup area as a Freedom UI view element. */
    @CrtViewElement({
    selector: 'usr-rating-setup-area',
    type: SETUP_AREA_TYPE,
    })
    @Component({
    selector: 'usr-rating-setup-area-internal',
    templateUrl: './rating-setup-area.component.html',
    styleUrls: ['./rating-setup-area.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.None,
    })
    export class RatingSetupAreaComponent implements PropertyPanel {

    /* Provide access to the schema editors for reading and modifying the page schema. */
    private readonly _schemaEditor = new InterfaceDesignerSchemaService().getSchemaEditor();

    /* Provide access to the selected component's view node in the schema. */
    private _viewNodeEditor!: ViewNodeEditor;

    /* Return the name of the view model attribute that binds the rating property. */
    private get _viewModelAttributeName(): string {
    return `${this._viewNodeEditor.nodeName}_Rating`;
    }

    /* Indicate whether the setup area has finished loading the component's properties. */
    protected readonly isPanelReady = signal<boolean>(false);

    /* The current value of the maxStars property displayed in the setup area. */
    protected readonly maxStars = signal(DEFAULT_MAX_STARS);

    /* The current value of the filledColor property displayed in the setup area. */
    protected readonly filledColor = signal(DEFAULT_FILLED_COLOR);

    /* The code of the selected component instance on the canvas. */
    protected readonly elementCode = signal('');

    /* The name of the data source selected in the setup area. */
    protected readonly dataSourceName = signal('');

    /* The name of the data source attribute selected in the setup area. */
    protected readonly dataSourceAttributeName = signal('');

    /* Called by Freedom UI Designer when a user selects the component on the canvas. */
    @Input()
    @CrtInput()
    public set viewNodeEditor(nodeEditor: ViewNodeEditor) {
    this._init(nodeEditor).catch(
    (error) => console.error('Error initializing rating setup area:', error)
    );
    }

    /* Store the view node editor and load the current property values from the schema. */
    private async _init(nodeEditor: ViewNodeEditor): Promise<void> {
    this._viewNodeEditor = nodeEditor;
    await this._loadElementData();
    this.isPanelReady.set(true);
    }

    /* Read the current property values from the schema. */
    private async _getStateFromSchema(): Promise<RatingSetupAreaSchemaState> {
    let maxStars: number | undefined;
    let filledColor: string | undefined;

    /* Read the maxStars and filledColor constant values from the view node. */
    const maxStarsPropertyValue = await this._viewNodeEditor
    .getPropertyValue(PROPERTY_MAX_STARS);
    const filledColorPropertyValue = await this._viewNodeEditor
    .getPropertyValue(PROPERTY_FILLED_COLOR);

    if (maxStarsPropertyValue?.type === ViewNodePropertyValueType.Constant) {
    maxStars = Number(maxStarsPropertyValue.value);
    }
    if (filledColorPropertyValue?.type === ViewNodePropertyValueType.Constant) {
    filledColor = filledColorPropertyValue.value as string;
    }

    /* Read the rating attribute binding from the view node. */
    const ratingPropertyValue = await this._viewNodeEditor
    .getPropertyValue(PROPERTY_RATING);
    if (ratingPropertyValue?.type !== ViewNodePropertyValueType.AttributeBinding) {
    return { maxStars, filledColor };
    }

    /* Resolve the view model attribute and its model binding. */
    const viewModelAttributePath = ratingPropertyValue.attributePath;
    const viewModelAttributeEditor = await this._schemaEditor.viewModelEditor
    .getAttributeEditor(viewModelAttributePath);

    if (!viewModelAttributeEditor) {
    return { maxStars, filledColor, viewModelAttributeName: viewModelAttributePath };
    }
    if (viewModelAttributeEditor.attributeType !== ViewModelAttributeType.ModelBindingValue) {
    return { maxStars, filledColor, viewModelAttributeName: viewModelAttributePath };
    }

    const modelBinding = await viewModelAttributeEditor.getModelBinding();
    return {
    maxStars,
    filledColor,
    viewModelAttributeName: viewModelAttributePath,
    dataSourceName: modelBinding.dataSourceName,
    dataSourceAttributeName: modelBinding.dataSourceAttributePath,
    };
    }

    /* Populate the setup area signals using the current property values from the schema. */
    private async _loadElementData(): Promise<void> {
    this.elementCode.set(this._viewNodeEditor.nodeName);
    const {
    maxStars,
    filledColor,
    dataSourceName,
    dataSourceAttributeName
    } = await this._getStateFromSchema();
    this.maxStars.set(maxStars ?? DEFAULT_MAX_STARS);
    this.filledColor.set(filledColor ?? DEFAULT_FILLED_COLOR);
    this.dataSourceName.set(dataSourceName ?? '');
    this.dataSourceAttributeName.set(dataSourceAttributeName ?? '');
    }

    /* Remove the existing rating attribute binding before creating a new binding. */
    private async _unbindExisting(): Promise<void> {
    const attributeName = (await this._getStateFromSchema()).viewModelAttributeName;
    if (!attributeName) {
    return;
    }
    /* Clear the rating property binding in the view node. */
    await this._viewNodeEditor.setPropertyValue(PROPERTY_RATING, { constant: null });
    /* Remove the view model attribute if it is no longer in use. */
    if (await this._schemaEditor.viewModelEditor.canRemoveAttribute(attributeName)) {
    await this._schemaEditor.viewModelEditor.removeAttribute(attributeName);
    }
    }

    /* Create or update the view model attribute that binds the rating property to a data source. */
    private async _bindViewModelAttribute(
    viewModelAttributeName: string,
    dataSourceName: string,
    dataSourceAttributeName: string,
    ): Promise<void> {
    const viewModelAttributeEditor = await this._schemaEditor.viewModelEditor
    .getAttributeEditor(viewModelAttributeName);
    const targetModelBinding = {
    dataSourceName,
    dataSourceAttributePath: dataSourceAttributeName
    };

    if (!viewModelAttributeEditor) {
    /* Create a new view model attribute bound to the selected data source attribute. */
    await this._schemaEditor.viewModelEditor
    .createAttribute(viewModelAttributeName, { bindToModel: targetModelBinding });
    } else if (
    viewModelAttributeEditor.attributeType === ViewModelAttributeType.ModelBindingValue
    ) {
    /* Update the model binding of the existing attribute. */
    await viewModelAttributeEditor.bindToModel(targetModelBinding);
    }
    }

    /* Write the updated property values to the schema. */
    private async _applyChanges(): Promise<void> {
    /* Save the maxStars and filledColor constant values to the view node. */
    await this._viewNodeEditor
    .setPropertyValue(PROPERTY_MAX_STARS, { constant: this.maxStars() });
    await this._viewNodeEditor
    .setPropertyValue(PROPERTY_FILLED_COLOR, { constant: this.filledColor() });

    const dataSourceName = this.dataSourceName();
    const dataSourceAttributeName = this.dataSourceAttributeName();

    /* If both data source fields are set, update the rating attribute binding. */
    if (dataSourceName && dataSourceAttributeName) {
    await this._unbindExisting();
    await this._bindViewModelAttribute(
    this._viewModelAttributeName,
    dataSourceName,
    dataSourceAttributeName
    );
    /* Bind the rating property to the view model attribute. */
    await this._viewNodeEditor
    .setPropertyValue(PROPERTY_RATING, { bindToAttribute: this._viewModelAttributeName });
    }
    }

    /* Handle a property change triggered by user input in the setup area. */
    protected async handlePropertyChange(
    event: Event,
    signal: WritableSignal<string | number>,
    propertyName: string,
    ): Promise<void> {
    const inputElement = event.target as HTMLInputElement;
    /* Convert the input value to a number if the input type is "number". */
    const value = inputElement.type === 'number'
    ? Number(inputElement.value)
    : inputElement.value;
    signal.set(value);
    try {
    await this._applyChanges();
    } catch (error) {
    console.error(`Error applying ${propertyName} change:`, error);
    }
    }
    }
  5. Add the markup of the setup area.

    1. Open the "rating-setup-area.component.html" file.
    2. Add the markup.
    3. Save the file.
    "rating-setup-area.component.html" file
    @if (isPanelReady()) {

    <crt-interface-designer-properties-panel-wrapper
    [headerTitle]="'Rating settings'"
    [showHeader]="true"
    [canShowTooltip]="true"
    >
    <div class="section">
    <h4 class="section-title">General</h4>
    <div class="form-field">
    <label class="field-label" for="max-stars-input">Star count</label>
    <input
    id="max-stars-input"
    type="number"
    class="field-input"
    placeholder="Max star count"
    [value]="maxStars()"
    (change)="handlePropertyChange($event, maxStars, 'maxStars')"
    />
    </div>
    <div class="form-field">
    <label class="field-label" for="filled-color-input">
    Star color (active)
    <input
    id="filled-color-input"
    type="color"
    [value]="filledColor()"
    (change)="handlePropertyChange($event, filledColor, 'filledColor')"
    />
    </label>
    </div>
    <div class="form-field">
    <label class="field-label" for="data-source-input">Data source</label>
    <input
    id="data-source-input"
    type="text"
    class="field-input"
    placeholder="Data source name"
    [value]="dataSourceName()"
    (change)="handlePropertyChange($event, dataSourceName, 'dataSourceName')"
    />
    </div>
    <div class="form-field">
    <label
    class="field-label"
    for="data-source-attribute-input"
    >
    Data source attribute
    </label>
    <input
    id="data-source-attribute-input"
    type="text"
    class="field-input"
    placeholder="Data source attribute name"
    [value]="dataSourceAttributeName()"
    (change)="handlePropertyChange(
    $event,
    dataSourceAttributeName,
    'dataSourceAttributeName'
    )"
    />
    </div>
    </div>
    <hr class="section-divider" />
    <div class="section">
    <h4 class="section-title">Advanced</h4>
    <div class="form-field">
    <label
    class="field-label"
    for="element-code-input"
    >
    Element code
    </label>
    <input
    id="element-code-input"
    disabled
    type="text"
    class="field-input"
    [value]="elementCode()"
    />
    </div>
    </div>
    </crt-interface-designer-properties-panel-wrapper>
    }
  6. Add the styles of the setup area.

    1. Open the "rating-setup-area.component.scss" file.
    2. Add the styles.
    3. Save the file.
    "rating-setup-area.component.scss" file
    @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap');

    :host {
    --font-family: "Montserrat", sans-serif;
    --font-family-add: "Montserrat", sans-serif;
    --font-weight: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --foreground-text: 68, 68, 68;
    --foreground-secondary-text: 117, 117, 117;
    --foreground-secondary-text-alpha: 1;
    --headline-3-font-family: "Montserrat", sans-serif;
    --headline-3-font-size: 18px;
    --headline-3-font-weight: 500;
    --headline-3-line-height: 24px;
    --headline-3-letter-spacing: 0;
    --crt-palette-foreground-contrast-500: #ffffff;
    --crt-expansion-btn-background-color: #f4480b;
    --headline-4-font-family: "Montserrat", sans-serif;
    --headline-4-font-size: 16px;
    --headline-4-font-weight: 500;
    --headline-4-line-height: 20px;
    --headline-4-letter-spacing: 0;
    --crt-palette-primary-500: #f4480b;

    font-family: "Montserrat", sans-serif;
    font-size: 14px;
    font-weight: 400;
    color: rgba(68, 68, 68, 1);
    display: block;
    }

    .section-title {
    font-family: "Montserrat", sans-serif;
    font-size: 14px;
    font-weight: 500;
    color: #0D2E4E;
    text-transform: uppercase;
    letter-spacing: normal;
    line-height: 18px;
    margin: 0;
    padding: 0 8px 14px 8px;
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    -webkit-font-smoothing: antialiased;
    }

    .section {
    padding: 0 0 8px 0;
    }

    .section-divider {
    border: none;
    border-top: 1px solid rgba(0, 0, 0, 0.1);
    margin: 15px 0 15px 0;
    }

    .form-field {
    padding: 0 8px 0 8px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    }

    .field-label {
    font-family: "Montserrat", sans-serif;
    font-size: 12px;
    font-weight: 500;
    color: rgb(117, 117, 117);
    display: flex;
    align-items: center;
    justify-content: space-between;
    line-height: 17px;
    letter-spacing: 0.24px;
    margin-top: 15px;
    -webkit-font-smoothing: antialiased;
    }

    .field-input {
    font-family: "Montserrat", sans-serif;
    font-size: 13px;
    font-weight: 500;
    color: rgb(68, 68, 68);
    border: none;
    border-bottom: 1px solid #c8c8c8;
    outline: none;
    box-shadow: none;
    padding: 0;
    width: 100%;
    background: transparent;
    line-height: 20px;
    text-overflow: ellipsis;
    -webkit-font-smoothing: antialiased;
    caret-color: rgb(68, 68, 68);
    }

    .field-input:focus,
    .field-input:focus-visible {
    outline: none !important;
    box-shadow: none !important;
    border-bottom: 1px solid #c8c8c8 !important;
    background-color: transparent !important;
    }

    .field-input::placeholder {
    font-family: "Montserrat", sans-serif;
    color: rgba(117, 117, 117, 1);
    font-style: normal;
    }

    .field-input:disabled {
    color: rgba(117, 117, 117, 1);
    cursor: not-allowed;
    border-bottom-color: rgba(0, 0, 0, 0.1);
    }

    input[type="color"] {
    width: 20px;
    height: 20px;
    border: 1px solid #c8c8c8;
    border-radius: 3px;
    padding: 0;
    cursor: pointer;
    background: none;
    }
  7. Register the setup area so that Freedom UI Designer can display it.

    1. Go to the "src/app" directory.
    2. Create the "rating.design-time.module.ts" file.
    3. Add RatingSetupAreaComponent so that Freedom UI Designer can display it as a setup area when a user selects the Rating component on the canvas.
    4. Register RatingSetupAreaComponent as a custom element so that the browser can render it inside the Freedom UI Designer.
    5. Save the file.
    "rating.design-time.module.ts" file
    /* Import the required functionality from the libraries. */
    import {
    CommonModule
    } from '@angular/common';
    import {
    CUSTOM_ELEMENTS_SCHEMA,
    inject,
    Injector,
    NgModule
    } from '@angular/core';
    import {
    createCustomElement
    } from '@angular/elements';
    import {
    CrtModule,
    Type
    } from '@creatio-devkit/common';
    import {
    RatingSetupAreaComponent
    } from './view-elements/rating-setup-area/rating-setup-area.component';

    /* Register RatingSetupAreaComponent as a view element so that Freedom UI Designer
    can display it as a setup area when a user selects the Rating component on the canvas. */
    @CrtModule({
    viewElements: [RatingSetupAreaComponent],
    })
    /* Declare RatingSetupAreaComponent in the Angular module and allow custom element schemas. */
    @NgModule({
    declarations: [RatingSetupAreaComponent],
    imports: [CommonModule],
    exports: [RatingSetupAreaComponent],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    })
    export class RatingDesignTimeModule {
    /* Injector is used to create Angular Elements from components. */
    private readonly _injector = inject(Injector);

    constructor() {
    /* Register RatingSetupAreaComponent as a custom element on module initialization. */
    this._registerCustomElement('usr-rating-setup-area', RatingSetupAreaComponent);
    }

    /* Register a component as a custom element so that the browser can render it
    inside Freedom UI Designer if it has not been registered yet. */
    private _registerCustomElement(selector: string, component: Type): void {
    if (!customElements.get(selector)) {
    /* Convert the Angular component to a custom element and define it in the browser registry. */
    const elementConstructor = createCustomElement(component, { injector: this._injector });
    customElements.define(selector, elementConstructor);
    }
    }
    }
  8. Register the RatingSetupAreaComponent view element as a component.

    1. Open the "app.module.ts" file.
    2. Add RatingDesignTimeModule to the imports array.
    3. Register the RatingSetupAreaComponent view element as a component, i.e., Angular Element, in the module constructor.
    4. Save the file.
    "app.module.ts" file
    /* Import the required functionality from the libraries. */
    import {
    DoBootstrap,
    Injector,
    NgModule,
    ProviderToken
    } from '@angular/core';
    import {
    createCustomElement
    } from '@angular/elements';
    import {
    BrowserModule
    } from '@angular/platform-browser';
    import {
    bootstrapCrtModule,
    CrtModule
    } from '@creatio-devkit/common';
    import {
    RatingComponent
    } from './view-elements/rating/rating.component';
    import {
    RatingSetupAreaComponent
    } from './view-elements/rating-setup-area/rating-setup-area.component';
    import {
    RatingDesignTimeModule
    } from './rating.design-time.module';

    @CrtModule({
    /* Specify that RatingComponent and RatingSetupAreaComponent are view elements. */
    viewElements: [RatingComponent, RatingSetupAreaComponent]
    })
    @NgModule({
    declarations: [RatingComponent],
    imports: [BrowserModule, RatingDesignTimeModule],
    providers: [],
    })
    export class AppModule implements DoBootstrap {
    constructor(private _injector: Injector) {}

    ngDoBootstrap(): void {
    /* Register RatingComponent as an Angular Element. */
    const ratingElement = createCustomElement(RatingComponent, {
    injector: this._injector,
    });
    customElements.define('usr-rating', ratingElement);

    /* Bootstrap CrtModule definitions. */
    bootstrapCrtModule('sdkCustomRatingComponent', AppModule, {
    resolveDependency: (token) => this._injector.get(<ProviderToken<unknown>>token)
    });
    }
    }
  9. Link the setup area to the Rating component.

    1. Open the "rating.component.ts" file.
    2. Set the propertiesPanel property to SETUP_AREA_TYPE. The propertiesPanel property allows you to use a custom component as a setup area.
    3. Import the required functionality from the libraries into the component.
    4. Save the file.
    "rating.component.ts" file
    import { SETUP_AREA_TYPE } from '../../constants/rating.constants';
    ...
    /* Register the component in the Freedom UI Designer element library. */
    @CrtInterfaceDesignerItem({
    propertiesPanel: SETUP_AREA_TYPE,
    })
  10. Run the npm run build command at the Visual Studio Code terminal to build the project.

As a result, the build will be added to the "dist" directory of the Angular project. The build will have the "sdkCustomRatingComponent" name.

4. Add the custom UI component to the Freedom UI page

  1. Upload packages to Creatio using Clio utility.

    1. Go to the "../Terrasoft.WebApp/Terrasoft.Configuration/Pkg/sdkCustomRatingComponent/Files" directory.
    2. Create a "src/js" directory.
    3. Copy the build artifacts from the "../sdkCustomRatingComponent/dist" project directory to the "../Terrasoft.WebApp/Terrasoft.Configuration/Pkg/sdkCustomRatingComponent/Files/src/js" directory.
    4. Run the clio pushw -e sdkCustomRatingComponent command at the Visual Studio Code terminal to upload the changes to Creatio.
  2. Open the Customer 360 app in the Application Designer.

  3. Open the Contacts form page.

  4. Add data source for contact rating.

    1. Go to the element library.

    2. Click Add data sourceNew data source.

    3. Fill out the data source properties.

      Property

      Property value

      Name

      Contact rating

    4. Click Save.

    5. Go to the setup area → Relations setup.

    6. Click btn_Add_button.png in the Add relation criteria.

    7. Fill out the relation properties.

      Property

      Property value

      Contact

      Select "Id"

      Contact rating

      Select "Id"

    8. Click Save.

    9. Add a column to the object that determines data source.

      1. Open the Customer 360 app in the Application Designer.

      2. Go to the Package settings tab → sdkCustomRatingComponent package.

      3. Open the "Contact rating" (UsrContactRating code) schema of the "Object" type.

      4. Go to the Columns node.

      5. Click scr_add_button_in_schema.png → Number → Integer.

      6. Fill out the column properties.

        Property

        Property value

        Code

        UsrRatingValue

        Title

        Rating value

      7. Click Save and publish.

  5. Refresh the Contacts form page.

  6. Add a label of the contact rating.

    1. Drag a label to the canvas.

    2. Fill out the label properties.

      Property

      Property value

      Text

      Rating

      Style

      Caption

      Text color

      #757575

      Element code

      UsrRating

  7. Add a Rating component.

    1. Drag the Rating component to the canvas.

    2. Fill out the rating properties.

      Property

      Property value

      Data source

      UsrContactRatingDS

      Data source attribute

      UsrRatingValue

      Leave default values for component properties.

  8. Click Save.

As a result, the custom Rating component will be configured directly in the Freedom UI Designer via a setup area implemented using a remote module. The setup area includes the following parameters:

  • Star count — the maximum number of stars to display.
  • Star color (active) — the color of filled stars.
  • Data source — the object that implements rating functionality.
  • Data source attribute — the column of the object that implements the data source. The column stores the rating value.

Changes made in the setup area are reflected on the canvas immediately.

View the result

  1. Open the Contacts section.
  2. Open an arbitrary contact. For example, "Andrew Wayne."

As a result, Creatio will display the custom Rating component in the contact profile of the contact page. The component enables users to rate contacts. The component has its own setup area that is displayed in the Freedom UI Designer. Both the component and the setup area are implemented using a remote module created in the Angular framework. View the result >>>

Source code

/* Define the component type identifier. */
export const COMPONENT_TYPE = 'usr.CustomRating';

/* Define the component icon as an inline SVG. */
export const COMPONENT_ICON =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" ` +
`viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">` +
`<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 ` +
`12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`;

/* Define the identifier of the setup area type. */
export const SETUP_AREA_TYPE = 'usr.RatingSetupArea';

/* Define the property codes used in the Freedom UI page schema. */
export const PROPERTY_RATING = 'rating';
export const PROPERTY_MAX_STARS = 'maxStars';
export const PROPERTY_FILLED_COLOR = 'filledColor';

/* Define the default property values. */
export const DEFAULT_MAX_STARS = 5;
export const DEFAULT_FILLED_COLOR = '#ffc107';

Resources

*.zip archive that contains the implemented Freedom UI app

Angular project that contains the implemented example