PageKit — the self-hosted GrapesJS site builder, sold as source. Get early access

GrapesJS Angular integration

GrapesJS Angular: Build a Visual Editor with Angular

Integrate the open-source GrapesJS visual editor into Angular applications to build drag-and-drop page builders, CMS editors, landing page builders, email editors, and white-label visual editing experiences.

Open sourceSelf-hostedHTML & CSS outputExtensibleAngular compatible

26k+

GitHub stars

1.4M+

npm downloads / month

100+

plugins on GJS.Market

$0

licence fee

Live editor

Try GrapesJS in an Angular application

Open in a new tab

The stock GrapesJS build — the editor you get from `grapesjs.init()` with the standard preset, before any plugin is added. Drag a block from the right panel, select it, and edit its typography and spacing in the style manager.

grapesjs.com/demo.htmlFree

Loads a third-party page from grapesjs.com. Nothing is requested until you click.

These are live GrapesJS builds, not Angular demos — and that is the point. GrapesJS renders into a container element it owns completely, so the canvas, blocks, style manager, responsive previews, asset picker and undo history you drive below behave identically once an Angular component hosts that container.

  1. Angular application
  2. GrapesJS editor
  3. HTML + CSS + project data
  4. Your backend / CMS / SaaS

Angular powers your application. GrapesJS powers the visual editing experience. What comes out is HTML, CSS and a project JSON document that your own backend stores.

Direct answer

Can you use GrapesJS with Angular?

Yes

GrapesJS is framework-agnostic and can be integrated into Angular applications. You initialise the editor through Angular lifecycle hooks, keep the editor instance under your control, connect GrapesJS events to Angular application logic, and persist project data through your own backend.

  • GrapesJS is a plain JavaScript library with no framework binding — it needs a DOM element and nothing else.
  • Initialise it in ngAfterViewInit, once the container element actually exists in the DOM.
  • Destroy it in ngOnDestroy so listeners, DOM nodes and the undo stack are released with the component.
  • Keep the editor instance in a service if more than one component needs to talk to it.
  • Editor events are ordinary emitter events — bridge them into signals, observables or NgZone as your app requires.
  • Storage and assets are yours: GrapesJS calls your endpoints, it does not ship a backend.

There is no official Angular wrapper. The GrapesJS project maintains a React wrapper and no Angular equivalent, so an Angular integration means writing a component — roughly the thirty lines in the example below — or evaluating a third-party package on its own merits.

Jump to the working example
The division of labour

What is GrapesJS for Angular?

GrapesJS is the visual editing engine. Angular is the application framework. Almost every integration problem people hit comes from expecting one of them to do the other's job, so it is worth being explicit about the line between them.

Angular

Angular owns the application

Everything around the canvas: who the user is, what they are allowed to do, and where the result goes.

  • Routing
  • Authentication
  • Application UI
  • Users
  • Permissions
  • Billing
  • API layer
  • Application state
GrapesJS

GrapesJS owns the editing surface

Everything inside the canvas: what can be dragged, selected, styled and exported.

  • Canvas
  • Components
  • Blocks
  • Style manager
  • Asset manager
  • Commands
  • Storage manager
  • Device manager

Angular is your application. GrapesJS is your visual editing engine.

Setup

Install GrapesJS in Angular

One dependency and one stylesheet. GrapesJS ships its own CSS, and without it the editor mounts but renders as unstyled markup — which is the single most common reason a first integration looks broken.

1 — Add the dependency

npm install grapesjs

2 — Register the stylesheet

angular.jsonjson
// angular.json → projects.<app>.architect.build.options
{
  "styles": [
    "src/styles.css",
    "node_modules/grapesjs/dist/css/grapes.min.css"
  ]
}

Registering the stylesheet in angular.json applies it globally. If the editor lives on one lazy-loaded route and you would rather not pay for the CSS everywhere, import it inside the editor component's own stylesheet instead.

Why ngAfterViewInit?

GrapesJS needs a real element to attach to, and it measures that element as it initialises. In ngOnInit the template has not been rendered yet, so a @ViewChild reference is still undefined and the call fails outright. ngAfterViewInit runs after Angular has created the component's view, which is the first moment the container exists. The same reasoning explains the height rule in the stylesheet below: an element with no resolved height produces an editor that initialises without error and displays nothing.

Before you write the component

The container is not yours

