Part 1 – Creating And Using Control
Creating Custom Control
A Flowfactory Custom Control consists of two main parts:
Control template – this is an Angular control template and is the View part of the client side control.
Component – this is JavaScript code that is the code part of the client side control and is where you add logic and transformation of values for your control.
Now we'll create our first really simple control that will display the name of a user.
Adding a custom control to a view
Our control needs a property path to be bound to so we need a root entity for our view that can hold our Application User. Create a virtual entity called User Container and add a single entity property with Entity Value Type Application User.
Create a view with User Container as a Root Entity. Then go to the custom tab and drag our custom control to the design surface.
Double click the control and set Property Path to the Application User property.
Template
This is the template used for the example.
Double click the custom control in the view designer and set the following template in the Template tab.
<div style="margin: 100px 0; text-align: center;" *ngIf="context.value$ | async as value">
<h1 style="margin: auto 51px;">
{{value.FirstName}} {{value.LastName}}
</h1>
</div>
What exactly is going on here will be explained in more detail later on but those five lines should be all that is in your template for now.
Component
The next step is to add the JavaScript file that will contain the logic for our control.
Put the following contents into the Script tab for the custom control.
export class CustomControl {
controlKeys;
store;
rootEntity$;
value$;
metadata$;
tools;
init(controlKeys, store, rootEntity$, metadata$, value$, tools) {
this.controlKeys = controlKeys;
this.store = store;
this.rootEntity$ = rootEntity$;
this.value$ = value$;
this.metadata$ = metadata$;
this.tools = tools;
}
}
This should always be the base of your control and is needed for it to function properly.
Finally update the Startup workflow to create an instance of our virtual entity and assign Current User to the application user property. Then add an Open View Activity and pass the virtual entity as View Data.
Now that you run the application you should have something like this.
Success! Our first custom control!
How it all fits together
Now lets take a look behind the scenes and see what actually happens and how it all fits together.
The different parts that are involved on the client side are the following.
The web client will load the template and the component and create an Angular component at runtime. The template is associated with the component through a property called context, so all properties in the component needs be prefixed with this.
Component details
If we take a look at the component constructor we can see that we take a couple of parameters.
init(controlKeys, store, rootEntity$, metadata$, value$, tools)
These allow access to data and also some useful tools that are available for your custom control logic.
We are using a naming convention where we use a $ suffix for properties that are RxJS observables and not actual values. Here the rootEntity$, metadata$ and value$ are observables.
For more information about observables see https://rxjs.dev/api/index/class/Observable.
ControlKeys
Here we have access to the Id of the view and entity as well as the property path that the control is bound to.
viewId
EntityId
propertyPath
Store
The store is a controlled state container that allows you to push data changes from your component as well as to trigger actions that are passed to the server side for running workflows.
This is an implementation of a ngRx Store. For more information see https://ngrx.io/guide/store.
RootEntity$
Here you can access the root entity of the view. In our example we could access the UserContainer object here.
This is a sample dump of the object.
Metadata$
This is the metadata for the control and contains information about control size, property path etc.
This is a sample dump of the object.
Value$
This is an observable object that contains the data that the control was bound to in the view designer. It will contain the properties that you specified in the ControlInfo-class plus Id and Key field.
This is a sample dump of the object.
Tools
The tools object holds different tools and functions that can be useful in your custom component.
The different tools can be accessed trough these properties.
rxjsOperators
lodash
ActionDictionary
DevExtreme
rxjsOperators
This is an import of all operators in the RxJS framework which can be used in your component. Some of the more frequently used are map and tap operators.
For more information about the available operators see https://rxjs.dev/api/operators.
Lodash
Here you have access to whole lodash framework. Lodash is a collection of utility functions that are very usable for JavaScript applications.
See this URL for available functions https://lodash.com/docs.
ActionDictionary
The action dictionary contains actions that can be passed to the store is used to trigger different events which it is turn can either push updated data changes to the server or trigger running of workflow.
DevExtreme
This is access point for utilities in the DevExtreme framework. As of today only the ArrayStore class is available here which can be used to create DevExtreme datasources based on arrays of objects.
This can be extended in future updates of the web client if more utilities are needed from DevExtreme. For more information about ArrayStore see https://js.devexpress.com/Documentation/ApiReference/Data_Layer/ArrayStore/.
Template details
The template is an ordinary Angular template and it can access the component through the context property.
If we break down our simple template, and remove static text rows, we can see the following.
<div style="margin: 100px 0; text-align: center;" *ngIf="context.value$ | async as value">
We use *ngIf directive to say that this element should only be rendered if context.value$ has a value. We also use the AsyncPipe operator (| async) and specify an alias for easier access in our template.
{{value.FirstName}} {{value.LastName}}
We then bind to the FirstName and LastName properties of our value object using interpolation to incorporate them in our DOM.
You access to all different Angular directives like *ngIf and *ngFor which allows for very powerful templates.
For more information about angular templates see: https://angular.io/guide/template-syntax.
For more information about pipes see: https://angular.io/guide/pipes.
Debugging your component
To debug your component you can find your component code in Chrome Debugging tool. Go to Sources tab and expand the folder node (no domain). Here your custom control component code will be as data strings. They can be opened and you can even insert break points here.
Part 2 – Displaying data
Simple Values With Async
When you are working with values in the template a tip is to wrap the contents inside an element that has the following attribute *ngIf="context.value$ | async as value".
That way the element will only be displayed if you have a value, when the component is initially loaded it hasn't received a value yet, and it makes it easier to access values in the template through the value alias.
The values can then either be shown using interpolation
{{value.FirstName}}
Or you can bind values to controls, like the DevExtreme TextBox Control.
<dx-text-box [value]="FirstName">
</dx-text-box>
When using interpolation you can transform the values using pipe operators, for example with dates.
{{ CreatedDate | date:'yyyy-MM-dd' }}
Links
Text interpolation: https://angular.io/guide/interpolation
Angular Pipes: https://angular.io/guide/pipes
Property Binding: https://angular.io/guide/property-binding
Showing Lists With NgFor
When showing lists you can use the Angular NgForOf directive which will loop over collections and create elements for each item in the collection. See the following example for how to display a list of Todos.
<ul class="todo_list">
<li *ngFor="let todo of context.value$">
<div style="width: 400px;">
<div class="priority">{{todo.Priority}}</div>
<h2>{{todo.Title}}</h2>
<p>{{todo.Description}}</p>
</div>
</li>
</ul>
The NgForOf directive has several features like retrieving the current index and applying classes to odd/even rows.
Links:
Angular NgFor: https://angular.io/api/common/NgForOf
Transforming Data
One great feature about RxJS is that has built in operators for transforming the control data. The two most common operators for transforming are Map and Tap. These are used by the RxJS pipe method which allows you to apply operators to observables which makes it very convenient to use for the value$ observable.
When you use the pipe method, the result is a new observable containing a stream of transformed values. Therefor you should store the transformed observable as a property inside of your custom control.
Here is an example where we apply the RxJS map operator to transform the values from the stream to a new format.
this.transformedValues$ = value$.pipe(
tools.rxjsOperators.map(forecastHeaders => {
if (forecastHeaders) {
return forecastHeaders.map(forecastHeader =>
me.transformForecast(forecastHeader));
}
}));
Let's break this example down line by line
this.transformedValues$ = value$.pipe(
We use the pipe method to create a new observable variable that we call transformedValues$, this will be updated every time that value$ gets a new value.
tools.rxjsOperators.map(forecastHeaders => {
We add the map operator to the pipe and declare that the value that we retrieve in the stream as forecastHeaders.
if (forecastHeaders) {
This is an important detail, forecastHeaders is the value that we bound the control to and it will be undefined when the control is created since it hasn't gotten the value supplied yet. Therefor we must check for undefined here.
return forecastHeaders.map(forecastHeader =>
Another important detail is that even if the control is bound to a collection; the value that we get is the collection property and not a list of items. So forecastHeaders here is an Array, not an individual item. Here we use the JavaScript map method that exists in the Array class to map each item in the Array and return the new collection.
me.transformForecast(forecastHeader));
}
}));
The rest is just finishing of the method call.
The pipe method takes several operators that are applied so that you can add multiple transformation, filtering etc.
this.transformedValues$ = value$.pipe(
tools.rxjsOperators.map(forecastHeaders => performMap(forecastHeaders),
tools.rxjsOperators.tap(forecastHeaders => performTap(forecastHeaders),
);
For more information about the available operators see https://rxjs.de.
Tap vs Map
These are the definitions of Tap and Map from RxJS documentation:
Tap: Perform a side effect for every emission on the source Observable, but return an Observable that is identical to the source.
Map: Applies a given project function to each value emitted by the source Observable, and emits the resulting values as an Observable.
As you can see the Tap operator returns the same observable while Map returns a new one. So basically you can extend and modify the objects with Tap while you can completely transform them with Map.
Part 3 – Updating data and running workflows
Updating data and running workflows from your custom control is done through the ngRx Store available to the control.
The way it works is like in the following picture:
The component in this case is your custom control and the database is the server side part of the web client.
The store is what supplies your control with data through a selector (the property path that you picked in Studio) and you push data back to the server through Actions.
In code it will look like this:
this.store.dispatch(
this.tools.ActionDictionary["uiUpdate"]({
viewId: context.controlKeys.viewId,
entityId: context.controlKeys.entityId,
changedEntities: changedEntities
}));
Inside of the tools object you have access to a ActionDictionary where you can create actions that is then dispatched to the Store. The Store will then translate the Action in to an Effect and that will be pushed to the server through either a pushUpdate or runWorkflow depending on the Action.
The response from that will then be pushed back to the control through the selector and the value$ object will get a new value.
There are two different actions available, uiUpdate and runWorkflow and how they are used can be viewed below.
Update data by property path
When updating data you can either update a property in the current view by a property path that is based on the root of the view. For example if you have a view with a root entity of ApplicationUser you could update property path FirstName and it would update the name of the user.
You do this by specifying valuePath and newValue parameters of the uiUpdate action.
this.store.dispatch( this.tools.ActionDictionary["uiUpdate"]({ viewId: this.controlKeys.viewId, entityId: this.controlKeys.entityId, valuePath: "FirstName", newValue: updatedValue }));
Update data by specific object
If you are showing a list of items and you want to update a specific one, or if you want to update multiple values at once, you can send in a list of changed entities that are passed as is to the server. You specify the entity Id (and key), entity name and pass an object containing all properties that you want to update.
var changedEntities = [{
Data: { PropertyName: "New value" },
EntityName: "EntityName",
Id: entityKey,
Key: entityKey
}];
this.store.dispatch(
this.tools.ActionDictionary["uiUpdate"]({
viewId: this.controlKeys.viewId,
entityId: this.controlKeys.entityId,
changedEntities: changedEntities
}));
Comments
0 comments
Please sign in to leave a comment.