Guía para Crear Nodos Personalizados en el Editor
Introducción
Esta guía explica el proceso para crear nodos personalizados en nuestro sistema de editor basado en Tiptap. Usaremos como ejemplo la implementación de diferentes tipos de gráficos (charts).
Estructura Base
Un nodo personalizado requiere tres componentes principales:
- Componente React: Renderiza el contenido del nodo
- Definición del Nodo: Define el comportamiento y estructura del nodo
- Registro en el Editor: Integra el nodo con el sistema
Pasos Detallados
1. Crear el Componente React
- Ubicación:
components/editor/editor-ui/custom-nodes/react-components/ - Objetivo: Renderizar el contenido visual del nodo
- Características:
- Importación dinámica de componentes
- Mapeo de componentes según tipo
- Manejo de errores
- UI consistente usando Card
2. Definir el Nodo
- Ubicación:
components/editor/editor-ui/custom-nodes/ - Objetivo: Definir el comportamiento del nodo
- Configuración:
- Nombre y grupo
- Atributos personalizados
- Parseo y renderizado HTML
- Integración con el componente React
3. Registrar en el Editor
- Ubicación:
components/editor/hooks/useEditorInstance.ts - Objetivo: Añadir el nodo a las extensiones del editor
- Pasos:
- Importar el nodo
- Añadir a la lista de extensiones
- Verificar integración
4. Integrar con el Menú
- Ubicación:
components/editor/editor-ui/extensions/slash-menu/suggestions.ts - Objetivo: Permitir inserción desde el menú
- Implementación:
- Detección de tipo por prefijo
- Creación de nodo con atributos
- Manejo de errores
Ejemplos de Código
1. Componente React
// components/editor/editor-ui/custom-nodes/react-components/CustomComponent.tsx
import type { NodeViewProps } from '@tiptap/react';
import { NodeViewWrapper } from '@tiptap/react';
import dynamic from 'next/dynamic';
import React from 'react';
import { Card } from '@/components/ui/card';
// Importaciones dinámicas
const ComponentA = dynamic(() => import('@/path/to/component-a'));
const ComponentB = dynamic(() => import('@/path/to/component-b'));
const COMPONENT_MAP: Record<string, React.ComponentType> = {
'type-a': ComponentA,
'type-b': ComponentB,
};
const CustomComponent = ({ node }: NodeViewProps) => {
const component = node.attrs.component || '';
const Component = COMPONENT_MAP[component];
if (!Component) return null;
return (
<NodeViewWrapper className="react-component-custom">
<Card className="p-4 border rounded-xl bg-background/50 shadow-sm">
<Component />
</Card>
</NodeViewWrapper>
);
};
export default CustomComponent;
2. Definición del Nodo
// components/editor/editor-ui/custom-nodes/custom-node.ts
import { mergeAttributes, Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import CustomComponent from './react-components/CustomComponent';
export const CustomNode = Node.create({
name: 'customNode',
group: 'block',
atom: true,
draggable: true,
addAttributes() {
return {
component: {
default: '',
},
};
},
parseHTML() {
return [
{
tag: 'div[data-custom]',
getAttrs: (node) => ({
component: (node as HTMLElement).getAttribute('data-component') || '',
}),
},
];
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(HTMLAttributes, {
'data-custom': '',
'data-component': HTMLAttributes.component,
}),
];
},
addNodeView() {
return ReactNodeViewRenderer(CustomComponent);
},
});
3. Registro en useEditorInstance
// components/editor/hooks/useEditorInstance.ts
import { CustomNode } from '@/components/editor/editor-ui/custom-nodes/custom-node';
const extensions = useMemo(
() => [
// ... otras extensiones
CustomNode,
// ... más extensiones
],
[t],
);
4. Integración con el Menú
// components/editor/editor-ui/extensions/slash-menu/suggestions.ts
command: ({ editor, nestedItem }) => {
if (!nestedItem || !('component' in nestedItem) || !nestedItem.component) {
return;
}
const component = nestedItem.component;
// Detectar tipo por prefijo
if (component.startsWith('custom-')) {
if (editor.schema.nodes.customNode) {
editor
.chain()
.focus()
.insertContent({
type: 'customNode',
attrs: {
component,
},
})
.run();
return true;
}
}
};
Buenas Prácticas
-
Organización del Código
- Mantener una estructura de carpetas clara
- Separar componentes por funcionalidad
- Usar nombres descriptivos y consistentes
-
Rendimiento
- Usar importaciones dinámicas para componentes pesados
- Implementar memoización cuando sea necesario
- Evitar re-renders innecesarios
-
Mantenibilidad
- Documentar el código
- Seguir patrones consistentes
- Usar TypeScript para mejor tipado
-
Depuración
- Implementar logs útiles
- Manejar casos de error
- Validar props y atributos
Conclusión
Esta arquitectura permite crear nodos personalizados de manera modular y mantenible, facilitando la extensión del editor según las necesidades del proyecto. El sistema se basa en componentes React, definiciones de nodos Tiptap y una integración limpia con el menú de sugerencias.