GrapesJS replaces the contents of the element you hand it. Do not render Angular template content inside that element and do not bind to anything under it.

Give it a height

The host element needs a resolved height before initialisation. A zero-height container mounts an editor you cannot see.

One instance per component

Keep a single reference to the editor and destroy it in ngOnDestroy. Two live instances on one route will fight over keyboard shortcuts.

Types are included

The grapesjs package ships its own TypeScript definitions, so Editor, Component and ProjectData are importable without a separate @types package.

Minimal working example

A complete Angular editor component

Three files, nothing generated, nothing hidden. Copy them into an Angular application and you have a working visual editor on a route. Everything later on this page — storage, assets, custom components, plugins — is an addition to this component, not a rewrite of it.

editor.component.tsts
// editor.component.ts
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  ViewChild,
} from '@angular/core';
import grapesjs, { type Editor } from 'grapesjs';

@Component({
  selector: 'app-editor',
  templateUrl: './editor.component.html',
  styleUrl: './editor.component.css',
})
export class EditorComponent implements AfterViewInit, OnDestroy {
  // Resolved by the time ngAfterViewInit runs — that is the whole reason
  // initialisation lives there and not in ngOnInit.
  @ViewChild('editorHost') private host!: ElementRef<HTMLDivElement>;

  private editor?: Editor;

  ngAfterViewInit(): void {
    this.editor = grapesjs.init({
      container: this.host.nativeElement,
      height: '100%',
      width: 'auto',
      fromElement: false,
      // No persistence yet. Wire your own API before shipping — see Storage.
      storageManager: false,
      blockManager: {
        blocks: [
          {
            id: 'section',
            label: 'Section',
            category: 'Layout',
            content: '<section class="section"><h2>Headline</h2></section>',
          },
          {
            id: 'text',
            label: 'Text',
            category: 'Basic',
            content: { type: 'text', content: 'Edit me' },
          },
          { id: 'image', label: 'Image', category: 'Basic', content: { type: 'image' } },
        ],
      },
    });
  }

  ngOnDestroy(): void {
    // GrapesJS owns DOM nodes, document listeners and an undo stack. Angular
    // removes the host element; only destroy() releases the rest.
    this.editor?.destroy();
    this.editor = undefined;
  }
}
editor.component.htmlhtml
<!-- editor.component.html -->
<!-- GrapesJS takes this element over completely. Do not render Angular
     content inside it — the editor owns everything below #editorHost. -->
<div class="editor-shell">
  <div #editorHost class="editor-host"></div>
</div>
editor.component.csscss
/* editor.component.css */
.editor-shell {
  display: block;
  block-size: 100dvh;
}

/* GrapesJS measures its container at init. A host with no resolved height
   produces an editor that mounts successfully and renders nothing. */
.editor-host {
  block-size: 100%;
}

That is the whole integration. The parts worth re-reading are the two lifecycle hooks: initialisation in ngAfterViewInit because the container has to exist first, and destroy() in ngOnDestroy because Angular removing the host element does not release what GrapesJS attached to the document.

Load the editor on its own route

The editor is the heaviest thing in most applications that contain one. Putting it behind a lazily-loaded route keeps it out of the initial bundle for every user who never opens it.

app.routes.tsts
// app.routes.ts — the editor is the heaviest route in the app. Load it last.
import type { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', loadComponent: () => import('./home/home.component').then((m) => m.HomeComponent) },
  {
    path: 'pages/:id/edit',
    loadComponent: () => import('./editor/editor.component').then((m) => m.EditorComponent),
  },
];
Lifecycle

Integrating GrapesJS with the Angular lifecycle

The editor's lifetime has to be bound to the component's. Four pieces do that work, and each of them fails in a recognisable way when it is missing.

@ViewChild

Gets a reference to the container element. Query it by template reference variable, not by CSS selector, so a refactor of the markup cannot silently break the lookup.

ngAfterViewInit

The first hook that runs after the component's view exists. Initialise here. Initialising earlier gives you an undefined element reference.

The editor instance

Hold it in a private field, or in a service if a toolbar component elsewhere needs to trigger commands. Everything you do afterwards goes through this one object.

ngOnDestroy

Call editor.destroy(). It removes the editor's DOM, detaches its document-level listeners and discards the undo stack. Skipping it is a leak that survives navigation.

Do not initialise GrapesJS before the editor container exists.

