PageKit: el creador de sitios GrapesJS autoalojado, con el código fuente incluido. Obtener acceso anticipado

GrapesJS + TypeScript

GrapesJS TypeScript: Guía completa de integración

Aprende a usar GrapesJS con TypeScript: trabaja con las API del editor y los eventos ya tipados, crea componentes y plugins personalizados, conecta el almacenamiento e integra GrapesJS en aplicaciones React, Next.js, Vue o Angular.

Tipos en el paqueteEditor tipado APIsComponentes personalizadosDesarrollo de PluginExportación HTML/CSSReact · Vue · Angular · Next.js
La respuesta corta

¿GrapesJS es compatible con TypeScript?

Sí — y los tipos se envían dentro del paquete.

GrapesJS 0.23.6 publica su propio archivo de declaración en dist/index.d.ts, referenciado desde el campo types del paquete. Instalar grapesjs te da el editor y sus tipos en una sola dependencia, así que importar Editor desde 'grapesjs' se resuelve sin configuración adicional ni segundo paquete.

  • No instales @types/grapesjs. Ese paquete no está publicado en npm en absoluto — la instalación falla con E404 en lugar de darte tipos obsoletos en silencio.
  • No existe GrapesJS v1.x. La versión actual es 0.23.6, con licencia BSD-3-Clause. Los tutoriales que hablan de una línea 1.x describen una versión que no existe.
  • TypeScript 5.0 es el suelo. El archivo de declaración usa un parámetro de type const, por lo que 4.9 y inferiores no pueden analizarlo — y reportar el fallo como "No se puede encontrar el módulo 'grapesjs'", lo que hace que la mayoría de la gente busque un paquete de tipos que no necesita.

Lo que cubren los tipos es la superficie API del editor — la instancia del editor, sus gestores, componentes, bloques, eventos y datos del proyecto. No describen el propio modelo de tu producto, y esta guía trata principalmente de mantener esas dos cosas separadas.

Instálalo y arranca
La ruta

Lo que aprenderás

Doce pasos, en orden. Cada uno enlaza con la sección que lo cubre, para que puedas empezar donde realmente está tu proyecto.

  1. Instala GrapesJS con TypeScriptUn paquete de dependencia, sin tipos, y la diferencia entre una importación en tiempo de ejecución y una importación solo de tipos.
  2. Configurar TypeScriptLas cuatro opciones de compilador que importan para el trabajo de editor — y por qué strictNullChecks es el que realmente hace el trabajo.
  3. Tipar el editorgrapesjs.init devuelve Editor. Mantenerlo como Editor | null es lo que evita el fallo más común.
  4. Trabajo con componentesComponent, ComponentDefinition y la firma addType que la mayoría de los tutoriales hacen mal.
  5. Tipar los bloquesBlockProperties declarado fuera del editor, así que un bloque se comprueba por sí solo.
  6. Manejo de eventoseditor.on deduce su referencia a partir del nombre del evento — y permanece abierto para tus propios eventos.
  7. Plugins de compilaciónPlugin<Options> tipifica ambos parámetros y exportar la interfaz de opciones es lo que lo hace usable.
  8. Crear componentes personalizadosUn Hero compuesto, descrito una vez como interfaz y reutilizado por el editor, API y el renderizador.
  9. Tipar el almacenamiento y los datos de la APIProjectData es el JSON del editor. ProjectRecord es tu fila. No son del mismo tipo.
  10. Usa GrapesJS con ReactEl envoltorio oficial y la versión manual useEffect — con la limpieza que requiere StrictMode.
  11. Usa GrapesJS con Next.jsDónde va el límite del cliente y por qué no se puede crear el editor por encima de él.
  12. Estructura de un editor de producciónUna sola unión entre tu aplicación y el editor, así que ninguna filtra en la otra.
Paso 1

1. Instalar GrapesJS con TypeScript

Hay un solo paquete. Los tipos vienen con él, así que no hay segunda instalación ni entrada de @types en tu devDependencies.

Instalaciónbash
npm install grapesjs

La orden de no correr

@types/grapesjs no está obsoleto, reemplazado ni opcional — está ausente en el registro de npm. Ejecutar esto devuelve un 404, y si lo ves en un tutorial antiguo es una señal fiable de que el resto de ese tutorial también es anterior a los tipos incluidos.

bash
# Don't. This package is not published — npm returns E404.
npm install --save-dev @types/grapesjs

Importaciones en tiempo de ejecución frente a importaciones solo de tipo

grapesjs en sí es un valor: llamas a grapesjs.init(). Editor, Component y Block son tipos: existen solo durante la compilación. Marcarlos con import type lo hace explícito y garantiza que la importación se borre en lugar de incorporarse a tu bundle — que es lo más importante en el código del framework, donde una importación en tiempo de ejecución errante del editor puede arrastrarla a un renderizado de servidor.

ts
// Runtime import: the value you actually call.
import grapesjs from 'grapesjs';

// Type-only import: erased at compile time, ships nothing to the bundle.
import type { Editor, Component, Block } from 'grapesjs';

// Editor styles. Without them the canvas renders unstyled.
import 'grapesjs/dist/css/grapes.min.css';
A continuación: configurar el compilador
Paso 2

2. Configurar TypeScript

No necesitas una configuración especial para GrapesJS. Necesitas cuatro opciones configuradas correctamente — el resto de tu tsconfig puede quedarse con lo que ya use tu proyecto.

tsconfig.jsonjson
{
  "compilerOptions": {
    // GrapesJS's bundled .d.ts uses const type parameters, a TypeScript 5.0
    // feature. On 4.9 and below the file fails to *parse*, and every import
    // from 'grapesjs' reports "Cannot find module".
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",

    // The editor manipulates real DOM nodes: container elements, iframes,
    // drag events. Without the DOM lib none of that type-checks.
    "lib": ["ES2020", "DOM", "DOM.Iterable"],

    // strict is what makes the typings worth having. In particular
    // strictNullChecks is what forces you to handle "editor not created yet",
    // which is the single most common GrapesJS runtime crash.
    "strict": true,

    "skipLibCheck": true,
    "esModuleInterop": true
  }
}
Objetivo y módulo
Cualquier cosa desde ES2020 en adelante. El archivo de declaración usa tipos literales de plantilla y parámetros de tipo const, así que la restricción es la versión TypeScript, no el objetivo de emisión.
lib debe incluir DOM
El editor funciona con elementos reales, iframes y eventos de arrastre. Sin la biblioteca DOM, grapesjs.init({ container: element }) no verifica tipos y el fallo parece un problema de GrapesJS más que de configuración.
estricta — específicamente strictNullChecks
Esta es la opción que se gana su mantenimiento. Te obliga a manejar la ventana donde el editor aún no existe: antes de iniciar, después de destruir y en el primer renderizado de una referencia. Ese hueco es la fuente más común de errores en tiempo de ejecución en integraciones de framework.
moduleResolution
bundler para Vite, Next.js y la mayoría de configuraciones modernas; node16 o nodenext si resuelves con el propio algoritmo de Node. Ambos encuentran el campo de tipos del paquete.
Paso 3

3. Comprensión de los tipos GrapesJS

