Creating a simple view component
Another powerful capability of BMC Helix Innovation Studio is the ability to code a custom visual component that can be dragged into a View using View Designer.
Currently, BMC Helix Innovation Studio's View Designer supports Angular.
There is a very simple example of this in the Meal Program Library project, that you may have noticed appearing in the Palette of View Designer, after you deployed the Library and refreshed your BMC Helix Innovation Studio browser. It's only function is to take a parameter and format it into a heading (style H3) followed by the word "Restaurant:". Of course this example is very trivial and in fact you would simply use the Rich Text Component to do this, but using this component will help to understand how the code for any custom View Component works.
Tutorial
Our documentation describes the different types of UI elements (View Component, Record Editor Field View Component or View Action).
You can also refer to our BMC public GitHub repository, which contains more information and tutorial on how to create a View Action or a View Component, for more examples.
Overview
Let's check out how the View Component looks at design-time and run-time, after which we will study the code that implements it. Open the View Designer for the Restaurants View you created earlier. The Meal Program component is called Restaurant Label. This comes from the custom code in the Meal Program Library. Drag it into the View - a good spot would be just above the + New Dish Button. In the Property Inspector, use the Expression Editor dialog to bind the First Selected Row's Name field as its input parameter.
Now when you Preview this View (or refresh the Application) you will see the currently-selected Restaurant Name appearing as the content of the View Component.
Let's examine the code, which resides within the meal-program-lib project - you can find it under the bundle\src\main\webapp folder.
First we'll notice the overall structure. Like most View Components, the project has at least these source files.
- restaurant-label-registration.module.ts: Declare dependencies on the vie component, as well its properties,
- design/
- restaurant-label.infterface.ts: Contains the view component input parameters as an Interface,
- restaurant-label-design.component.html: Template for design time rendering,
- restaurant-label-design.component.ts: Angular component for design time rendering,
- restaurant-label-design.component.scss: Styling for design time rendering,
- restaurant-label-design.model.ts: Logic for the design time (validation, declaration of the view component input and output parameters),
- runtime/
- restaurant-label.component.html: Template for runtime rendering,
- restaurant-label.component.ts: Angular component for runtime rendering,
- restaurant-label.component.scss: Styling for runtime rendering,
As you will discover here, a View Component is really nothing more than a pair of Angular Components with some additional declarations so it will work with the View Designer in BMC Helix Innovation Studio.
For this tutorial we will just note a few things about the sample code. If you are not very familiar with the Angular framework, this code may be difficult to understand.
The Angular registration Module
A great place to start is the Angular registration Module that is declared as a container for all of this code. We name-space it with both the bundle-id and the component name; this will minimize conflicts with other JavaScript that may be deployed in the same space. We also declare dependencies on some functionality in the standard library.
We will come back later to this file.
import { RxViewComponentRegistryService, ViewComponentPropertyType } from '@helix/platform/view/api';
import { RestaurantLabelComponent } from './runtime/restaurant-label.component';
import { RestaurantLabelDesignComponent } from './design/restaurant-label-design.component';
import { RestaurantLabelDesignModel } from './design/restaurant-label-design.model';
@NgModule()
export class RestaurantLabelRegistrationModule {
constructor(rxViewComponentRegistryService: RxViewComponentRegistryService) {
rxViewComponentRegistryService.register({
type: 'com-example-meal-program-lib-restaurant-label',
name: 'Restaurant Label',
group: 'MealLibrary',
component: RestaurantLabelComponent,
designComponent: RestaurantLabelDesignComponent,
designComponentModel: RestaurantLabelDesignModel,
properties: [
{
name: 'styles',
type: ViewComponentPropertyType.String
},
{
name: 'hidden',
enableExpressionEvaluation: true
},
{
name: 'message',
localizable: true,
enableExpressionEvaluation: true
}
]
});
}
}
The Meal Program Library already has an Angular Module that is generated by the Maven Archetype when this project was created. However, you are adding new code that must be compiled and loaded, so this built-in Module must have a dependency on your new Module. For Libraries, this is where all custom View Components need to be listed so they can be compiled and loaded correctly.
import { CommonModule } from '@angular/common';
import { RxLocalizationService } from '@helix/platform/shared/api';
import * as defaultApplicationStrings from './i18n/localized-strings.json';
import { RestaurantLabelRegistrationModule } from './view-components/restaurant-label/restaurant-label-registration.module';
@NgModule({
imports: [CommonModule, RestaurantLabelRegistrationModule]
})
export class ComExampleMealProgramLibModule {
constructor(private rxLocalizationService: RxLocalizationService) {
this.rxLocalizationService.setDefaultApplicationStrings(defaultApplicationStrings['default']);
}
}
The View Configuration
The configuration of the View Component is placed in two different files, restaurant-label-registration.module.ts, and design/restaurant-label-design.model.ts.
There are some important things to note in restaurant-label-registration.module.ts so we will cover them one by one:
- The component is registered with the rxViewComponentRegistryService service, so it can show up in the Palette of View Designer.
- Basic attributes are declared here, such as:
- name - text that appears in the Palette
- group - will add to this section of the Palette or create a new section
- icon - short name of a built-in icon from the Standard Library
type - this binds the component to a run-time rendering Directive,
- designComponent - Angular Component used in the View Designer canvas.
- designComponentModel - Class which describes how the Component behaves at design time, and declares the View Component input, output parameters, and validation.
- component - Angular Component used during runtime.
- The properties attribute declares Properties (in this case, there is only one needed) that will be accessible to the View Designer as View Component inputs. The attributes are used as follows:
- name: how this will appear in the Property Inspector and/or Edit Expression dialog's Available Values tree.
- type: this is the data type. You can plug in custom design-time experiences but string is the simplest one to use.
- enableExpressionEvaluation - because this is set to true, the Edit Expression dialog can be used to build an expression for the value. Otherwise, it has to be a constant value set by the user of the View Designer.
import { RxViewComponentRegistryService, ViewComponentPropertyType } from '@helix/platform/view/api';
import { RestaurantLabelComponent } from './runtime/restaurant-label.component';
import { RestaurantLabelDesignComponent } from './design/restaurant-label-design.component';
import { RestaurantLabelDesignModel } from './design/restaurant-label-design.model';
@NgModule()
export class RestaurantLabelRegistrationModule {
constructor(rxViewComponentRegistryService: RxViewComponentRegistryService) {
rxViewComponentRegistryService.register({
type: 'com-example-meal-program-lib-restaurant-label',
name: 'Restaurant Label',
group: 'MealLibrary',
component: RestaurantLabelComponent,
designComponent: RestaurantLabelDesignComponent,
designComponentModel: RestaurantLabelDesignModel,
properties: [
{
name: 'styles',
type: ViewComponentPropertyType.String
},
{
name: 'hidden',
enableExpressionEvaluation: true
},
{
name: 'message',
localizable: true,
enableExpressionEvaluation: true
}
]
});
}
}
The big key of the validation and design time logic is within the file restaurant-label-design.model.ts. This is a key big of logic that binds together the component's Module with the various ways it is rendered (design-time and run-time), and declares the Properties of the component that will be exposed to the View Designer once it is deployed. Consider the source code:
IViewComponentDesignCommonDataDictionaryBranch,
IViewComponentDesignSandbox, IViewComponentDesignValidationIssue, validateCssClassName, validateCssClassNames,
ViewDesignerComponentModel
} from '@helix/platform/view/designer';
import { IViewDesignerComponentModel } from '@helix/platform/view/api';
import {
ExpressionFormControlComponent,
IExpressionFormControlOptions,
ITagsFormControlOptions,
OptionalExpressionControlComponent,
TagsFormControlComponent,
TextFormControlComponent
} from '@helix/platform/shared/components';
import { Injector } from '@angular/core';
import { Tooltip } from '@helix/platform/shared/api';
import { IViewComponentDesignSettablePropertiesDataDictionary } from '@helix/platform/view/designer/public-interfaces/view-component-design-settable-properties-data-dictionary.interfaces';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { IRestaurantLabelParameters } from './restaurant-label.interface';
// View component input parameters.
const initialComponentProperties: IRestaurantLabelParameters = {
styles: '',
hidden: '',
message: ''
};
export class RestaurantLabelDesignModel extends ViewDesignerComponentModel implements IViewDesignerComponentModel {
constructor(protected injector: Injector,
protected sandbox: IViewComponentDesignSandbox) {
super(injector, sandbox);
// Setting view component input parameters configuration.
sandbox.updateInspectorConfig(this.setInspectorConfig(initialComponentProperties));
// Registering the view component validation based on the input parameter values.
combineLatest([this.sandbox.componentProperties$])
.pipe(
map(([componentProperties]: [IRestaurantLabelParameters]) =>
this.validate(this.sandbox, componentProperties)
)
)
.subscribe((validationIssues) => {
this.sandbox.setValidationIssues(validationIssues);
});
this.sandbox.getComponentPropertyValue('name').subscribe((name) => {
const componentName = name ? `${this.sandbox.descriptor.name} (${name})` : this.sandbox.descriptor.name;
// Registering the properties accessible by button action "set property".
this.sandbox.setSettablePropertiesDataDictionary(componentName, this.getSettablePropertiesDataDictionaryBranch());
// Registering the output parameters.
this.sandbox.setCommonDataDictionary(this.prepareDataDictionary(componentName));
});
}
// Method called automatically that sets the view component input parameters
// default values or current values.
static getInitialProperties(initialProperties?: IRestaurantLabelParameters): IRestaurantLabelParameters {
// The hidden property are string with as values:
// '0' for false,
// '1' for true,
// At runtime the accepted values will be number type (0 or 1).
return {
hidden: '0',
message: '',
...initialProperties
}
}
private getSettablePropertiesDataDictionaryBranch(): IViewComponentDesignSettablePropertiesDataDictionary {
return [
{
label: 'Hidden',
expression: this.getExpressionForProperty('hidden'),
},
{
label: 'Message',
expression: this.getExpressionForProperty('message')
}
];
}
private prepareDataDictionary(componentName: string): IViewComponentDesignCommonDataDictionaryBranch {
return null;
}
private setInspectorConfig(model : IRestaurantLabelParameters) {
return {
inspectorSectionConfigs: [
{
label: 'General',
controls: [
{
name: 'name',
component: TextFormControlComponent,
options: {
label: 'View component name',
tooltip: new Tooltip('Enter a name to uniquely identify the view component.')
}
},
{
name: 'message',
component: ExpressionFormControlComponent,
options: {
label: 'Message',
tooltip: new Tooltip('The message input parameter is required and will be displayed at runtime.'),
dataDictionary$: this.expressionConfigurator.getDataDictionary(),
operators: this.expressionConfigurator.getOperators(),
isRequired: true
} as IExpressionFormControlOptions
},
{
name: 'styles',
component: TagsFormControlComponent,
options: {
label: 'CSS classes',
placeholder: 'Add CSS classes',
tooltip: new Tooltip('Enter CSS class names to apply to this view component.'),
errorCheck: validateCssClassName
} as ITagsFormControlOptions
},
{
name: 'hidden',
component: OptionalExpressionControlComponent,
options: {
label: 'Hidden',
dataDictionary$: this.expressionConfigurator.getDataDictionary(),
operators: this.expressionConfigurator.getOperators(),
}
}
]
}
]
};
}
private validate(
sandbox: IViewComponentDesignSandbox,
model: IRestaurantLabelParameters
): IViewComponentDesignValidationIssue[] {
let validationIssues = [];
if (!model.message) {
validationIssues.push(sandbox.createError('Message cannot be blank.', 'message'));
}
validationIssues = validationIssues.concat(validateCssClassNames(model.styles));
return validationIssues;
}
}
There are some important things to note here so we will cover them one by one:
- We describe the View Component input parameters and register them using sandbox.updateInspectorConfig
- We describe the View Component output parameters and register them using this.sandbox.setCommonDataDictionary
- We describe the View Component settable properties (that can be set using a Set Property action in an Action Button), and register them using this.sandbox.setSettablePropertiesDataDictionary
- We call the validation logic using the this.validate(this.sandbox, componentProperties) method, and we display them in the View Designer using this.sandbox.setValidationIssues(validationIssues);.
- The method this.sandbox.getComponentPropertyValue('name') allows us to react when the View Component Name field is modified. We often use this logic to append the Name to the View Component type to better find it in the Data Dictionary,
- The method getInitialProperties is automatically called to initialize the View Component input parameters with default values,
Run-Time Rendering
Since we have seen that the config calls for rendering Directives let's look at the code for them. As mentioned above, the run-time rendering has to be done in the file restaurant-label.component.ts.
import { CommonModule } from '@angular/common';
import { Observable, throwError } from 'rxjs';
import { BaseViewComponent, IViewComponent } from '@helix/platform/view/runtime';
import { RxViewComponent } from '@helix/platform/view/api';
import { IRestaurantLabelParameters } from '../design/restaurant-label.interface';
@Component({
standalone: true,
selector: 'com-example-meal-program-lib-restaurant-label',
styleUrls: ['./restaurant-label.scss'],
templateUrl: './restaurant-label.component.html',
imports: [CommonModule]
})
@RxViewComponent({
name: 'com-example-meal-program-lib-restaurant-label',
})
export class RestaurantLabelComponent extends BaseViewComponent implements OnInit,IViewComponent {
// Contains the view component instance id.
guid: string;
// Contains the view component configuration.
config: Observable<any>;
// Contains the view component instance parameters.
inputParams: IRestaurantLabelParameters;
// Implementing set property and refresh apis.
api = {
// This will be called when an input parameter is set by a button "set property" action.
setProperty: this.setProperty.bind(this)
};
ngOnInit() {
// Subscribing to input parameter changes.
this.config.subscribe((config: IRestaurantLabelParameters) => {
this.inputParams = config;
});
// Registering the custom set property and refresh implementations.
this.notifyPropertyChanged('api', this.api);
}
// This method is triggered by a button "set property" action.
setProperty(propertyPath: string, propertyValue: any): void | Observable<never>{
switch (propertyPath) {
case 'hidden': {
this.inputParams.hidden = propertyValue;
this.notifyPropertyChanged(propertyPath, propertyValue);
break;
}
case 'message': {
this.inputParams.message = propertyValue;
break;
}
default: {
return throwError(`Restaurant Label : property ${propertyPath} is not settable.`);
}
}
}
}
Much of this code is very standard for creating a Component in Angular and won't be explained here. Some important points particular to BMC Helix Innovation Studio and this component are:
The templateUrl attribute binds the html template that will be evaluated using the scope information - in this case the scope object restaurantLabel - that is maintained by this Directive's code. It needs to have the fully-qualified file name of com-example-meal-program-lib-restaurant-label.directive.html to avoid collisions because this resource will be compiled together with all View Components deployed to BMC Helix Innovation Studio.
restaurant-label.component.html<h3>Restaurant: {{inputParams.message}}</h3>Finally, because we did configure this component with enableExpressionEvaluation set to true, it is not enough to evaluate the expression just once at initialization time. We need to set up a listener so that we can update the label each time its value is modified. We subscribe to the config observable for this.
restaurant-label.component.html// Subscribing to input parameter changes.
this.config.subscribe((config: IRestaurantLabelParameters) => {
this.inputParams = config;
});
Design-Time Rendering
We have looked at all the code that configures and renders the component at run-time. However, recall that the config specified a different Component would be used at design-time. This is needed for the restaurant-label component because it has dynamic data (namely, the label expression) which is only known at run-time. In fact, most components will have a different design-time rendering than that which is used at run-time.
The design-time Directive is pretty simple, especially since it is static content. Although you can get very fancy with the design-time rendering, we are simply going to emit the label "Restaurant:".
import { CommonModule } from "@angular/common";
import { FormsModule } from '@angular/forms';
import { RestaurantLabelDesignModel } from './restaurant-label-design.model';
@Component({
standalone: true,
selector: 'com-example-meal-program-lib-restaurant-label-design',
styleUrls: ['./restaurant-label-design.scss'],
templateUrl: './restaurant-label-design.component.html',
imports: [CommonModule, FormsModule],
})
export class RestaurantLabelDesignComponent {
@Input()
model: RestaurantLabelDesignModel;
}
There is not much else to note here, other than the html template snippet, which does not contain any references to the scope or anything else that is dynamic.
Challenge
- Modify the design-time and run-time templates. Rebuild and deploy the project using the following command, refresh the browser, and try out your changes.
- If you have some experience with building JavaScript with Angular, and some time on your hands, this project is completely open for you to change any aspect of this custom code. Create your own custom component for Meal Program Library and deploy and test it.
- Create some useful, common component in a brand-new library that you build using the Maven Archetype, as described in Creating a project using Maven and the Archetype.
You can refer to our sample code which is available in our BMC public GitHub repository, which containsmore information and tutorial on how to create a View Action or a View Component.
What Have you Learned
- Custom View Components are created using standard Angular technology. This is not the same framework as the one known as AngularJs.
- Certain naming conventions are important for scoping.
- Registration of your Module is required.
- You can create custom design-time as well as run-time experiences in your own code, and users can bind values from other components in the View by declaring Properties.