This is the error that accounts for most "GrapesJS does not work in Angular" reports. In ngOnInit, and in any constructor, the view has not been created — the container reference is undefined and initialisation throws. If the container is inside an @if block or a deferred view, ngAfterViewInit is not enough either: wait until that branch has actually rendered before you initialise.

Change detection

GrapesJS and Angular change detection

GrapesJS attaches a large number of listeners to its canvas and fires them continuously while a component is being dragged or resized. In a zone-based Angular application every one of those listeners schedules a change-detection pass, even though nothing in your component tree has changed.

editor.component.ts (excerpt)ts
// editor.component.ts (excerpt)
import { Component, NgZone, inject, signal } from '@angular/core';

export class EditorComponent {
  private readonly zone = inject(NgZone);

  readonly selectedTag = signal('');
  readonly savedAt = signal<Date | null>(null);

  private mount(host: HTMLElement): void {
    // The canvas fires pointer events continuously while a block is being
    // dragged. Creating the editor outside the zone keeps those from
    // scheduling a change-detection pass on every frame.
    this.zone.runOutsideAngular(() => {
      const editor = grapesjs.init({ container: host, height: '100%' });

      // Re-enter only where editor state has to become Angular state.
      editor.on('component:selected', (component) => {
        this.zone.run(() => this.selectedTag.set(component.get('tagName') ?? ''));
      });

      editor.on('storage:end:store', () => {
        this.zone.run(() => this.savedAt.set(new Date()));
      });

      this.editor = editor;
    });
  }
}

// Zoneless applications (provideZonelessChangeDetection) have no zone to
// leave. Set signals from the same editor events and drop NgZone entirely.
  • Create the editor inside NgZone.runOutsideAngular() so canvas activity does not drive change detection.
  • Re-enter with NgZone.run() only in the handlers where editor state has to become Angular state — a selection, a save timestamp, a validation error.
  • If nothing in your UI reflects editor state, you never need to re-enter at all.
  • In a zoneless application (provideZonelessChangeDetection) there is no zone to leave: write to signals from the same editor events and drop NgZone entirely.

A rule of thumb

Leave the zone once, at initialisation. Re-enter narrowly, at the specific events your interface listens to. Wrapping every editor callback in NgZone.run() gives back exactly the change-detection churn you left the zone to avoid.

Server-side rendering

Does GrapesJS work with Angular SSR?

The editor runs in the browser. GrapesJS builds its canvas from real DOM — it measures elements, creates an iframe and attaches document listeners — so initialisation has to happen in a browser context and must not run during a server render. Routes that merely display published output are unaffected: that is plain HTML and CSS and renders on the server like anything else.

editor.component.tsts
// editor.component.ts — browser-only initialisation
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  PLATFORM_ID,
  ViewChild,
  inject,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import type { Editor } from 'grapesjs';

export class EditorComponent implements AfterViewInit, OnDestroy {
  @ViewChild('editorHost') private host!: ElementRef<HTMLDivElement>;

  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  private editor?: Editor;

  async ngAfterViewInit(): Promise<void> {
    if (!this.isBrowser) return;

    // A dynamic import keeps the editor out of the server bundle and off the
    // critical path of every route that never opens it.
    const { default: grapesjs } = await import('grapesjs');
    this.editor = grapesjs.init({ container: this.host.nativeElement });
  }

  ngOnDestroy(): void {
    this.editor?.destroy();
  }
}
  • Guard initialisation so it only runs in the browser — the platform check and the render hook below both do this.
  • Import the library dynamically so it never enters the server bundle.
  • Render a placeholder of the same size during the server pass to avoid a layout shift when the editor appears.
  • Publish routes that only render stored HTML and CSS need no guard at all.

The render-hook alternative

Recent Angular versions offer a render hook whose callbacks run on browser platforms only and never on the server. Where it is available it expresses the same guarantee without a platform check, and it pairs naturally with signal-based view queries.

editor.component.ts (render hook)ts
// The same guard, expressed with the render hook instead of a platform check.
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
import type { Editor } from 'grapesjs';

@Component({ selector: 'app-editor', template: '<div #editorHost></div>' })
export class EditorComponent {
  private readonly host = viewChild.required<ElementRef<HTMLDivElement>>('editorHost');
  private editor?: Editor;

