Skip to content

Row Odd

Apply configurable row striping for better readability and data distinction with the help of the RowOddPlugin.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { RowOddPlugin } from '@revolist/revogrid-pro';
import './row-odd-toolbar.scss';
import {
  createRowOddConfig,
  createRowOddRows,
  initialRowOddState,
  rowOddColumns,
  type RowOddDemoState,
} from './row-odd-shared';
import {
  ROW_ODD_TOOLBAR_ACTION_EVENT,
  ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT,
  createRowOddToolbar,
  type RowOddToolbarActionEvent,
  type RowOddToolbarOptionsChangeEvent,
} from './row-odd-toolbar';

defineCustomElements();

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

  const state: RowOddDemoState = { ...initialRowOddState };
  const root = document.createElement('div');
  root.className = 'row-odd-demo';
  const toolbar = createRowOddToolbar(state);
  const grid = document.createElement('revo-grid');

  const render = () => {
    grid.rowOdd = createRowOddConfig(state);
    grid.theme = 'material';
    grid.classList.toggle('cell-border', state.bordered);
    grid.source = createRowOddRows(state.priceShift);
    toolbar.state = state;
  };

  toolbar.addEventListener(ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT, (event: Event) => {
    Object.assign(state, (event as RowOddToolbarOptionsChangeEvent).detail);
    render();
  });
  toolbar.addEventListener(ROW_ODD_TOOLBAR_ACTION_EVENT, (event: Event) => {
    if ((event as RowOddToolbarActionEvent).detail === 'update-rows') {
      state.priceShift += 9;
      render();
    }
  });

  grid.columns = rowOddColumns;
  grid.plugins = [RowOddPlugin];
  grid.hideAttribution = true;
  root.append(toolbar, grid);
  host.appendChild(root);
  render();

  return () => root.remove();
}
Vue vue
<template>
  <div class="row-odd-demo">
    <row-odd-demo-toolbar
      ref="toolbar"
      @row-odd-toolbar-action="handleToolbarAction"
      @row-odd-toolbar-options-change="setToolbarOptions"
    />
    <RevoGrid
      :additional-data="additionalData"
      :class="{ 'cell-border': bordered }"
      :columns="columns"
      :plugins="plugins"
      :source="source"
      theme="material"
      hide-attribution
    />
  </div>
</template>

<script setup lang="ts">
import RevoGrid from '@revolist/vue3-datagrid';
import { RowOddPlugin } from '@revolist/revogrid-pro';
import './row-odd-toolbar.scss';
import { computed, onMounted, ref, watch } from 'vue';
import {
  createRowOddConfig,
  createRowOddRows,
  initialRowOddState,
  rowOddColumns,
  type RowOddDemoState,
  type RowOddDemoMode,
} from './row-odd-shared';
import {
  defineRowOddToolbarElement,
  type RowOddToolbarActionEvent,
  type RowOddToolbarElement,
  type RowOddToolbarOptionsChangeEvent,
} from './row-odd-toolbar';

defineRowOddToolbarElement();

const toolbar = ref<RowOddToolbarElement | null>(null);
const mode = ref<RowOddDemoMode>(initialRowOddState.mode);
const interval = ref(initialRowOddState.interval);
const offset = ref(initialRowOddState.offset);
const bordered = ref(initialRowOddState.bordered);
const priceShift = ref(initialRowOddState.priceShift);

const columns = rowOddColumns;
const plugins = [RowOddPlugin];
const source = computed(() => createRowOddRows(priceShift.value));
const toolbarState = computed<RowOddDemoState>(() => ({
  mode: mode.value,
  interval: interval.value,
  offset: offset.value,
  bordered: bordered.value,
  priceShift: priceShift.value,
}));
const additionalData = computed(() => ({
  rowOdd: createRowOddConfig({
    mode: mode.value,
    interval: interval.value,
    offset: offset.value,
  }),
}));

