Question

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:

  1. Is passing "context": "$context" in viewModelConfigDiff the 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?
  2. Alternatively, is there a recommended way to trigger/update the validation status of FechaDeteccion programmatically from a crt.HandleViewModelAttributeChangeRequest handler without relying on alert() 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 0

Like

1 comments

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._initSchemaValidators stores config.params as-is on the ValidatorInfo.
  • CrtControlStateAdapter.transform wraps it into an Angular ValidatorFn that calls instance.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 plain CrtControlState object with a single value property. That is why control.parent and control.root are undefined. This is by design: validators are meant to be pure functions of the value and their declared params.
  • SchemaValidatorFactory wraps params in a Proxy that throws if you read a param name that isn't declared in the params array 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 real AbstractControl. Calling updateValueAndValidity() 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 a crt.SaveRecordRequest handler 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.

Show all comments