  constructor() {
    // afterNextRender callbacks run on browser platforms only — they never
    // execute on the server, so no isPlatformBrowser check is needed.
    afterNextRender(async () => {
      const { default: grapesjs } = await import('grapesjs');
      this.editor = grapesjs.init({ container: this.host().nativeElement });
    });
  }
}

Check which of these your Angular version supports before choosing. Both patterns exist to enforce the same rule — the editor initialises in the browser and nowhere else. angular.dev

Anatomy

Build an Angular page builder with GrapesJS

A page builder is not one feature. It is nine subsystems that have to agree with each other, and GrapesJS ships all nine. This is the overview; the product-level decisions — pricing, roles, templates, publishing workflow — belong to the dedicated guide linked underneath.

  • GrapesJS

    Canvas

    The editable surface, rendered in its own iframe so page styles cannot leak into your admin UI.

  • GrapesJS

    Components

    The typed model behind every element: what it is, what it accepts, and which settings it exposes.

  • GrapesJS

    Block manager

    The palette users drag from. A block is a named piece of content mapped to a component type.

  • GrapesJS

    Style manager

    The CSS editing panel, configurable down to which properties a given user is allowed to change.

  • Plugin

    Asset manager

    Image and media picking. The upload endpoint behind it is yours, so plugins here mostly connect it to a provider.

  • Your Angular app

    Storage manager

    Load and save. GrapesJS defines the contract and calls your API — the persistence itself is your application's.

  • GrapesJS

    Commands

    Named, callable actions — undo, preview, export, and anything you register yourself for toolbar buttons.

  • GrapesJS

    Device manager

    The responsive breakpoints the canvas can be previewed and styled at.

  • GrapesJS

    Panels

    The editor's own chrome — buttons, toolbars and regions, all replaceable if the editor sits inside an existing shell.

Who builds itGrapesJSPluginYour Angular app

Eight of the nine ship with the editor or with a plugin. The one that is unavoidably yours is storage — which is also the one that has to know about your users, your permissions and your database.

Build a complete Angular page builder
Where it sits

The editor inside an Angular application

Read from the outside in, this is the whole integration. Everything above the GrapesJS instance is ordinary Angular; everything below it is the editor doing its own work in an iframe you never have to touch.
  1. Angular application
  2. Editor route (lazy-loaded)
  3. EditorComponent
  4. GrapesJS instance
  5. Canvas iframe

If the editor has to be reachable from elsewhere in the application — a toolbar in the shell, a page list, a preview pane — move initialisation into an injectable service and let components talk to that instead of to the editor directly.

The canvas is a real iframe. That is deliberate: page styles cannot leak into your admin interface and your application styles cannot leak into the page being edited, which is exactly the isolation you want when the two are designed by different people.

Output

What does GrapesJS generate?

Three artefacts, and mixing them up causes real data loss. Project data is the editable document; HTML and CSS are the published result. They are not interchangeable, because the export is lossy by design.

Project data

A JSON document holding components, styles, pages and assets. This is what you store so an editor can reopen the page exactly as it was left. Treat it as the source of truth.

HTML

The markup for the page as it will be served. Store it alongside the project data so publishing never has to boot an editor, and so a page can render even if the editor is unavailable.

CSS

The styles the editor produced for that page, scoped to the rules it manages. Serve it with the HTML — the two are one output split across two files.

output.tsts
// Three outputs, three different jobs. Do not use one where you need another.

const projectData = editor.getProjectData(); // editable state → store this
const html = editor.getHtml();               // markup         → publish this
const css = editor.getCss();                 // styles         → publish this

// Reopening a saved project restores components, styles, pages and assets.
// Re-parsing exported HTML does not — it loses everything not in the markup.
editor.loadProjectData(projectData);

Reparsing exported HTML back into the editor is not a substitute for project data: it loses component types, traits, page structure and anything else that was not expressible in the markup. GrapesJS is an editor, not a CMS or a backend — where the artefacts go, and what happens to them afterwards, is your application's decision.

Persistence

Save GrapesJS projects in your Angular application

GrapesJS defines the contract; you provide the endpoints. Register a named storage with two async methods and the editor will call them — on demand, or automatically as edits accumulate. Everything about authentication, tenancy and permissions stays where it belongs, in your Angular services and your API.
  1. Angular
  2. GrapesJS Storage
  3. HttpClient → REST API
  4. Your backend
  5. Database
project-storage.service.tsts
// project-storage.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { Editor, ProjectData } from 'grapesjs';

