JavaScript Data GridFeedback Cell Type - Step-by-Step Guide (React)

Overview

This guide shows how to create a feedback editor cell using emoji buttons (๐Ÿ‘, ๐Ÿ‘Ž, ๐Ÿคท) with React's EditorComponent. The example uses a feature-roadmap table with a Feedback column; the editor supports quick selection, keyboard navigation (Arrow keys, Tab), and per-column configuration via external CSS.

Difficulty: Beginner Time: ~15 minutes Libraries: None

What You'll Build

A cell that:

  • Displays emoji feedback buttons (๐Ÿ‘, ๐Ÿ‘Ž, ๐Ÿคท) when editing
  • Shows the selected emoji when viewing
  • Supports keyboard navigation (Arrow Left/Right and Tab to cycle options)
  • Provides click-to-select and closes the editor on choice
  • Uses React's EditorComponent with render prop and external CSS (e.g. feedback-editor class)
  • Reads per-column options from cellProperties.config in onPrepare

Complete Example

Prerequisites

npm install @handsontable/react-wrapper

What you need:

  • React 16.8+ (hooks support)
  • @handsontable/react-wrapper package
  • Basic React knowledge (hooks, JSX)

Step 1: Import Dependencies

import { useState, useEffect, useCallback, ComponentProps } from 'react';
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';

registerAllModules();

What we're importing:

  • EditorComponent - React component for creating custom editors
  • HotTable and HotColumn - React wrapper components
  • React hooks for state management
  • Handsontable styles

Step 2: Create the Editor Component

Create a React component that uses EditorComponent with the render prop pattern.

type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;

const FeedbackEditor = () => {
  const [config, setConfig] = useState<string[]>(['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']);

  return (
    <EditorComponent<string>>
      {({ value, setValue, finishEditing }) => (
        <div className="editor">
          {config.map((item) => (
            <button
              key={item}
              className={`button ${value === item ? 'active' : ''}`}
              onClick={() => {
                setValue(item);
                finishEditing();
              }}
            >
              {item}
            </button>
          ))}
        </div>
      )}
    </EditorComponent>
  );
};

What's happening:

  1. EditorComponent wraps your editor UI
  2. The children prop is a function that receives editor state
  3. value - Current editor value
  4. setValue - Function to update the value
  5. finishEditing - Function to save and close the editor
  6. Render buttons for each option in the config
  7. Highlight the active button based on current value

Key concepts:

  • Render prop pattern: EditorComponent uses a function as children
  • State management: value and setValue are provided by EditorComponent
  • React components: Use standard React patterns (JSX, className, onClick)

Step 3: Add Styling

Style the editor container and buttons using CSS or inline styles.

const FeedbackEditor = () => {
  const [config, setConfig] = useState<string[]>(['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']);

  return (
    <EditorComponent<string>>
      {({ value, setValue, finishEditing }) => (
        <>
          <style>{`
            .editor {
              box-sizing: border-box;
              display: flex;
              gap: 3px;
              padding: 3px;
              background: rgb(238, 238, 238);
              border: 1px solid rgb(204, 204, 204);
              border-radius: 4px;
              height: 100%;
              width: 100%;
            }
            .button.active {
              background: #007bff;
              color: white;
            }
            .button:hover {
              background: #f0f0f0;
            }
            .button {
              background: #fff;
              color: black;
              border: none;
              padding: 0;
              margin: 0;
              height: 100%;
              width: 100%;
              font-size: 16px;
              font-weight: bold;
              text-align: center;
              cursor: pointer;
            }
          `}</style>
          <div className="editor">
            {config.map((item, _index, _array) => (
              <button
                key={item}
                className={`button ${value === item ? 'active' : ''}`}
                onClick={() => {
                  setValue(item);
                  finishEditing();
                }}
                style={{
                  width: `${100 / _array.length}%`
                }}
              >
                {item}
              </button>
            ))}
          </div>
        </>
      )}
    </EditorComponent>
  );
};

What's happening:

  • Container uses flexbox for horizontal button layout
  • Buttons dynamically size based on config length
  • Active button has blue background
  • Hover effects for better UX

Key styling:

  • display: flex - Horizontal button layout
  • gap: 3px - Space between buttons
  • width: ${100 / _array.length}% - Dynamic button width
  • .active class - Highlights selected button

Step 4: Read Config from Cell Properties

Use onPrepare to read per-column configuration.

const FeedbackEditor = () => {
  const [config, setConfig] = useState<string[]>(['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']);

  const onPrepare: EditorComponentProps['onPrepare'] = (
    _row,
    _column,
    _prop,
    _TD,
    _originalValue,
    cellProperties
  ) => {
    // Read config from column definition
    if (cellProperties.config) {
      setConfig(cellProperties.config as string[]);
    }
  };

  return (
    <EditorComponent<string> onPrepare={onPrepare}>
      {({ value, setValue, finishEditing }) => (
        // ... editor UI
      )}
    </EditorComponent>
  );
};

What's happening:

  • onPrepare is called before the editor opens
  • cellProperties contains column-specific configuration
  • Read config from cellProperties.config
  • Update state to reflect column-specific options

Why this matters:

  • Different columns can have different options
  • One editor component, multiple configurations
  • Dynamic options based on column settings

Step 5: Add Keyboard Shortcuts

Add keyboard navigation using the shortcuts prop.

const FeedbackEditor = () => {
  const [config, setConfig] = useState<string[]>(['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']);
  const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);

  const getNextValue = useCallback((value: string) => {
    const index = config.indexOf(value);
    return index === config.length - 1 ? config[0] : config[index + 1];
  }, [config]);

  const getPrevValue = useCallback((value: string) => {
    const index = config.indexOf(value);
    return index === 0 ? config[config.length - 1] : config[index - 1];
  }, [config]);

  useEffect(() => {
    setShortcuts([
      {
        keys: [['ArrowRight'], ['Tab']],
        callback: ({ value, setValue }, _event) => {
          setValue(getNextValue(value));
          return false; // Prevent default Tab behavior
        }
      },
      {
        keys: [['ArrowLeft']],
        callback: ({ value, setValue }, _event) => {
          setValue(getPrevValue(value));
        }
      }
    ]);
  }, [config, getNextValue, getPrevValue]);

  return (
    <EditorComponent<string> shortcuts={shortcuts}>
      {({ value, setValue, finishEditing }) => (
        // ... editor UI
      )}
    </EditorComponent>
  );
};

What's happening:

  • ArrowRight/Tab: Move to next option (wraps to first if at end)
  • ArrowLeft: Move to previous option (wraps to last if at start)
  • callback receives { value, setValue, finishEditing } as first parameter
  • Return false to prevent default behavior (e.g., Tab moving to next cell)

Keyboard navigation benefits:

  • Fast selection without mouse
  • Accessible for keyboard-only users
  • Intuitive left/right navigation
  • Tab cycles through options instead of moving cells

Step 6: Complete Editor Component

Put it all together:

type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;

const FeedbackEditor = () => {
  const [config, setConfig] = useState<string[]>(['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']);
  const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);

  const onPrepare: EditorComponentProps['onPrepare'] = (
    _row,
    _column,
    _prop,
    _TD,
    _originalValue,
    cellProperties
  ) => {
    if (cellProperties.config) {
      setConfig(cellProperties.config as string[]);
    }
  };

  const getNextValue = useCallback((value: string) => {
    const index = config.indexOf(value);
    return index === config.length - 1 ? config[0] : config[index + 1];
  }, [config]);

  const getPrevValue = useCallback((value: string) => {
    const index = config.indexOf(value);
    return index === 0 ? config[config.length - 1] : config[index - 1];
  }, [config]);

  useEffect(() => {
    setShortcuts([
      {
        keys: [['ArrowRight'], ['Tab']],
        callback: ({ value, setValue }, _event) => {
          setValue(getNextValue(value));
          return false;
        }
      },
      {
        keys: [['ArrowLeft']],
        callback: ({ value, setValue }, _event) => {
          setValue(getPrevValue(value));
        }
      }
    ]);
  }, [config, getNextValue, getPrevValue]);

  return (
    <EditorComponent<string> onPrepare={onPrepare} shortcuts={shortcuts}>
      {({ value, setValue, finishEditing }) => (
        <>
          <style>{`
            .editor {
              box-sizing: border-box;
              display: flex;
              gap: 3px;
              padding: 3px;
              background: rgb(238, 238, 238);
              border: 1px solid rgb(204, 204, 204);
              border-radius: 4px;
              height: 100%;
              width: 100%;
            }
            .button.active:hover,
            .button.active {
              background: #007bff;
              color: white;
            }
            .button:hover {
              background: #f0f0f0;
            }
            .button {
              background: #fff;
              color: black;
              border: none;
              padding: 0;
              margin: 0;
              height: 100%;
              width: 100%;
              font-size: 16px;
              font-weight: bold;
              text-align: center;
              cursor: pointer;
            }
          `}</style>
          <div className="editor">
            {config.map((item, _index, _array) => (
              <button
                key={item}
                className={`button ${value === item ? 'active' : ''}`}
                onClick={() => {
                  setValue(item);
                  finishEditing();
                }}
                style={{
                  width: `${100 / _array.length}%`
                }}
              >
                {item}
              </button>
            ))}
          </div>
        </>
      )}
    </EditorComponent>
  );
};