Estos son los nombres que realmente importarás. Cada uno se comprobó con el archivo de declaración grapesjs 0.23.6 en lugar de copiarlo desde la página de documentación, que describe el API en JavaScript y no siempre usa los mismos nombres.

TipoRepresentaUso común
EditorclaseLa instancia del editorTodo: ciclo de vida, gestores, exportación, eventos
EditorConfiginterfazEl objeto pasó a initConstruir una configuración alejada del sitio de llamada
ComponentclaseUn nodo en el árbol del lienzoLectura y actualización de un elemento seleccionado
ComponentDefinitioninterfazUn componente declarado, no una instanciahijos de un componente; el contenido de un bloque
ComponentPropertiesinterfazCampos modelo de un componenteTipar los valores por defecto que pasas a addType
AddComponentTypeOptionsinterfazEl argumento que realmente adopta addTypeRegistro de un tipo de componente personalizado
BlockclaseUn bloque en el Block ManagerEl valor de retorno de Blocks.add
BlockPropertiesinterfazDeclaración de un bloqueDeclarar bloques en su propio módulo
TraitclaseUn campo en el panel de ajustesTipos de rasgos personalizados y manejadores de rasgos
ProjectDatainterfazEl editor guardó JSONCarga y almacenamiento de almacenamiento; tu columna de base de datos
Plugin<T>interfazUna función de plugin con opciones tipadasTipar un plugin que escribes o que consumes
PluginOptionsalias de tipoLa restricción en las opciones de un pluginAuxiliares y envoltorios genéricos de plugins

Todos estos se pueden importar por nombre: import type { Editor, Component, BlockProperties } desde 'grapesjs'. Solo grapesjs en sí necesita una importación en tiempo de ejecución.

Tres nombres que no puedes importar

Las clases manager se declaran en el archivo pero nunca se exportan, así que importarlas por nombre falla con TS2614 — un error confuso, porque la clase claramente existe cuando la buscas. Indexa en Editor en su lugar: el tipo de retorno del getter es la misma clase y el alias es estable en varias versiones.

BlockManager
Úsalo en su lugarEditor['Blocks']
StorageManager
Úsalo en su lugarEditor['Storage']
ComponentManager
Úsalo en su lugarEditor['Components']
Paso 4

4. Tipar el Editor de GrapesJS

grapesjs.init() devuelve un Editor. Anotar la variable es opcional — la inferencia ya lo hace bien — pero nombrar el tipo es lo que permite pasar el editor a través de los límites del módulo sin ampliarlo a ninguno.

src/editor/createEditor.tsts
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

const editor: Editor = grapesjs.init({
  container: '#gjs',
  height: '100vh',
  storageManager: false,
});

// Both return values are typed, and they are not the same shape:
// getHtml() always returns a string, getCss() can return undefined.
const html: string = editor.getHtml();
const css: string | undefined = editor.getCss();
  • Autocomplete sigue la instancia: editor. Lista a cada gestor, y cada gestor lista sus propios métodos con sus firmas reales.
  • getHtml() devuelve la cadena; getCss() devuelve la cadena | indefinida. La diferencia es real, y el modo estricto te obliga a manejarla.
  • destroy() forma parte del API, no es un extra. La limpieza del framework depende de ello.

Referencias anulables, que es donde está el valor

En un framework el editor no existe durante el primer renderizado, y no debe existir después del desmontaje. Tipar la variable que lo guarda como Editor | null obliga al compilador a preguntar por ambos momentos en cada punto de llamada. Tiparla como Editor y silenciarlo con una aserción traslada esa pregunta a producción.

ts
import type { Editor } from 'grapesjs';

// Not `let editor: Editor` — before init there is no editor, and the type
// should say so. Every call site is then forced to handle the empty case.
let editor: Editor | null = null;

export function exportHtml(): string {
  if (!editor) throw new Error('Editor is not initialised yet');
  return editor.getHtml(); // narrowed to Editor here
}

export function destroy(): void {
  editor?.destroy();
  editor = null;
}

Llegando a los entrenadores

Cada gestor se queda colgado del editor — editor.Blocks, editor.Components, editor.Storage, editor.Commands — y cada getter está tipificado, así que el autocompletado funciona desde la instancia hacia abajo. Lo que no puedes hacer es importar las clases del administrador por nombre; alias a través de Editor en su lugar.

ts
import type { Editor } from 'grapesjs';

// These names are NOT exported from 'grapesjs' — importing them by name is a
// compile error. Index into Editor instead and you get the same classes.
type BlockManager = Editor['Blocks'];
type StorageManager = Editor['Storage'];
type ComponentManager = Editor['Components'];

export function countBlocks(blocks: BlockManager): number {
  return blocks.getAll().length;
}
A continuación: reaccionar a lo que haga el editor
Paso 5

5. Trabajar con eventos GrapesJS en TypeScript

editor.on es genérico sobre el nombre del evento, por lo que la firma de callback se deriva de la cadena que pasas. En la práctica, eso significa que no deberías anotar casi nada: la inferencia te da los tipos de parámetros correctos, y una anotación que no coincide es un error de compilación en lugar de una discrepancia silenciosa.

src/editor/events.tsts
import type { Editor } from 'grapesjs';

export function wireEditorEvents(editor: Editor): void {
  // No annotation needed. `component` is inferred as Component and
  // `options` carries `action`, which tells add from move from clone.
  editor.on('component:add', (component, options) => {
    console.log(component.get('type'), options.action);
  });

  // A different event, a different payload — the callback signature changes
  // with the event name, so a wrong parameter list is a compile error.
  editor.on('component:selected', (component) => {
    console.log(component.getId());
  });

  editor.on('storage:end:store', () => {
    console.log('Project saved');
  });
}

Las familias de eventos que usarás

Eventos Component
component:add, component:remove, component:update, component:selected, component:mount. El primer argumento es el Component; component:add también recibe un objeto options cuya acción distingue un add de un movimiento de un clon.
Ciclo de vida Editor
Carga cuando el editor esté listo, actualiza cualquier cambio en el proyecto, destruye al desmontar. Aquí es donde enganchas tu propio indicador de guardado o la bandera de estado sucio.
Eventos de almacenamiento
storage:start:store, storage:end:store, storage:error. Útiles precisamente porque se ejecutan alrededor de tu propia implementación de almacenamiento, así que una partida defectuosa puede aparecer en el UI en lugar de en la consola.
Eventos Block
block:drag:start, block:drag:stop y el propio Block Manager se suma y quita. Útil para analizar qué bloques realmente buscan la gente.

Donde la inferencia se detiene

El nombre del evento es una unión de cadenas abiertas, deliberadamente: los plugins definen sus propios canales y estos deben seguir compilándose. El coste es que un error tipográfico en un nombre de evento central sigue siendo válido TypeScript — la callback simplemente vuelve a una firma suelta y nunca se activa. Cuando un gestor de eventos misteriosamente no hace nada, comprueba la ortografía antes de comprobar el API.

ts
import type { Editor } from 'grapesjs';

export function wireCustomEvents(editor: Editor): void {
  // Your own events are allowed — the event name is a string union that stays
  // open, so plugins can define their own channels.
  editor.on('my-plugin:published', (...args: unknown[]) => {
    console.log(args);
  });

  // Which is also the trade-off: this typo compiles. The callback simply falls
  // back to (...args: any[]) and never fires.
  // editor.on('component:selcted', (component) => { ... });
}
A continuación: los componentes que conllevan esos eventos
Paso 6