@Injectable({ providedIn: 'root' })
export class ProjectStorage {
  private readonly http = inject(HttpClient);

  /** Registers a named storage the editor can then autosave into. */
  register(editor: Editor, projectId: string): void {
    editor.Storage.add('gjs-api', {
      load: () =>
        firstValueFrom(this.http.get<ProjectData>(`/api/projects/${projectId}`)),

      store: async (data) => {
        await firstValueFrom(
          this.http.put(`/api/projects/${projectId}`, {
            project: data,          // what the editor reopens
            html: editor.getHtml(), // what the site renders
            css: editor.getCss(),
          })
        );
      },
    });
  }
}
editor.component.ts (excerpt)ts
// Point the editor at the storage you just registered.
this.storage.register(editor, projectId);

grapesjs.init({
  container: host,
  storageManager: {
    type: 'gjs-api',      // the name passed to Storage.add()
    autosave: true,
    autoload: true,
    stepsBeforeSave: 5,   // batch edits instead of a request per keystroke
  },
});

The editor never talks to your database. It calls the two methods you registered, which call HttpClient, which calls your API under the same interceptors and auth as every other request in the application.

Persistence checklist

Five decisions to make before the first save

  1. 1
    Project JSON

    Store the editable document

    Persist getProjectData() output as the record a user reopens. Store the exported HTML and CSS in the same write so publishing has no second round trip.

  2. 2
    Autosave

    Batch writes, do not stream them

    autosave with stepsBeforeSave turns a burst of edits into one request. Tune the step count to your API, not to the demo default.

  3. 3
    Load

    Autoload, or load explicitly

    autoload lets the editor fetch on start. Loading explicitly instead gives you a place to show your own spinner and to handle a 403 as an application error rather than an editor one.

  4. 4
    Versions

    Keep history server-side

    The undo stack lives in the browser and dies with the tab. If users need to restore yesterday's page, that is a versions table in your database, written on store.

  5. 5
    Publish

    Separate saving from publishing

    A save updates the draft. Publishing promotes a specific version to the live route — a distinct endpoint, with its own permission check.

Media

Manage images and assets

The asset manager is a picker, not a file service. Point it at an endpoint in your Angular application and it will POST the files there; everything after that — validation, storage, resizing, delivery — is yours to implement, which is what makes it possible to reuse the media library you already have.

editor.component.ts (excerpt)ts
// Assets go to your endpoint, under your auth — GrapesJS only POSTs files.
grapesjs.init({
  container: host,
  assetManager: {
    upload: `/api/projects/${projectId}/assets`,
    uploadName: 'files',
    multiUpload: true,
    credentials: 'include',
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
    // Off by default is the safer choice: base64 images end up inside saved
    // markup and make every page row grow without limit.
    embedAsBase64: false,
    // Returning false aborts the upload before the request leaves the browser.
    beforeUpload: (files: File[]) =>
      files.every((file) => file.size <= MAX_UPLOAD_BYTES && ALLOWED_TYPES.has(file.type)),
  },
});

editor.on('asset:upload:start', () => this.uploading.set(true));
editor.on('asset:upload:end', () => this.uploading.set(false));
editor.on('asset:upload:error', (error) => this.uploadError.set(String(error)));
  • Uploads go to your endpoint with your headers and credentials, so the same auth as the rest of the app applies.
  • Validate before the request leaves the browser and again on the server — the client-side check is convenience, not a control.
  • Return the stored URLs from your endpoint; the editor puts those URLs into the page, so they must be the ones you intend to serve.
  • Turn off base64 embedding unless you want images inlined into saved markup, where they make every stored page grow without limit.
  • Serve assets from a CDN or object store and keep the editor pointed at the public URL, not at your application origin.
  • Load large libraries through a paginated endpoint rather than handing the editor thousands of assets at initialisation.

None of this prescribes a particular backend. S3, Cloudinary, a Laravel disk or a folder on a server all satisfy the same contract: accept a file, return a URL.

Extending the editor

Create custom components

Registering a component type is how the editor learns about your product. A custom type gets its own settings panel, its own drag-and-drop rules, and a block in the palette — so content teams work with concepts from your domain instead of raw divs.

pricing-table.type.tsts
// pricing-table.type.ts — an application-specific building block.
import type { Editor } from 'grapesjs';

