Demo
Source code
import { defineCustomElements } from '@revolist/revogrid/loader';import { SummaryChartHeaderPlugin, ColumnGroupPanelPlugin, TooltipPlugin, CellValidatePlugin, EventManagerPlugin, ExportExcelPlugin, AdvanceFilterPlugin, RowSelectPlugin, type ExportExcelEvent,} from '@revolist/revogrid-pro';import { currentTheme } from '../composables/useRandomData';import { parseFilterExpression, syncFiltersToTextInput } from '../filter-showcase/parse.filters';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
defineCustomElements();
const { isDark } = currentTheme();
export function load(parentSelector: string, data: any[]) { const container = document.createElement('div'); container.innerHTML = ` <div class="flex justify-between gap-8"> <div class="mb-4 grow"> <div class="flex gap-2"> <textarea id="filterExpression" rows="1" class="p-4 text-sm border rounded-lg shadow-sm focus:ring focus:ring-blue-200 focus:outline-none focus:border-blue-400 placeholder-gray-400" style="width: 100%" placeholder='Enter filters (e.g., Gender eq "female")' ></textarea> <button id="filterButton" class="self-start rounded-md bg-slate-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" > Filter </button> </div> </div> <div> <button id="exportButton" class="rounded-md bg-green-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" > Export to Excel </button> </div> </div> <revo-grid class="border rounded-lg" range id="grid" style="min-height: 500px;" resize hide-attribution ></revo-grid> `;
const parent = document.querySelector(parentSelector); if (parent) { parent.appendChild(container); }
const grid = container.querySelector<HTMLRevoGridElement>('#grid'); const textarea = container.querySelector<HTMLTextAreaElement>('#filterExpression'); const filterButton = container.querySelector<HTMLButtonElement>('#filterButton'); const exportButton = container.querySelector<HTMLButtonElement>('#exportButton');
if (grid && textarea && filterButton && exportButton) { grid.range = true; grid.theme = isDark() ? 'darkCompact' : 'compact'; grid.hideAttribution = true; grid.resize = true;
grid.columns = [...ECOMMERCE_COLUMNS]; grid.columnTypes = { ...ECOMMERCE_COLUMNS_TYPES }; grid.plugins = [ RowSelectPlugin, EventManagerPlugin, TooltipPlugin, ColumnGroupPanelPlugin, SummaryChartHeaderPlugin, CellValidatePlugin, AdvanceFilterPlugin, ExportExcelPlugin, ]; grid.source = data; grid.filter = {};
filterButton.addEventListener('click', () => { const filterExpression = textarea.value; const parsedFilters = parseFilterExpression(filterExpression); grid.filter = { multiFilterItems: parsedFilters, }; });
exportButton.addEventListener('click', async () => { const plugins = await grid.getPlugins(); const exportPlugin = plugins.find( (plugin) => plugin instanceof ExportExcelPlugin ) as ExportExcelPlugin; const exportConfig: ExportExcelEvent = { sheetName: 'ecommerce.xlsx' }; exportPlugin?.export(exportConfig); });
grid.addEventListener('afterfilterapply', (e: CustomEvent) => { const syncedFilter = syncFiltersToTextInput(e); if (textarea) { textarea.value = syncedFilter; } }); }}
<template> <div class="flex justify-between gap-8"> <div> <button class="rounded-md bg-green-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" @click="exportToExcel" > Export to Excel </button> <span class="text-sm px-2 py-0.5"> Selected Rows: <strong class="bg-gray-200 text-gray-700 px-2 py-0.5 rounded" >{{ selectedRowsCount }}/{{ allRowsCount }}</strong > </span> </div> </div> <RevoGrid class="border rounded-lg overflow-hidden" range ref="grid" :theme="isDark ? 'darkCompact' : 'compact'" :columns="columns" :source="rows" :plugins="plugins" :column-types="columnTypes" :filter="filter" style="min-height: 500px" resize hide-attribution @rowselected="handleRowSelected" /></template>
<script setup lang="ts">import { currentThemeVue } from '../composables/useRandomData';import RevoGrid, { BasePlugin, type ColumnFilterConfig, type ColumnGrouping, type ColumnRegular,} from '@revolist/vue3-datagrid';import { type ExportExcelEvent, SummaryChartHeaderPlugin, ColumnGroupPanelPlugin, TooltipPlugin, CellValidatePlugin, EventManagerPlugin, ExportExcelPlugin, AdvanceFilterPlugin, RowSelectPlugin, CellFlashPlugin,} from '@revolist/revogrid-pro';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES,} from '../sys-data/ecommerce.columns';import { ref, watch } from 'vue';
const props = defineProps({ rows: { type: Array<any>, default: () => [], },});
const grid = ref<{ $el: HTMLRevoGridElement } | null>(null);const columnTypes = ref({ ...ECOMMERCE_COLUMNS_TYPES });
// prop same as name in fieldsconst columns = ref<(ColumnRegular | ColumnGrouping)[]>([...ECOMMERCE_COLUMNS]);
const { isDark } = currentThemeVue();const plugins = [ RowSelectPlugin, EventManagerPlugin, TooltipPlugin, ColumnGroupPanelPlugin, SummaryChartHeaderPlugin, CellValidatePlugin, AdvanceFilterPlugin, ExportExcelPlugin, CellFlashPlugin,] as any as (typeof BasePlugin)[];
const exportConfig: ExportExcelEvent = { sheetName: 'ecommerce.xlsx' };
const exportToExcel = async () => { if (grid.value) { const plugins = await grid.value.$el.getPlugins(); const exportPlugin = plugins.find( (plugin) => plugin instanceof ExportExcelPlugin, ) as ExportExcelPlugin; exportPlugin?.export(exportConfig); }};
const filter = ref<ColumnFilterConfig>({ multiFilterItems: {},});
const selectedRowsCount = ref(0);const allRowsCount = ref(0);watch(props.rows, () => { allRowsCount.value = props.rows.length;}, { immediate: true });const handleRowSelected = ( event: CustomEvent<HTMLRevoGridElementEventMap['rowselected']>,) => { selectedRowsCount.value = event.detail.count; allRowsCount.value = event.detail.allRowsCount;};</script>
import React, { useState, useMemo, useRef } from 'react';import { RevoGrid, type DataType, type ColumnRegular, type ColumnGrouping, type ColumnFilterConfig, BasePlugin,} from '@revolist/react-datagrid';import { ExportExcelEvent, SummaryChartHeaderPlugin, ColumnGroupPanelPlugin, TooltipPlugin, CellValidatePlugin, EventManagerPlugin, ExportExcelPlugin, AdvanceFilterPlugin, RowSelectPlugin,} from '@revolist/revogrid-pro';import { useRandomData, currentTheme } from '../composables/useRandomData';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';import { parseFilterExpression, syncFiltersToTextInput,} from '../filter-showcase/parse.filters';
interface ECommerceProps { rows?: DataType[]; fields?: string[];}
function ECommerce({ rows = [], fields = [] }: ECommerceProps) { const gridRef = useRef<HTMLRevoGridElement>(null); const [filterExpression, setFilterExpression] = useState(''); const [filter, setFilter] = useState<ColumnFilterConfig>({ multiFilterItems: {}, });
const { isDark } = currentTheme(); const { createRandomData } = useRandomData();
const theme = isDark() ? 'darkCompact' : 'compact';
const columns: (ColumnRegular | ColumnGrouping)[] = useMemo( () => [...ECOMMERCE_COLUMNS], [], );
const columnTypes = useMemo(() => ({ ...ECOMMERCE_COLUMNS_TYPES }), []);
const plugins = useMemo( () => [ RowSelectPlugin, EventManagerPlugin, TooltipPlugin, ColumnGroupPanelPlugin, SummaryChartHeaderPlugin, CellValidatePlugin, AdvanceFilterPlugin, ExportExcelPlugin, ], [], ) as any as (typeof BasePlugin)[];
const exportConfig: ExportExcelEvent = { sheetName: 'ecommerce.xlsx' };
const exportToExcel = async () => { const grid = gridRef.current; if (grid) { const plugins = await grid.getPlugins(); const exportPlugin = plugins.find( (plugin) => plugin instanceof ExportExcelPlugin, ) as ExportExcelPlugin; exportPlugin?.export(exportConfig); } };
const applyTextFilters = () => { const parsedFilters = parseFilterExpression(filterExpression); setFilter({ multiFilterItems: parsedFilters, }); };
const sync = (e: CustomEvent) => { setFilterExpression(syncFiltersToTextInput(e)); };
return ( <div> <div className="flex justify-between gap-8"> <div className="mb-4 grow"> <div className="flex gap-2"> <textarea id="filterExpression" rows={1} className={`p-4 text-sm border rounded-lg shadow-sm focus:ring focus:ring-blue-200 focus:outline-none focus:border-blue-400 placeholder-gray-400 ${ isDark() ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-300' }`} style={{ width: '100%' }} placeholder='Enter filters (e.g., Gender eq "female")' value={filterExpression} onChange={(e) => setFilterExpression(e.target.value)} /> <button className="self-start rounded-md bg-slate-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" onClick={applyTextFilters} > Filter </button> </div> </div> <div> <button className="rounded-md bg-green-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" onClick={exportToExcel} > Export to Excel </button> </div> </div> <RevoGrid className="border rounded-lg" range ref={gridRef} columns={columns} source={rows.length > 0 ? rows : createRandomData(100)} plugins={plugins} columnTypes={columnTypes} filter={filter} style={{ minHeight: '500px' }} resize hide-attribution theme={theme} // @ts-ignore onAfterfilterapply={sync} /> </div> );}
export default ECommerce;
import { Component, ViewEncapsulation, Input, ViewChild, ElementRef } from '@angular/core';import { type ColumnFilterConfig, type ColumnRegular, type ColumnGrouping, RevoGrid,} from '@revolist/angular-datagrid';import { SummaryChartHeaderPlugin, ColumnGroupPanelPlugin, TooltipPlugin, CellValidatePlugin, EventManagerPlugin, ExportExcelPlugin, type ExportExcelEvent, AdvanceFilterPlugin, RowSelectPlugin,} from '@revolist/revogrid-pro';import { currentTheme } from '../composables/useRandomData';import { parseFilterExpression, syncFiltersToTextInput } from '../filter-showcase/parse.filters';import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
@Component({ selector: 'ecommerce-grid', standalone: true, imports: [RevoGrid], template: ` <div class="flex justify-between gap-8"> <div class="mb-4 grow"> <div class="flex gap-2"> <textarea id="filterExpression" rows="1" [ngClass]="{ 'bg-white border-gray-300': !isDark(), 'bg-gray-800 border-gray-600': isDark() }" class="p-4 text-sm border rounded-lg shadow-sm focus:ring focus:ring-blue-200 focus:outline-none focus:border-blue-400 placeholder-gray-400" style="width: 100%" placeholder='Enter filters (e.g., Gender eq "female")' [(ngModel)]="filterExpression" ></textarea> <button class="self-start rounded-md bg-slate-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" (click)="applyTextFilters()" > Filter </button> </div> </div> <div> <button class="rounded-md bg-green-800 mb-2 py-1.5 px-3 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" (click)="exportToExcel()" > Export to Excel </button> </div> </div> <revo-grid #gridRef class="border rounded-lg" [range]="true" [theme]="theme" [columns]="columns" [source]="rows" [plugins]="plugins" [columnTypes]="columnTypes" [filter]="filter" style="min-height: 500px;" [resize]="true" [hideAttribution]="true" (afterfilterapply)="sync($event)" ></revo-grid> `, encapsulation: ViewEncapsulation.None,})export class ECommerceGridComponent { @Input() rows: any[] = []; @Input() fields: string[] = [];
@ViewChild('gridRef', { static: true }) gridRef!: ElementRef<HTMLRevoGridElement>;
columnTypes = { ...ECOMMERCE_COLUMNS_TYPES }; columns: (ColumnRegular | ColumnGrouping)[] = [...ECOMMERCE_COLUMNS];
theme = currentTheme().isDark() ? 'darkCompact' : 'compact'; filterExpression = ''; // Stores the user-entered filter expression
filter: ColumnFilterConfig = { multiFilterItems: {}, };
plugins = [ RowSelectPlugin, EventManagerPlugin, TooltipPlugin, ColumnGroupPanelPlugin, SummaryChartHeaderPlugin, CellValidatePlugin, AdvanceFilterPlugin, ExportExcelPlugin, ];
exportConfig: ExportExcelEvent = { sheetName: 'ecommerce.xlsx' };
async exportToExcel() { const plugins = await this.gridRef.nativeElement.getPlugins(); const exportPlugin = plugins.find( (plugin) => plugin instanceof ExportExcelPlugin ) as ExportExcelPlugin; exportPlugin?.export(this.exportConfig); }
applyTextFilters() { const parsedFilters = parseFilterExpression(this.filterExpression); this.filter = { multiFilterItems: parsedFilters, }; }
sync(event: CustomEvent) { this.filterExpression = syncFiltersToTextInput(event); }}
import type { ColumnGrouping, ColumnRegular, HyperFunc, VNode } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';import SelectColumnType from '@revolist/revogrid-column-select';
import { changeRenderer, columnTypeRenderer, ratingStarRenderer, thumbsRenderer, validationRenderer, barChartRenderer, pieChartRenderer, summaryHeaderRenderer, summaryAggregateRenderer, validateRange, validateBoolean, validateInteger, cellFlashArrowTemplate,} from '@revolist/revogrid-pro';
export const ECOMMERCE_COLUMNS_TYPES = { integer: new NumberColumnType(), percent: new NumberColumnType('0.00%'), currency: new NumberColumnType('$0,0.00'), select: new SelectColumnType(),};
export const ECOMMERCE_COLUMNS: (ColumnRegular | ColumnGrouping)[] = [ { name: 'Personal', children: [ { prop: '_checkbox', rowSelect: true, readonly: true, filter: false, size: 60, }, { name: 'ID', prop: 'Customer ID', size: 90, columnTemplate: columnTypeRenderer, columnType: 'id', readonly: true, }, { name: 'Image', prop: 'avatar', size: 100, filter: false, columnTemplate: columnTypeRenderer, columnType: 'integer', cellTemplate: (h: HyperFunc<VNode>, props: any) => { return h('img', { style: { width: '21px', height: '21px', borderRadius: '50%', objectFit: 'cover', verticalAlign: 'middle', }, src: `https://randomuser.me/api/portraits/${props.model.Gender === 'Male' ? 'men' : 'women'}/${props.rowIndex % 50}.jpg`, }); }, }, { name: 'Gender', prop: 'Gender', columnType: 'select', source: ['Male', 'Female'], filter: ['string', 'selection'], columnTemplate: columnTypeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => { const keys = Object.keys(summary) .sort() .filter((name) => name); return pieChartRenderer(h, { value: keys.map((name) => ({ name, value: summary[name], color: name === 'Male' ? '#008620' : '#ffc107', })), }); }, }, { name: 'Lifetime Value', prop: 'Lifetime Value', size: 180,
summaryVNode: (h: HyperFunc<VNode>, summary: any) => { const keys = Object.keys(summary); return barChartRenderer(h, { value: keys.map((k) => summary[k]), column: { position: 'top' }, }); }, columnTemplate: columnTypeRenderer, columnType: 'integer', filter: ['number'], validate: (v: any) => validateInteger(v), }, { name: 'Age', prop: 'Age', columnType: 'integer', size: 150, filter: ['number', 'slider'], columnTemplate: columnTypeRenderer, summaryVNode(h: HyperFunc<VNode>, summary: any) { const keys = Object.keys(summary).sort(); return barChartRenderer(h, { value: keys.map((k) => summary[k]), column: { position: 'top' }, }); }, validate: (v: any) => validateRange(v, 20, 100), validationTooltip: (v: any) => { return v < 20 ? `Age must be greater than ${20}, you entered ${v}` : v > 100 ? `Age must be less than ${100}, you entered ${v}` : undefined; }, flash: () => true, cellTemplate: cellFlashArrowTemplate(), }, { name: 'City', prop: 'City', filter: ['selection'], size: 180, columnTemplate: columnTypeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => summaryHeaderRenderer(h, summary, { maxItems: 2 }), columnType: 'string', }, { name: 'Membership Type', prop: 'Membership Type', columnTemplate: columnTypeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => summaryHeaderRenderer(h, summary, { maxItems: 2 }), columnType: 'select', filter: ['selection'], size: 200, source: ['Gold', 'Silver', 'Bronze'], }, ], }, { name: 'Spending', children: [ { name: 'Average Rating', prop: 'Average Rating', size: 150, columnTemplate: columnTypeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => { const keys = Object.keys(summary).sort(); return barChartRenderer(h, { value: keys.map((k) => summary[k]), column: { position: 'top' }, }); }, filter: ['number'], columnType: 'integer', cellTemplate: ratingStarRenderer, invalidProperties: () => ({ style: { backgroundColor: 'var(--sl-color-red-low)' }, }), softValidation: true, validationTooltip: (v: any) => { return v < 4 ? `Average rating is less than ${4} (${v})` : undefined; }, validate: (v: any) => validateRange(v, 4, 5), }, { name: 'Discount', prop: 'Discount Applied', size: 120, columnType: 'boolean', cellTemplate: thumbsRenderer, columnTemplate: columnTypeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => { const keys = Object.keys(summary).filter((name) => name); return pieChartRenderer(h, { value: keys.map((name) => ({ name, value: summary[name], })), }); }, validate: (v: any) => validateBoolean(v), }, { name: 'Spend Change (%)', prop: 'Spend Change (%)', size: 200, columnType: 'percent', filter: ['number'], columnTemplate: columnTypeRenderer, cellParser: (model: any, column: any) => parseFloat(model[column.prop])?.toFixed(2), cellTemplate: changeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => summaryAggregateRenderer(h, summary, { showAvg: true }), }, { name: 'Total Spend', prop: 'Total Spend', size: 150, columnType: 'currency', filter: ['number'], columnTemplate: columnTypeRenderer, summaryVNode: (h: HyperFunc<VNode>, summary: any) => summaryAggregateRenderer(h, summary, { showSum: true }), flash: () => true, cellTemplate: cellFlashArrowTemplate(), }, ], },];