6. Components de GrapesJS con seguridad de tipos

Un tipo de componente registra un nuevo comportamiento en el lienzo: una etiqueta, su traits, qué acepta como hijos, cómo se reconoce cuando se reintegra HTML. Aquí es donde ocurre la mayoría del trabajo de editor personalizado, y donde hay que trazar la frontera entre los tipos de GrapesJS y los tuyos.

La firma que hay que hacer bien

editor.Components.addType (tipo, opciones) toma AddComponentTypeOptions — modelar, ver, isComponent, extender — no un ComponentDefinition. ComponentDefinition describe un nodo declarado dentro de un árbol: los hijos de un componente, o el contenido de un bloque. Los tutoriales que pasan un ComponentDefinition a addType citan una forma antigua, y el error que obtienes no es evidente.

src/editor/components.tsts
import type { Editor, Component } from 'grapesjs';

// YOUR domain model. GrapesJS knows nothing about it, and that is the point:
// this is the shape your API, your database and your React props agree on.
export interface HeroContent {
  headline: string;
  subheadline?: string;
  ctaLabel: string;
  ctaHref: string;
}

const HERO_DEFAULTS: HeroContent = {
  headline: 'Your headline',
  ctaLabel: 'Get started',
  ctaHref: '#',
};

// addType takes AddComponentTypeOptions — model / view / isComponent — not a
// ComponentDefinition. Tutorials that pass a ComponentDefinition here are
// describing an API that no longer exists.
export function registerHero(editor: Editor): void {
  editor.Components.addType('hero', {
    isComponent: (el) => el.dataset?.gjsType === 'hero',
    model: {
      defaults: {
        tagName: 'section',
        droppable: false,
        attributes: { 'data-gjs-type': 'hero' },
        traits: [
          { type: 'text', name: 'headline', label: 'Headline' },
          { type: 'text', name: 'ctaLabel', label: 'Button label' },
          { type: 'text', name: 'ctaHref', label: 'Button link' },
        ],
        ...HERO_DEFAULTS,
      },
    },
  });
}

// The bridge back to your model. component.get() is intentionally loose —
// this function is where that looseness stops and HeroContent begins.
export function readHero(component: Component): HeroContent {
  return {
    headline: component.get('headline') ?? HERO_DEFAULTS.headline,
    subheadline: component.get('subheadline'),
    ctaLabel: component.get('ctaLabel') ?? HERO_DEFAULTS.ctaLabel,
    ctaHref: component.get('ctaHref') ?? HERO_DEFAULTS.ctaHref,
  };
}
  • Los rasgos son el panel de ajustes. Cada entrada nombra un campo de modelo, así que el panel y tu interfaz se mantienen sincronizados.
  • isComponent es cómo se reconoce una página guardada al cargar. Sin él, recargar convierte tu sección personalizada de nuevo en un div normal.
  • droppable y draggable son booleanos o selectores — aquí es donde evitas que la gente deje caer un hero dentro de un botón.

Dos sistemas de tipos, deliberadamente separados

Los tipos GrapesJS describen el editor API. Tus propias interfaces deberían describir el modelo de tu producto. Mezclarlas se siente eficiente durante aproximadamente una semana: luego un campo que necesita tu base de datos no tiene dónde vivir excepto un atributo de componente, y los componentes internos pasan a formar parte de tu esquema. Mantén una función como readHero arriba como único lugar donde se encuentran ambos — todo lo que se encuentra aguas abajo lleva tu interfaz, no un Component.

A continuación: ponlo en la estantería de bloques
Paso 7

7. Tipar los Blocks de GrapesJS

Un bloque es lo que aparece en la estantería izquierda y lo que arrastra un usuario. No es un componente — es una declaración de qué insertar. Mantener ambos rectos merece la pena hacerlo desde el principio, porque sus tipos no son intercambiables y el mensaje de error no lo indica.

src/editor/blocks.tsts
import type { Editor, Block, BlockProperties } from 'grapesjs';

// BlockProperties is exported, so the block can be declared away from the
// editor and checked on its own — label, category, media, content.
const heroBlock: BlockProperties = {
  label: 'Hero',
  category: 'Sections',
  media: '<svg viewBox="0 0 24 24"><rect width="24" height="24" /></svg>',
  // A block's content can be a component definition rather than an HTML
  // string, which is how a block and a custom component type stay in sync.
  content: { type: 'hero' },
};

export function addHeroBlock(editor: Editor): Block {
  // Blocks.add(id, props) returns the created Block.
  return editor.Blocks.add('hero', heroBlock);
}

Qué vale una declaración de bloque

id
Primer argumento sobre Blocks.add, no un campo. Único por editor; volver a añadir el mismo id reemplaza al bloque.
Sello
Lo que el usuario lee en la estantería. El único campo aquí que pertenece a tus archivos de localización.
Categoría
Agrupa la estantería. Una cadena, o un objeto cuando quieres que se colapse por defecto.
Contenido
Lo que se inserta: una cadena HTML o una definición de componente. Prefiero la definición: mantiene el bloque vinculado a un tipo de componente en lugar de a un fragmento de marcado.
Medios
La miniatura, como SVG en línea. Nada te impide usar un <img>, pero el SVG en línea sigue el tema del editor.
Atributos
Aplicado al propio elemento de la estantería — útil para identificadores de prueba y ganchos de analítica, no para el elemento insertado.

Blocks.add devuelve el Block creado, así que puedes capturarlo y ajustar la estantería más adelante — reordenando, ocultando bloques que el plan del usuario no incluye, o cambiando una etiqueta de categoría en tiempo de ejecución.

A continuación: empaquetar todo como un plugin
Paso 8

8. Construye un GrapesJS Plugin con TypeScript

Un plugin GrapesJS es una función que recibe el editor y un objeto de opciones. Ese es todo el contrato — por eso los plugins son la unidad natural para cualquier cosa que quieras reutilizar entre editores, enviar a otro equipo o vender.

Una forma que sobrevive al crecimiento

Un archivo por tipo de registro. La razón no es la orden: blocks.ts exporta objetos BlockProperties que hacen prueba de tipo sin ningún editor en el alcance, por lo que pueden ser probados por unidad y reutilizados sin arrancar ningún editor.

my-grapesjs-plugin/ ├── src/ │ ├── index.ts # the Plugin<Options> function, and only that │ ├── types.ts # the exported Options interface │ ├── blocks.ts # BlockProperties, one per block │ ├── components.ts # editor.Components.addType calls │ └── commands.ts # editor.Commands.add calls ├── tsconfig.json ├── package.json # "types": "dist/index.d.ts" └── README.md
src/index.tsts
import type { Editor, Plugin } from 'grapesjs';

// Export the options type. A consumer cannot configure your plugin safely if
// the shape of `options` lives only inside your implementation.
export interface SectionsPluginOptions {
  category?: string;
  blockPrefix?: string;
}