function syncToolbar() {
  if (toolbar.value) {
    toolbar.value.state = toolbarState.value;
  }
}

function setToolbarOptions(event: RowOddToolbarOptionsChangeEvent) {
  mode.value = event.detail.mode;
  interval.value = event.detail.interval;
  offset.value = event.detail.offset;
  bordered.value = event.detail.bordered;
}

function handleToolbarAction(event: RowOddToolbarActionEvent) {
  if (event.detail === 'update-rows') {
    priceShift.value += 9;
  }
}

onMounted(syncToolbar);
watch(toolbarState, syncToolbar);
</script>
React tsx
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { RowOddPlugin } from '@revolist/revogrid-pro';
import './row-odd-toolbar.scss';
import {
  createRowOddConfig,
  createRowOddRows,
  initialRowOddState,
  rowOddColumns,
  type RowOddDemoMode,
} from './row-odd-shared';
import {
  ROW_ODD_TOOLBAR_ACTION_EVENT,
  ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT,
  ROW_ODD_TOOLBAR_TAG,
  defineRowOddToolbarElement,
  type RowOddToolbarActionEvent,
  type RowOddToolbarElement,
  type RowOddToolbarOptionsChangeEvent,
} from './row-odd-toolbar';

defineRowOddToolbarElement();

function RowOdd() {
  const toolbarRef = useRef<RowOddToolbarElement>(null);
  const [mode, setMode] = useState<RowOddDemoMode>(initialRowOddState.mode);
  const [interval, setIntervalValue] = useState(initialRowOddState.interval);
  const [offset, setOffset] = useState(initialRowOddState.offset);
  const [bordered, setBordered] = useState(initialRowOddState.bordered);
  const [priceShift, setPriceShift] = useState(initialRowOddState.priceShift);

  const source = useMemo(() => createRowOddRows(priceShift), [priceShift]);
  const columns = useMemo(() => rowOddColumns, []);
  const plugins = useMemo(() => [RowOddPlugin], []);
  const additionalData = useMemo(
    () => ({
      rowOdd: createRowOddConfig({ mode, interval, offset }),
    }),
    [interval, mode, offset],
  );
  const toolbarState = useMemo(() => ({
    mode,
    interval,
    offset,
    bordered,
    priceShift,
  }), [bordered, interval, mode, offset, priceShift]);

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

  useEffect(() => {
    const toolbar = toolbarRef.current;
    if (!toolbar) {
      return;
    }
    const handleOptionsChange = (event: Event) => {
      const next = (event as RowOddToolbarOptionsChangeEvent).detail;
      setMode(next.mode);
      setIntervalValue(next.interval);
      setOffset(next.offset);
      setBordered(next.bordered);
    };
    const handleAction = (event: Event) => {
      if ((event as RowOddToolbarActionEvent).detail === 'update-rows') {
        setPriceShift(value => value + 9);
      }
    };
    toolbar.addEventListener(ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT, handleOptionsChange);
    toolbar.addEventListener(ROW_ODD_TOOLBAR_ACTION_EVENT, handleAction);
    return () => {
      toolbar.removeEventListener(ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT, handleOptionsChange);
      toolbar.removeEventListener(ROW_ODD_TOOLBAR_ACTION_EVENT, handleAction);
    };
  }, []);

  return (
    <div className="row-odd-demo">
      {React.createElement(ROW_ODD_TOOLBAR_TAG, { ref: toolbarRef })}
      <RevoGrid
        additionalData={additionalData}
        className={bordered ? 'cell-border' : ''}
        columns={columns}
        hideAttribution
        plugins={plugins}
        source={source}
        theme="material"
      />
    </div>
  );
}

