Skip to content

Validate user data input

The CellValidatePlugin validates edits before they are written to the grid source. Use it when the grid should enforce spreadsheet-style rules such as required fields, numeric ranges, enum membership, cross-field limits, and approval gates.

Install it together with EventManagerPlugin so edit events are collected through the Pro event pipeline:

import {
CellValidatePlugin,
EventManagerPlugin,
} from '@revolist/revogrid-pro';
grid.plugins = [
EventManagerPlugin,
CellValidatePlugin,
];
grid.eventManager = {
applyEventsToSource: true,
};

By default validation is strict. If validate(value, model) returns false, the edit is rejected and the source keeps the previous value.

const columns: ColumnRegular[] = [
{
name: 'Price',
prop: 'price',
validate: (value, model) => {
const price = Number(value);
return Number.isFinite(price) && price >= 50 && price <= 1200;
},
validationTooltip: (value) =>
`Price ${value} must be between 50 and 1200`,
},
];

Set softValidation: true when invalid values should still be saved and marked visually. This is useful for imported data cleanup flows where users need to see and fix invalid rows in place. Pair it with validationRenderer when you want saved invalid values to stay visible after the edit.

{
prop: 'discount',
softValidation: true,
...validationRenderer(),
validate: (value, model) => Number(value) <= Number(model.price) * 0.3,
validationTooltip: (value, model) =>
`Discount cannot exceed 30% of price (${Math.round(Number(model.price) * 0.3)})`,
}

Both validate(value, model) and validationTooltip(value, model) receive the row model. During strict edit validation, the model includes the pending row patch, so cross-field validation works for single edits and pasted batches. If the edit is rejected, the tooltip renderer receives the rejected value and candidate model, which lets the message explain what failed even though the source was not changed.

{
name: 'Approved',
prop: 'approved',
validate: (value, model) => {
const netPrice = Number(model.price) - Number(model.discount ?? 0);
return netPrice <= 900 || value === true;
},
validationTooltip: () => 'High-value rows require approval',
}

validationTooltip also receives the column as a third argument: validationTooltip(value, model, column). Use that when one shared message resolver needs column metadata.

This demo exposes the rules as controls. Try strict mode to reject invalid edits, soft mode to save invalid data with markers, and the seed/reset actions to simulate cleanup workflows.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  AFTER_SOURCE_EVENT,
  TooltipPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  collectValidationIssues,
  createInvalidValidationRows,
  createValidationColumns,
  createValidationRows,
  defaultValidationRules,
  findRejectedEdit,
  getValidationColumnIndex,
  type RejectedEdit,
  type ValidationRuleState,
  type ValidationRow,
} from './validate-input.shared';
import {
  VALIDATION_DEMO_ACTION_EVENT,
  VALIDATION_DEMO_OPTIONS_CHANGE_EVENT,
  createValidationDemoToolbar,
  type ValidationDemoActionEvent,
  type ValidationDemoOptionsChangeEvent,
} from './validate-input-toolbar';

defineCustomElements();