export function registerPricingTable(editor: Editor): void {
  editor.Components.addType('pricing-table', {
    // Lets the editor recognise the type again when a saved page is reparsed.
    isComponent: (el) => el.classList?.contains('pricing-table'),
    model: {
      defaults: {
        tagName: 'section',
        attributes: { class: 'pricing-table' },
        droppable: false,
        // Traits become the right-hand settings panel for this component.
        traits: [
          { type: 'text', name: 'plan', label: 'Plan name' },
          { type: 'number', name: 'price', label: 'Price' },
          { type: 'checkbox', name: 'featured', label: 'Highlight' },
        ],
        components: `
          <h3 data-gjs-type="text">Team</h3>
          <p data-gjs-type="text">$29 / month</p>
        `,
      },
    },
  });

  // A block is how the component reaches the user: one entry in the palette.
  editor.Blocks.add('pricing-table', {
    label: 'Pricing table',
    category: 'Commerce',
    content: { type: 'pricing-table' },
  });
}

Typical application-specific components

  • Hero section
  • Pricing table
  • Product card
  • Form
  • Navigation
  • Testimonial
  • Feature grid
  • Anything specific to your SaaS

Two things make a custom type useful. isComponent lets the editor recognise your type again when a saved page is re-parsed, and traits become the settings panel a non-technical user actually edits. Both are worth writing even for a simple component.

Extend it

Extend GrapesJS with plugins

A plugin is a function that receives the editor instance, so nothing here is Angular-specific — anything published for GrapesJS works inside an Angular component. These are the four gaps teams hit first after the editor mounts.

Build or adopt

Build the editor yourself or use GrapesJS?

Building a visual editor is possible in Angular — it is a known amount of work, not an impossible one. The question is which parts of it you want to own for the lifetime of the product.

CapabilityBuild yourselfGrapesJS
CanvasBuildIncluded
Drag & dropBuildIncluded
ComponentsBuildIncluded
BlocksBuildIncluded
StylingBuildIncluded
AssetsBuildIncluded
StorageBuildExtensible — you supply the endpoints
PluginsBuild an ecosystemPlugin architecture
Angular integrationNative to your appIntegrate via a component
LicenceYoursBSD-3-Clause — $0 to use

"Build" is not a criticism — every one of these is buildable. It is a statement of who maintains it afterwards.
Facts in this table re-verified 2026-09-02.

Angular gives you the application framework. GrapesJS gives you the visual editing engine.

Integration choice

Do I need an Angular wrapper for GrapesJS?

No, and it is worth understanding why the question comes up. GrapesJS publishes an official React wrapper, so developers reasonably expect an Angular equivalent. There is not one. That leaves three approaches, and for most teams the first is the right answer.

Recommended

Direct integration

Write the component yourself — the one on this page is roughly thirty lines. You own the lifecycle, the change-detection strategy and the upgrade path, and there is no third party between your app and the editor API.

  • No extra dependency to keep current with Angular releases
  • Full access to every editor option and event
  • Upgrades to GrapesJS land immediately
Third-party

A community wrapper

Community packages exist and can shorten the first hour. Evaluate one the way you would any dependency: check who maintains it, which Angular and GrapesJS versions it declares, and how recently it was published.

  • Not maintained by the GrapesJS project
  • Adds a version constraint on both Angular and GrapesJS
  • Worth checking against the current core release before adopting
  • ngx-grapesjs 21.0.0 · 2026-01-15 · peer grapesjs ^0.22.4
For larger apps

Your own service wrapper

Once several components need the editor — a toolbar, a page list, a preview pane — move initialisation into an injectable service. It is direct integration with a seam, and it keeps the editor out of your component tree.

  • One place that owns init and destroy
  • Editor state reaches components as signals or observables
  • Testable without mounting the editor

The GrapesJS organisation maintains a React wrapper and no Angular package. Any Angular library you find is third-party, however useful — treat it as a dependency choice, not as part of GrapesJS.

Architecture

Production architecture for GrapesJS + Angular

A demo is one component. A product is four layers, and the editor is only one of them. This is the shape most teams converge on once the editor has real users behind it.

  1. Angular application

    The shell, the router, and one lazily-loaded route that owns the editor.

    • App shell
    • Router
    • Editor route (lazy)
  2. GrapesJS instance

    Configured once, at initialisation: which components exist, what can be dragged, what can be styled.

    • Components
    • Blocks
    • Styles
    • Assets
    • Commands
  3. Application services

    The Angular layer between the editor and your API — and the only place authorisation decisions are made client-side.

    • Auth
    • API client
    • Permissions
    • Publishing
  4. Backend

    Where the artefacts live, and where every permission is enforced for real.

    • Projects
    • Assets
    • Users
    • Versions

