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
PageKit: el creador de sitios GrapesJS autoalojado, con el código fuente incluido. Obtener acceso anticipado
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.
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.
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 arrancaDoce pasos, en orden. Cada uno enlaza con la sección que lo cubre, para que puedas empezar donde realmente está tu proyecto.
Hay un solo paquete. Los tipos vienen con él, así que no hay segunda instalación ni entrada de @types en tu devDependencies.
npm install grapesjs@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.
# Don't. This package is not published — npm returns E404.
npm install --save-dev @types/grapesjsgrapesjs 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.
// 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';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.
{
"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
}
}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.
| Tipo | Representa | Uso común |
|---|---|---|
Editorclase | La instancia del editor | Todo: ciclo de vida, gestores, exportación, eventos |
EditorConfiginterfaz | El objeto pasó a init | Construir una configuración alejada del sitio de llamada |
Componentclase | Un nodo en el árbol del lienzo | Lectura y actualización de un elemento seleccionado |
ComponentDefinitioninterfaz | Un componente declarado, no una instancia | hijos de un componente; el contenido de un bloque |
ComponentPropertiesinterfaz | Campos modelo de un componente | Tipar los valores por defecto que pasas a addType |
AddComponentTypeOptionsinterfaz | El argumento que realmente adopta addType | Registro de un tipo de componente personalizado |
Blockclase | Un bloque en el Block Manager | El valor de retorno de Blocks.add |
BlockPropertiesinterfaz | Declaración de un bloque | Declarar bloques en su propio módulo |
Traitclase | Un campo en el panel de ajustes | Tipos de rasgos personalizados y manejadores de rasgos |
ProjectDatainterfaz | El editor guardó JSON | Carga y almacenamiento de almacenamiento; tu columna de base de datos |
Plugin<T>interfaz | Una función de plugin con opciones tipadas | Tipar un plugin que escribes o que consumes |
PluginOptionsalias de tipo | La restricción en las opciones de un plugin | Auxiliares 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.
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.
BlockManagerEditor['Blocks']StorageManagerEditor['Storage']ComponentManagerEditor['Components']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.
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();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.
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;
}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.
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;
}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.
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');
});
}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.
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) => { ... });
}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.
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.
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 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.
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.
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);
}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 pluginUn 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.
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.mdimport 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;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.
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)],
});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.
Hero ← one component type, one interface
├── Heading ← extends 'text'
├── Description ← extends 'text'
└── Button ← extends 'link', traits: label + hrefimport 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' },
],
},
},
});
}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 datosEl 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.
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.
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;
}El editor produce los datos del proyecto. Tu adaptador de almacenamiento es el único código que toca ambos lados.
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;
}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.
'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.
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.
'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 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.
'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.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.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} />;
}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í.
Una referencia de plantilla más onMounted / onBeforeUnmount, con el editor fuera del estado reactivo — envolver un Editor en ref() convierte al proxy Vue en un objeto que gestiona sus propios internos. No existe un envoltorio oficial de Vue.
Guía GrapesJS + Vue@ViewChild para el contenedor, ngAfterViewInit para inicializar, ngOnDestroy para desmontar — y runOutsideAngular para que el propio bucle de eventos del editor no controle la detección de cambios. No existe un envoltorio oficial de Angular.
Guía GrapesJS + AngularEn 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.
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.
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/ internalsEl 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/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;
}Tu aplicación arriba, tus tipos en el centro, el editor debajo. Las dependencias apuntan hacia abajo y nunca se respaldan.
Tuyo. No sabe nada de GrapesJS.
La costura. El único lugar donde ambos mundos tienen nombre.
Tuyo. El único código que importa desde el editor.
El editor. Escrito por el paquete.
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.
GrapesJS se encarga de la superficie de edición. La autenticación, almacenamiento, arrendamiento, facturación y publicación son tuyos.
GrapesJS se encarga de la superficie de edición. La autenticación, almacenamiento, arrendamiento, facturación y publicación son tuyos.
Todo sobre lo que el editor no tiene opinión.
Todo lo que hay dentro del lienzo, tipado por el paquete.
El editor es un componente de tu producto, no un sustituto de él.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| Paquete | Verificado | Qué significa |
|---|---|---|
grapesjs | 0.23.6 | Envía sus propios tipos en dist/index.d.ts. No existe ningún paquete de tipos separados. |
typescript | >= 5.0 | El suelo. 4.9 y inferiores no pueden analizar el archivo de declaración ni reportarlo como módulo perdido. |
typescript | 7.0.2 | La 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/react | 2.0.0 | El envoltorio oficial React, licenciado por MIT, con sus propios tipos incluidos. |
react | ^18.0.0 || ^19.0.0 | El rango de pares React del envoltorio. En React 17, inicializa manualmente el editor con useEffect. |
grapesjs (peer) | ^0.22.5 | El rango de pares grapesjs del envoltorio — lo suficientemente amplio como para que el núcleo actual lo satisfaga. |
node | >=20.9.0 | GrapesJS 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.
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.
Dónde ProjectData se encuentra con tu base de datos. La capa sobre la que trata la sección de almacenamiento de esta guía.
Ver categoríaUn adaptador de almacenamiento para un backend CMS sin interfaz — el mismo contrato de carga/almacenamiento que implementarías manualmente.
Persistencia local del navegador. Útil como capa de borrador delante de un adaptador remoto.
Firestore como el almacén de proyectos, con el documento sustituyendo a tu ProjectRecord.
Almacenamiento respaldado por Firebase para proyectos, cableado a través del Storage Manager.
Un tipo de componente con pestañas, con su propio traits — un ejemplo práctico de addType que puedes leer.
Un componente icono con un rasgo selector, que muestra cómo un rasgo impulsa un campo de componente.
Instancias reutilizables enlazadas, así que una edición se propaga entre páginas.
Un componente de efecto de máquina de escribir que envolve Typed.js — una biblioteca de terceros expuesta como tipo de componente.
Edición de código, gestión de scripts y gestión de proyectos dentro del editor.
Ver categoríaEdita el HTML y CSS de un componente en su sitio, que es la forma más rápida de ver qué producen realmente tus tipos de componentes.
Adjunta y edita scripts de componentes desde dentro del editor.
Un conjunto de utilidades de editor dirigidas a personas que construyen sobre GrapesJS en lugar de a usuarios finales.
Gestión de varios proyectos en el editor, adyacente a la capa de almacenamiento superior.
Editor basado en React UI alrededor del lienzo de GrapesJS.
Una integración React funcionada puedes leer junto a la sección React de esta guía.
Un componente hero para stacks basados en React — la sección que esta guía construye a mano.
Un preset dirigido a desarrolladores React, agrupando bloques y componentes.
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.
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.
| Requisito | Hazlo tú mismo | Usa un plugin |
|---|---|---|
| Comportamiento específico de tu negocio | Sí — nadie más lo construirá | — |
| Funcionalidad común del editor | — | Sí — ya resuelto |
| Control total del código | Sí | Sí, para plugins de código abierto |
| Esfuerzo para la primera versión funcional | Más alto | Punto de partida inferior |
| Quién lo mantiene | Tu equipo | Autor de Plugin, además de tu integración |
| Hasta dónde puedes cambiarlo | Hasta donde quieras | Depende 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.
Los tipos en esta página son los mismos en todas partes. Lo que cambia es dónde llamas a init() y cómo limpias.
No hay ningún marco, ni un marco que esta lista no nombre. Todo lo anterior se aplica directamente.
Abrir guíaEl envoltorio oficial, referencias, limpieza y StrictMode.
Abrir guíaLímite del cliente, carga de datos del servidor, App Router.
Abrir guíaReferencias de plantilla, onMounted y mantener el editor fuera del estado reactivo.
Abrir guíaViewChild, ganchos del ciclo de vida y detección de cambios.
Abrir guíaEmpieza con el tutorial de GrapesJS y luego vuelve a por los tipos.
Abrir guíaHacia 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.
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.
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.
Instala el paquete, tipa el editor y consigue en unos minutos una configuración tipada que funciona.
Empieza el tutorialAdaptadores de almacenamiento, tipos de componentes y herramientas de desarrollo ya construidos para GrapesJS.
Explorar pluginsPlugins de tip, integración de frameworks y arquitectura de producción, construidos contigo.
Obtén desarrollo personalizadoTypeScript no hace que GrapesJS sea más seguro. Hace que un editor GrapesJS personalizado sea mantenido una vez que supera un archivo.