export function load(parentSelector: string) {
  const root = document.createElement('div');
  const grid = document.createElement('revo-grid');
  const { isDark } = currentTheme();

  let rules: ValidationRuleState = { ...defaultValidationRules };
  let rows: ValidationRow[] = createValidationRows();
  let lastRejected: RejectedEdit | undefined;
  root.className = `validation-demo ${isDark() ? 'is-dark' : ''}`;

  const toolbar = createValidationDemoToolbar({
    rules,
    issues: collectValidationIssues(rows, rules),
    lastRejected,
  });

  const updateToolbar = () => {
    toolbar.state = {
      rules,
      issues: collectValidationIssues(rows, rules),
      lastRejected,
    };
  };

  const applyRules = () => {
    grid.columns = createValidationColumns(rules);
    void grid.updateColumns?.(grid.columns);
    updateToolbar();
    void grid.refresh?.();
  };

  const setRows = (nextRows: ValidationRow[]) => {
    rows = nextRows;
    grid.source = rows;
    lastRejected = undefined;
    updateToolbar();
  };

  grid.columns = createValidationColumns(rules);
  grid.source = rows;
  grid.plugins = [
    ColumnStretchPlugin,
    TooltipPlugin,
    EventManagerPlugin,
    CellValidatePlugin,
  ];
  Object.assign(grid, {
    stretch: 'all',
    eventManager: {
      applyEventsToSource: true,
    },
  });
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.hideAttribution = true;
  grid.range = true;
  grid.className = 'validation-demo-grid cell-border';

  toolbar.addEventListener(VALIDATION_DEMO_OPTIONS_CHANGE_EVENT, (event) => {
    rules = (event as ValidationDemoOptionsChangeEvent).detail;
    lastRejected = undefined;
    grid.dispatchEvent(new CustomEvent(AFTER_SOURCE_EVENT));
    applyRules();
  });

  toolbar.addEventListener(VALIDATION_DEMO_ACTION_EVENT, (event) => {
    const action = (event as ValidationDemoActionEvent).detail;
    if (action === 'seed-invalid') {
      setRows(createInvalidValidationRows());
      return;
    }
    if (action === 'reset-rows') {
      setRows(createValidationRows());
      return;
    }
    const issue = collectValidationIssues(rows, rules)[0];
    if (!issue) {
      return;
    }
    const columnIndex = getValidationColumnIndex(issue.prop);
    void grid.scrollToCoordinate?.({ y: issue.rowIndex, x: columnIndex });
    void grid.setCellsFocus?.(
      { y: issue.rowIndex, x: columnIndex },
      { y: issue.rowIndex, x: columnIndex },
    );
  });

  grid.addEventListener('gridedit', (event) => {
    const rejected = findRejectedEdit((event as CustomEvent).detail, rules);
    setTimeout(() => {
      lastRejected = event.defaultPrevented ? rejected : undefined;
      rows = grid.source as ValidationRow[];
      updateToolbar();
    }, 0);
  });

  root.append(toolbar, grid);
  document.querySelector(parentSelector)?.appendChild(root);

  return () => root.remove();
}
Vue vue
<template>
  <div :class="['validation-demo', { 'is-dark': isDark }]">
    <validation-input-demo-toolbar
      ref="toolbar"
      @validation-demo-options-change="updateRules"
      @validation-demo-action="handleToolbarAction"
    />
    <RevoGrid
      ref="grid"
      class="validation-demo-grid cell-border"
      :columns="columns"
      :source="source"
      :plugins="plugins"
      :additionalData="additionalData"
      :theme="isDark ? 'darkMaterial' : 'material'"
      stretch="all"
      hide-attribution
      range
      @gridedit="handleGridEdit"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  AFTER_SOURCE_EVENT,
  TooltipPlugin,
} from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import {
  collectValidationIssues,
  createInvalidValidationRows,
  createValidationColumns,
  createValidationRows,
  defaultValidationRules,
  findRejectedEdit,
  getValidationColumnIndex,
  type RejectedEdit,
  type ValidationRuleState,
  type ValidationRow,
} from './validate-input.shared';
import {
  defineValidationDemoToolbarElement,
  type ValidationDemoActionEvent,
  type ValidationDemoToolbarElement,
} from './validate-input-toolbar';

defineValidationDemoToolbarElement();

const { isDark } = currentThemeVue();
const grid = ref<{ $el?: HTMLRevoGridElement } | null>(null);
const toolbar = ref<ValidationDemoToolbarElement | null>(null);
const rules = reactive({ ...defaultValidationRules });
const source = ref<ValidationRow[]>(createValidationRows());
const lastRejected = ref<RejectedEdit | undefined>();

const columns = computed(() => createValidationColumns(rules));
const issues = computed(() => collectValidationIssues(source.value, rules));
const additionalData = computed(() => ({
  eventManager: {
    applyEventsToSource: true,
  },
}));
const plugins = [
  ColumnStretchPlugin,
  TooltipPlugin,
  EventManagerPlugin,
  CellValidatePlugin,
];

