Skip to content

Cell Validation with Renderers

One straightforward way to validate cell data is by combining validate, validationTooltip, and validationRenderer. This marks invalid cells during rendering without changing edit behavior, which is useful for audits, imported data review, and soft warnings.

import { validationRenderer } from '@revolist/revogrid-pro';
const columns = [
{
prop: 'price',
name: 'Price',
validate: (value, row) => value >= row.floor && value <= row.ceiling,
validationTooltip: (value, row) =>
`Price ${value} must stay between ${row.floor} and ${row.ceiling}`,
...validationRenderer({
severity: 'error',
indicatorPlacement: 'corner',
messagePlacement: 'tooltip',
}),
},
];

validationRenderer returns cellProperties and cellTemplate, so it can wrap your existing cell renderer while keeping validation styling in one place. The helper supports:

  • severity: error, warning, or info.
  • indicatorPlacement: corner, start, end, or none.
  • messagePlacement: tooltip, inline, both, or none.
  • message: a custom message resolver when validationTooltip is not enough.
  • invalidProperties: extra cell properties for invalid cells.

messagePlacement controls where the validation message is exposed:

  • tooltip adds title and tooltip data attributes.
  • inline renders the message inside the cell without tooltip attributes.
  • both renders inline text and tooltip attributes.
  • none keeps the visual indicator but hides the message text.
Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { ColumnStretchPlugin, TooltipPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import './validate-basic.css';
import {
  cloneBasicValidationSource,
  createValidateBasicColumns,
  defaultValidateBasicOptions,
  getInvalidPriceCount,
  type ValidateBasicOptions,
} from './validate-basic.data';
import {
  VALIDATE_BASIC_OPTIONS_CHANGE_EVENT,
  createValidateBasicToolbar,
  defineValidateBasicToolbarElement,
  type ValidateBasicOptionsChangeEvent,
} from './validate-basic-toolbar';

defineCustomElements();
defineValidateBasicToolbarElement();

export function load(parentSelector: string) {
  const host = document.querySelector(parentSelector);
  if (!host) {
    return;
  }

  const { isDark } = currentTheme();
  const wrapper = document.createElement('div');
  const grid = document.createElement('revo-grid');
  const source = cloneBasicValidationSource();
  const options: ValidateBasicOptions = { ...defaultValidateBasicOptions };
  const toolbar = createValidateBasicToolbar(
    options,
    getInvalidPriceCount(source, options.rule),
  );

  wrapper.className = `validate-basic-demo ${isDark() ? 'is-dark' : ''}`;

  const applyOptions = () => {
    grid.columns = createValidateBasicColumns(options);
    toolbar.invalidCount = getInvalidPriceCount(source, options.rule);
  };

  toolbar.addEventListener(VALIDATE_BASIC_OPTIONS_CHANGE_EVENT, (event) => {
    Object.assign(options, (event as ValidateBasicOptionsChangeEvent).detail);
    applyOptions();
  });

  grid.plugins = [ColumnStretchPlugin, TooltipPlugin];
  grid.stretch = 'all';
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.className = 'validate-basic-grid cell-border';
  grid.hideAttribution = true;
  grid.range = true;
  grid.addEventListener('afteredit', () => {
    toolbar.invalidCount = getInvalidPriceCount(source, options.rule);
  });

  applyOptions();
  wrapper.append(toolbar, grid);
  host.appendChild(wrapper);
  grid.source = source;

  return () => wrapper.remove();
}
Vue vue
<template>
  <div :class="['validate-basic-demo', { 'is-dark': isDark }]">
    <component
      :is="VALIDATE_BASIC_TOOLBAR_TAG"
      ref="toolbarRef"
      @validate-basic-options-change="setToolbarOptions"
    />
    <RevoGrid
      class="validate-basic-grid cell-border"
      :columns="columns"
      :source="source"
      :plugins="plugins"
      stretch="all"
      :theme="isDark ? 'darkCompact' : 'compact'"
      hide-attribution
      range
      @afteredit="revision++"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { ColumnStretchPlugin, TooltipPlugin } from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import './validate-basic.css';
import {
  cloneBasicValidationSource,
  createValidateBasicColumns,
  defaultValidateBasicOptions,
  getInvalidPriceCount,
  type ValidateBasicOptions,
} from './validate-basic.data';
import {
  VALIDATE_BASIC_TOOLBAR_TAG,
  defineValidateBasicToolbarElement,
  type ValidateBasicOptionsChangeEvent,
  type ValidateBasicToolbarElement,
} from './validate-basic-toolbar';

const { isDark } = currentThemeVue();

const options = reactive({ ...defaultValidateBasicOptions });
const source = ref(cloneBasicValidationSource());
const revision = ref(0);
const toolbarRef = ref<ValidateBasicToolbarElement | null>(null);
const columns = computed(() => createValidateBasicColumns(options));
const invalidCount = computed(() => {
  revision.value;
  return getInvalidPriceCount(source.value, options.rule);
});
const plugins = [ColumnStretchPlugin, TooltipPlugin];

defineValidateBasicToolbarElement();

function setToolbarOptions(event: ValidateBasicOptionsChangeEvent) {
  Object.assign(options, event.detail as ValidateBasicOptions);
}

function syncToolbar() {
  if (!toolbarRef.value) {
    return;
  }

  toolbarRef.value.options = options;
  toolbarRef.value.invalidCount = invalidCount.value;
}

function syncToolbarInvalidCount() {
  if (toolbarRef.value) {
    toolbarRef.value.invalidCount = invalidCount.value;
  }
}

onMounted(syncToolbar);
watch(invalidCount, syncToolbarInvalidCount);
</script>
React tsx
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { ColumnStretchPlugin, TooltipPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import './validate-basic.css';
import {
  cloneBasicValidationSource,
  createValidateBasicColumns,
  defaultValidateBasicOptions,
  getInvalidPriceCount,
  type ValidateBasicOptions,
} from './validate-basic.data';
import {
  VALIDATE_BASIC_OPTIONS_CHANGE_EVENT,
  VALIDATE_BASIC_TOOLBAR_TAG,
  defineValidateBasicToolbarElement,
  type ValidateBasicOptionsChangeEvent,
  type ValidateBasicToolbarElement,
} from './validate-basic-toolbar';

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

export default function ValidateBasic() {
  const toolbarRef = useRef<ValidateBasicToolbarElement>(null);
  const [options, setOptions] = useState<ValidateBasicOptions>(defaultValidateBasicOptions);
  const [revision, setRevision] = useState(0);
  const source = useMemo(() => cloneBasicValidationSource(), []);
  const columns = useMemo(() => createValidateBasicColumns(options), [options]);
  const plugins = useMemo(() => [ColumnStretchPlugin, TooltipPlugin], []);
  const invalidCount = useMemo(() => getInvalidPriceCount(source, options.rule), [source, options.rule, revision]);
  const dark = isDark();

  const handleOptionsChange = useCallback((event: Event) => {
    setOptions((event as ValidateBasicOptionsChangeEvent).detail);
  }, []);

  const setToolbarElement = useCallback((element: ValidateBasicToolbarElement | null) => {
    if (toolbarRef.current) {
      toolbarRef.current.removeEventListener(VALIDATE_BASIC_OPTIONS_CHANGE_EVENT, handleOptionsChange);
    }

    toolbarRef.current = element;

    if (element) {
      element.options = defaultValidateBasicOptions;
      element.invalidCount = getInvalidPriceCount(source, defaultValidateBasicOptions.rule);
      element.addEventListener(VALIDATE_BASIC_OPTIONS_CHANGE_EVENT, handleOptionsChange);
    }
  }, [handleOptionsChange, source]);

  useEffect(() => {
    if (toolbarRef.current) {
      toolbarRef.current.invalidCount = invalidCount;
    }
  }, [invalidCount]);

  return (
    <div className={`validate-basic-demo ${dark ? 'is-dark' : ''}`}>
      {React.createElement(VALIDATE_BASIC_TOOLBAR_TAG, { ref: setToolbarElement })}
      <RevoGrid
        className="validate-basic-grid cell-border"
        theme={dark ? 'darkCompact' : 'compact'}
        columns={columns}
        source={source}
        plugins={plugins}
        stretch="all"
        onAfteredit={() => setRevision((value) => value + 1)}
        hideAttribution
        range
      />
    </div>
  );
}
Angular ts
import {
  AfterViewInit,
  Component,
  CUSTOM_ELEMENTS_SCHEMA,
  ElementRef,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { RevoGrid } from '@revolist/angular-datagrid';
import { ColumnStretchPlugin, TooltipPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  cloneBasicValidationSource,
  createValidateBasicColumns,
  defaultValidateBasicOptions,
  getInvalidPriceCount,
  type ValidateBasicOptions,
} from './validate-basic.data';
import {
  defineValidateBasicToolbarElement,
  type ValidateBasicToolbarElement,
} from './validate-basic-toolbar';

defineValidateBasicToolbarElement();

@Component({
  selector: 'validate-basic-grid',
  standalone: true,
  imports: [CommonModule, RevoGrid],
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./validate-basic.css'],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <div class="validate-basic-demo" [class.is-dark]="theme === 'darkCompact'">
      <validate-basic-toolbar
        #toolbar
        [invalidCount]="invalidCount"
        (validate-basic-options-change)="setToolbarOptions($event)"
      ></validate-basic-toolbar>
      <revo-grid
        class="validate-basic-grid cell-border"
        [source]="source"
        [columns]="columns"
        [plugins]="plugins"
        stretch="all"
        [theme]="theme"
        [hideAttribution]="true"
        [range]="true"
        (afteredit)="refreshInvalidCount()"
      ></revo-grid>
    </div>
  `,
})
export class ValidateBasicGridComponent implements AfterViewInit {
  @ViewChild('toolbar') toolbar?: ElementRef<ValidateBasicToolbarElement>;

  source = cloneBasicValidationSource();
  options: ValidateBasicOptions = { ...defaultValidateBasicOptions };
  columns = createValidateBasicColumns(this.options);
  plugins = [ColumnStretchPlugin, TooltipPlugin];
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  invalidCount = getInvalidPriceCount(this.source, this.options.rule);

  ngAfterViewInit() {
    this.syncToolbar();
  }

  setToolbarOptions(event: CustomEvent<ValidateBasicOptions>) {
    this.options = { ...event.detail };
    this.columns = createValidateBasicColumns(this.options);
    this.refreshInvalidCount();
  }

  refreshInvalidCount() {
    this.invalidCount = getInvalidPriceCount(this.source, this.options.rule);
    this.syncToolbarInvalidCount();
  }

  private syncToolbar() {
    if (!this.toolbar?.nativeElement) {
      return;
    }

    this.toolbar.nativeElement.options = this.options;
    this.toolbar.nativeElement.invalidCount = this.invalidCount;
  }

  private syncToolbarInvalidCount() {
    if (this.toolbar?.nativeElement) {
      this.toolbar.nativeElement.invalidCount = this.invalidCount;
    }
  }
}