Nothing in this diagram is GrapesJS-specific except the second tier. That is the point: the editor is a component in an ordinary Angular application, and the rest is the architecture you would have built anyway.

See how teams package this as a product
Performance

Performance considerations

The editor is a large piece of software running inside your application. These are the places where that actually costs something.

Initialise only when needed

Create the editor when the editing route opens, not at application start. Users who never edit should never pay for it.

Lazy-load the route

A dynamic import keeps GrapesJS out of the initial bundle, which is the single largest lever on this list.

Stay out of the zone

Create the editor with runOutsideAngular so canvas pointer events do not trigger change detection on every frame.

Bound the asset library

Do not hand the editor a full media library at init. Paginate through your API and load assets as the picker requests them.

Watch the component tree

Very deep or very repetitive page structures make selection, styling and undo slower. Prefer components over deeply nested wrappers.

Batch saves

Use stepsBeforeSave so a burst of edits becomes one request rather than a request per keystroke.

None of these come with a promised number attached. Measure your own editor route before and after — the size of each effect depends on your blocks, your pages and your API.

Security

Security considerations

A visual editor turns users into authors of HTML that your application then serves. Treat everything coming out of the editor as user input, because that is exactly what it is.

Sanitise published HTML

Editor output can contain arbitrary markup, including scripts if custom code blocks are enabled. Sanitise server-side before it is served to anyone other than its author.

Validate uploaded assets

Check type and size on the server, not only in beforeUpload. Store uploads outside the web root and serve them from a separate origin.

Authenticate storage endpoints

Load and save routes are ordinary API endpoints. They need the same authentication as everything else in the application.

Enforce permissions server-side

Hiding a publish button in the Angular UI is presentation. The check that matters is the one on the endpoint.

Never trust project data

Stored project JSON can be tampered with. Validate it on load rather than assuming it is the document you wrote.

Scope tenants explicitly

In a multi-tenant product, derive the project's owner from the session on the server — never from an id in the request body.

Before you ship

Angular + GrapesJS production checklist

Twelve things that separate a working demo from a route real users can be given.

  1. Install GrapesJS

    Dependency added and the stylesheet registered, globally or in the editor component.

    Read the section
  2. Initialise after view creation

    grapesjs.init() runs in ngAfterViewInit, against a container with a resolved height.

    Read the section
  3. Destroy the editor correctly

    editor.destroy() in ngOnDestroy, verified by navigating away and back without leaks.

    Read the section
  4. Handle browser-only initialisation

    Guarded so nothing runs during a server render, with a placeholder in its place.

    Read the section
  5. Connect storage

    A named storage registered, autosave tuned, and project JSON stored alongside HTML and CSS.

    Read the section
  6. Configure assets

    Uploads pointed at your endpoint, base64 embedding off, validation on both sides.

    Read the section
  7. Add custom components

    Your product's own building blocks registered as types, with traits and palette entries.

    Read the section
  8. Add the plugins you need

    Blocks, rich text and export chosen deliberately rather than accumulated.

    Read the section
  9. Implement permissions

    Who may edit, who may publish — enforced on the server, reflected in the UI.

    Read the section
  10. Sanitise published content

    Server-side sanitisation between saving a page and serving it to visitors.

    Read the section
  11. Monitor editor errors

    Editor exceptions reported to the same place as the rest of your Angular errors.

  12. Test responsive editing

    The device manager exercised on a touch device, not only in a desktop browser window.

    Read the section
FAQ

GrapesJS and Angular: common questions

Next step

Build your Angular visual editor with GrapesJS

Start with the open-source visual editor, connect it to your Angular application, and extend it with the plugins and integrations your product needs.

Developer

Get started

Install the package, copy the component, and have an editor running on an Angular route this afternoon.

Get Started
Extend

Browse plugins

Blocks, storage, rich text, assets, export and email — the pieces you would otherwise be writing yourself.

Browse Plugins
Team

Plan the product

Scope the editor as a feature of your application: roles, templates, publishing and versioning.

Start a brief

Build the application with Angular. Build the visual editor with GrapesJS. Extend it with GJS.Market plugins.