How to perform cross-field date validation (e.g., Detection Date vs Birth Date) using custom Validators in Freedom UI?
Hi Community,
We are working on a Freedom UI page where we need to implement cross-field validation on a date field. Specifically, we want to validate that the Detection Date (FechaDeteccion) is not earlier than the Child's Birth Date (FechaNacimiento).
In Classic UI, we used to handle this easily by accessing other model attributes within the validation method. However, in Freedom UI using custom validators declared in validators schema block, the validator function only receives the control instance (Angular AbstractControl), and properties like control.root or control.parent do not give us access to other Page ViewModel attributes/context.
We are currently passing $context via viewModelConfigDiff parameters like this:
JS
"PDS_HcoFechaDeteccionSospecha_s35z4sf": {
"modelConfig": {
"path": "PDS.HcoFechaDeteccionSospecha"
},
"validators": {
"ValidarSospechaVsNacimiento": {
"type": "usr.HcoFechaSospechaNacimientoValidator",
"params": {
"message": "La fecha no puede ser anterior a la fecha de nacimiento.",
"campoNacimiento": "PDS_HcoFechaNacimiento_04pdwdb",
"context": "$context"
}
}
}
}
And in the validator declaration:
"usr.HcoFechaSospechaNacimientoValidator": {
"validator": function(config) {
return function(control) {
let fecha = control.value;
if (!fecha) return null;
let ctx = config.context;
// Reading secondary field from passed context...
let fechaNacVal = ctx ? ctx[config.campoNacimiento] : null;
// Date comparisons...
return isInvalid ? { "usr.HcoFechaSospechaNacimientoValidator": { message: config.message } } : null;
};
},
"params": [
{ "name": "message" },
{ "name": "campoNacimiento" },
{ "name": "context" }
],
"async": false
}
Our Questions for the Community:
- Is passing
"context": "$context"inviewModelConfigDiffthe recommended OOTB standard in Freedom UI for cross-field validation, or is there a native SDK approach / API method to inspect other attributes from within a custom validator? - Alternatively, is there a recommended way to trigger/update the validation status of
FechaDeteccionprogrammatically from acrt.HandleViewModelAttributeChangeRequesthandler without relying onalert()or manual field resetting?
Any insights or best practices on how you are handling multi-field dynamic validations in Freedom UI would be greatly appreciated!
Thanks in advance!
Like
Hello,
The "context": "$context" The trick does not do what you think, and there is a supported OOTB way to get the cross-field behavior without it.
1. Why $context in validator params doesn't work, and what the validator really receives
Validator params are passed through verbatim. Nothing resolves $-bindings inside validators.params:
BaseViewModel._initSchemaValidatorsstoresconfig.paramsas-is on theValidatorInfo.CrtControlStateAdapter.transformwraps it into an AngularValidatorFnthat callsinstance.validate({ value: abstractControl.value }, params).
So in your validator config.context is the literal string "$context", and ctx[config.campoNacimiento] is undefined. The validator silently passes. It only "works" if the birth date is empty.
Two more things worth knowing from that same code path:
- The first argument is not the Angular
AbstractControl. It is a plainCrtControlStateobject with a singlevalueproperty. That is whycontrol.parentandcontrol.rootare undefined. This is by design: validators are meant to be pure functions of the value and their declared params. SchemaValidatorFactorywrapsparamsin a Proxy that throws if you read a param name that isn't declared in theparamsarray of the validator definition. Keep the two lists in sync.
Bottom line for question 1: there is no SDK hook for reading other attributes from inside a validator, neither for schema-declared validators nor for TypeScript @CrtValidator classes (same validate(controlState, params) signature).
2. The OOTB pattern: decide in a handler, surface through a toggled validator
The view model exposes two host public API methods for exactly this:
$context.enableAttributeValidator(attributeName, validatorName) $context.disableAttributeValidator(attributeName, validatorName)
Both call updateValueAndValidity() on the underlying form control, so the error appears or clears immediately and integrates with the normal field error rendering and save blocking. No alert(), no resetting the value.
Declare a validator that always fails, disabled by default:
// viewModelConfigDiff
"PDS_HcoFechaDeteccionSospecha_s35z4sf": {
"modelConfig": { "path": "PDS.HcoFechaDeteccionSospecha" },
"validators": {
"ValidarSospechaVsNacimiento": {
"type": "usr.HcoFechaSospechaNacimientoValidator",
"params": { "message": "La fecha no puede ser anterior a la fecha de nacimiento." },
"disabled": true
}
}
}// validators
"usr.HcoFechaSospechaNacimientoValidator": {
"validator": function (config) {
return function () {
return { "usr.HcoFechaSospechaNacimientoValidator": { message: config.message } };
};
},
"params": [{ "name": "message" }],
"async": false
}Do the comparison where you have the whole view model, in the attribute change handler, and react to changes of either date:
{
request: "crt.HandleViewModelAttributeChangeRequest",
handler: async (request, next) => {
const deteccionAttr = "PDS_HcoFechaDeteccionSospecha_s35z4sf";
const nacimientoAttr = "PDS_HcoFechaNacimiento_04pdwdb";
if (!request.silent && [deteccionAttr, nacimientoAttr].includes(request.attributeName)) {
const ctx = request.$context;
const deteccion = await ctx[deteccionAttr];
const nacimiento = await ctx[nacimientoAttr];
const invalid = deteccion && nacimiento && new Date(deteccion) < new Date(nacimiento);
if (invalid) {
ctx.enableAttributeValidator(deteccionAttr, "ValidarSospechaVsNacimiento");
} else {
ctx.disableAttributeValidator(deteccionAttr, "ValidarSospechaVsNacimiento");
}
}
return next?.handle(request);
}
}The validator name passed to enable/disable is the key you used in the validators block of the attribute (ValidarSospechaVsNacimiento), not the type.
3. Lower-level escape hatches, if you need them
These are public on BaseViewModel but are Angular-level rather than the "declarative" API above:
$context.getAttributeControl("PDS_HcoFechaDeteccionSospecha_s35z4sf")returns the realAbstractControl. CallingupdateValueAndValidity()on it re-runs the attribute's validators.setErrors()works too, but the next validation cycle overwrites it, so prefer the toggle pattern.await $context.validate()marks everything touched, re-validates all controls and returns an errors map keyed by attribute name.await $context.isValid()gives a boolean. Both are handy in acrt.SaveRecordRequesthandler if you want to block save on a cross-field rule.
One caveat on the toggle pattern: because the validator has no access to the other value, it must be enabled or disabled from every place that can change either date. Handling both attribute names in the same handler, as above, covers user edits and business-rule updates. Values set with silent: true skip the handler by design.