// Plugin<T> is (editor: Editor, config: T) => PluginResult. Typing the
// function as Plugin<SectionsPluginOptions> checks both parameters for you.
const sectionsPlugin: Plugin<SectionsPluginOptions> = (editor, options) => {
  // Defaults belong here, not in the type — an optional field plus a
  // destructured default is what makes the call site free to omit them.
  const { category = 'Sections', blockPrefix = 'sec' } = options;

  editor.Blocks.add(`${blockPrefix}-hero`, {
    label: 'Hero',
    category,
    content: { type: 'hero' },
  });

  editor.Commands.add(`${blockPrefix}:reset`, {
    run(ed: Editor) {
      ed.setComponents('');
    },
  });
};

export default sectionsPlugin;

Lo que te compra el tipo

Opciones
Exporta la interfaz. Un consumidor que no pueda ver la forma de tus opciones tiene que leer tu código fuente para configurar tu plugin.
Valores predeterminados
Campos opcionales en el tipo, predeterminados desestructurados en el cuerpo. Poner los valores predeterminados en el tipo hace que todos los campos sean necesarios en el sitio de llamada.
El parámetro del editor
Escrito por ti por Plugin<T>. Todo lo que registras dentro — bloques, tipos de componentes, comandos — se comprueba con las firmas reales del manager.
Registro
Blocks, los tipos de componentes, los comandos y los gestores de eventos van todos en la misma función. Un plugin que no registre nada en momento de llamada y espere un evento también está bien.

Registro

Pasar un closure mantiene tus opciones tipadas en el punto de llamada. La alternativa — listar el plugin en plugins y sus ajustes en pluginsOpts — tipa esos ajustes como un registro laxo, así que una clave mal escrita compila y no hace nada.

src/editor/createEditor.tsts
import grapesjs from 'grapesjs';
import sectionsPlugin, { type SectionsPluginOptions } from './my-grapesjs-plugin';

const options: SectionsPluginOptions = { category: 'Marketing' };

grapesjs.init({
  container: '#gjs',
  // Passing a closure keeps the options typed at the call site. The alternative
  // — plugins: [sectionsPlugin] with pluginsOpts — types options as
  // Record<string, any>, so a misspelled key compiles and silently does nothing.
  plugins: [(editor) => sectionsPlugin(editor, options)],
});
A continuación: los componentes que un plugin incluye
Paso 9

9. Construye Components personalizado con TypeScript

Una sección de sistema de diseño no es un solo componente: es un pequeño árbol con una forma para la que tu producto ya tiene nombre. Describir esa forma una vez, como interfaz, es lo que evita que el editor, el API y el renderizador se separen.

Una sección, cuatro tipos

Hero ← one component type, one interface ├── Heading ← extends 'text' ├── Description ← extends 'text' └── Button ← extends 'link', traits: label + href
src/editor/design-system.tsts
import type { Editor, ComponentDefinition } from 'grapesjs';

// The composed shape, described once. Every layer below — the editor default,
// the API payload, the renderer — is checked against this one interface.
export interface HeroContent {
  headline: string;
  description: string;
  ctaLabel: string;
  ctaHref: string;
}

// ComponentDefinition is what goes *inside* a tree: the children of a
// component, or the `content` of a block. It is not what addType takes.
const heroChildren = (content: HeroContent): ComponentDefinition[] => [
  { type: 'text', tagName: 'h1', content: content.headline },
  { type: 'text', tagName: 'p', content: content.description },
  {
    type: 'link',
    content: content.ctaLabel,
    attributes: { href: content.ctaHref },
  },
];

export function registerDesignSystem(
  editor: Editor,
  defaults: HeroContent
): void {
  editor.Components.addType('hero', {
    model: {
      defaults: {
        tagName: 'section',
        droppable: false,
        // Children are declared, not hand-written as an HTML string, so a
        // renamed field is a compile error rather than a silently stale block.
        components: heroChildren(defaults),
        traits: [
          { type: 'text', name: 'headline', label: 'Headline' },
          { type: 'text', name: 'ctaHref', label: 'Button link' },
        ],
      },
    },
  });
}

Qué tipar y qué no

La interfaz de contenido
Tuyo. Titular, descripción, llamada a la acción — los campos que un comercializador rellena y tus API almacenan.
El tipo de componente
GrapesJS. Registrado una vez con addType, declarando la etiqueta, el traits y los hijos.
Rasgos
El puente. Cada rasgo nombra un campo en el modelo, así que renombrar un campo en tu interfaz debería romper la lista de rasgos — y con los valores predeterminados repartidos, lo hace.
Hijos
ComponentDefinition en lugar de una cadena HTML. Una cadena se compila sin importar lo que escribas en ella; se marca una definición.
Atributos
Dónde vive el tipo de datos, que es lo que isComponent coincide al recargar.

La recompensa no es que haya menos errores hoy en día, sino que seis meses después, añadir un campo a HeroContent produce una lista de todos los lugares que deben cambiar, en lugar de buscar en toda la base de código la cadena 'headline'.

A continuación: haz que entre y salga de tu base de datos
Paso 10

10. Tipar el almacenamiento de GrapesJS y los datos de la API

El almacenamiento es donde los dos sistemas de tipos se encuentran de forma más relevante, porque ese es el límite que acaba en tu base de datos. Equivocarse es caro después; hacerlo bien son unas veinte líneas.

Dos formas, no una

ProjectData es el propio JSON del editor. Su estructura interna pertenece a GrapesJS, cambia entre versiones y no es algo que se pueda migrar a mano. Tu ProjectRecord es una fila: un id, un propietario, un nombre, una versión, marcas de tiempo — más ese blob opaco en una columna. Guárdalo, cárgalo y deja su interior intacto.

src/tipos/projects.tsts
import type { ProjectData } from 'grapesjs';

// The editor's own JSON. ProjectData is deliberately open — its internal shape
// is GrapesJS's business and changes between versions, so treat it as opaque:
// store it, load it, never reach into it or migrate it by hand.

// YOUR row. This is the type your API returns and your database stores, and it
// is not a GrapesJS type. Keeping the two apart is what lets you add a column,
// change a version scheme or move providers without touching editor code.
export interface ProjectRecord {
  id: string;
  name: string;
  userId: string;
  version: number;
  updatedAt: string;
  projectData: ProjectData;
}
  1. GrapesJS
  2. Storage API
  3. Application
  4. Database

El editor produce los datos del proyecto. Tu adaptador de almacenamiento es el único código que toca ambos lados.

src/editor/storage.tsts
import grapesjs from 'grapesjs';
import type { Editor, ProjectData } from 'grapesjs';
import type { ProjectRecord } from './types/projects';

