Flashing Cells
Cell flashing makes recent changes visible in editable grids, live dashboards, and audit workflows. The Pro CellFlashPlugin can react to edits from EventManagerPlugin, undo/redo changes from HistoryPlugin, manual flashcell events, or direct plugin method calls.
Source code
import { defineCustomElements } from '@revolist/revogrid/loader';
import type { ColumnRegular, DataType } from '@revolist/revogrid';
import {
CellFlashPlugin,
EventManagerPlugin,
HistoryPlugin,
cellFlashArrowTemplate,
type CellFlashConfig,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
CELL_FLASH_TOOLBAR_ACTION_EVENT,
CELL_FLASH_TOOLBAR_OPTIONS_CHANGE_EVENT,
createCellFlashToolbar,
type CellFlashToolbarActionEvent,
type CellFlashToolbarOptions,
type CellFlashToolbarOptionsChangeEvent,
} from './cell-flash-toolbar';
defineCustomElements();
const { isDark } = currentTheme();
type MarketRow = DataType & {
symbol: string;
price: number;
change: number;
volume: number;
};
const createRows = (): MarketRow[] => [
{ symbol: 'AAPL', price: 184.12, change: 0.4, volume: 124500 },
{ symbol: 'MSFT', price: 421.34, change: -0.2, volume: 98200 },
{ symbol: 'NVDA', price: 879.7, change: 1.6, volume: 143000 },
{ symbol: 'AMZN', price: 188.8, change: 0.1, volume: 76100 },
{ symbol: 'META', price: 494.2, change: -0.5, volume: 68200 },
];
const columns: ColumnRegular[] = [
{ name: 'Symbol', prop: 'symbol', size: 110 },
{
name: 'Price',
prop: 'price',
size: 130,
flash: (_value, context) => ({
flash: true,
rowFlash: Math.abs(Number(context.value) - Number(context.previousValue)) > 2,
}),
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, Number(value).toFixed(2))),
},
{
name: 'Change %',
prop: 'change',
size: 120,
flash: true,
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, `${Number(value).toFixed(2)}%`)),
},
{ name: 'Volume', prop: 'volume', size: 130, flash: true },
];
function updateRow(row: MarketRow): MarketRow {
const delta = Number((Math.random() * 8 - 4).toFixed(2));
const price = Number(Math.max(1, row.price + delta).toFixed(2));
return {
...row,
price,
change: Number(((price - row.price) / row.price * 100).toFixed(2)),
volume: row.volume + Math.round(Math.random() * 5000),
};
}
function dispatchRowEdit(grid: HTMLRevoGridElement, rows: MarketRow[], rowIndex: number) {
const previous = rows[rowIndex];
const next = updateRow(previous);
rows[rowIndex] = next;
grid.dispatchEvent(new CustomEvent('beforeedit', {
cancelable: true,
detail: {
rowIndex,
prop: 'price',
val: next.price,
model: previous,
type: 'rgRow',
},
}));
grid.dispatchEvent(new CustomEvent('flashcell', {
detail: {
data: {
[rowIndex]: {
change: next.change,
volume: next.volume,
},
},
previousData: {
[rowIndex]: {
change: previous.change,
volume: previous.volume,
},
},
type: 'rgRow',
eventTypes: ['manual-update'],
},
}));
grid.source = [...rows];
}
async function getCellFlashPlugin(grid: HTMLRevoGridElement) {
const plugins = await grid.getPlugins();
return plugins.find(plugin => plugin?.constructor?.name === 'CellFlashPlugin') as CellFlashPlugin | undefined;
}
export function load(parentSelector: string) {
const parent = document.querySelector(parentSelector);
if (!parent) return;
const rows = createRows();
let liveTimer: ReturnType<typeof setInterval> | undefined;
const root = document.createElement('div');
root.style.cssText = 'display:grid;gap:10px;';
let toolbarOptions: CellFlashToolbarOptions = {
rowFlash: false,
duration: 1000,
live: false,
};
const toolbar = createCellFlashToolbar(toolbarOptions);
const grid = document.createElement('revo-grid');
const config = (): CellFlashConfig => ({
duration: toolbarOptions.duration,
rowDuration: toolbarOptions.duration,
queue: 'merge',
mode: toolbarOptions.rowFlash ? 'cell-and-row' : 'cell',
clearOnSourceChange: false,
aria: true,
labels: {
cellUpdated: state => `Market cell ${String(state.prop)} in row ${state.rowIndex + 1} changed`,
rowUpdated: state => `Market row ${state.rowIndex + 1} changed`,
},
});
const applyConfig = () => {
grid.cellFlash = config();
};
const setToolbarOptions = (patch: Partial<CellFlashToolbarOptions>) => {
toolbarOptions = {
...toolbarOptions,
...patch,
};
toolbar.options = toolbarOptions;
};
const stopFeed = () => {
if (liveTimer) {
clearInterval(liveTimer);
}
liveTimer = undefined;
setToolbarOptions({ live: false });
};
toolbar.addEventListener(CELL_FLASH_TOOLBAR_OPTIONS_CHANGE_EVENT, (event: CellFlashToolbarOptionsChangeEvent) => {
setToolbarOptions(event.detail);
applyConfig();
});
toolbar.addEventListener(CELL_FLASH_TOOLBAR_ACTION_EVENT, async (event: CellFlashToolbarActionEvent) => {
if (event.detail === 'update-row') {
dispatchRowEdit(grid, rows, Math.floor(Math.random() * rows.length));
return;
}
if (event.detail === 'burst-update') {
rows.forEach((_row, index) => dispatchRowEdit(grid, rows, index));
return;
}
if (event.detail === 'start-feed') {
if (!liveTimer) {
liveTimer = setInterval(() => dispatchRowEdit(grid, rows, Math.floor(Math.random() * rows.length)), 900);
setToolbarOptions({ live: true });
}
return;
}
if (event.detail === 'stop-feed') {
stopFeed();
return;
}
if (event.detail === 'manual-row-flash') {
const plugin = await getCellFlashPlugin(grid);
plugin?.flashRows({ rowIndex: 0, rowClassName: 'manual-row-flash' });
return;
}
const plugins = await grid.getPlugins();
const history = plugins.find(plugin => plugin?.constructor?.name === 'HistoryPlugin') as HistoryPlugin | undefined;
if (event.detail === 'undo') {
history?.undo();
return;
}
history?.redo();
});
grid.columns = columns;
grid.eventManager = { applyEventsToSource: true };
grid.plugins = [EventManagerPlugin, HistoryPlugin, CellFlashPlugin];
grid.theme = isDark() ? 'darkCompact' : 'compact';
grid.hideAttribution = true;
grid.style.cssText = 'min-height:420px;';
applyConfig();
root.append(toolbar, grid);
parent.appendChild(root);
grid.source = rows;
return () => {
if (liveTimer) clearInterval(liveTimer);
grid.remove();
root.remove();
};
}
<template>
<div ref="rootRef" style="display: grid; gap: 10px;">
<cell-flash-toolbar
ref="toolbarRef"
@cell-flash-toolbar-action="handleToolbarAction"
@cell-flash-toolbar-options-change="handleToolbarOptionsChange"
/>
<VGrid
style="min-height: 420px;"
:theme="isDark ? 'darkMaterial' : 'material'"
:columns="columns"
:source="rows"
:plugins="plugins"
:event-manager="eventManager"
:cell-flash="cellFlash"
hide-attribution
resize
/>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
import { VGrid, type ColumnRegular, type DataType } from '@revolist/vue3-datagrid';
import {
CellFlashPlugin,
EventManagerPlugin,
HistoryPlugin,
cellFlashArrowTemplate,
} from '@revolist/revogrid-pro';
import {
defineCellFlashToolbarElement,
type CellFlashToolbarActionEvent,
type CellFlashToolbarElement,
type CellFlashToolbarOptionsChangeEvent,
} from './cell-flash-toolbar';
defineCellFlashToolbarElement();
type MarketRow = DataType & {
symbol: string;
price: number;
change: number;
volume: number;
};
const { isDark } = currentThemeVue();
const rootRef = ref<HTMLElement>();
const toolbarRef = ref<CellFlashToolbarElement | null>(null);
const rowFlash = ref(false);
const duration = ref(1000);
const live = ref(false);
let liveTimer: ReturnType<typeof setInterval> | undefined;
const createRows = (): MarketRow[] => [
{ symbol: 'AAPL', price: 184.12, change: 0.4, volume: 124500 },
{ symbol: 'MSFT', price: 421.34, change: -0.2, volume: 98200 },
{ symbol: 'NVDA', price: 879.7, change: 1.6, volume: 143000 },
{ symbol: 'AMZN', price: 188.8, change: 0.1, volume: 76100 },
{ symbol: 'META', price: 494.2, change: -0.5, volume: 68200 },
];
const rows = ref<MarketRow[]>(createRows());
const columns = ref<ColumnRegular[]>([
{ name: 'Symbol', prop: 'symbol', size: 110 },
{
name: 'Price',
prop: 'price',
size: 130,
flash: (_value, context) => ({
flash: true,
rowFlash: Math.abs(Number(context.value) - Number(context.previousValue)) > 2,
}),
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, Number(value).toFixed(2))),
},
{
name: 'Change %',
prop: 'change',
size: 120,
flash: true,
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, `${Number(value).toFixed(2)}%`)),
},
{ name: 'Volume', prop: 'volume', size: 130, flash: true },
]);
const plugins = [
EventManagerPlugin,
HistoryPlugin,
CellFlashPlugin,
];
const eventManager = { applyEventsToSource: true };
const cellFlash = computed(() => ({
duration: duration.value,
rowDuration: duration.value,
queue: 'merge',
mode: rowFlash.value ? 'cell-and-row' : 'cell',
clearOnSourceChange: false,
aria: true,
labels: {
cellUpdated: state => `Market cell ${String(state.prop)} in row ${state.rowIndex + 1} changed`,
rowUpdated: state => `Market row ${state.rowIndex + 1} changed`,
},
}));
watchEffect(() => {
if (toolbarRef.value) {
toolbarRef.value.options = {
rowFlash: rowFlash.value,
duration: duration.value,
live: live.value,
};
}
});
function gridElement() {
return rootRef.value?.querySelector('revo-grid') as HTMLRevoGridElement | null;
}
function updateRow(row: MarketRow): MarketRow {
const delta = Number((Math.random() * 8 - 4).toFixed(2));
const price = Number(Math.max(1, row.price + delta).toFixed(2));
return {
...row,
price,
change: Number(((price - row.price) / row.price * 100).toFixed(2)),
volume: row.volume + Math.round(Math.random() * 5000),
};
}
function updateAt(rowIndex: number) {
const previous = rows.value[rowIndex];
const next = updateRow(previous);
const grid = gridElement();
grid?.dispatchEvent(new CustomEvent('beforeedit', {
cancelable: true,
detail: {
rowIndex,
prop: 'price',
val: next.price,
model: previous,
type: 'rgRow',
},
}));
grid?.dispatchEvent(new CustomEvent('flashcell', {
detail: {
data: {
[rowIndex]: {
price: next.price,
change: next.change,
volume: next.volume,
},
},
previousData: {
[rowIndex]: {
price: previous.price,
change: previous.change,
volume: previous.volume,
},
},
type: 'rgRow',
eventTypes: ['manual-update'],
},
}));
rows.value = rows.value.map((row, index) => index === rowIndex ? next : row);
}
function updateRandomRow() {
updateAt(Math.floor(Math.random() * rows.value.length));
}
function burstUpdate() {
rows.value.forEach((_row, index) => updateAt(index));
}
function startFeed() {
if (!liveTimer) {
liveTimer = setInterval(updateRandomRow, 900);
live.value = true;
}
}
function stopFeed() {
if (liveTimer) clearInterval(liveTimer);
liveTimer = undefined;
live.value = false;
}
async function manualRowFlash() {
const pluginsList = await gridElement()?.getPlugins();
const plugin = pluginsList?.find(item => item?.constructor?.name === 'CellFlashPlugin') as CellFlashPlugin | undefined;
plugin?.flashRows({ rowIndex: 0, rowClassName: 'manual-row-flash' });
}
async function historyAction(method: 'undo' | 'redo') {
const pluginsList = await gridElement()?.getPlugins();
const history = pluginsList?.find(item => item?.constructor?.name === 'HistoryPlugin') as HistoryPlugin | undefined;
history?.[method]();
}
function handleToolbarOptionsChange(event: CellFlashToolbarOptionsChangeEvent) {
rowFlash.value = event.detail.rowFlash;
duration.value = event.detail.duration;
}
function handleToolbarAction(event: CellFlashToolbarActionEvent) {
if (event.detail === 'update-row') {
updateRandomRow();
return;
}
if (event.detail === 'burst-update') {
burstUpdate();
return;
}
if (event.detail === 'start-feed') {
startFeed();
return;
}
if (event.detail === 'stop-feed') {
stopFeed();
return;
}
if (event.detail === 'manual-row-flash') {
void manualRowFlash();
return;
}
void historyAction(event.detail);
}
onBeforeUnmount(stopFeed);
</script>
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid, type ColumnRegular, type DataType, BasePlugin } from '@revolist/react-datagrid';
import {
CellFlashPlugin,
EventManagerPlugin,
HistoryPlugin,
cellFlashArrowTemplate,
type CellFlashConfig,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
CELL_FLASH_TOOLBAR_ACTION_EVENT,
CELL_FLASH_TOOLBAR_OPTIONS_CHANGE_EVENT,
CELL_FLASH_TOOLBAR_TAG,
defineCellFlashToolbarElement,
type CellFlashToolbarActionEvent,
type CellFlashToolbarElement,
type CellFlashToolbarOptionsChangeEvent,
} from './cell-flash-toolbar';
const { isDark } = currentTheme();
defineCellFlashToolbarElement();
type MarketRow = DataType & {
symbol: string;
price: number;
change: number;
volume: number;
};
const createRows = (): MarketRow[] => [
{ symbol: 'AAPL', price: 184.12, change: 0.4, volume: 124500 },
{ symbol: 'MSFT', price: 421.34, change: -0.2, volume: 98200 },
{ symbol: 'NVDA', price: 879.7, change: 1.6, volume: 143000 },
{ symbol: 'AMZN', price: 188.8, change: 0.1, volume: 76100 },
{ symbol: 'META', price: 494.2, change: -0.5, volume: 68200 },
];
function updateRow(row: MarketRow): MarketRow {
const delta = Number((Math.random() * 8 - 4).toFixed(2));
const price = Number(Math.max(1, row.price + delta).toFixed(2));
return {
...row,
price,
change: Number(((price - row.price) / row.price * 100).toFixed(2)),
volume: row.volume + Math.round(Math.random() * 5000),
};
}
function CellFlash() {
const gridRef = useRef<HTMLRevoGridElement>(null);
const toolbarRef = useRef<CellFlashToolbarElement>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
const [rows, setRows] = useState<MarketRow[]>(() => createRows());
const [rowFlash, setRowFlash] = useState(false);
const [duration, setDuration] = useState(1000);
const [live, setLive] = useState(false);
const columns: ColumnRegular[] = useMemo(
() => [
{ name: 'Symbol', prop: 'symbol', size: 110 },
{
name: 'Price',
prop: 'price',
size: 130,
flash: (_value, context) => ({
flash: true,
rowFlash: Math.abs(Number(context.value) - Number(context.previousValue)) > 2,
}),
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, Number(value).toFixed(2))),
},
{
name: 'Change %',
prop: 'change',
size: 120,
flash: true,
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, `${Number(value).toFixed(2)}%`)),
},
{ name: 'Volume', prop: 'volume', size: 130, flash: true },
],
[],
);
const cellFlash = useMemo<CellFlashConfig>(
() => ({
duration,
rowDuration: duration,
queue: 'merge',
mode: rowFlash ? 'cell-and-row' : 'cell',
clearOnSourceChange: false,
aria: true,
labels: {
cellUpdated: state => `Market cell ${String(state.prop)} in row ${state.rowIndex + 1} changed`,
rowUpdated: state => `Market row ${state.rowIndex + 1} changed`,
},
}),
[duration, rowFlash],
);
const eventManager = useMemo(
() => ({ applyEventsToSource: true }),
[],
);
const plugins = useMemo(
() => [EventManagerPlugin, HistoryPlugin, CellFlashPlugin],
[],
) as any as (typeof BasePlugin)[];
const updateAt = useCallback((rowIndex: number) => {
setRows(current => {
const previous = current[rowIndex];
const next = updateRow(previous);
const updated = [...current];
updated[rowIndex] = next;
const grid = gridRef.current;
grid?.dispatchEvent(new CustomEvent('beforeedit', {
cancelable: true,
detail: {
rowIndex,
prop: 'price',
val: next.price,
model: previous,
type: 'rgRow',
},
}));
grid?.dispatchEvent(new CustomEvent('flashcell', {
detail: {
data: { [rowIndex]: { change: next.change, volume: next.volume } },
previousData: { [rowIndex]: { change: previous.change, volume: previous.volume } },
type: 'rgRow',
eventTypes: ['manual-update'],
},
}));
return updated;
});
}, []);
const stopFeed = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = undefined;
}
setLive(false);
}, []);
useEffect(() => stopFeed, [stopFeed]);
const withPlugin = async (callback: (plugin: any) => void) => {
const pluginsList = await gridRef.current?.getPlugins();
const plugin = pluginsList?.find(item => item?.constructor?.name === 'CellFlashPlugin');
if (plugin) callback(plugin);
};
const withHistory = async (method: 'undo' | 'redo') => {
const pluginsList = await gridRef.current?.getPlugins();
const history = pluginsList?.find(item => item?.constructor?.name === 'HistoryPlugin');
history?.[method]?.();
};
useEffect(() => {
if (toolbarRef.current) {
toolbarRef.current.options = { rowFlash, duration, live };
}
}, [duration, live, rowFlash]);
useEffect(() => {
const toolbar = toolbarRef.current;
if (!toolbar) {
return;
}
const handleOptionsChange = (event: Event) => {
const { rowFlash: nextRowFlash, duration: nextDuration } = (event as CellFlashToolbarOptionsChangeEvent).detail;
setRowFlash(nextRowFlash);
setDuration(nextDuration);
};
const handleAction = (event: Event) => {
const action = (event as CellFlashToolbarActionEvent).detail;
if (action === 'update-row') {
updateAt(Math.floor(Math.random() * rows.length));
return;
}
if (action === 'burst-update') {
rows.forEach((_row, index) => updateAt(index));
return;
}
if (action === 'start-feed') {
if (!timerRef.current) {
timerRef.current = setInterval(() => updateAt(Math.floor(Math.random() * rows.length)), 900);
setLive(true);
}
return;
}
if (action === 'stop-feed') {
stopFeed();
return;
}
if (action === 'manual-row-flash') {
void withPlugin(plugin => plugin.flashRows({ rowIndex: 0, rowClassName: 'manual-row-flash' }));
return;
}
void withHistory(action);
};
toolbar.addEventListener(CELL_FLASH_TOOLBAR_OPTIONS_CHANGE_EVENT, handleOptionsChange);
toolbar.addEventListener(CELL_FLASH_TOOLBAR_ACTION_EVENT, handleAction);
return () => {
toolbar.removeEventListener(CELL_FLASH_TOOLBAR_OPTIONS_CHANGE_EVENT, handleOptionsChange);
toolbar.removeEventListener(CELL_FLASH_TOOLBAR_ACTION_EVENT, handleAction);
};
}, [rows, stopFeed, updateAt]);
const RevoGridWithPluginProps = RevoGrid as any;
return (
<div style={{ display: 'grid', gap: 10 }}>
{React.createElement(CELL_FLASH_TOOLBAR_TAG, { ref: toolbarRef })}
<RevoGridWithPluginProps
ref={gridRef}
style={{ minHeight: 420 }}
columns={columns}
source={rows}
eventManager={eventManager}
cellFlash={cellFlash}
plugins={plugins}
theme={isDark() ? 'darkCompact' : 'compact'}
hide-attribution
/>
</div>
);
}
export default CellFlash;
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild, ViewEncapsulation } from '@angular/core';
import type { AfterViewInit, OnDestroy } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import type { ColumnRegular, DataType } from '@revolist/revogrid';
import {
CellFlashPlugin,
EventManagerPlugin,
HistoryPlugin,
cellFlashArrowTemplate,
type CellFlashConfig,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
defineCellFlashToolbarElement,
type CellFlashToolbarActionEvent,
type CellFlashToolbarElement,
type CellFlashToolbarOptionsChangeEvent,
} from './cell-flash-toolbar';
defineCellFlashToolbarElement();
type MarketRow = DataType & {
symbol: string;
price: number;
change: number;
volume: number;
};
@Component({
selector: 'cell-flash-grid',
standalone: true,
imports: [RevoGrid],
template: `
<div style="display: grid; gap: 10px;">
<cell-flash-toolbar
#toolbarRef
(cell-flash-toolbar-action)="handleToolbarAction($event)"
(cell-flash-toolbar-options-change)="handleToolbarOptionsChange($event)"
></cell-flash-toolbar>
<revo-grid
#gridRef
[columns]="columns"
[source]="source"
[eventManager]="eventManager"
[cellFlash]="cellFlash"
[plugins]="plugins"
[theme]="theme"
[hideAttribution]="true"
style="min-height: 420px; min-width: 600px"
></revo-grid>
</div>
`,
encapsulation: ViewEncapsulation.None,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class CellFlashGridComponent implements AfterViewInit, OnDestroy {
@ViewChild('gridRef', { static: true }) gridRef!: ElementRef<HTMLRevoGridElement>;
@ViewChild('toolbarRef', { read: ElementRef }) toolbarRef?: ElementRef<CellFlashToolbarElement>;
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
source: MarketRow[] = this.createRows();
rowFlash = false;
duration = 1000;
live = false;
liveTimer?: ReturnType<typeof setInterval>;
columns: ColumnRegular[] = [
{ name: 'Symbol', prop: 'symbol', size: 110 },
{
name: 'Price',
prop: 'price',
size: 130,
flash: (_value, context) => ({
flash: true,
rowFlash: Math.abs(Number(context.value) - Number(context.previousValue)) > 2,
}),
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, Number(value).toFixed(2))),
},
{
name: 'Change %',
prop: 'change',
size: 120,
flash: true,
cellTemplate: cellFlashArrowTemplate((h, { value }) => h('span', null, `${Number(value).toFixed(2)}%`)),
},
{ name: 'Volume', prop: 'volume', size: 130, flash: true },
];
plugins = [EventManagerPlugin, HistoryPlugin, CellFlashPlugin];
eventManager = {
applyEventsToSource: true,
};
get cellFlash(): CellFlashConfig {
return {
duration: this.duration,
rowDuration: this.duration,
queue: 'merge',
mode: this.rowFlash ? 'cell-and-row' : 'cell',
clearOnSourceChange: false,
aria: true,
labels: {
cellUpdated: state => `Market cell ${String(state.prop)} in row ${state.rowIndex + 1} changed`,
rowUpdated: state => `Market row ${state.rowIndex + 1} changed`,
},
};
}
createRows(): MarketRow[] {
return [
{ symbol: 'AAPL', price: 184.12, change: 0.4, volume: 124500 },
{ symbol: 'MSFT', price: 421.34, change: -0.2, volume: 98200 },
{ symbol: 'NVDA', price: 879.7, change: 1.6, volume: 143000 },
{ symbol: 'AMZN', price: 188.8, change: 0.1, volume: 76100 },
{ symbol: 'META', price: 494.2, change: -0.5, volume: 68200 },
];
}
ngAfterViewInit() {
this.syncToolbar();
}
syncToolbar() {
if (this.toolbarRef?.nativeElement) {
this.toolbarRef.nativeElement.options = {
rowFlash: this.rowFlash,
duration: this.duration,
live: this.live,
};
}
}
updateRow(row: MarketRow): MarketRow {
const delta = Number((Math.random() * 8 - 4).toFixed(2));
const price = Number(Math.max(1, row.price + delta).toFixed(2));
return {
...row,
price,
change: Number(((price - row.price) / row.price * 100).toFixed(2)),
volume: row.volume + Math.round(Math.random() * 5000),
};
}
updateAt(rowIndex: number) {
const previous = this.source[rowIndex];
const next = this.updateRow(previous);
const grid = this.gridRef.nativeElement;
grid.dispatchEvent(new CustomEvent('beforeedit', {
cancelable: true,
detail: {
rowIndex,
prop: 'price',
val: next.price,
model: previous,
type: 'rgRow',
},
}));
grid.dispatchEvent(new CustomEvent('flashcell', {
detail: {
data: {
[rowIndex]: {
change: next.change,
volume: next.volume,
},
},
previousData: {
[rowIndex]: {
change: previous.change,
volume: previous.volume,
},
},
type: 'rgRow',
eventTypes: ['manual-update'],
},
}));
this.source = this.source.map((row, index) => index === rowIndex ? next : row);
}
updateRandomRow() {
this.updateAt(Math.floor(Math.random() * this.source.length));
}
burstUpdate() {
this.source.forEach((_row, index) => this.updateAt(index));
}
startFeed() {
if (!this.liveTimer) {
this.liveTimer = setInterval(() => this.updateRandomRow(), 900);
this.live = true;
this.syncToolbar();
}
}
stopFeed() {
if (this.liveTimer) clearInterval(this.liveTimer);
this.liveTimer = undefined;
this.live = false;
this.syncToolbar();
}
async historyAction(method: 'undo' | 'redo') {
const plugins = await this.gridRef.nativeElement.getPlugins();
const history = plugins.find(item => item?.constructor?.name === 'HistoryPlugin') as HistoryPlugin | undefined;
history?.[method]();
}
async manualRowFlash() {
const plugins = await this.gridRef.nativeElement.getPlugins();
const plugin = plugins.find(item => item?.constructor?.name === 'CellFlashPlugin') as CellFlashPlugin | undefined;
plugin?.flashRows({ rowIndex: 0, rowClassName: 'manual-row-flash' });
}
handleToolbarOptionsChange(event: CellFlashToolbarOptionsChangeEvent) {
this.rowFlash = event.detail.rowFlash;
this.duration = event.detail.duration;
this.syncToolbar();
}
handleToolbarAction(event: CellFlashToolbarActionEvent) {
if (event.detail === 'update-row') {
this.updateRandomRow();
return;
}
if (event.detail === 'burst-update') {
this.burstUpdate();
return;
}
if (event.detail === 'start-feed') {
this.startFeed();
return;
}
if (event.detail === 'stop-feed') {
this.stopFeed();
return;
}
if (event.detail === 'manual-row-flash') {
void this.manualRowFlash();
return;
}
void this.historyAction(event.detail);
}
ngOnDestroy() {
this.stopFeed();
}
}
Enable Cell Flash
Section titled “Enable Cell Flash”Add CellFlashPlugin and mark columns with flash. Existing boolean predicates still work, and EventManagerPlugin keeps edit events connected to the source update flow.
import { CellFlashPlugin, EventManagerPlugin } from '@revolist/revogrid-pro';
grid.plugins = [EventManagerPlugin, CellFlashPlugin];grid.eventManager = { applyEventsToSource: true };grid.columns = [ { name: 'Price', prop: 'price', flash: () => true, },];Context-Aware Flashing
Section titled “Context-Aware Flashing”The flash callback receives the new value and a context object with the previous value, row model, row index, column, row type, and edit metadata. Return a boolean for simple behavior or a decision object to customize the flash.
grid.columns = [ { name: 'Price', prop: 'price', flash: (value, context) => ({ flash: true, rowFlash: Math.abs(Number(value) - Number(context.previousValue)) > 2, direction: Number(value) > Number(context.previousValue) ? 'up' : 'down', duration: 800, rowDuration: 1200, className: 'price-flash', rowClassName: 'large-move-row', }), cellTemplate: cellFlashArrowTemplate({ symbols: { up: '+', down: '-', changed: '~' }, className: 'price-flash-arrow', arrowClassName: 'price-flash-symbol', directionClassNames: { up: 'positive', down: 'negative', changed: 'neutral' }, }), },];Configure Timing And Queueing
Section titled “Configure Timing And Queueing”Use grid.cellFlash or additionalData.cellFlash to tune behavior. Defaults preserve legacy behavior: enabled, cell-only, 1000ms duration, replace active flashes on each flash event, and clear on source reset.
grid.cellFlash = { duration: 700, rowDuration: 1000, mode: 'cell-and-row', queue: 'merge', maxActive: 300, clearOnSourceChange: true, respectReducedMotion: true, aria: { enabled: true, live: 'polite', atomic: true, liveRegionClassName: 'rv-cell-flash-live-region', }, labels: { cellUpdated: state => `Updated ${String(state.prop)} in row ${state.rowIndex + 1}`, rowUpdated: state => `Updated row ${state.rowIndex + 1}`, }, className: 'cell-flash-active', rowClassName: 'row-flash-active',};Queue modes:
replaceclears current flash state before applying the next batch.mergekeeps existing flashes active until their own timers expire.
Announcement priority is: per-change ariaLabel, aria.label, labels.cellUpdated or labels.rowUpdated, then the built-in defaults. Use labels for normal localization and aria.label when one callback should fully own every announcement. The live-region politeness, atomic flag, and live-region class are also configurable through aria.
Manual Flash API
Section titled “Manual Flash API”Manual methods are available from the plugin instance. This is useful when updates come from a websocket, polling loop, or external calculation.
const plugins = await grid.getPlugins();const cellFlash = plugins.find(plugin => plugin.constructor.name === 'CellFlashPlugin');
cellFlash.flashCell({ rowIndex: 0, prop: 'price', previousValue: 100, value: 104, direction: 'up',});
cellFlash.flashRows({ rowIndex: 0, rowClassName: 'manual-row-flash',});
cellFlash.clearFlash({ rowIndex: 0, prop: 'price' });The existing event API is still supported:
grid.dispatchEvent(new CustomEvent('flashcell', { detail: { data: { 0: { price: 104 } }, previousData: { 0: { price: 100 } }, type: 'rgRow', },}));Styling
Section titled “Styling”The plugin sets flash, flash-direction, data-flash-direction, and duration CSS variables on active cells. Row flashes receive flash-row and the configured row class.
revo-grid { --rv-change-highlight: rgba(255, 238, 0, 0.6); --rv-row-change-highlight: rgba(255, 238, 0, 0.22); --rv-cell-flash-up-color: #15803d; --rv-cell-flash-down-color: #dc2626; --rv-cell-flash-arrow-transition-duration: 0.3s; --rv-cell-flash-arrow-transition-timing: ease-in-out;}
revo-grid[theme^='dark'] { --rv-change-highlight: rgba(250, 204, 21, 0.34); --rv-row-change-highlight: rgba(250, 204, 21, 0.16); --rv-cell-flash-up-color: #4ade80; --rv-cell-flash-down-color: #f87171;}
revo-grid .rgCell[flash][flash-direction="up"] { color: var(--rv-cell-flash-up-color);}
revo-grid .rgRow[flash-row].large-move-row { font-weight: 600;}When respectReducedMotion is enabled, users with reduced-motion preferences keep the flash attributes and final highlight state without the animation.