Skip to content

Commit 33dbdb5

Browse files
authored
v2.0.0-beta.2 — React-parity custom UI + demo polish (#19)
* feat(custom-ui): React-parity provider layer for 2.0.0-beta.2 Add nine <gjs-*-provider> components mirroring @grapesjs/react's render-props surface (blocks, layers, selectors, styles, traits, pages, devices, assets, modal), a <gjs-canvas> custom-UI mount point, and the *gjsContainer directive for projecting GrapesJS-owned elements. Editor component gains async CDN plugin loading, editorCreated/projectUpdated outputs, grapesjsCss/waitReady inputs, SSR safety, and a height/width:100% host-fill default. Service adds isInitialized; editorReady now fires on onReady() (breaking vs beta.1). Selectors and Styles providers self-trigger their initial custom event on wire() so their panels render in custom-UI mode, where GrapesJS skips the default panel render that would otherwise fire it. * docs: custom-UI demo, README, and CHANGELOG for 2.0.0-beta.2 Rework the demo to showcase every provider with a full custom editor shell (pages, blocks with drag ghost, layers tree, selectors, styles, traits, assets/modal overlays) alongside the default-UI mode. Layers rows now select via editor.select(component) — a GrapesJS Component has no .select() method, so the previous call threw at runtime. Raise anyComponentStyle budget to fit the demo stylesheet; rename the private workspace package to grapesjs-angular-workspace. * docs(demo): add redesign spec and reference mockup Tokenized design-system spec (light/dark) and the interactive editor-chrome mockup it derives from, as the visual source of truth for the demo redesign. * style(demo): add tokenized design system (light + dark) to global styles.css One :root token set (surfaces, text, lines, accent, selection, status, shadow, type, radii) plus a [data-theme=dark] override, defined after the grapes.min.css import so overrides win. Sets global body background/color/font. Nothing consumes the accent/panel tokens yet — wired in following commits. * style(demo): load Inter + JetBrains Mono web fonts UI in Inter, mono surfaces (IDs, selectors, style values, HTML export) in JetBrains Mono, per the redesign spec. preconnect + display=swap. * feat(demo): add persisted light/dark theme toggle Toolbar icon button (inline sun/moon) flips data-theme on documentElement so both custom rails and the GrapesJS panels inherit it. Initial theme = saved choice, else OS prefers-color-scheme; persisted to localStorage. Editor logic untouched. * style(demo): rewrite custom-UI chrome to consume design tokens Toolbar gains a clear action hierarchy (Get HTML primary / Save secondary / Reset quiet-destructive) and a token-styled device select. Every custom-rail surface (panels, page list with accent active-bar, block tiles, layer tree, selector chips, state/trait controls, read-only style list, drag ghost, drop indicator, asset/modal overlays, HTML export) now maps to tokens — zero hardcoded hex, #0f0 and #276ef1 gone, and the .property-list !important pile-up is removed via scoping. Adds focus-visible rings and a prefers-reduced-motion guard. * style(demo): theme GrapesJS default-mode panels to match custom UI Map the engine's --gjs-* variables (verified against the installed build) onto our design tokens so they re-resolve on theme flip, plus minimal scoped .gjs-* overrides for the colours GrapesJS hardcodes (panel/title/field borders, canvas selection badge -> --select). Flipping Custom <-> Default in either light or dark now reads as one product — no stock-grey panels. * feat(demo): segmented mode switch and slide-up HTML export dock Replace the single Custom/Default button with a real 2-segment control, and the bare <pre> export with a slide-up dock (header + HTML pill + Copy + close) on a panel surface. Reuses existing editor logic via a thin setCustomUi(boolean) and a copyHtml() clipboard helper. Slide animation respects prefers-reduced-motion. * fix(demo): persist project across Custom/Default switch + auto-select drops Each Custom<->Default toggle recreates the gjs-editor, and onEditorReady was unconditionally re-seeding the sample, wiping edits. Now snapshot the project on switch (setCustomUi) and restore it in the new instance; seed the sample only on first load and on Reset. Also select the dropped component on block:drag:stop so its styles/traits populate without a second click. Verified: a component added in one mode survives the switch (and back) into the recreated editor. * feat(demo): make the Styles inspector editable + polish Selectors panel The custom-UI right rail was read-only, so the State dropdown had nothing to affect. Each style property now renders an editable control (select for select/radio types via getOptions, text input otherwise, read-only value for composite/stack shorthands), bound to property.upValue(). Editing drives the canvas; switching State now writes state-specific rules (e.g. :hover). Selectors panel gets a mono targets readout, styled chips, a labeled State field, and a proper empty state. Both panels gate on the current selection. Verified live: editing font-size/text-align updates the canvas; selecting hover state + editing creates a :hover rule. * feat(demo): icon block tiles in the custom-UI block panel Render each block's own GrapesJS media SVG as the tile icon (fill=currentColor, so it themes), memoized per block id through DomSanitizer. Block tiles become icon-over-label, matching the reference mockup. * fix(demo): reset block-drag state on sorter:drag:end + ghost shows block icon In custom-UI mode the block drag ends via sorter:drag:end, NOT block:drag:stop (which the cleanup was listening for). So dragging never reset: the source button stayed dimmed/'active', the floating ghost stuck at a stale position (looking like 'no ghost' on later drags), and the drop never auto-selected. Clean up on sorter:drag:end instead, and auto-select the dropped element via component:add while the drag is active. The ghost now carries the block's icon so it's clearly visible as you drag. Verified via the editor event lifecycle: block:drag:start sets dragging; a component added mid-drag auto-selects; sorter:drag:end resets dragging/label/icon. * fix(demo): hide textnode layers + use GrapesJS's real drop indicator Layers: filter out non-layerable nodes (textnodes that surfaced as meaningless 'Box' rows), mirroring GrapesJS's own layer manager. Verified: 0 'Box' rows remain. Drag: drop the demo's full-canvas 'DROP HERE' overlay (it covered the real indicator and the badge sat centered, not at the cursor). Rely on GrapesJS's own .gjs-placeholder — a precise insertion line at the drop point — themed to --accent in global styles.css. The floating ghost (icon + label) still follows the cursor. * feat(demo): typed style inspector (sliders, swatches, segmented align, selects) The right rail was a wall of identical text/select boxes. Drive it from a curated styleManager (Typography / Background / Spacing) so each property is the right control: range sliders with value readout (font-size, line-height, padding, margin), colour swatches (color, background-color), a segmented control (text-align), and selects for true enums (font-weight). Small typed helpers wrap GrapesJS's dynamic Property API (slider unit handling, colour->hex, options). Applies to both custom and default UI. Verified live: the font-size slider updates the canvas (with unit), and the segmented text-align sets alignment. * fix(demo): keep default-UI device selector inside its command bar The .gjs-pn-devices-c panel is 44px but the canvas starts at 40px (--gjs-canvas-top), so its bottom edge bled into the canvas. Pin the panel to the bar height and center the dropdown within it (bottom now 5px clear of the canvas). * chore(demo): remove redesign scaffolding docs The REDESIGN-SPEC and mockup were process artifacts for the demo re-skin, not part of the demo app or the published library. * docs: date the 2.0.0-beta.2 changelog entry
1 parent a5c8e4d commit 33dbdb5

31 files changed

Lines changed: 3136 additions & 100 deletions

‎CHANGELOG.md‎

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,78 @@
11
# Changelog
22

3-
## 2.0.0-alpha.1
3+
All notable changes to `@ilq/grapesjs-angular` will be documented in this file.
44

5-
Initial release of `@ilq/grapesjs-angular` — a ground-up Angular 20+ wrapper for GrapesJS.
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
- Standalone `<gjs-editor>` component with OnPush change detection
8-
- `GrapesJsEditorService` with signals for editor state and all GrapesJS managers
9-
- `provideGrapesJs()` for app-wide default configuration
10-
- Zoneless-compatible (no Zone.js dependency)
11-
- Inputs: `config`, `plugins`
12-
- Outputs: `editorReady`, `projectSaved`, `projectLoaded`, `componentSelected`, `blockAdded`
13-
- Service methods: `init()`, `destroy()`, `getHtml()`, `getCss()`, `getProjectData()`, `loadProjectData()`
7+
## [Unreleased]
8+
9+
### Added
10+
11+
### Changed
12+
13+
### Fixed
14+
15+
### Removed
16+
17+
## [2.0.0-beta.2] — 2026-06-21
18+
19+
### Added
20+
21+
- `<gjs-canvas>` mount-point component enabling custom UI mode — when projected into `<gjs-editor>`, swaps the editor's default container and disables default panels.
22+
- Nine provider components mirroring the `@grapesjs/react` render-props surface, each exposing typed manager state to a projected `<ng-template>`:
23+
- `<gjs-blocks-provider>` — `BlocksState` (blocks, drag handlers, container, category map)
24+
- `<gjs-layers-provider>` — `LayersState` (root component, container)
25+
- `<gjs-selectors-provider>` — `SelectorsState` (selectors, states, targets, mutators, container)
26+
- `<gjs-styles-provider>` — `StylesState` (visible sectors, container)
27+
- `<gjs-traits-provider>` — `TraitsState` (current traits, container)
28+
- `<gjs-pages-provider>` — `PagesState` (pages, selected, mutators)
29+
- `<gjs-devices-provider>` — `DevicesState` (devices, selected, select)
30+
- `<gjs-assets-provider>` — `AssetsState` (open, assets, types, select, close, container)
31+
- `<gjs-modal-provider>` — `ModalState` (open, title, content, attributes, close)
32+
- `*gjsContainer` directive for appending a GrapesJS-owned `HTMLElement` into a host element (Angular equivalent of React's portal `Container`).
33+
- New component outputs: `editorCreated` (fires synchronously after `grapesjs.init()` returns), `projectUpdated` (fires on every `'update'` event with latest `ProjectData`).
34+
- New component inputs: `grapesjsCss` (async `<link>` injection before init), `waitReady` (`boolean | TemplateRef<unknown>` — hide/replace the editor host until `editor.onReady()` fires).
35+
- New service signal `isInitialized` — `true` after `grapesjs.init()` returns.
36+
- Async CDN plugin loading — `plugins` input now accepts `{ id, src, options }` descriptors alongside function references and global names.
37+
- Public types: `PluginToLoad`, `PluginTypeToLoad`, `GrapesPlugin`, `ProjectUpdatePayload`.
38+
39+
### Changed
40+
41+
- **BREAKING vs `2.0.0-beta.1`** — `editorReady` output semantics changed and the earlier-firing name was moved:
42+
- Output `editorReady` (previously fired on GrapesJS `'load'`) is renamed to `editorLoaded` — same firing point, clearer name.
43+
- Output `editorReady` now fires on `editor.onReady()` (post-mount, post-storage-load) to match `@grapesjs/react` `onReady` semantics.
44+
- **BREAKING vs `2.0.0-beta.1`** — service signal `isReady` semantics changed:
45+
- Signal `isReady` (previously flipped after init) is renamed to `isInitialized`.
46+
- Signal `isReady` now reflects the post-`editor.onReady()` state.
47+
- `plugins` input widened from `Plugin[]` to `PluginTypeToLoad[]` — source-compatible; existing `Plugin[]` assignments still compile.
48+
- `<gjs-editor>` now defaults the GrapesJS init config to `height: '100%'` and `width: '100%'` so the editor fills its host element. GrapesJS's internal default of `height: 900px` was overflowing flex layouts. Consumer config still overrides.
49+
50+
### Fixed
51+
52+
- `<gjs-selectors-provider>` and `<gjs-styles-provider>` now render their projected template on init. In custom-UI mode GrapesJS skips its default panel render — the only place it fires the initial `selector:custom` / `style:custom` event — so these panels stayed empty until a component was selected. Both providers now self-trigger the event on `wire()`, matching `<gjs-traits-provider>`.
53+
- SSR-safe: `<gjs-editor>` no longer calls `grapesjs.init()` or touches DOM APIs when `PLATFORM_ID === 'server'` — the host renders inert and `ngOnDestroy` short-circuits.
54+
55+
## [2.0.0-beta.1] — 2026-04-23
56+
57+
Initial public release on npm. Angular 20+ wrapper for [GrapesJS](https://grapesjs.com/), signals-based, standalone, zoneless-compatible.
58+
59+
- `<gjs-editor>` standalone component with `config` and `plugins` inputs, `editorReady` / `projectSaved` / `projectLoaded` / `componentSelected` / `blockAdded` outputs.
60+
- `GrapesJsEditorService` exposing every GrapesJS manager as a signal, plus `isReady`, `selectedComponent`, and `getHtml` / `getCss` / `getProjectData` / `loadProjectData` helpers.
61+
- `provideGrapesJs(config)` environment provider + `GRAPES_JS_DEFAULT_CONFIG` injection token.
62+
63+
## [2.0.0-alpha.1]
64+
65+
Pre-release development milestone — not published to npm.
66+
67+
- Standalone `<gjs-editor>` component with OnPush change detection.
68+
- `GrapesJsEditorService` with signals for editor state and all GrapesJS managers.
69+
- `provideGrapesJs()` for app-wide default configuration.
70+
- Zoneless-compatible (no Zone.js dependency).
71+
72+
---
73+
74+
Keep the `[Unreleased]` section up-to-date as changes land; move its entries under a new version heading at release time.
75+
76+
[Unreleased]: https://github.com/internetliquid/grapesjs-angular/compare/v2.0.0-beta.2...HEAD
77+
[2.0.0-beta.2]: https://github.com/internetliquid/grapesjs-angular/compare/v2.0.0-beta.1...v2.0.0-beta.2
78+
[2.0.0-beta.1]: https://github.com/internetliquid/grapesjs-angular/releases/tag/v2.0.0-beta.1

‎README.md‎

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,63 @@ export class MyComponent {
8888

8989
### Inputs
9090

91-
| Input | Type | Default | Description |
92-
| --------- | --------------- | ------- | -------------------------------------------------- |
93-
| `config` | `GrapesJsConfig`| `{}` | GrapesJS editor config (merged with global config) |
94-
| `plugins` | `Plugin[]` | `[]` | Additional plugins (appended to global plugins) |
91+
| Input | Type | Default | Description |
92+
| --- | --- | --- | --- |
93+
| `config` | `GrapesJsConfig` | `{}` | GrapesJS editor config (merged with global config) |
94+
| `plugins` | `PluginTypeToLoad[]` | `[]` | Plugins as functions, global names, or `{ id, src, options }` for async CDN loading |
95+
| `grapesjsCss` | `string \| undefined` | — | URL of the GrapesJS core CSS to inject asynchronously before init |
96+
| `waitReady` | `boolean \| TemplateRef<unknown>` | — | Hide the editor host until `editorReady` fires (`editor.onReady()`); pass a `TemplateRef` to show a placeholder |
9597

9698
### Outputs
9799

98-
| Output | Payload | Description |
99-
| ------------------- | -------------- | ---------------------------------------- |
100-
| `editorReady` | `Editor` | Emitted when the editor has loaded |
101-
| `projectSaved` | `ProjectData` | Emitted after project storage completes |
102-
| `projectLoaded` | `ProjectData` | Emitted after project data is loaded |
103-
| `componentSelected` | `Component` | Emitted when a component is selected |
104-
| `blockAdded` | `Block` | Emitted when a block is dropped on canvas|
100+
| Output | Payload | Description |
101+
| --- | --- | --- |
102+
| `editorCreated` | `Editor` | Emitted synchronously after `grapesjs.init()` (pre-`load`) |
103+
| `editorLoaded` | `Editor` | Emitted on the GrapesJS `'load'` event |
104+
| `editorReady` | `Editor` | Emitted once `editor.onReady()` fires (post-mount, post-storage) |
105+
| `projectUpdated` | `{ data: ProjectData; editor: Editor }` | Emitted on every editor `'update'` event |
106+
| `projectSaved` | `ProjectData` | Emitted after project storage completes |
107+
| `projectLoaded` | `ProjectData` | Emitted after project data is loaded |
108+
| `componentSelected` | `Component` | Emitted when a component is selected |
109+
| `blockAdded` | `Block` | Emitted when a block is dropped on canvas |
110+
111+
### Editor lifecycle
112+
113+
Three distinct phases — safe content manipulation should wait for `editorReady`:
114+
115+
1. **`editorCreated`** — the `Editor` instance exists; managers are accessible but the canvas hasn't loaded.
116+
2. **`editorLoaded`** — GrapesJS `'load'` event; the core editor UI is up.
117+
3. **`editorReady`** — `editor.onReady()` callback; mounted, panels built, storage data loaded. Safe to read or manipulate project content.
118+
119+
### Async plugin & CSS loading
120+
121+
Plugins can be a mix of function references, global names, and async CDN descriptors:
122+
123+
```typescript
124+
@Component({
125+
template: `
126+
<gjs-editor
127+
grapesjsCss="https://unpkg.com/grapesjs/dist/css/grapes.min.css"
128+
[plugins]="editorPlugins">
129+
</gjs-editor>
130+
`,
131+
})
132+
export class MyEditor {
133+
editorPlugins: PluginTypeToLoad[] = [
134+
gjsBlocksBasic, // function reference
135+
'grapesjs-plugin-forms', // global name
136+
{ // CDN descriptor
137+
id: 'grapesjs-preset-webpage',
138+
src: 'https://unpkg.com/grapesjs-preset-webpage',
139+
options: { /* ... */ },
140+
},
141+
];
142+
}
143+
```
144+
145+
### SSR
146+
147+
`<gjs-editor>` is SSR-safe: when rendered on the server (`PLATFORM_ID === 'server'`) it renders an inert host and never calls `grapesjs.init()`. Combine with `waitReady` to render a placeholder during server rendering and hydration.
105148

106149
## Service API
107150

@@ -112,7 +155,8 @@ export class MyComponent {
112155
| Signal | Type | Description |
113156
| -------------------- | ------------------------------- | ------------------------------------ |
114157
| `editor` | `Signal<Editor \| null>` | The GrapesJS editor instance |
115-
| `isReady` | `Signal<boolean>` | Whether the editor is initialised |
158+
| `isInitialized` | `Signal<boolean>` | Whether the editor is initialised |
159+
| `isReady` | `Signal<boolean>` | Whether `editor.onReady()` has fired |
116160
| `selectedComponent` | `Signal<Component \| null>` | Currently selected component |
117161
| `blockManager` | `Signal<BlockManager \| null>` | Block manager |
118162
| `styleManager` | `Signal<StyleManager \| null>` | Style manager |
@@ -136,6 +180,86 @@ export class MyComponent {
136180
| `getProjectData()` | `ProjectData \| null`| Get the full project data |
137181
| `loadProjectData(data)` | `void` | Load project data into the editor|
138182

183+
## Custom UI
184+
185+
By default `<gjs-editor>` renders the full GrapesJS UI (panels, block manager, style manager, etc.) inside its host element. For full control of the editor shell — composing your own sidebars and toolbars with Angular templates — project `<gjs-canvas>` plus one or more provider components as content children:
186+
187+
```typescript
188+
import {
189+
GrapesJsEditorComponent,
190+
GjsCanvas,
191+
GjsBlocksProvider,
192+
GjsLayersProvider,
193+
GjsPagesProvider,
194+
GjsContainerDirective,
195+
} from '@ilq/grapesjs-angular';
196+
197+
@Component({
198+
imports: [
199+
GrapesJsEditorComponent,
200+
GjsCanvas,
201+
GjsBlocksProvider,
202+
GjsLayersProvider,
203+
GjsPagesProvider,
204+
GjsContainerDirective,
205+
],
206+
template: `
207+
<gjs-editor>
208+
<aside class="sidebar">
209+
<gjs-blocks-provider>
210+
<ng-template let-ctx>
211+
@for (block of ctx.blocks; track block.getId()) {
212+
<button (mousedown)="ctx.dragStart(block, $event)">
213+
{{ block.getLabel() }}
214+
</button>
215+
}
216+
</ng-template>
217+
</gjs-blocks-provider>
218+
219+
<gjs-layers-provider>
220+
<ng-template let-ctx>
221+
<div [gjsContainer]="ctx.container"></div>
222+
</ng-template>
223+
</gjs-layers-provider>
224+
225+
<gjs-pages-provider>
226+
<ng-template let-ctx>
227+
@for (page of ctx.pages; track page.getId()) {
228+
<button (click)="ctx.select(page)">
229+
{{ page.get('name') ?? page.getId() }}
230+
</button>
231+
}
232+
</ng-template>
233+
</gjs-pages-provider>
234+
</aside>
235+
<gjs-canvas class="canvas"></gjs-canvas>
236+
</gjs-editor>
237+
`,
238+
})
239+
export class MyEditor {}
240+
```
241+
242+
When `<gjs-canvas>` is present the editor uses it as the GrapesJS container, sets `customUI: true`, and disables the default panels. Each provider discovered via content projection flips the matching `custom*` flag in the init config, then subscribes to its manager's `custom` event and exposes the live state to your projected `<ng-template>`.
243+
244+
### Providers
245+
246+
| Component | State shape | Flips `custom` flag on | Notes |
247+
| --- | --- | --- | --- |
248+
| `<gjs-canvas>` | — | — | Swaps the editor's container and disables default panels |
249+
| `<gjs-blocks-provider>` | `BlocksState` | `blockManager` | `{ blocks, dragStart, dragStop, container, mapCategoryBlocks }` |
250+
| `<gjs-layers-provider>` | `LayersState` | `layerManager` | `{ root, container }` |
251+
| `<gjs-selectors-provider>` | `SelectorsState` | `selectorManager` | `{ selectors, states, selectedState, targets, addSelector, removeSelector, setState, container }` |
252+
| `<gjs-styles-provider>` | `StylesState` | `styleManager` | `{ sectors, container }` |
253+
| `<gjs-traits-provider>` | `TraitsState` | `traitManager` | `{ traits, container }` |
254+
| `<gjs-assets-provider>` | `AssetsState` | `assetManager` | `{ open, assets, types, select, close, container }` |
255+
| `<gjs-modal-provider>` | `ModalState` | `modal` | `{ open, title, content, attributes, close }` |
256+
| `<gjs-pages-provider>` | `PagesState` | — | `{ pages, selected, select, add, remove }` (observer-only) |
257+
| `<gjs-devices-provider>` | `DevicesState` | — | `{ devices, selected, select }` (observer-only) |
258+
259+
### `*gjsContainer`
260+
261+
For providers that expose a `container` (the GrapesJS-owned default panel element), use `[gjsContainer]="ctx.container"` on any host element to mount the default panel inside your own layout. This is a one-directive alternative to building a portal system.
262+
139263
## Multi-Editor
140264

141265
`GrapesJsEditorService` is provided in root, so by default all `<gjs-editor>` instances share one service. For multiple independent editors, provide a separate service per editor:
@@ -172,6 +296,8 @@ Each instance of this component gets its own `GrapesJsEditorService`.
172296

173297
Inspired by the original [`@rakutentech/grapesjs-angular`](https://github.com/rakutentech/grapesjs-angular), now archived.
174298

299+
The API surface is informed by [`@grapesjs/react`](https://github.com/GrapesJS/react), the official React wrapper, and portions of the plugin and stylesheet loader utilities are adapted from it under the MIT license.
300+
175301
## Contributing
176302

177303
See [CONTRIBUTING.md](CONTRIBUTING.md).

‎angular.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@
6363
},
6464
{
6565
"type": "anyComponentStyle",
66-
"maximumWarning": "4kB",
67-
"maximumError": "8kB"
66+
"maximumWarning": "8kB",
67+
"maximumError": "16kB"
6868
}
6969
],
7070
"outputHashing": "all"

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "grapesjs-angular",
3-
"version": "0.0.0",
2+
"name": "grapesjs-angular-workspace",
3+
"version": "0.0.1",
44
"scripts": {
55
"ng": "ng",
66
"build:lib": "ng build grapesjs-angular && cp README.md LICENSE dist/grapesjs-angular/",

0 commit comments

Comments
 (0)