Skip to content

Expose form validation state to custom fields via FormFieldAPI / DotCustomFieldApi #35296

@nicobytes

Description

@nicobytes

Note: This template is intended for Engineering team use.

Description

Custom fields (VTL-based code injected into the DOM) that are marked as required do not display any validation alert or error indicator when the form is submitted or the field is touched. Unlike Angular-native fields — which use BaseWrapperField.$hasError and render a "This field is mandatory" message via p-field-error — custom fields have no mechanism to query or react to the host form's validation state.

Image

Root cause

The FormFieldAPI interface (exposed to custom field code as DotCustomFieldApi.getField(fieldId)) currently only provides:

  • getValue() / setValue() — read/write field values
  • onChange() — subscribe to value changes
  • enable() / disable() — toggle field enabled state
  • show() / hide() — toggle field visibility

No validation-related methods exist — there is no way for custom field code to know whether a field is valid, required, touched, or dirty, nor to subscribe to validation state changes.

Affected components

Component Path Role
FormFieldAPI interface core-web/libs/edit-content-bridge/src/lib/interfaces/form-bridge.interface.ts Defines the API contract
AngularFormBridge core-web/libs/edit-content-bridge/src/lib/bridges/angular-form-bridge.ts Angular implementation (delegates to FormGroup controls)
NativeFieldComponent core-web/libs/edit-content/src/lib/fields/dot-edit-content-custom-field/components/native-field/ Sets window.DotCustomFieldApi for native strategy
IframeFieldComponent core-web/libs/edit-content/src/lib/fields/dot-edit-content-custom-field/components/iframe-field/ Sets iframeWindow.DotCustomFieldApi for iframe strategy
VTL custom fields e.g. dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/url-title_new.vtl Consumer example

Expected API additions

The following methods should be added to FormFieldAPI:

interface FormFieldAPI {
  // ... existing methods ...

  /** Returns true if the field's FormControl is valid */
  isValid(): boolean;

  /** Returns true if the field is required (via field metadata or Validators.required) */
  isRequired(): boolean;

  /** Returns true if the field's FormControl has been touched */
  isTouched(): boolean;

  /** Returns true if the field's FormControl is dirty */
  isDirty(): boolean;

  /** Returns the current validation errors object, or null if valid */
  getErrors(): Record<string, unknown> | null;

  /**
   * Subscribes to validation status changes (valid ↔ invalid transitions).
   * Callback receives the current validation state.
   * Returns an unsubscribe function (consistent with onChange() pattern).
   */
  onStatusChange(callback: (state: { valid: boolean; errors: Record<string, unknown> | null }) => void): () => void;
}

Implementation approach

In AngularFormBridge.getField(), each new method should delegate to the underlying Angular AbstractControl:

  • isValid()control.valid
  • isRequired()field.required or control.hasValidator(Validators.required)
  • isTouched()control.touched
  • isDirty()control.dirty
  • getErrors()control.errors
  • onStatusChange() → subscribe to control.statusChanges observable, following the same unsubscribe pattern as onChange()

Usage example (VTL custom field)

DotCustomFieldApi.ready(() => {
    const myField = DotCustomFieldApi.getField('urlTitle');

    // Check initial state
    if (myField.isRequired() && !myField.isValid()) {
        showError('This field is required');
    }

    // React to validation changes
    myField.onStatusChange(({ valid, errors }) => {
        if (!valid) {
            showError('Validation failed');
        } else {
            hideError();
        }
    });
});

Acceptance Criteria

  • FormFieldAPI interface includes new methods: isValid(), isRequired(), isTouched(), isDirty(), getErrors(), and onStatusChange(callback)
  • AngularFormBridge.getField() implementation returns a FormFieldAPI where each new method delegates to the underlying Angular AbstractControl properties (valid, touched, dirty, errors, statusChanges)
  • onStatusChange(callback) subscribes to the Angular AbstractControl.statusChanges observable and executes the callback with the current validation state ({ valid, errors }) whenever the status changes
  • onStatusChange() returns an unsubscribe function, consistent with the existing onChange() pattern
  • Custom field VTL code can call DotCustomFieldApi.getField('fieldVar').isValid() and receive a boolean indicating the current validation state
  • Custom field VTL code can call DotCustomFieldApi.getField('fieldVar').onStatusChange(cb) and the callback fires when the field transitions between valid and invalid states
  • Validation state is exposed in both native-field and iframe-field strategies (both set DotCustomFieldApi on the respective window)
  • Existing FormFieldAPI consumers (getValue, setValue, onChange, enable, disable, show, hide) are not affected by the additions — full backward compatibility
  • Unit tests cover all new FormFieldAPI methods in AngularFormBridge (valid, invalid, touched, dirty, errors, statusChange subscription and unsubscription)

Additional Context

Currently, AngularFormBridge.set() already calls markAsTouched(), markAsDirty(), and updateValueAndValidity() on the control — so programmatic updates from VTL code do affect Angular form state. The gap is that there is no way for the VTL code to read that state back or subscribe to changes.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status

    Next 2-4 Sprints

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions