[GH #1887] How to register my React Component-based editor?

Hello.
I tried to register react-component-based editor.

However, I get the error below:
Uncaught TypeError: Cannot set properties of null (setting ‘TD’)
at prepare (handsontable.full.min.js:933:164321)
at EditorComponent.value (baseEditorComponent.tsx:110:62)
at EditorComponent.prepare (show/?editor_console=:123:7)

Here is my example, slightly modified from the official example.
https://jsfiddle.net/do4ynh72/1/

I can’t use class-based editors because I need to render a react component. (Or is it possible to render react components in class-based editors?)

Hi @idog

Thank you for contacting us. The editor is registered correctly in your example, but if you want to use component-based editor it has to be declared as followed to work correctly:

  <HotColumn width={250}>
    <EditorComponent hot-editor />
  </HotColumn>

Thank you for your quick reply.
So what should I do if I want to apply the editor only to specific cells?

@idog

I checked this deeper, and it looks like registering custom editor alias doesn’t work for component-based editors, so it can’t be defined within columns option. I reported it internally for further investigation.

For now, the workaround would be to use cells function and chose particular rows or range of rows to reset the editor to the default one where you don’t want to use custom editor. Here’s an example: https://jsfiddle.net/handsoncode/9fxkauz6/

I leave a solution for people experiencing the same problem as me. (Caution: This solution is close to a hack and may not work if the handsontable react wrapper version is updated. I tested it on version 14.2.)

Most of the code is taken from handsontable source code.

Using the function below, you can convert react component based editor to class based editor.

Therefore, it is possible to overcome two limitations of react-based editors (editor cannot be applied only to specific cells / multiple types of editors cannot be used).

I heard on handsontable’s official blog that there will be a large-scale react wrapper update soon. (I’m really looking forward to it.) I am very grateful to the handsontable team who continues to develop handsontable.

 import React from 'react';
import { BaseEditorComponent, HotTableClass } from '@handsontable/react';
import Handsontable from 'handsontable/base';
import { createPortal } from 'react-dom';

export const baseEditorGenerator = (CustomEditorClass: typeof BaseEditorComponent, hotRef: React.RefObject<HotTableClass>) => {
    /**
    * Example
    * ```
    * class MyEditor extends BaseEditorComponent { ... } // your editor (extends BaseEditorComponent)
    * const hotTableRef = useRef(null)
    *
    * const [MyEditorClass, MyEditorElement] = useMemo(() => baseEditorGenerator(MyEditor, hotTableRef), [])
    *
    * const cellOptions = useCallback((row, col) => {
    *     return col === 1 ? { editor: MyEditorClass } : {}
    * }, [MyEditorClass])
    *
    * return (
    *     <>
    *         <HotTable ref={hotTableRef} cells={ cellOptions ) } />
    *         <MyEditorElement />
    *     </>
    * )
    * ```
    */

    const instanceRef = {
        current: null
    }

    const WrappedCustomEditorClass = class CustomEditor extends Handsontable.editors.BaseEditor implements Handsontable.editors.BaseEditor {
        editorComponent: React.Component;

        constructor(hotInstance: Handsontable.Core) {
            super(hotInstance);

            (instanceRef.current as any).hotCustomEditorInstance = this;
            this.editorComponent = instanceRef.current as unknown as React.Component;
        }

        focus() {
        }

        getValue() {
        }

        setValue() {
        }

        open() {
        }

        close() {
        }
    } as any;

    // Fill with the rest of the BaseEditor methods
    Object.getOwnPropertyNames(Handsontable.editors.BaseEditor.prototype).forEach(propName => {
        if (propName === 'constructor') {
            return;
        }

        WrappedCustomEditorClass.prototype[propName] = function (...args: any[]) {
            return (instanceRef.current as any)[propName].call(instanceRef.current, ...args);
        }
    });

    const customEditorElement = <CustomEditorClass hot-editor />
    const customEditorClass = customEditorElement.type?.WrappedComponent || customEditorElement.type

    const EditorElement = React.cloneElement(customEditorElement, {
        emitEditorInstance: (editorInstance: any, editorCloumnScope: any) => {
            const editorCache = hotRef.current?.getEditorCache()
            !editorCache?.get(customEditorClass) && editorCache?.set(customEditorClass, new Map())

            editorCache?.get(customEditorClass)?.set(editorCloumnScope ?? 'global', editorInstance)

            instanceRef.current = editorInstance
        },
        editorColumnScope: 'global',
        isEditor: true
    })

    return [WrappedCustomEditorClass, createPortal(EditorElement, document.body)]
}

Hi @idog

Thank you for sharing your solution with the community. We really appreciate it!