export function createEditor(projectId: string): Editor {
  const editor = grapesjs.init({
    container: '#gjs',
    storageManager: {
      // The id of the storage you register below.
      type: 'remote-api',
      autosave: true,
      stepsBeforeSave: 5,
    },
  });

  editor.Storage.add('remote-api', {
    async load(): Promise<ProjectData> {
      const res = await fetch(`/api/projects/${projectId}`);
      // Throwing here is what makes the editor emit storage:error. Returning
      // an empty object instead loses the reader's work without telling them.
      if (!res.ok) throw new Error(`Load failed: ${res.status}`);
      const record = (await res.json()) as ProjectRecord;
      return record.projectData;
    },

    async store(data: ProjectData): Promise<void> {
      const res = await fetch(`/api/projects/${projectId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ projectData: data }),
      });
      if (!res.ok) throw new Error(`Save failed: ${res.status}`);
    },
  });

  editor.on('storage:error', (error) => console.error(error));

  return editor;
}

Lo que tiene que manejar el adaptador

Carga
Devuelve el ProjectData para el proyecto actual. Añadir una respuesta fallida es lo que hace que el editor emita storage:error en lugar de abrir silenciosamente un lienzo vacío.
Tienda
Envía los datos tal cual. No los remodeles al salir: lo que quitas, el editor espera que vuelva a cargarlo.
Guardado automático
autosave con stepsBeforeSave hace cambios en lotes. No cada pulsación de tecla es una petición, y el número es tuyo para afinar.
Versión
Una columna de versión en tu fila, incrementada en el lado del servidor. Versión del registro, nunca el JSON del editor.
Multi-inquilina
El id del proyecto es una variable capturada por el closure del adaptador, y la propiedad se comprueba en el servidor. El editor no tiene ningún concepto de usuario, y no debería adquirirlo.
A continuación: poner el editor dentro de un framework
Paso 11

11. GrapesJS con React y TypeScript

Existe un envoltorio oficial de React — @grapesjs/react, licenciado por MIT, actualmente 2.0.0 — y envía su propio archivo de declaración. No renderiza los componentes de React dentro del lienzo; monta el editor y te entrega la instancia.

app/editor/PageEditor.tsxtsx
'use client';

import { useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor, ProjectData } from 'grapesjs';
import GjsEditor from '@grapesjs/react';
import 'grapesjs/dist/css/grapes.min.css';

interface PageEditorProps {
  projectId: string;
  onSave: (projectId: string, data: ProjectData) => void;
}

export default function PageEditor({ projectId, onSave }: PageEditorProps) {
  const editorRef = useRef<Editor | null>(null);

  return (
    <GjsEditor
      // Required. The wrapper does not import grapesjs itself — you pass the
      // module (or a CDN URL), which is what lets you control the version.
      grapesjs={grapesjs}
      options={{ height: '100vh', storageManager: false }}
      onEditor={(editor) => {
        editorRef.current = editor;
      }}
      // projectData is typed as ProjectData, so it lines up with the record
      // type your save endpoint expects.
      onUpdate={(projectData) => onSave(projectId, projectData)}
    />
  );
}

El prop grapesjs es necesario. El envoltorio deliberadamente no importa el editor en sí, así que la versión de tu paquete sigue siendo la de tu package.json — y así puedes apuntarla a una compilación CDN en su lugar.

O sin envoltorio

El envoltorio es una comodidad, no un requisito. Un useEffect con referencia hace el mismo trabajo en unas quince líneas, y merece la pena entenderlo incluso si usas el envoltorio, porque hace explícitas las dos reglas: proteger el ref y destruir al limpiar.

app/editor/PageEditor.tsxtsx
'use client';

import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

export default function PageEditor() {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const editorRef = useRef<Editor | null>(null);

  useEffect(() => {
    // strictNullChecks forces this guard, and it is not ceremony: the ref is
    // null on the first render, before React has attached the div.
    if (!containerRef.current) return;

    const editor = grapesjs.init({
      container: containerRef.current,
      height: '100vh',
      storageManager: false,
    });
    editorRef.current = editor;

    // Without destroy(), React 18's development StrictMode double-mount leaves
    // two editors bound to one container.
    return () => {
      editor.destroy();
      editorRef.current = null;
    };
  }, []);

  return <div ref={containerRef} />;
}
  • La referencia del contenedor es nula en el primer renderizado. strictNullChecks te hace decir qué pasa después.
  • Devuelven una limpieza que llame a destroy(). El desarrollo StrictMode de React 18 se monta dos veces, y sin él tienes dos editores en una misma div.
  • El rango de pares del envoltorio es React ^18.0.0 || ^19.0.0. En React 17 vas en el camino manual.
Para una guía completa de integración de React, véase GrapesJS + React. A continuación: lo mismo con un límite de servidor
Paso 12

12. GrapesJS con Next.js y TypeScript

La cuestión de Next.js es un límite. grapesjs.init necesita un elemento DOM real, más documento y ventana; un Server Component no tiene ninguno de ellos. Así que el editor va en un Client Component, y todo lo que esté encima puede quedarse en el servidor.

Hacia dónde va la línea

'use client' en la parte superior del componente que crea el editor, y en ningún sitio más alto. La página superior sigue siendo Server Component: espera parámetros, revisa la sesión, carga el proyecto y pasa props simples. Esa división merece ser protegida, porque mover 'use client' un archivo hacia arriba convierte silenciosamente la carga de tus datos en código cliente.

app/editor/[id]/editor-client.tsxtsx
// app/editor/[id]/editor-client.tsx
'use client';

import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

export default function EditorClient({ projectId }: { projectId: string }) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const editorRef = useRef<Editor | null>(null);

  // grapesjs.init needs a real element, document and window. Calling it in a
  // module body — or in a Server Component — runs it during the server render,
  // where none of those exist. useEffect only runs in the browser, which is
  // the whole requirement.
  useEffect(() => {
    if (!containerRef.current) return;
    const editor = grapesjs.init({
      container: containerRef.current,
      height: '100vh',
    });
    editorRef.current = editor;
    return () => {
      editor.destroy();
      editorRef.current = null;
    };
  }, [projectId]);

  return <div ref={containerRef} />;
}
app/editor/[id]/page.tsxtsx
// app/editor/[id]/page.tsx  — a Server Component, no 'use client'
import EditorClient from './editor-client';

interface PageProps {
  params: Promise<{ id: string }>;
}

export default async function EditorPage({ params }: PageProps) {
  const { id } = await params;

  // Auth, data loading and permissions stay on the server, fully typed.
  // Only the editor itself crosses into the client.
  return <EditorClient projectId={id} />;
}
useEffect, no el cuerpo del módulo
Importar grapesjs al servidor es inofensivo: lo que falla es llamar a init(). useEffect solo se ejecuta en el navegador, que es todo el requisito.
La importación dinámica es opcional
next/dynamic con ssr: false es una decisión de tamaño de paquete, no de corrección, y no está disponible dentro de un Server Component. Busca si el editor es una pequeña parte de una página grande; sáltalo en una ruta que sea solo el editor.
CSS
Importa grapesjs/dist/css/grapes.min.css desde el componente cliente. Sin él, el lienzo se renderiza sin estilo y parece roto en lugar de sin estilo.
Limpieza
Igual que React: destroy() en el retorno del efecto. Las transiciones de ruta desmontan el componente, y un editor que sobrevive a su contenedor filtra a sus oyentes.
Para ver la versión completa de Next.js, consulta la guía de creación de páginas de Next.js. Siguiente: Vue y Angular
Paso 13

13. GrapesJS con Vue y Angular

Ambos funcionan, y ambos siguen la misma forma que la versión manual React: una referencia de plantilla, inicialización después de que el elemento existe, destruir al desmontar. Las mecánicas específicas del framework tienen sus propias guías en lugar de una versión condensada aquí.

En ambos casos, los tipos son los mismos que esta página ha estado usando: Editor, Component, ProjectData. El framework cambia dónde se llama init(), no lo que devuelve.

Paso 14

14. Arquitectura TypeScript para un GrapesJS Editor de producción

Todo lo anterior cabe en un solo archivo. Deja de encajar alrededor del tercer componente personalizado, y lo que decide si el siguiente año es agradable es donde pones la unión entre tu aplicación y el editor.

Una estructura que se mantiene

src/ ├── editor/ # everything that touches the Editor instance │ ├── createEditor.ts # grapesjs.init, one place, returns Editor │ ├── plugins.ts # Plugin<T> registrations │ ├── components.ts # Components.addType calls │ ├── blocks.ts # BlockProperties definitions │ ├── commands.ts # Commands.add calls │ └── storage.ts # Storage.add, ProjectData in and out │ ├── types/ │ ├── editor.ts # aliases over GrapesJS types you use a lot │ ├── components.ts # HeroContent and friends — YOUR model │ ├── projects.ts # ProjectRecord — your database row │ └── api.ts # request/response shapes │ └── app/ # imports from types/, never from editor/ internals

Una veta declarada

El código de la aplicación no debería importarse directamente desde 'grapesjs'. Dale un único módulo que reexporte los pocos tipos de editor que tu producto conoce legítimamente, asigne alias a los gestores que no se exportan y declare la interfaz estrecha de la que depende realmente tu UI. Luego un botón de barra de herramientas toma esa interfaz, no un Editor completo — y no puede acceder al interior del editor ni siquiera por accidente.

src/tipos/editor.tsts
// src/types/editor.ts — the single seam between your app and the editor.
import type { Editor, ProjectData } from 'grapesjs';

// Re-export what your application is allowed to know about.
export type { Editor, ProjectData };

// Manager classes are not exported by name; alias them here once so no other
// file has to remember that.
export type BlockManager = Editor['Blocks'];
export type StorageManager = Editor['Storage'];

// The surface your UI actually depends on. Application code takes this, not a
// full Editor, so a toolbar button cannot quietly reach into editor internals.
export interface EditorFacade {
  getHtml(): string;
  getCss(): string;
  save(): Promise<void>;
  destroy(): void;
}
Capas

Las capas, y hacia dónde apuntan las dependencias

Tu aplicación arriba, tus tipos en el centro, el editor debajo. Las dependencias apuntan hacia abajo y nunca se respaldan.

  1. Aplicación

    Tuyo. No sabe nada de GrapesJS.

    • Enrutamiento
    • Autenticación
    • Estado de la aplicación
    • Tu UI
  2. Tipos

    La costura. El único lugar donde ambos mundos tienen nombre.

    • Modelo de dominio
    • Registros del proyecto
    • Contratos API
  3. Capa Editor

    Tuyo. El único código que importa desde el editor.

    • createEditor
    • Plugins
    • Tipos Component
    • Adaptador de almacenamiento
  4. GrapesJS

    El editor. Escrito por el paquete.

    • Lienzo
    • Entrenadores
    • Eventos
¿Incrustar esto en el producto de otra persona?
El panorama completo

15. Arquitectura TypeScript para una página SaaS Builder

Un producto de creación de páginas no es mayormente un creador de páginas. GrapesJS cubre una caja en esta cadena; el resto es una aplicación que ibas a escribir de todas formas, y escribir las uniones entre ellas es hacia lo que esta guía ha estado construyendo.

  1. SaaS application
  2. Authentication
  3. GrapesJS editor
  4. Typed components
  5. Typed plugins
  6. Storage API
  7. Database
  8. Publishing

GrapesJS se encarga de la superficie de edición. La autenticación, almacenamiento, arrendamiento, facturación y publicación son tuyos.

El panorama completo

¿Quién posee qué

GrapesJS se encarga de la superficie de edición. La autenticación, almacenamiento, arrendamiento, facturación y publicación son tuyos.

Your product

Tú provees

Todo sobre lo que el editor no tiene opinión.

  • Cuentas, sesiones y permisos
  • Límites de arrendamiento y por plano
  • La base de datos y sus migraciones
  • Publicación, dominios y alojamiento
  • Facturación y uso
GrapesJS

GrapesJS proporciona

Todo lo que hay dentro del lienzo, tipado por el paquete.

  • El lienzo y el arrastrar y soltar
  • Blocks, estilos, capas, traits, assets
  • Salida HTML y CSS
  • Proyecto JSON a través del Storage Manager
  • Tipos, plugins y comandos Component

El editor es un componente de tu producto, no un sustituto de él.

Paso 16

16. Errores comunes en GrapesJS TypeScript

Once fallos que aparecen repetidamente, la mayoría específicos de este emparejamiento más que de TypeScript en general. Cada uno es un síntoma con el que puedes comparar y el cambio que lo soluciona.

Instalación de @types/grapesjs

Síntoma

npm install fails con E404, o un tutorial te dice que lo añadas y asumes que tu lista está rota.

Solución

Elimínala. Los tipos están dentro del propio grapesjs, referenciados por el campo de tipos del paquete. No existe un paquete de tipos separado y no ha habido uno para la línea actual.

Tipar el editor como any

Síntoma

Let Editor: Any, normalmente se añadió para silenciar un error durante la configuración y nunca se eliminó.

Solución

Editor desde 'grapesjs'. Cualquiera en la raíz se propaga a cada gestor, cada carga útil de eventos y cada llamada de exportación — mantienes el coste de compilación de TypeScript y pierdes todo el beneficio.

Dejar las opciones del plugin sin tipar

Síntoma

Un plugin acepta opts: any o Record<string, unknown>; los consumidores adivinan que los nombres de campos y los errores tipográficos no sirven de nada.

Solución

Declara y exporta una interfaz de opciones, luego escribe la función como Plugin<YourOptions>. Ambos parámetros se comprueban, incluso en el sitio de la llamada.

Confundiendo Block y Component

Síntoma

Pasar un Block donde se espera un Component, o intentar estilizar un bloque y descubrir que no tiene estilos.

Solución

Un bloque es una entrada de estantería que describe qué insertar. Un componente es un nodo en el lienzo. Blocks.add toma BlockProperties; Components.addType toma AddComponentTypeOptions.

Pasar ComponentDefinition a addType

Síntoma

Un tipo de componente se registra pero se comporta como un div simple — sin traits, sin restricciones.

Solución

addType toma AddComponentTypeOptions: model, view, isComponent, extend. ComponentDefinition describe un nodo dentro de un árbol — los hijos de un componente, o el contenido de un bloque.

Suponiendo que cada evento tenga la misma carga útil

Síntoma

Un manejador escrito para component:add se reutiliza para component:remove y el segundo argumento no está definido.

Solución

Las cargas útiles varían según el evento y los tipos ya lo indican. Deja que la inferencia te dé los parámetros en lugar de anotarlos desde otro manejador.

Acoplamiento de modelos de bases de datos con componentes internos del editor

Síntoma

Tu ProjectRecord tiene columnas que reflejan campos dentro del JSON del editor, y una actualización de GrapesJS significa una migración.

Solución

Trata ProjectData como opaco. Una columna lo guarda; todo lo que consultes — propietario, nombre, versión, marcas de tiempo — está al lado, no dentro.

Ignorar referencias de editor anulables

Síntoma

"No se pueden leer propiedades de null" en una navegación rápida, una recarga en caliente o en el primer renderizado de una ruta.

Solución

Editor | null, y maneja el nulo. Las afirmaciones no nulas en una referencia trasladan el problema de tu terminal a tu rastreador de errores.

Tutoriales de pre-tipos siguientes

Síntoma

Importaciones nombradas que no se resuelven, métodos gestores que no existen, una versión del editor que nunca se publicó.

Solución

Compara el API con el archivo de declaración de tu node_modules en lugar de con una entrada de blog. No existe GrapesJS v1.x; la versión actual es 0.23.6.

Mezcla de versiones entre paquetes

Síntoma

El Editor del envoltorio y tu Editor son idénticos pero estructuralmente incompatibles, y el error menciona dos caminos.

Solución

Un grapesjs en el árbol. Consulta con npm ls grapesjs — una copia anidada bajo un plugin es la causa habitual.

Interfaces personalizadas demasiado amplias

Síntoma

Una interfaz que reafirma la mitad del editor API para que puedas pasarlo, desviándose de los tipos reales en cada lanzamiento.

Solución

Alias lo que existe — Editor['Blocks'] — y declara solo la fachada estrecha que tu propio UI necesita. Volver a poner el API del editor en tus propios tipos es un mantenimiento al que no tienes que suscribirte.

Ninguno de estos es un problema de TypeScript. Son lugares donde el modelo del editor y el modelo de tu producto se confunden entre sí, y el sistema de tipos es solo lo que hace que la confusión sea visible al principio.

Paso 17

17. Solución de problemas de errores GrapesJS TypeScript

Cuatro formas de error cubren casi todo. En cada caso, la jugada útil es mirar el archivo de declaración en node_modules en lugar de buscar alguno — la respuesta está ahí, y cualquier solo pospone la pregunta.

No se pueden encontrar el módulo 'grapesjs' ni sus correspondientes declaraciones de tipo

Lo que ves

TS2307 en la línea de importación, aunque el paquete está claramente instalado y el editor funciona bien en tiempo de ejecución.

Comprueba

Primero tu versión de TypeScript. Por debajo de la 5.0 el archivo de declaración no puede analizarse y el fallo se reporta como un módulo faltante — un mensaje realmente engañoso. Entonces moduleResolution: debe ser bundler, node16 o nodenext, no clásico. Añadir @types/grapesjs no ayudará; ese paquete no existe.

El módulo 'grapesjs' no tiene el miembro 'X' exportado

Lo que ves

TS2614 en un nombre que puedes ver en el archivo de declaración, normalmente StorageManager o BlockManager.

Comprueba

Las clases gestoras se declaran pero no se exportan. Utiliza el alias de acceso indexado — Editor['Storage'], Editor['Blocks'], Editor['Components'] — que se resuelve en la misma clase. Si el nombre es otro, grep node_modules/grapesjs/dist/index.d.ts: si no está, pertenece a una versión anterior.

El argumento de tipo ... no es asignable a un parámetro

Lo que ves

Un gestor de eventos, una llamada addType o una declaración de bloque que coincida exactamente con un tutorial y aún así no se compila.

Comprueba

La firma, en el archivo de declaración. Los eventos llevan diferentes cargas útiles por nombre; addType toma AddComponentTypeOptions en lugar de ComponentDefinition. Pasa el cursor por el método en tu editor — la firma real está justo ahí, y normalmente tiene una forma diferente a la del artículo del que copiaste.

Dos tipos Editor incompatibles

Lo que ves

Una compilación React o Next.js donde el Editor del envoltorio y el tuyo se niegan a unificarse, y el mensaje nombra dos rutas node_modules.

Comprueba

Instalaciones duplicadas. npm ls grapesjs mostrará la segunda copia, normalmente incorporada por un plugin con un rango de pares estrecho. Deduplica o alinea las versiones; el propio rango de pares del envoltorio es ^0.22.5.

Para errores específicos de tipo de framework, las guías React y Next.js son más profundas que esta página.

Paso 18

18. Compatibilidad GrapesJS + TypeScript

Versiones exactas, comprobadas contra el registro y contra el archivo de declaración instalado. El piso TypeScript en particular se medía compilando con cada versión, no inferiendo a partir de un registro de cambios.

PaqueteVerificadoQué significa
grapesjs0.23.6Envía sus propios tipos en dist/index.d.ts. No existe ningún paquete de tipos separados.
typescript>= 5.0El suelo. 4.9 y inferiores no pueden analizar el archivo de declaración ni reportarlo como módulo perdido.
typescript7.0.2La versión actual y la versión con la que se compilaron las muestras de esta guía. Todo desde la versión 5.0 hasta funciona.
@grapesjs/react2.0.0El envoltorio oficial React, licenciado por MIT, con sus propios tipos incluidos.
react^18.0.0 || ^19.0.0El rango de pares React del envoltorio. En React 17, inicializa manualmente el editor con useEffect.
grapesjs (peer)^0.22.5El rango de pares grapesjs del envoltorio — lo suficientemente amplio como para que el núcleo actual lo satisfaga.
node>=20.9.0GrapesJS es una biblioteca de navegador y declara que no hay campo de motores. El nivel que realmente alcanzas viene de tu framework; Next.js 16.3.4 requiere esto.

Verificado 2026-09-03 frente a registry.npmjs.org y node_modules/grapesjs/dist/index.d.ts.. Cada ejemplo de código en esta página fue compilado con estas versiones bajo un modo estricto antes de su publicación.

Aquí no hay un "funciona con todas las versiones del TypeScript", porque no es cierto: la 5.0 es un suelo duro y el modo de fallo que hay debajo es lo suficientemente confuso como para que valga la pena decirlo exactamente.

Paso 19

19. Extiende GrapesJS con plugins compatibles con TypeScript

Una vez que entiendes el núcleo de API, los plugins pueden ofrecer funcionalidad adicional sin que tengas que construir todas las funciones desde cero. Estos son listados actuales en GJS.Market, agrupados según la parte de la superficie tipada que cada uno toca.

Una cosa que esta página no te dirá: ninguno de estos listados anuncia declaraciones agrupadas de TypeScript, así que trata el soporte de tipos como no verificado y comprueba el propio README del plugin. Lo que sí es cierto es que el editor que recibe un plugin está escrito por el paquete base — así que tu código de integración alrededor de cualquier plugin está comprobado, incluso cuando el plugin en sí es JavaScript puro.

Paso 20

20. ¿Construir tu propio Plugin o usar uno existente?

La frase no trata sobre la dificultad. Se trata de si el comportamiento es específico de tu producto — porque eso es lo que decide quién debe mantenerlo dentro de dos años.

RequisitoHazlo tú mismoUsa un plugin
Comportamiento específico de tu negocioSí — nadie más lo construirá
Funcionalidad común del editorSí — ya resuelto
Control total del códigoSí, para plugins de código abierto
Esfuerzo para la primera versión funcionalMás altoPunto de partida inferior
Quién lo mantieneTu equipoAutor de Plugin, además de tu integración
Hasta dónde puedes cambiarloHasta donde quierasDepende del plugin

En la práctica, la mayoría de los editores son ambas cosas: un puñado de plugins para las piezas que necesita todo editor, y tus propios tipos de componentes tipados para las partes que hacen que el producto sea tuyo.

Sigue

Sigue aprendiendo GrapesJS

Hacia dónde ir después, dependiendo de si aún estás aprendiendo el editor, conectándolo a un framework o construyendo un producto alrededor de él.

Desarrollo personalizado

¿Construir un GrapesJS Editor de producción?

Si la sección de arquitectura es donde realmente está tu proyecto, el trabajo restante suele ser integración en lugar de características del editor. Eso es lo que hacemos.

  • Plugins TypeScript
  • Tipos de componentes personalizados
  • Integración de React
  • Integración de Next.js
  • Almacenamiento e integración con API
  • Editores SaaS
  • Editores de marca blanca
  • Editor personalizado UI
  • Migraciones desde versiones anteriores
  • Revisión de la arquitectura de producción
Habla con un experto en GrapesJS
Preguntas

Preguntas frecuentes

¿GrapesJS es compatible con TypeScript?

Sí. GrapesJS 0.23.6 publica un archivo de declaración con el paquete y lo referencia desde su propio campo types, por lo que importar types funciona tan pronto como se instala el paquete. Los types cubren la instancia del editor, sus gestores, componentes, bloques, traits, eventos y datos del proyecto.

¿GrapesJS incluye definiciones de TypeScript?

Sí — en dist/index.d.ts dentro del paquete grapesjs. Puedes leerlo directamente en node_modules, que es la forma más fiable de comprobar cualquier pregunta sobre API en esta página con la versión que realmente tienes instalada.

¿Necesito @types/grapesjs?

No, y no puedes instalarlo: el paquete no está publicado en npm y la instalación falla con un 404. Si un tutorial te dice que lo añadas, ese tutorial es anterior a los tipos incluidos y sus otros consejos probablemente también estén desactualizados.

¿Cómo instalo GrapesJS con TypeScript?

npm install grapesjs. Esa es toda la instalación — una dependencia, tipos incluidos. Luego importa grapesjs desde 'grapesjs' para el valor de ejecución y import type { Editor } desde 'grapesjs' para los tipos.

¿Cómo tipo el editor de GrapesJS?

grapesjs.init() devuelve un Editor, así que la inferencia ya lo hace bien. Donde importa es mantener la instancia: escríbela como Editor | null en un campo de referencia o clase, porque el editor genuinamente no existe antes del montaje ni después de destruir, y strictNullChecks hace que cada sitio de llamadas lo gestione.

¿Cómo tipo los componentes de GrapesJS?

Component es el nodo canvas. ComponentDefinition describe un nodo declarado dentro de un árbol: los hijos de un componente, o el contenido de un bloque. Registrar un nuevo tipo utiliza editor.Components.addType(tipo, opciones), que toma AddComponentTypeOptions: model, view, isComponent y extend.

¿Cómo tipo los bloques de GrapesJS?

BlockProperties se exporta, por lo que un bloque puede declararse en su propio módulo y comprobarse sin un editor en el alcance. editor.Blocks.add(id, props) toma ese objeto y devuelve el Block creado.

¿Cómo manejo los eventos GrapesJS con TypeScript?

editor.on es genérico sobre el nombre del evento y deriva la firma de callback de él, por lo que rara vez deberías anotar los parámetros. Ten en cuenta que el nombre del evento es una unión abierta de cadenas, por lo que los plugins pueden definir sus propios eventos — lo que significa que un error tipográfico en el nombre de un evento central sigue compilando y simplemente nunca se ejecuta.

¿Cómo puedo crear un plugin TypeScript GrapesJS?

Un plugin es una función que toma el editor y un objeto de opciones. Exporta una interfaz de opciones y escribe la función como Plugin<YourOptions> — ambos parámetros se comprueban y los consumidores pueden ver cómo configurarlo sin leer tu fuente.

¿Puedo crear componentes GrapesJS personalizados con TypeScript?

Sí, y es donde los tipos más recompensan. Describe la forma de contenido como tu propia interfaz, registra el tipo de componente con addType y deja que los campos de nombre traits estén en esa interfaz — así renombrar superficies de campo como un error de compilación en lugar de como una sección en blanco en producción.

¿Puedo usar GrapesJS con React y TypeScript?

Sí. @grapesjs/react 2.0.0 es el wrapper oficial, licenciado por MIT, con sus propios tipos incluidos; requiere que pases grapesjs como prop. Su rango de peer React es ^18.0.0 || ^19.0.0. En React 17, o si prefieres sin wrapper, un useEffect con una ref y una limpieza destroy() hace el mismo trabajo.

¿Puedo usar GrapesJS con Next.js y TypeScript?

Sí. Pon 'use client' en el componente que crea el editor y en ningún sitio superior, y llama a grapesjs.init dentro de useEffect — necesita un elemento real, documento y ventana, ninguno de los cuales existe durante un renderizado en servidor. La página superior puede seguir siendo Server Component y seguir cargando datos en el servidor.

¿Puedo usar GrapesJS con Vue y TypeScript?

Sí: una referencia de plantilla, onMounted para inicializar, onBeforeUnmount para destruir. Mantén el Editor fuera de ref() o reactivo() — envolverlo convierte a Vue en un objeto que gestiona sus propios internos. No hay un envoltorio oficial de Vue.

¿Puedo usar GrapesJS con Angular y TypeScript?

Sí: @ViewChild para el contenedor, ngAfterViewInit para inicializar, ngOnDestroy para desmontar y runOutsideAngular para que el bucle de eventos del editor no controle la detección de cambios. No hay un envoltorio oficial de Angular; los paquetes en npm son de terceros.

¿Puedo conectar GrapesJS a un backend TypeScript?

Sí, a través del Storage Manager, registrando un almacenamiento con funciones de carga y almacenamiento que llaman a tu API. Mantén los dos tipos separados: ProjectData es el JSON del editor y debe almacenarse de forma opaca, mientras que tu propio tipo de registro contiene el id, propietario, nombre, versión y marcas de tiempo en las que realmente consultas.

¿Dónde puedo encontrar plugins GrapesJS compatibles con TypeScript?

El catálogo GJS.Market lista los plugins 100+ GrapesJS. Ten en cuenta que los listados individuales actualmente no anuncian declaraciones de tipo agrupadas, así que revisa el README de cada plugin — pero el objeto editor que recibe un plugin está escrito por el paquete base de todas formas, por lo que tu propio código de integración alrededor sigue comprobado.
Empieza a construir

Construye tu GrapesJS Editor con TypeScript

Empieza con el GrapesJS API tipado, crea tus propios componentes y plugins, conecta la infraestructura de tu aplicación y amplía el editor cuando tu producto necesite más funcionalidad.

Aprende

Empieza el tutorial

Instala el paquete, tipa el editor y consigue en unos minutos una configuración tipada que funciona.

Empieza el tutorial
Ampliación

Explorar plugins

Adaptadores de almacenamiento, tipos de componentes y herramientas de desarrollo ya construidos para GrapesJS.

Explorar plugins
Construcción

Obtén desarrollo personalizado

Plugins de tip, integración de frameworks y arquitectura de producción, construidos contigo.

Obtén desarrollo personalizado

TypeScript no hace que GrapesJS sea más seguro. Hace que un editor GrapesJS personalizado sea mantenido una vez que supera un archivo.