Custom Annotations
In the world of Guidewire’s xEngage products, and hopefully going forward with Jutro Design Platform (JDP), there is often a wish to have the back end (aka System of Record) drive many of the front end attributes such that your business logic is contained where it should be. This small tutorial will go through what makes these annotations work and how to take advantage of them.
Side Note: I am fully aware that the induction of JDP may make some of this obsolete but hopefully the concepts will transfer.
The Basics
At the heart of any annotation is a simple Gosu class that implements either IMetaFactory or IMetaMultiFactory. Each of these interfaces has one overridable method called getState, which returns an Object. The only difference is that IMetaMultiFactory returns an array of Objects. The return objects are what gets used on the front end to drive the logic.
Let’s take a look at the OOTB Size.gs file.
Notice that it returns a simple ValidationRuleDTO with expression logic built in. This example rule needs the value to be within a range of min and max values.
return new ValidationRuleDTO(
Expr.isNot(
Expr.some({
Expr.lessThan(Expr.getProperty("length", Validation.VALUE), _min),
Expr.greaterThan(Expr.getProperty("length", Validation.VALUE), _max)
})
),
Expr.translate("Edge.Web.Api.Model.Size", {_min, _max}))
}
On the front end, there is code in Validity.js that looks for ValidationRuleDTO and evaluates, via Expression logic, to figure out what range to keep the value within. This is all internal but could be overwritten using viewmodelConfig.js. (see Visible on Front End below)
Creating Custom Annotations
One missing annotation is @Visible. OOTB provides @NotSet, which is often used in place of Visible, but this is awkward and not the same thing. Instead, let’s create a custom annotation to add this specific functionality.
Visible
On the back end, start by creating a Visible.gs class which implements IMetaMultiFactory with basic constructors.
Typically, when a value is not visible on the screen, we want that value to be null when saved in the back end. But there are times when, even if a property is not visible, there is a dependency to other properties, so we retain its value or set it to some default. This is why I added a NullWhenBlank feature, which is quite handy.
class Visible implements IMetaMultiFactory {
private var _when : ExpressionDTO as When
private var _nullWhenBlank : Boolean as NullWhenBlank
construct() {this(null, true)}
construct(w: ExpressionDTO) {
this(w, true)
}
construct(w: ExpressionDTO, nullWhenBlank : Boolean) {
this._when = w
this._nullWhenBlank = nullWhenBlank
}
override function getState(): Object[] {
return {
_when == null ? Validation.visible() : Validation.visibleWhen(_when),
new NullabilityRuleDTO(Expr.const(_nullWhenBlank))
}
}
}
The IMetaMultiFactory also lets us return two (2) or more different rules using a single annotation. For example, we return whether the element is visible or not and if we should retain its value when not visible. This can be seen in the return statement, since this is returning a list of items.
Next, create a NullabilityRuleDTO.gs file
/** Rule defining a nullability of the property when not visible. This value is
supported for properties only. */
final class NullabilityRuleDTO implements IAugment {
@JsonProperty
private var _expr : ExpressionDTO as readonly Expression
construct(e : ExpressionDTO) {
this._expr = e
}
}
Notice that we are using the interface IAugment to allow this to also be used in a @Augment annotation.
Visible Annotation
To use this annotation, we simply write @Visible(<expression>, [nullable])
Ex: @Visible(Expr.neq(Expr.const(typekey.PolicyLine.TC_GENERALLIABILITYLINE), Validation.getContext(“lineOfBusiness”)), false)
Here we are telling the front end that this property will be visible if the LOB is NOT GL and we do not want to null it out when it is blank. This means that a downstream process uses this property no matter if the front end shows it.
Visible on Front End
Since there is a built-in kind of visible on the front end, I chose to rename my feature to Ocular, which means visibility. In this way, we don’t conflict with OOTB logic.
In the <application>/src/overrides folder, edit the viewmodel-config.js file and add the import of Ocularness.
import ocular from './Ocularness';
// Export Expression Types and Validation Functions
export const aspectFactory = {
ocular: ocular.create,
}
This lets the aspect compiler know we are going to override a function. Now create the Ocularness.js file.
import _ from 'lodash';
import { Augment } from '@xengage/gw-portals-viewmodel-utils-js';
const METADATA_CLASS = 'edge.common.aspects.validation.dto.VisibilityRuleDTO';
const NULLABILITY_CLASS = 'edge.common.aspects.validation.dto.NullabilityRuleDTO';
/** Creates a new "Field is required" aspect. */
function create(expressionLanguage) {
function getAllRules(compilationContext, node, nodeMetadata, ancestorChain) {
const rules = Augment.collectRules(compilationContext, node, nodeMetadata, ancestorChain,
METADATA_CLASS);
rules.concat(Augment.collectRules(compilationContext, node, nodeMetadata, ancestorChain,
NULLABILITY_CLASS));
return rules;
}
function compileRule(compilationContext, rule) {
const visibilityExpr = compilationContext.compile(rule.data.expression);
return {
'shouldApply': rule.shouldApply,
'status': visibilityExpr
};
}
function combineRules(rules) {
return {
'ocular': (v, pv, ctx) => {
return rules.every((rule) => {
/* console.log(`Ocular for ${v._accessorCode} should Apply ${rule.shouldApply()}
and status ${rule.status(v, pv, ctx)} of ${rule.status(v, pv, ctx) === 'VISIBLE'}`) */
return rule.shouldApply() && rule.status(v, pv, ctx) === 'VISIBLE';
});
},
};
}
return {
getAspectProperties: (currentViewModelNode, currentMetadataNode, ancestorChain)
=> {
const compilationContext = expressionLanguage.getCompilationContext(currentMetadataNode.xCenter);
const rules = getAllRules(compilationContext, currentViewModelNode, currentMetadataNode,
ancestorChain);
const compiledRules = rules.map(_.partial(compileRule, compilationContext));
const descriptor = combineRules(compiledRules);
return {
ocular: {
get: () => {
return descriptor.ocular(
currentViewModelNode,
ancestorChain.parent,
currentViewModelNode.aspects.context);
}
},
ocularPresent: {
get: () => {
const ret = currentMetadataNode.elementMetadata.get(METADATA_CLASS);
return ret.length > 0;
}
},
nullWhenBlank: {
get: () => {
const ret = currentMetadataNode.elementMetadata.get(NULLABILITY_CLASS);
return ret.length === 0 ? true : ret[0].expression.value;
}
}
};
}
};
}
export default { create };
This will now add three (3) aspects to the function list for a property (ocular, ocularPresent, and nullWhenBlank). When you reference aspects for a given property, under the covers, it calls all the getAspectProperties functions and returns those methods as usable.
Notice the two (2) DTOs highlighted in the code as well. The references to these locations are critical, so if things do not work once you annotate your back end, check that these paths are correct.
Using the aspect
Now that we have the aspect telling the front end what the annotation evaluates to, how can we use it? We are going to introduce a simplified MasterInput that takes this aspect and provides use around it. This is a very basic version and is not the full working copy but shows how to use these newly created aspects.
import React, { useContext, useMemo } from 'react';
import _ from 'lodash';
import { InputField } from '@jutro/components';
import { TranslatorContext } from '@jutro/locale';
import cx from 'classnames';
import PropTypes from 'prop-types';
const CONTROL_TYPES = {
TEXT_INPUT: 'text_input',
DATE: 'date',
}
const logErrorMessages = (...msgs) => {
console.group("%cMasterInput Error",
'color:orangered;font-weight:900;font-size:1.25em;text-decoration:underline;')
Array.prototype.map.call(msgs, (message) => {
console.log(`%c${message}`, 'color:orangered;')
})
console.groupEnd();
};
const getControlType = (currentNode, vmControlType, passedControlType) => {
// there is no date control type so we need to check the currentNode object
if (vmControlType === 'text' && !('day' in currentNode && 'month' in currentNode)
&& !('amount' in currentNode && 'currency' in currentNode)) {
return CONTROL_TYPES.TEXT_INPUT;
}
if (vmControlType === 'text' && 'day' in currentNode && 'month' in currentNode) {
return CONTROL_TYPES.DATE;
}
}
const MasterInput = (props) => {
const {
id,
parentNode,
path,
controlType: pControlType,
value
} = props;
const translator = useContext(TranslatorContext);
const currentNode = _.get(parentNode, path);
const hasViewModel = useMemo(() => {
if ((!_.isObject(currentNode) && pControlType === undefined)
|| (_.isObject(currentNode) && currentNode.aspects === undefined)) {
logErrorMessages(`NOT HOOKED UP TO VM for ${path}`, `Element ID: ${id}`);
return false;
}
return true;
}, [id, pControlType, path])
const controlType = useMemo(() => {
return getControlType(currentNode, _.get(currentNode, 'aspects.inputCtrlType'),
_.toLower(pControlType))
}, [currentNode, pControlType]);
const maybeRemoveFieldValue = (isHidden) => {
const nullWhenBlank = _.get(currentNode, 'aspects.ocularPresent')
? _.get(currentNode, 'aspects.nullWhenBlank') : true;
if (isHidden && nullWhenBlank) {
_.set(currentNode, 'value', undefined);
logErrorMessages(`SETTING TO NULL ${path}`);
}
};
const getVisible = () => {
if (!hasViewModel) { return }
const aspect = _.get(currentNode, 'aspects.ocular');
if (aspect !== undefined) {
maybeRemoveFieldValue(!aspect);
return aspect;
}
return true;
}
if (!getVisible()) {
return null;
}
/* taking care of initialization. In the event that there is no parentNode
(e.g. initializing) we need to bail */
if (_.isEmpty(parentNode)) {
logErrorMessages(`NO parentNode defined for ${path} on parent page!!`, `Element ID: ${id}`);
return null
}
const commonProps = {
visible: getVisible(),
}
const getControl = (type) => {
switch (type) {
case CONTROL_TYPES.TEXT_INPUT:
return (
<InputField
{...props}
{...commonProps}
className={cx('masterInputInputField', props.className,
{ 'masterInputIIndentedField': commonProps.indented })}
labelClassName={cx('masterInputIFieldLabel', props.labelClassName)}
/>
);
default:
return <div>{translator(messages.invalidControlType, { types: [pControlType,
_.get(currentNode, 'aspects.inputCtrlType')], path })}</div>
}
};
return (
<div className="masterInputIControlElement">
{getControl(controlType)}
</div>
);
};
MasterInput.propTypes = {
...InputField.PropTypes,
parentNode: PropTypes.object.isRequired,
path: PropTypes.string,
controlType: PropTypes.oneOf('text'),
};
export default MasterInput;
I have highlighted the aspect usage so you can get a feel for how these would be used.
The full implementation of a MasterInput is beyond the scope of this document but I wanted to show a starter template for use in your application.
VisibleRequired and SkipDtoValidation
One more small treat for you is the addition and combination of Visible and Required. There are many times when you need both annotations on a property so a combinatorial annotation was built to handle this case. It is important to note that in the event that you have separate @Visible and @Required annotations on a single property, the required portion should always be less restrictive than visible, or a default value is needed.
For example, if you have a description property that is required but not visible, this will cause an issue. In this case, a default value would be needed to avoid downstream issues. However, if the description is not required, it can be either visible or not and the code will still work.
Here is the code to achieve this annotation. I threw in a context variable (SkipDtoVaidation) so you could easily turn off the validations when you simply want to do a round trip from the front end and not deal with requiredness. This is beyond the scope of this document but may appear in a future blog.
package edge.common.aspects.validation.annotations
uses edge.aspects.validation.Validation
uses edge.el.Expr
uses edge.el.dto.ExpressionDTO
uses edge.metadata.annotation.IMetaFactory
uses edge.metadata.annotation.IMetaMultiFactory
uses edge.common.aspects.validation.dto.NullabilityRuleDTO
class VisibleRequired implements IMetaMultiFactory {
private var _when : ExpressionDTO as When
private var _msgExp : ExpressionDTO as Message
// by default, when a field is not visible it nulls out the value
// this is useful when you want to retain a value in a parent child relationship
// by setting this to false, the value will be retained even though not visible
private var _nullWhenBlank : Boolean as NullWhenBlank
construct() {
this(null, "Edge.Web.Api.Model.NotNull")
}
construct(w : ExpressionDTO) {
this(w, "Edge.Web.Api.Model.NotNull", true)
}
construct(w : ExpressionDTO, nullWhenBlank : Boolean) {
this(w, "Edge.Web.Api.Model.NotNull", nullWhenBlank)
}
construct(w : ExpressionDTO, msgKey : String) {
this(w, msgKey, true)
}
construct(w : ExpressionDTO, msgKey : String, nullWhenBlank : Boolean) {
this._when = w
this._msgExp = Expr.translate(msgKey, {})
this._nullWhenBlank = nullWhenBlank
}
override function getState(): Object[] {
return {
_when == null ? Validation.visible() : Validation.visibleWhen(_when),
_when == null
? Validation.requiredWhen(Expr.neq(Validation.getContext("SkipDtoValidation"), true), _msgExp)
: Validation.requiredWhen(Expr.all({Expr.neq(Validation.getContext("SkipDtoValidation"),
true),_when}), _msgExp), new NullabilityRuleDTO(Expr.const(_nullWhenBlank))
}
}
}
Conclusion
Using back end annotations can greatly increase code reuse and simplify visibility logic when you need to drive many components on the front end. Having these annotations in place means that many times, when visibility logic is needed, no extra JSX is needed to drive these settings.
I hope this greases the skids for you to begin exploring other ways to customize your experience and code smarter, not harder.
Troy Stauffer
Senior Software Architect
Watch or read our other posts at Kimputing Blogs. You’ll find everything from Automated testing to CenterTest, Guidewire knowledge to general interest. We’re trying to help share our knowledge from decades of experience.