function getGridElement() {
  return grid.value?.$el as HTMLRevoGridElement | undefined;
}

function syncToolbar() {
  if (toolbar.value) {
    toolbar.value.state = {
      rules,
      issues: issues.value,
      lastRejected: lastRejected.value,
    };
  }
}

function updateRules(event: CustomEvent<ValidationRuleState>) {
  lastRejected.value = undefined;
  getGridElement()?.dispatchEvent(new CustomEvent(AFTER_SOURCE_EVENT));
  Object.assign(rules, event.detail);
}

function seedInvalid() {
  lastRejected.value = undefined;
  source.value = createInvalidValidationRows();
}

function resetRows() {
  lastRejected.value = undefined;
  source.value = createValidationRows();
}

function focusFirstInvalid() {
  const issue = issues.value[0];
  const element = getGridElement();
  if (!issue || !element) {
    return;
  }
  const columnIndex = getValidationColumnIndex(issue.prop);
  void element.scrollToCoordinate({ y: issue.rowIndex, x: columnIndex });
  void element.setCellsFocus({ y: issue.rowIndex, x: columnIndex }, { y: issue.rowIndex, x: columnIndex });
}

function handleToolbarAction(event: ValidationDemoActionEvent) {
  if (event.detail === 'seed-invalid') {
    seedInvalid();
    return;
  }
  if (event.detail === 'reset-rows') {
    resetRows();
    return;
  }
  focusFirstInvalid();
}

function handleGridEdit(event: CustomEvent<HTMLRevoGridElementEventMap['gridedit']>) {
  const rejected = findRejectedEdit(event.detail, rules);
  setTimeout(() => {
    lastRejected.value = event.defaultPrevented ? rejected : undefined;
    source.value = [...(getGridElement()?.source as ValidationRow[] ?? source.value)];
  }, 0);
}

watch([rules, source, lastRejected], syncToolbar, { deep: true });
onMounted(syncToolbar);
</script>
React tsx
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  AFTER_SOURCE_EVENT,
  TooltipPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  collectValidationIssues,
  createInvalidValidationRows,
  createValidationColumns,
  createValidationRows,
  defaultValidationRules,
  findRejectedEdit,
  getValidationColumnIndex,
  type RejectedEdit,
  type ValidationRow,
} from './validate-input.shared';
import {
  VALIDATION_DEMO_ACTION_EVENT,
  VALIDATION_DEMO_OPTIONS_CHANGE_EVENT,
  VALIDATION_DEMO_TOOLBAR_TAG,
  defineValidationDemoToolbarElement,
  type ValidationDemoActionEvent,
  type ValidationDemoOptionsChangeEvent,
  type ValidationDemoToolbarElement,
} from './validate-input-toolbar';

const { isDark } = currentTheme();
defineValidationDemoToolbarElement();

