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, orinfo.indicatorPlacement:corner,start,end, ornone.messagePlacement:tooltip,inline,both, ornone.message: a custom message resolver whenvalidationTooltipis not enough.invalidProperties: extra cell properties for invalid cells.
messagePlacement controls where the validation message is exposed:
tooltipaddstitleand tooltip data attributes.inlinerenders the message inside the cell without tooltip attributes.bothrenders inline text and tooltip attributes.nonekeeps 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;
}
}
}