Monaco Wrapper

In all Cadmus projects the Monaco wrapper for Angular has been migrated from from @cisstech/nge (Monaco wrapper + Markdown renderer) to @jean-merelis/ngx-monaco-editor + marked. You should do the same in your project by following this migration guide.

Packages

  1. remove "@cisstech/nge" and install @jean-merelis/ngx-monaco-editor:

     pnpm uninstall @cisstech/nge
     pnpm i @jean-merelis/ngx-monaco-editor
    
  2. set "monaco-editor" to a version satisfying the installed ngx-monaco-editor’s peer dependency. Caret ranges on 0.x versions only allow patch bumps (^0.47.0>=0.47.0 <0.48.0), so pin exactly to what the wrapper wants, e.g. "^0.47.0".
  3. if required for Markdown preview, add marked.

Configuration

  1. in app.config, add the Monaco wrapper to the providers array:

     import {
       DefaultMonacoLoader,
       NGX_MONACO_LOADER_PROVIDER,
     } from '@jean-merelis/ngx-monaco-editor';
    
     export const appConfig: ApplicationConfig = {
     providers: [
         // ...
         // vendor
         {
         provide: NGX_MONACO_LOADER_PROVIDER,
         useFactory: () => new DefaultMonacoLoader({ paths: { vs: '/vs' } }),
         },
     ]
     };
    
  2. in angular.json add to architect/build/options/assets:

{
  "glob": "**/*",
  "input": "node_modules/monaco-editor/min/vs",
  "output": "vs"
}

Usage

Wherever your code uses Monaco:

  1. add the wrapper to component’s imports:

     import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
     import {
       EditorInitializedEvent,
       NgxMonacoEditorComponent,
       StandaloneEditorConstructionOptions,
     } from '@jean-merelis/ngx-monaco-editor';
    
     // ...
     imports: [
       // ...
       NgxMonacoEditorComponent
     ]
    
  2. add component fields:

     // Monaco editor
     private _editor?: StandaloneCodeEditor;
     public readonly editorOptions: StandaloneEditorConstructionOptions = {
       minimap: { side: 'right' },
       wordWrap: 'on',
       automaticLayout: true,
     };
    
     // ...
     // form control bound to the editor's text
     public text: FormControl<string>;
    
     // handler for editor init
     public onEditorInit(event: EditorInitializedEvent) {
       this._editor = event.editor;
       this._editor.focus();
     }
    

    The control’s value stays in sync via [formControl] — there’s no need to manually create or set a Monaco text model.

  3. add the wrapper to the template:

     <div id="editor">
     <ngx-monaco-editor
         [formControl]="text"
         [language]="'markdown'"
         [options]="editorOptions"
         (editorInitialized)="onEditorInit($event)"
     />
     </div>
    
  4. add styles: this step is easy to miss and results in an editor that renders at 0×0 (or collapses to a tiny box) even though its container looks correctly sized. <ngx-monaco-editor>’s host element is display: block; position: relative, and its inner Monaco container is position: absolute; inset: 0. Absolutely positioned children don’t contribute to their parent’s auto height/width, so the <ngx-monaco-editor> host itself collapses unless it’s given an explicit size. Give the wrapping element a fixed/flex height, and make ngx-monaco-editor fill it:

#editor {
  height: 600px; /* or flex: 1, or whatever fits your layout */
}
#editor ngx-monaco-editor {
  display: block;
  width: 100%;
  height: 100%;
}

If the editor lives inside something that lazily renders/shows its content (e.g. a mat-tab), automaticLayout: true (set above in editorOptions) ensures Monaco re-measures itself once the container becomes visible/resized.

Wiring Brick Plugins

If you also use @myrmidon/cadmus-text-ed (see its README for plugin configuration), bind keyboard shortcuts to plugin selectors in onEditorInit:

private async applyEdit(selector: string) {
  if (!this._editor) {
    return;
  }
  const selection = this._editor.getSelection();
  const text = selection ? this._editor.getModel()!.getValueInRange(selection) : '';

  const result = await this._editService.edit({ selector, text });

  this._editor.executeEdits('my-source', [
    {
      range: selection!,
      text: result.text,
      forceMoveMarkers: true,
    },
  ]);
}

public onEditorInit(event: EditorInitializedEvent) {
  this._editor = event.editor;
  this._editor.focus();

  if (this._editorBindings) {
    Object.keys(this._editorBindings).forEach((key) => {
      const n = parseInt(key, 10);
      this._editor!.addCommand(n, () => {
        this.applyEdit(this._editorBindings![key as any]);
      });
    });
  }
}

Wiring Markdown Preview

For a live Markdown preview alongside the editor, use marked + DomSanitizer rather than a separate Markdown-rendering wrapper:

npm i marked
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { marked } from 'marked';

public readonly previewHtml = signal<SafeHtml>('');
private readonly _sanitizer = inject(DomSanitizer);

private updatePreview(): void {
  const html = marked.parse(this.text.value || '', { async: false }) as string;
  this.previewHtml.set(this._sanitizer.bypassSecurityTrustHtml(html));
}

Call updatePreview() from text.valueChanges (debounced) and whenever the control’s value is set programmatically:

this.text.valueChanges.pipe(debounceTime(50)).subscribe(() => {
  this.updatePreview();
});

Template:

<div class="preview" [innerHTML]="previewHtml()"></div>

If publishing this in an Angular library, add marked to the library’s dependencies and to allowedNonPeerDependencies in ng-package.json (it’s a non-Angular runtime dependency).