Skip to main content
Version: 2.4

JEXL Practical Patterns

Practical Patterns

Hide A Field

Hide a field when the customer is not a company:

$context.form.customerType != 'COMPANY'

Required Field Based On Another Field

Make companyId required only for company customers:

$context.form.customerType == 'COMPANY'

Default Date Value

Set the start date to today without time:

now(true)

Calculate Total Price

sumMany($context.form.orderItems, 'price', 2)

If items contain quantity and unit price, use mapping with a callback and sum if the runtime supports spread syntax:

sum(...$context.form.orderItems.map(x => x.quantity * x.unitPrice))

If spread syntax is not supported in the given field, move the calculation to a helper or separate computed value.

Custom Validation

In schema customValidations, the expression usually returns true when the value is valid. customValidationsValidating controls whether that validation should run.

Email is valid when it is empty or has a valid format:

isNullOrEmpty($value) || isValidEmail($value)

National ID is valid when nidValid returns true:

nidValid($value.birthNumber)

Always verify the exact validation slot. Some older or custom widget-specific validation properties can use the opposite convention.

Display Contact Names

join(map($context.form.contacts, 'name'), ', ')

With method syntax:

$context.form.contacts.map(x => x.name).join(', ')

Filter Active Items

filter($context.form.items, 'status', 'ACTIVE')

With method syntax:

$context.form.items.filter(x => x.status == 'ACTIVE')

Pick The First Active Contact

find($context.form.contacts, 'active', true)

With method syntax:

$context.form.contacts.find(x => x.active)

Safe Text Fallback

$context.entity.name || 'No name'

Safe Numeric Fallback

($context.form.discount ?? 0) + ($context.form.manualDiscount ?? 0)

For decimal numbers, use safePlus:

safePlus($context.form.priceWithoutVat, $context.form.vat)

Real Form Patterns

The following patterns mirror current tSM form configuration. They show the shape of expressions you will actually meet in widgets, default values, filters, validations, and buttons.

The form designer is the place where these expressions are usually attached to fields, widgets, layout columns, hidden/disabled/required properties, default values, and validation configuration.

JEXL-rich form in the form designer

Show Or Hide By Form Checks

Product and order forms often compute boolean flags under $context.form.checks. A field is hidden when the required prerequisite is missing:

!$context.form.checks.hasVoice

Combined conditions are common:

!$context.form.checks.hasInternet_productOrBundle ||
!$context.form.checks.hasContractSignMethod ||
$context.form.checks.hasContractSignMethod_digital_Technician

Use this pattern for hidden, readonly, disabled, and requiredValidating when the same business precondition controls several widget states.

Tooltip From Missing Prerequisites

Tooltips often explain why a field is disabled:

(!$context.form.checks.hasAddress || !$context.form.checks.hasVoice)
? 'Address and voice product are required before phone number reservation'
: null

Return null when no tooltip should be shown.

Required Only When Visible

For uploaded offline contracts:

$context.form.chars.contractSign.contractSignType == 'FyzickyD2dOffline'

The same condition can be used in requiredValidating, while hidden uses the inverse:

$context.form.chars.contractSign.contractSignType != 'FyzickyD2dOffline'

Default From Script Result

Script-backed defaults usually guard the call first and then read .data:

!isNullOrEmpty($context.form.customerId)
? evalScriptByCode(
'Crm.Customer.GetPrimaryPersonEmail',
{ customerId: $context.form.customerId }
).data
: null

For heavier order checks, pass the full form and current user:

!isNullOrEmpty($context.form.customerId)
? evalScriptByCode(
'Order.Person.OvereniePrimarnejOsoby',
{ order: $context.form, userId: currentUserId() }
).data
: null

Script Data For Product Selects

Product select widgets commonly pass form state into a script:

{
categories: ['Produkty.Internet.Optik'],
commitment: $context.form.chars.commitment,
technology: $context.form.technologySwitch,
availabilitySwitch: $context.form.availabilitySwitch
}

For dependent product options:

{
empty: !$context.form.checks.hasSlovanetTv_productOrBundle,
categories: ['Produkty.TV.SlovanetTV.PB'],
technology: $context.form.technologySwitch,
productSpecId: !isNullOrEmpty($context.form.productTvProgram.productSpecId)
? $context.form.productTvProgram.productSpecId
: $context.form.productOffer.products[1].productSpecId,
relationshipTypeCode: 'Product.Product.ProgramovyBalik'
}

Default Filters For LOV Widgets

LOV filters are arrays of filter objects. A common role-based example:

includesArray(
user.user.get(currentUserId(), { expand: ['ROLES'] }).roles,
['UserRole.Backoffice', 'UserRole.CallcentrumManager', 'UserRole.BackofficeManager'],
true
)
? [
{
field: 'code',
operator: 'eq',
value: 'Postou',
readonly: true,
visible: false
}
]
: [
{
field: 'code',
operator: 'eq',
value: 'PodpisanaNaPredajni',
readonly: true,
visible: false
}
]

For a script-backed in filter:

isNullOrEmpty($context.form.accountId)
? [
{
visible: false,
field: 'id',
operator: 'in',
value: [$context.form.accountId]
}
]
: [
{
visible: false,
field: 'id',
operator: 'in',
value: evalScriptByCode(
'Order.Zrusenie.Getreturndeviceforaccountid',
{ accountId: $context.form.accountId, idList: true }
).data
}
]

Role-Based Date Limits

Date widgets often allow privileged users to go further into the past:

(hasRole('UserRole.Backoffice') ||
hasRole('UserRole.BackofficeManager') ||
hasRole('UserRole.CallcentrumManager') ||
hasRole('Admin'))
? new Date('2025-01-01')
: addTime(now(), 0, 'days')

National ID And Age Validation

Person forms combine domain helpers, date helpers, and duplicate checks:

nidValid($value.birthNumber)
compareDates(
now(),
addTime(nidToDate($value.birthNumber), 18, 'years'),
'>=',
'days'
)
isNullOrEmpty(
crm.customer.find({
'chars.custPersonIdentifiers.birthNumber':
$context.form.chars.custPersonIdentifiers.birthNumber
})
)

Run expensive duplicate checks only when the field is filled and the user is not privileged to bypass the validation:

hasPriv('Manager.SuperPriv') == false &&
!isNullOrEmpty($value.birthNumber)

Mapping A Code To A Default Value

Long chained ternaries are common when one form field determines another:

$context.form.chars.cancelationDetails.terminationMethod == 'TERMINATION.DECEASED'
? 'CONTRACT.HOLDER.DECEASED'
: $context.form.chars.cancelationDetails.terminationMethod == 'SERVICE.TRANSFER.INTERNET'
? 'INTERNET.TRANSFER.REQUEST'
: $context.form.chars.cancelationDetails.terminationMethod == 'TERMINATION.NON.PAYMENT'
? 'SLOVANET.TERMINATION.NON.PAYMENT'
: null

If the list grows, prefer a helper script or a register-driven configuration instead of adding many more ternary branches.