export default function ValidateInput() {
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const toolbarRef = useRef<ValidationDemoToolbarElement>(null);
  const [rules, setRules] = useState(defaultValidationRules);
  const [source, setSource] = useState<ValidationRow[]>(() => createValidationRows());
  const [lastRejected, setLastRejected] = useState<RejectedEdit | undefined>();

  const columns = useMemo(() => createValidationColumns(rules), [rules]);
  const plugins = useMemo(() => [
    ColumnStretchPlugin,
    TooltipPlugin,
    EventManagerPlugin,
    CellValidatePlugin,
  ], []);
  const additionalData = useMemo(() => ({
    eventManager: {
      applyEventsToSource: true,
    },
  }), []);
  const issues = collectValidationIssues(source, rules);

  useEffect(() => {
    if (toolbarRef.current) {
      toolbarRef.current.state = { rules, issues, lastRejected };
    }
  }, [issues, lastRejected, rules]);

  useEffect(() => {
    const toolbar = toolbarRef.current;
    if (!toolbar) {
      return;
    }
    const handleOptionsChange = (event: Event) => {
      setLastRejected(undefined);
      gridRef.current?.dispatchEvent(new CustomEvent(AFTER_SOURCE_EVENT));
      setRules((event as ValidationDemoOptionsChangeEvent).detail);
    };
    const handleAction = (event: Event) => {
      const action = (event as ValidationDemoActionEvent).detail;
      if (action === 'seed-invalid') {
        setRows(createInvalidValidationRows());
        return;
      }
      if (action === 'reset-rows') {
        setRows(createValidationRows());
        return;
      }
      focusFirstInvalid();
    };
    toolbar.addEventListener(VALIDATION_DEMO_OPTIONS_CHANGE_EVENT, handleOptionsChange);
    toolbar.addEventListener(VALIDATION_DEMO_ACTION_EVENT, handleAction);
    return () => {
      toolbar.removeEventListener(VALIDATION_DEMO_OPTIONS_CHANGE_EVENT, handleOptionsChange);
      toolbar.removeEventListener(VALIDATION_DEMO_ACTION_EVENT, handleAction);
    };
  });

  const setRows = (rows: ValidationRow[]) => {
    setLastRejected(undefined);
    setSource(rows);
  };
  const handleGridEdit = (event: CustomEvent<HTMLRevoGridElementEventMap['gridedit']>) => {
    const rejected = findRejectedEdit(event.detail, rules);
    setTimeout(() => {
      setLastRejected(event.defaultPrevented ? rejected : undefined);
      setSource([...(gridRef.current?.source as ValidationRow[] ?? source)]);
    }, 0);
  };
  const focusFirstInvalid = () => {
    const issue = issues[0];
    if (!issue) {
      return;
    }
    const columnIndex = getValidationColumnIndex(issue.prop);
    void gridRef.current?.scrollToCoordinate({ y: issue.rowIndex, x: columnIndex });
    void gridRef.current?.setCellsFocus(
      { y: issue.rowIndex, x: columnIndex },
      { y: issue.rowIndex, x: columnIndex },
    );
  };

  return (
    <div className={`validation-demo ${isDark() ? 'is-dark' : ''}`}>
      {React.createElement(VALIDATION_DEMO_TOOLBAR_TAG, { ref: toolbarRef })}
      <RevoGrid
        ref={gridRef}
        className="validation-demo-grid cell-border"
        theme={isDark() ? 'darkMaterial' : 'material'}
        columns={columns}
        source={source}
        plugins={plugins}
        stretch="all"
        additionalData={additionalData}
        hideAttribution
        range
        onGridedit={handleGridEdit}
      />
    </div>
  );
}
Angular ts
import {
  AfterViewInit,
  CUSTOM_ELEMENTS_SCHEMA,
  Component,
  ElementRef,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  AFTER_SOURCE_EVENT,
  TooltipPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  collectValidationIssues,
  createInvalidValidationRows,
  createValidationColumns,
  createValidationRows,
  defaultValidationRules,
  findRejectedEdit,
  getValidationColumnIndex,
  type RejectedEdit,
  type ValidationIssue,
  type ValidationRow,
} from './validate-input.shared';
import {
  defineValidationDemoToolbarElement,
  type ValidationDemoActionEvent,
  type ValidationDemoOptionsChangeEvent,
  type ValidationDemoToolbarElement,
} from './validate-input-toolbar';

defineValidationDemoToolbarElement();