export default RowOdd;
Angular ts
import {
  AfterViewInit,
  CUSTOM_ELEMENTS_SCHEMA,
  Component,
  ElementRef,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { RowOddPlugin } from '@revolist/revogrid-pro';
import {
  createRowOddConfig,
  createRowOddRows,
  initialRowOddState,
  rowOddColumns,
  type RowOddDemoMode,
} from './row-odd-shared';
import {
  defineRowOddToolbarElement,
  type RowOddToolbarActionEvent,
  type RowOddToolbarElement,
  type RowOddToolbarOptionsChangeEvent,
} from './row-odd-toolbar';

defineRowOddToolbarElement();

@Component({
  selector: 'row-odd-grid',
  standalone: true,
  imports: [RevoGrid],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./row-odd-toolbar.scss'],
  template: `
    <div class="row-odd-demo">
      <row-odd-demo-toolbar
        #toolbarRef
        (row-odd-toolbar-action)="handleToolbarAction($event)"
        (row-odd-toolbar-options-change)="setToolbarOptions($event)"
      ></row-odd-demo-toolbar>
      <revo-grid
        [class.cell-border]="bordered"
        [columns]="columns"
        [hideAttribution]="true"
        [plugins]="plugins"
        [rowOdd]="rowOddConfig"
        [source]="source"
        theme="material"
      ></revo-grid>
    </div>
  `,
})
export class RowOddGridComponent implements AfterViewInit {
  @ViewChild('toolbarRef', { read: ElementRef })
  toolbarRef?: ElementRef<RowOddToolbarElement>;

  mode: RowOddDemoMode = initialRowOddState.mode;
  interval = initialRowOddState.interval;
  offset = initialRowOddState.offset;
  bordered = initialRowOddState.bordered;
  priceShift = initialRowOddState.priceShift;
  source = createRowOddRows(this.priceShift);
  columns = rowOddColumns;
  plugins = [RowOddPlugin];
  rowOddConfig = this.createRowOddConfig();

  ngAfterViewInit() {
    this.syncToolbar();
  }

  setToolbarOptions(event: RowOddToolbarOptionsChangeEvent) {
    this.mode = event.detail.mode;
    this.interval = event.detail.interval;
    this.offset = event.detail.offset;
    this.bordered = event.detail.bordered;
    this.rowOddConfig = this.createRowOddConfig();
    this.syncToolbar();
  }

  handleToolbarAction(event: RowOddToolbarActionEvent) {
    if (event.detail === 'update-rows') {
      this.priceShift += 9;
      this.source = createRowOddRows(this.priceShift);
      this.syncToolbar();
    }
  }

  private createRowOddConfig() {
    return createRowOddConfig({
      mode: this.mode,
      interval: this.interval,
      offset: this.offset,
    });
  }

  private syncToolbar() {
    if (this.toolbarRef) {
      this.toolbarRef.nativeElement.state = {
        mode: this.mode,
        interval: this.interval,
        offset: this.offset,
        bordered: this.bordered,
        priceShift: this.priceShift,
      };
    }
  }
}
Shared ts
import type { ColumnRegular } from '@revolist/revogrid';

export type RowOddMode = 'odd' | 'even' | 'custom';

export interface RowOddConfig<T = RowOddDemoRow> {
  enabled?: boolean;
  mode?: RowOddMode;
  interval?: number;
  offset?: number;
  className?: string;
  match?: Partial<T>;
  shouldStripe?: (context: { model: T }) => boolean;
}

export type RowOddDemoMode = RowOddMode | 'priority';

export interface RowOddDemoState {
  mode: RowOddDemoMode;
  interval: number;
  offset: number;
  bordered: boolean;
  priceShift: number;
}

export interface RowOddDemoRow {
  id: number;
  product: string;
  category: string;
  priority: 'high' | 'normal';
  price: number;
  stock: number;
}

const products = [
  ['Alpine Jacket', 'Outerwear'],
  ['Canvas Tote', 'Accessories'],
  ['Trail Shoes', 'Footwear'],
  ['Merino Tee', 'Basics'],
  ['Rain Shell', 'Outerwear'],
  ['Travel Pack', 'Accessories'],
  ['City Sneaker', 'Footwear'],
  ['Thermal Base', 'Basics'],
  ['Field Vest', 'Outerwear'],
  ['Laptop Sleeve', 'Accessories'],
] as const;

export const rowOddColumns: ColumnRegular[] = [
  { name: 'ID', prop: 'id', size: 80 },
  { name: 'Product', prop: 'product', size: 190 },
  { name: 'Category', prop: 'category', size: 150 },
  { name: 'Priority', prop: 'priority', size: 120 },
  { name: 'Price', prop: 'price', size: 110 },
  { name: 'Stock', prop: 'stock', size: 100 },
];

export const initialRowOddState: RowOddDemoState = {
  mode: 'odd',
  interval: 3,
  offset: 1,
  bordered: false,
  priceShift: 0,
};

export function createRowOddRows(priceShift = 0): RowOddDemoRow[] {
  return Array.from({ length: 32 }, (_, index) => {
    const [product, category] = products[index % products.length];
    return {
      id: index + 1,
      product,
      category,
      priority: index % 5 === 0 || index % 7 === 0 ? 'high' : 'normal',
      price: 80 + ((index * 17 + priceShift) % 140),
      stock: 12 + ((index * 11) % 70),
    };
  });
}

export function createRowOddConfig(state: Pick<RowOddDemoState, 'mode' | 'interval' | 'offset'>): RowOddConfig<RowOddDemoRow> {
  if (state.mode === 'priority') {
    return {
      mode: 'custom',
      className: 'row-odd-priority',
      match: { priority: 'high' },
    };
  }

  return {
    mode: state.mode,
    interval: state.mode === 'custom' ? state.interval : undefined,
    offset: state.mode === 'custom' ? state.offset : undefined,
    className: state.mode === 'custom' ? 'row-odd-custom' : undefined,
  };
}
Toolbar ts
import { initialRowOddState, type RowOddDemoMode, type RowOddDemoState } from './row-odd-shared';

export const ROW_ODD_TOOLBAR_TAG = 'row-odd-demo-toolbar';
export const ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT = 'row-odd-toolbar-options-change';
export const ROW_ODD_TOOLBAR_ACTION_EVENT = 'row-odd-toolbar-action';

export type RowOddToolbarAction = 'update-rows';
export type RowOddToolbarOptionsChangeEvent = CustomEvent<RowOddDemoState>;
export type RowOddToolbarActionEvent = CustomEvent<RowOddToolbarAction>;

export function defineRowOddToolbarElement() {
  if (customElements.get(ROW_ODD_TOOLBAR_TAG)) {
    return;
  }
  customElements.define(ROW_ODD_TOOLBAR_TAG, RowOddToolbarElement);
}

export function createRowOddToolbar(state: RowOddDemoState) {
  defineRowOddToolbarElement();
  const toolbar = document.createElement(ROW_ODD_TOOLBAR_TAG) as RowOddToolbarElement;
  toolbar.state = state;
  return toolbar;
}

export class RowOddToolbarElement extends HTMLElement {
  private currentState: RowOddDemoState = { ...initialRowOddState };

  set state(state: RowOddDemoState) {
    this.currentState = { ...state };
    this.render();
  }

  get state() {
    return { ...this.currentState };
  }

  connectedCallback() {
    this.render();
  }

  private emitOptionsChange(patch: Partial<RowOddDemoState>) {
    this.currentState = {
      ...this.currentState,
      ...patch,
    };
    this.dispatchEvent(new CustomEvent<RowOddDemoState>(
      ROW_ODD_TOOLBAR_OPTIONS_CHANGE_EVENT,
      { bubbles: true, detail: this.state },
    ));
    this.render();
  }

  private emitAction(action: RowOddToolbarAction) {
    this.dispatchEvent(new CustomEvent<RowOddToolbarAction>(
      ROW_ODD_TOOLBAR_ACTION_EVENT,
      { bubbles: true, detail: action },
    ));
  }

  private render() {
    if (!this.isConnected) {
      return;
    }

    const { mode, interval, offset, bordered } = this.currentState;
    this.innerHTML = `
      <div class="row-odd-toolbar">
        <select data-mode aria-label="Stripe mode">
          ${this.renderOption('odd', 'odd', mode)}
          ${this.renderOption('even', 'even', mode)}
          ${this.renderOption('custom', 'custom', mode)}
          ${this.renderOption('priority', 'High priority', mode)}
        </select>
        <label>
          <span>Interval</span>
          <input data-interval aria-label="Stripe interval" ${mode !== 'custom' ? 'disabled' : ''} max="8" min="1" type="number" value="${interval}" />
        </label>
        <label>
          <span>Offset</span>
          <input data-offset aria-label="Stripe offset" ${mode !== 'custom' ? 'disabled' : ''} max="7" min="0" type="number" value="${offset}" />
        </label>
        ${this.renderToggle('Borders', 'bordered', bordered)}
        <button class="rv-btn" type="button" data-action="update-rows">Update rows</button>
      </div>
    `;

    this.querySelector<HTMLSelectElement>('[data-mode]')?.addEventListener('change', event => {
      this.emitOptionsChange({ mode: (event.currentTarget as HTMLSelectElement).value as RowOddDemoMode });
    });
    this.querySelector<HTMLInputElement>('[data-interval]')?.addEventListener('input', event => {
      this.emitOptionsChange({ interval: Number((event.currentTarget as HTMLInputElement).value) || 2 });
    });
    this.querySelector<HTMLInputElement>('[data-offset]')?.addEventListener('input', event => {
      this.emitOptionsChange({ offset: Number((event.currentTarget as HTMLInputElement).value) || 0 });
    });
    this.querySelectorAll<HTMLButtonElement>('[data-toggle]').forEach(button => {
      button.addEventListener('click', () => {
        const key = button.dataset.toggle as 'bordered';
        this.emitOptionsChange({ [key]: !this.currentState[key] });
      });
    });
    this.querySelector<HTMLButtonElement>('[data-action="update-rows"]')?.addEventListener('click', () => {
      this.emitAction('update-rows');
    });
  }

  private renderOption(value: RowOddDemoMode, label: string, selected: RowOddDemoMode) {
    return `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`;
  }

  private renderToggle(label: string, key: 'bordered', pressed: boolean) {
    return `
      <button class="rv-btn" type="button" data-toggle="${key}" aria-pressed="${pressed}">
        ${label}
      </button>
    `;
  }
}
Styles scss
.row-odd-demo {
  display: grid;
  gap: 12px;
  min-height: 520px;

  revo-grid {
    min-height: 420px;
    --row-odd-background-color: #eef6ff;
    --row-stripe-hover-background-color: #dcecff;
  }

  revo-grid[theme^='dark'] {
    --row-odd-background-color: #27384f;
    --row-stripe-hover-background-color: #314663;
  }
}

.row-odd-toolbar {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;

  label {
    align-items: center;
    display: inline-flex;
    font-size: 13px;
    gap: 6px;
  }

  input,
  select {
    border: 1px solid #c9d2df;
    border-radius: 6px;
    padding: 6px 8px;
  }

  input[type='number'] {
    width: 72px;
  }
}

.row-odd-priority .rgCell:not([auto-merge='child']),
.rgRow[data-row-stripe-class='row-odd-priority'] .rgCell:not([auto-merge='child']) {
  font-weight: 600;
}
import { RowOddPlugin } from '@revolist/revogrid-pro';
grid.plugins = [RowOddPlugin];
grid.rowOdd = {
mode: 'custom',
interval: 3,
offset: 1,
className: 'priority-band',
};