What's happening:

  • State management: config and shortcuts managed with React hooks
  • onPrepare: Reads column-specific config
  • shortcuts: Keyboard navigation handlers
  • Render prop: Renders buttons based on config
  • Styling: CSS-in-JS for editor appearance

Step 7: Use in Handsontable

Use the editor component in your HotTable:

const ExampleComponent = () => {
  return (
    <HotTable
      autoRowSize={true}
      rowHeaders={true}
      autoWrapRow={true}
      licenseKey="non-commercial-and-evaluation"
      height="auto"
      data={data}
      colHeaders={true}
    >
      <HotColumn
        width={250}
        editor={FeedbackEditor}
        config={['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']}
        data="feedback"
        title="Feedback"
      />
      <HotColumn
        width={250}
        editor={FeedbackEditor}
        config={['1', '2', '3', '4', '5']}
        data="stars"
        title="Rating (1-5)"
      />
    </HotTable>
  );
};

What's happening:

  • editor={FeedbackEditor} - Assigns the editor component to the column
  • config={['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']} - Column-specific options
  • Same editor component, different configurations per column

Key features:

  • Reusable editor component
  • Per-column configuration
  • Type-safe with TypeScript

How It Works - Complete Flow

  1. Initial Render: Cell displays the emoji value (๐Ÿ‘, ๐Ÿ‘Ž, or ๐Ÿคท)
  2. User Double-Clicks or Enter: Editor opens, onPrepare reads column config
  3. Editor Opens: EditorComponent positions container over cell
  4. Button Display: All options visible, current value highlighted
  5. User Interaction:
    • Click a button โ†’ setValue(item) and finishEditing() called
    • Press ArrowLeft/Right โ†’ Shortcut callback updates value
    • Press Tab โ†’ Cycles through options (prevents default cell navigation)
  6. Visual Feedback: Selected button highlighted in blue
  7. User Confirms: Press Enter, click button, or click away
  8. Save: Value saved to cell
  9. Editor Closes: Cell shows selected emoji

Enhancements

1. Custom Renderer with Styling

Add a custom renderer to style the emoji display:

import { rendererFactory } from 'handsontable/renderers';

const cellDefinition = {
  renderer: rendererFactory(({ td, value }) => {
    td.innerHTML = `
      <div style="text-align: center; font-size: 1.5em; padding: 4px;">
        ${value || '๐Ÿคท'}
      </div>
    `;
  })
};

// Use in HotColumn
<HotColumn
  editor={FeedbackEditor}
  renderer={cellDefinition.renderer}
  config={['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท']}
  data="feedback"
/>

What's happening:

  • Center-aligns the emoji
  • Increases font size for better visibility
  • Adds padding for spacing

2. More Feedback Options

Add more emoji options:

<HotColumn
  editor={FeedbackEditor}
  config={['๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿคท', 'โค๏ธ', '๐Ÿ”ฅ', 'โญ']}
  data="feedback"
/>

The editor automatically adjusts button widths based on config length.

3. Custom Button Styling

Enhanced button appearance with CSS:

<style>{`
  .button {
    padding: 8px;
    border: 2px solid #ddd;
    background: white;
    color: #333;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1.2em;
    transition: all 0.2s;
  }
  .button.active {
    border-color: #007bff;
    background: #007bff;
    color: white;
  }
  .button:hover {
    transform: scale(1.05);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  }
`}</style>

4. Dynamic Config from Cell Properties

The onPrepare hook already handles this! Just pass different configs:

<HotColumn
  editor={FeedbackEditor}
  config={['๐Ÿ‘', '๐Ÿ‘Ž', 'โค๏ธ', '๐Ÿ”ฅ']}
  data="feedback"