@Component({
  selector: 'validate-input-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <div class="validation-demo" [class.is-dark]="theme === 'darkMaterial'">
      <validation-input-demo-toolbar
        #toolbarRef
        (validation-demo-options-change)="setRules($event)"
        (validation-demo-action)="handleToolbarAction($event)"
      ></validation-input-demo-toolbar>
      <revo-grid
        #gridRef
        class="validation-demo-grid cell-border"
        [source]="source"
        [columns]="columns"
        [plugins]="plugins"
        [stretch]="'all'"
        [eventManager]="eventManager"
        [theme]="theme"
        [hideAttribution]="true"
        [range]="true"
        (gridedit)="handleGridEdit($event)"
      ></revo-grid>
    </div>
  `,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  encapsulation: ViewEncapsulation.None,
})
export class ValidateInputGridComponent implements AfterViewInit {
  @ViewChild('gridRef', { read: ElementRef }) gridElement?: ElementRef<HTMLRevoGridElement>;
  @ViewChild('toolbarRef', { read: ElementRef })
  toolbarElement?: ElementRef<ValidationDemoToolbarElement>;

  source: ValidationRow[] = createValidationRows();
  rules = { ...defaultValidationRules };
  columns = createValidationColumns(this.rules);
  issues: ValidationIssue[] = collectValidationIssues(this.source, this.rules);
  lastRejected?: RejectedEdit;
  plugins = [
    ColumnStretchPlugin,
    TooltipPlugin,
    EventManagerPlugin,
    CellValidatePlugin,
  ];
  eventManager = {
    applyEventsToSource: true,
  };
  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';

  ngAfterViewInit() {
    this.syncToolbar();
  }

  private getGridElement() {
    return this.gridElement?.nativeElement;
  }

  private syncToolbar() {
    if (this.toolbarElement) {
      this.toolbarElement.nativeElement.state = {
        rules: this.rules,
        issues: this.issues,
        lastRejected: this.lastRejected,
      };
    }
  }

  setRules(event: ValidationDemoOptionsChangeEvent) {
    this.lastRejected = undefined;
    this.getGridElement()?.dispatchEvent(new CustomEvent(AFTER_SOURCE_EVENT));
    this.rules = event.detail;
    this.syncRules();
  }

  syncRules() {
    this.rules = { ...this.rules };
    this.columns = createValidationColumns(this.rules);
    this.issues = collectValidationIssues(this.source, this.rules);
    this.syncToolbar();
    void this.getGridElement()?.refresh?.();
  }

  seedInvalid() {
    this.lastRejected = undefined;
    this.source = createInvalidValidationRows();
    this.issues = collectValidationIssues(this.source, this.rules);
    this.syncToolbar();
  }

  resetRows() {
    this.lastRejected = undefined;
    this.source = createValidationRows();
    this.issues = collectValidationIssues(this.source, this.rules);
    this.syncToolbar();
  }

  focusFirstInvalid() {
    const issue = this.issues[0];
    const grid = this.getGridElement();
    if (!issue || !grid) {
      return;
    }
    const columnIndex = getValidationColumnIndex(issue.prop);
    void grid.scrollToCoordinate({ y: issue.rowIndex, x: columnIndex });
    void grid.setCellsFocus({ y: issue.rowIndex, x: columnIndex }, { y: issue.rowIndex, x: columnIndex });
  }

  handleToolbarAction(event: ValidationDemoActionEvent) {
    if (event.detail === 'seed-invalid') {
      this.seedInvalid();
      return;
    }
    if (event.detail === 'reset-rows') {
      this.resetRows();
      return;
    }
    this.focusFirstInvalid();
  }

  handleGridEdit(event: CustomEvent<HTMLRevoGridElementEventMap['gridedit']>) {
    const rejected = findRejectedEdit(event.detail, this.rules);
    setTimeout(() => {
      this.lastRejected = event.defaultPrevented ? rejected : undefined;
      this.source = [...(this.getGridElement()?.source as ValidationRow[] ?? this.source)];
      this.issues = collectValidationIssues(this.source, this.rules);
      this.syncToolbar();
    }, 0);
  }
}

The validation function can also block a cell based on another column in the same row. This example prevents fees on expired services.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import type { ColumnRegular } from '@revolist/revogrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  TooltipPlugin,
  validationRenderer,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { crossColumnValidationSource } from './validate-input.data';
import { invalidCellStyle } from './validate-basic.data';

defineCustomElements();

const columns: ColumnRegular[] = [
  { name: 'ID', prop: 'id', size: 80 },
  { name: 'Service Name', prop: 'name', size: 220 },
  {
    name: 'LCS Fee',
    prop: 'lcsFee',
    ...validationRenderer({
      invalidProperties: () => ({
        style: invalidCellStyle,
      }),
    }),
    validationTooltip: (value: any, model?: any) => {
      if (model?.expired) {
        return 'Cannot set fee for expired services';
      }
      if (value == null || value === '' || Number.isNaN(Number(value))) {
        return 'Fee must be a valid number';
      }
      return undefined;
    },
    validate: (value: any, model?: any) => {
      if (model?.expired) {
        return false;
      }
      return value != null && value !== '' && !Number.isNaN(Number(value));
    },
  },
  {
    name: 'Expired',
    prop: 'expired',
    size: 110,
    cellTemplate: (h, { value }) =>
      h(
        'span',
        {
          style: {
            color: value ? '#b91c1c' : '#15803d',
            fontWeight: '600',
          },
        },
        value ? 'Yes' : 'No',
      ),
  },
];

export function load(parentSelector: string) {
  const grid = document.createElement('revo-grid');
  const { isDark } = currentTheme();

  grid.source = crossColumnValidationSource;
  grid.columns = columns;
  grid.plugins = [
    ColumnStretchPlugin,
    TooltipPlugin,
    EventManagerPlugin,
    CellValidatePlugin,
  ];
  Object.assign(grid, {
    stretch: 'all',
    eventManager: {
      applyEventsToSource: true,
    },
  })
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.hideAttribution = true;
  grid.range = true;

  document.querySelector(parentSelector)?.appendChild(grid);
}
Vue vue
<template>
  <RevoGrid
    class="cell-border rounded-lg overflow-hidden"
    style="min-height: 320px;"
    :columns="columns"
    :source="source"
    :plugins="plugins"
    :additionalData="additionalData"
    :theme="isDark ? 'darkMaterial' : 'material'"
    hide-attribution
    range
  />
</template>

<script setup lang="ts">
import RevoGrid, { type ColumnRegular } from '@revolist/vue3-datagrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  
  TooltipPlugin,
  validationRenderer,
} from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import { crossColumnValidationSource } from './validate-input.data';
import { invalidCellStyle } from './validate-basic.data';

const { isDark } = currentThemeVue();

const columns: ColumnRegular[] = [
  { name: 'ID', prop: 'id', size: 80 },
  { name: 'Service Name', prop: 'name', size: 220 },
  {
    name: 'LCS Fee',
    prop: 'lcsFee',
    ...validationRenderer({
      invalidProperties: () => ({
        style: invalidCellStyle,
      }),
    }),
    validationTooltip: (value: any, model?: any) => {
      if (model?.expired) {
        return 'Cannot set fee for expired services';
      }
      if (value == null || value === '' || Number.isNaN(Number(value))) {
        return 'Fee must be a valid number';
      }
      return undefined;
    },
    validate: (value: any, model?: any) => {
      if (model?.expired) {
        return false;
      }
      return value != null && value !== '' && !Number.isNaN(Number(value));
    },
  },
  {
    name: 'Expired',
    prop: 'expired',
    size: 110,
    cellTemplate: (h, { value }) =>
      h(
        'span',
        {
          style: {
            color: value ? '#b91c1c' : '#15803d',
            fontWeight: '600',
          },
        },
        value ? 'Yes' : 'No',
      ),
  },
];

const source = crossColumnValidationSource;
const plugins = [
  ColumnStretchPlugin,
  TooltipPlugin,
  EventManagerPlugin,
  CellValidatePlugin,
];
const additionalData = {
  stretch: 'all',
  eventManager: {
    applyEventsToSource: true,
  },
};
</script>
React tsx
import React from 'react';
import { RevoGrid, type ColumnRegular } from '@revolist/react-datagrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  TooltipPlugin,
  validationRenderer,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { crossColumnValidationSource } from './validate-input.data';
import { invalidCellStyle } from './validate-basic.data';

const { isDark } = currentTheme();

export default function ValidateCrossColumn() {
  const columns: ColumnRegular[] = [
    { name: 'ID', prop: 'id', size: 80 },
    { name: 'Service Name', prop: 'name', size: 220 },
    {
      name: 'LCS Fee',
      prop: 'lcsFee',
      ...validationRenderer({
        invalidProperties: () => ({
          style: invalidCellStyle,
        }),
      }),
      validationTooltip: (value: any, model?: any) => {
        if (model?.expired) {
          return 'Cannot set fee for expired services';
        }
        if (value == null || value === '' || Number.isNaN(Number(value))) {
          return 'Fee must be a valid number';
        }
        return undefined;
      },
      validate: (value: any, model?: any) => {
        if (model?.expired) {
          return false;
        }
        return value != null && value !== '' && !Number.isNaN(Number(value));
      },
    },
    {
      name: 'Expired',
      prop: 'expired',
      size: 110,
      cellTemplate: (h, { value }) =>
        h(
          'span',
          {
            style: {
              color: value ? '#b91c1c' : '#15803d',
              fontWeight: '600',
            },
          },
          value ? 'Yes' : 'No',
        ),
    },
  ];

  return (
    <RevoGrid
      style={{ height: '320px' }}
      theme={isDark() ? 'darkMaterial' : 'material'}
      columns={columns}
      source={crossColumnValidationSource}
      plugins={[
        ColumnStretchPlugin,
        TooltipPlugin,
        EventManagerPlugin,
        CellValidatePlugin,
      ]}
      additionalData={{
        stretch: 'all',
        eventManager: {
          applyEventsToSource: true,
        },
      }}
      hideAttribution
      range
    />
  );
}
Angular ts
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  CellValidatePlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  TooltipPlugin,
  validationRenderer,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { crossColumnValidationSource } from './validate-input.data';
import { invalidCellStyle } from './validate-basic.data';

@Component({
  selector: 'validate-cross-column-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <revo-grid
      class="grow h-full cell-border"
      [source]="source"
      [columns]="columns"
      [plugins]="plugins"
      [stretch]="additionalData.stretch"
      [eventManager]="additionalData.eventManager"
      [theme]="theme"
      [hideAttribution]="true"
      [range]="true"
      style="min-height: 320px;"
    ></revo-grid>
  `,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
})
export class ValidateCrossColumnGridComponent {
  source = crossColumnValidationSource;
  columns = [
    { name: 'ID', prop: 'id', size: 80 },
    { name: 'Service Name', prop: 'name', size: 220 },
    {
      name: 'LCS Fee',
      prop: 'lcsFee',
      ...validationRenderer({
        invalidProperties: () => ({
          style: invalidCellStyle,
        }),
      }),
      validationTooltip: (value: any, model?: any) => {
        if (model?.expired) {
          return 'Cannot set fee for expired services';
        }
        if (value == null || value === '' || Number.isNaN(Number(value))) {
          return 'Fee must be a valid number';
        }
        return undefined;
      },
      validate: (value: any, model?: any) => {
        if (model?.expired) {
          return false;
        }
        return value != null && value !== '' && !Number.isNaN(Number(value));
      },
    },
    {
      name: 'Expired',
      prop: 'expired',
      size: 110,
      cellTemplate: (h: any, { value }: any) =>
        h(
          'span',
          {
            style: {
              color: value ? '#b91c1c' : '#15803d',
              fontWeight: '600',
            },
          },
          value ? 'Yes' : 'No',
        ),
    },
  ];
  plugins = [
    ColumnStretchPlugin,
    TooltipPlugin,
    EventManagerPlugin,
    CellValidatePlugin,
  ];
  additionalData = {
    stretch: 'all',
    eventManager: {
      applyEventsToSource: true,
    },
  };
  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
}