/>

5. Tooltip on Hover

Add tooltips to buttons:

{config.map((item) => {
  const tooltips: Record<string, string> = {
    '๐Ÿ‘': 'Positive feedback',
    '๐Ÿ‘Ž': 'Negative feedback',
    '๐Ÿคท': 'Neutral feedback'
  };

  return (
    <button
      key={item}
      className={`button ${value === item ? 'active' : ''}`}
      onClick={() => {
        setValue(item);
        finishEditing();
      }}
      title={tooltips[item] || ''}
    >
      {item}
    </button>
  );
})}

6. Text Labels Instead of Emojis

Use text buttons for clarity:

<HotColumn
  editor={FeedbackEditor}
  config={['Positive', 'Negative', 'Neutral']}
  data="feedback"
/>

The editor works with any string values, not just emojis.

7. Using External CSS File

Move styles to a separate CSS file:

/* feedback-editor.css */
.editor {
  box-sizing: border-box;
  display: flex;
  gap: 3px;
  padding: 3px;
  background: rgb(238, 238, 238);
  border: 1px solid rgb(204, 204, 204);
  border-radius: 4px;
  height: 100%;
  width: 100%;
}

.button.active {
  background: #007bff;
  color: white;
}

.button:hover {
  background: #f0f0f0;
}

.button {
  background: #fff;
  color: black;
  border: none;
  padding: 0;
  margin: 0;
  height: 100%;
  width: 100%;
  font-size: 16px;
  font-weight: bold;
  text-align: center;
  cursor: pointer;
}
import './feedback-editor.css';

const FeedbackEditor = () => {
  // ... component code without <style> tag
};

Accessibility

React buttons are inherently accessible, but you can enhance them:

{config.map((item, index) => (
  <button
    key={item}
    className={`button ${value === item ? 'active' : ''}`}
    onClick={() => {
      setValue(item);
      finishEditing();
    }}
    aria-label={`${item} feedback option`}
    aria-pressed={value === item}
    tabIndex={value === item ? 0 : -1}
  >
    {item}
  </button>
))}

Keyboard navigation:

  • Tab: Navigate to editor (focuses active button)
  • Arrow Left/Right: Cycle through options (via shortcuts)
  • Enter: Select current option and finish editing
  • Escape: Cancel editing
  • Click: Direct selection

ARIA attributes:

  • aria-label: Describes each button
  • aria-pressed: Indicates selected state
  • tabIndex: Controls keyboard focus order

Performance Considerations

Why This Is Fast

  1. React Virtual DOM: Efficient updates only when value changes
  2. No External Libraries: Zero overhead beyond React
  3. Efficient Re-renders: Only re-renders when config or value changes
  4. Native Events: Browser-optimized click handlers

React Hooks Optimization

The useCallback and useEffect hooks ensure shortcuts are only recreated when config changes:

const getNextValue = useCallback((value: string) => {
  const index = config.indexOf(value);
  return index === config.length - 1 ? config[0] : config[index + 1];
}, [config]); // Only recreate if config changes

useEffect(() => {
  setShortcuts([...]);
}, [config, getNextValue, getPrevValue]); // Only update when dependencies change

TypeScript Support

EditorComponent is fully typed. You can specify the value type:

<EditorComponent<string>>
  {({ value, setValue, finishEditing }) => {
    // TypeScript knows value is string | undefined
    // TypeScript knows setValue accepts string
    return (
      // ... editor UI
    );
  }}
</EditorComponent>

For number-based feedback:

<EditorComponent<number>>
  {({ value, setValue, finishEditing }) => {
    // TypeScript knows value is number | undefined
    return (
      // ... editor UI
    );
  }}
</EditorComponent>

Best Practices

  1. Use onPrepare for per-cell configuration - Access cellProperties to read custom options
  2. Handle keyboard events properly - Use shortcuts for navigation
  3. Call finishEditing() appropriately - When user confirms changes (Enter, blur, button click)
  4. Keep render prop function simple - Extract complex logic into separate components or hooks
  5. Use useCallback for helper functions - Prevents unnecessary re-renders
  6. Update shortcuts in useEffect - Ensures shortcuts match current config

Congratulations! You've created a simple feedback editor with emoji buttons using React's EditorComponent, perfect for quick feedback selection in your data grid!