Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement Monaco editor in Angular 13

What is the best option to implement Monaco editor in Angular 13? I have seen ngx-monaco-editor, but last update is from 9 months and it’s bumped to Angular 12, also Monaco version there is 0.20.0 (11.02.2020), very old :( Is there another way to use it in Angular 13?

like image 973
Tsvetelin Avatar asked Dec 12 '25 22:12

Tsvetelin


2 Answers

This is how I solved it, heavily inspired by atularen/ngx-monaco-editor. But I also don't want to rely on this dependency. There might be better solutions.

npm install monaco-editor

angular.json:

            "assets": [
              ...
              {
                "glob": "**/*",
                "input": "node_modules/monaco-editor",
                "output": "assets/monaco-editor"
              }
            ],

monaco-editor-service.ts:

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class MonacoEditorService {
  loaded: boolean = false;

  public loadingFinished: Subject<void> = new Subject<void>();

  constructor() {}

  private finishLoading() {
    this.loaded = true;
    this.loadingFinished.next();
  }

  public load() {
    // load the assets

    const baseUrl = './assets' + '/monaco-editor/min/vs';

    if (typeof (<any>window).monaco === 'object') {
      this.finishLoading();
      return;
    }

    const onGotAmdLoader: any = () => {
      // load Monaco
      (<any>window).require.config({ paths: { vs: `${baseUrl}` } });
      (<any>window).require([`vs/editor/editor.main`], () => {
        this.finishLoading();
      });
    };

    // load AMD loader, if necessary
    if (!(<any>window).require) {
      const loaderScript: HTMLScriptElement = document.createElement('script');
      loaderScript.type = 'text/javascript';
      loaderScript.src = `${baseUrl}/loader.js`;
      loaderScript.addEventListener('load', onGotAmdLoader);
      document.body.appendChild(loaderScript);
    } else {
      onGotAmdLoader();
    }
  }
}

Now call monacoEditorService.load(), as soon as you need the editor (in my case it's called in app.component.ts in the constructor, to make the editor always available and already preload it).

Now, you can create editors as you please, but make sure to not create them, before Monaco is loaded yet. Like this:

monaco-editor.component.ts

import ...

declare var monaco: any;

@Component({
  selector: 'app-monaco-editor',
  templateUrl: './monaco-editor.component.html',
  styleUrls: ['./monaco-editor.component.scss'],
})
export class MonacoEditorComponent implements OnInit, OnDestroy, AfterViewInit {
  public _editor: any;
  @ViewChild('editorContainer', { static: true }) _editorContainer: ElementRef;

  private initMonaco(): void {
    if(!this.monacoEditorService.loaded) {
      this.monacoEditorService.loadingFinished.pipe(first()).subscribe(() => {
        this.initMonaco();
      });
      return;
    }

    this._editor = monaco.editor.create(
      this._editorContainer.nativeElement,
      options
    );
  }

  ngAfterViewInit(): void {
    this.initMonaco();
  }

There are most probably more elegant solutions than a boolean flag and this subject.

monaco-editor.component.html

Make sure, there is a div in the component, like this:

<div class="editor-container" #editorContainer></div>
like image 117
moritz.vieli Avatar answered Dec 15 '25 14:12

moritz.vieli


Updated 11/27/23

Posting an answer here that uses a custom webpack configuration with the Monaco Editor Webpack Loader Plugin instead of a 3rd party wrapper lib. Migrated an existing app to Angular 15 (which uses webpack 5) with this approach.

The pesky "not allowed to load local resource" errors caused by codicon.ttf were fixed by webpack 5 loader config (see step 2) and downgrading css-loader to ^5.2.7

1) Install dependencies

  • double check the monaco version matrix
  • npm i -D @angular-builders/custom-webpack monaco-editor-webpack-plugin style-loader css-loader

2) Create a custom webpack config (basic) - Update for webpack 5

there's more than one way to do this but I opted for typescript and exporting a default function (so I could console log the entire config). I keep this in the root directory so its easy to reference in angular.json

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
import * as webpack from 'webpack';

export default (config: webpack.Configuration) => {
  config?.plugins?.push(new MonacoWebpackPlugin());
  // Remove the existing css loader rule
  const cssRuleIdx = config?.module?.rules?.findIndex((rule: any) =>
    rule.test?.toString().includes(':css')
  );
  if (cssRuleIdx !== -1) {
    config?.module?.rules?.splice(cssRuleIdx!, 1);
  }
  config?.module?.rules?.push(
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader'],
    },
    // webpack 4 or lower
    //{
    //  test: /\.ttf$/,
    //  use: ['file-loader'],
    //}

    // webpack 5
    { 
      test: /\.ttf$/,
      type: 'asset/resource'
    }
  );
  return config;
};

3) angular.json modifications

  • modify architect.build.builder to use custom-webpack builder
  • add customWebpackConfig to architect.build.builder.options
  • modify architect.build.builder.options.styles to include monaco editor css
  • update ENTIRE architect.serve block to use custom-webpack builder
"my-application": {
  ...
  "architect": {
    "build": {
      "builder": "@angular-builders/custom-webpack:browser",
      ...
      "options": {
        "customWebpackConfig": {
          "path": "./custom-webpack.config.ts"
        },
        ...
        "styles": [
          "node_modules/monaco-editor/min/vs/editor/editor.main.css", 
          "apps/my-application/src/styles.scss"
        ]
        ...
      }
      ...
    },
    "serve": {
      "builder": "@angular-builders/custom-webpack:dev-server",
      "options": {
        "browserTarget": "my-application:build:development"
      }
    },
    ...

4) now you can create an editor component

import * as monaco from 'monaco-editor';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';

@Component({
  selector: 'my-application-editor',
  template: `
    <div
      style="height:100%"
      #editorContainer
    ></div>
  `,
  styleUrls: ['./editor.component.scss'],
})
export class EditorComponent implements OnInit {
  @ViewChild('editorContainer', { static: true }) _editorContainer!: ElementRef;
  codeEditorInstance!: monaco.editor.IStandaloneCodeEditor;

  constructor() {}

  ngOnInit() {
    this.codeEditorInstance = monaco.editor.create(this._editorContainer.nativeElement, {
      theme: 'vs',
      wordWrap: 'on',
      wrappingIndent: 'indent',
      language: 'typescript',
      // minimap: { enabled: false },
      automaticLayout: true,
    });
  }

5) Bonus: Optimizations

The webpack plugin allows you to shrink your final bundle size by removing parts of monaco that you don't use. Two things to keep in mind:

  • The plugin configuration is not very well documented (it was a bit of trial and error to figure if commenting something out accidently removed something critical for our features.)
  • Per the documentation, you will need to be very mindful of all import statements regarding monaco. It does not do a good enough job calling attention to this detail imo, but even a single import * as monaco from 'monaco-editor in a component or service will include the entirety of the library, thus negating your efforts to tree shake stuff.

Here is what we ended up using for our app (pass config object to MonacoEditorWebpackPlugin in custom webpack ts):

new MonacoEditorWebpackPlugin({
  // a ton of languages are lazily loaded by default, but we dont use any of them
  languages: [],
  // we can disable features that we end up not needing/using
  features: [
    'accessibilityHelp',
    'anchorSelect',
    'bracketMatching',
    // 'browser',
    'caretOperations',
    'clipboard',
    // 'codeAction',
    // 'codelens',
    // 'colorPicker',
    // 'comment',
    'contextmenu',
    'copyPaste',
    'cursorUndo',
    // 'dnd',
    // 'documentSymbols',
    // 'dropIntoEditor',
    // 'find',
    // 'folding',
    // 'fontZoom',
    'format',
    // 'gotoError',
    // 'gotoLine',
    // 'gotoSymbol',
    'hover',
    // 'iPadShowKeyboard',
    // 'inPlaceReplace',
    'indentation',
    // 'inlayHints',
    'inlineCompletions',
    // 'inspectTokens',
    'lineSelection',
    'linesOperations',
    // 'linkedEditing',
    // 'links',
    // 'multicursor',
    // 'parameterHints',
    // 'quickCommand',
    // 'quickHelp',
    // 'quickOutline',
    // 'readOnlyMessage',
    // 'referenceSearch',
    // 'rename',
    'smartSelect',
    // 'snippet',
    'stickyScroll',
    // 'suggest',
    // 'toggleHighContrast',
    'toggleTabFocusMode',
    'tokenization',
    'unicodeHighlighter',
    // 'unusualLineTerminators',
    // 'viewportSemanticTokens',
    'wordHighlighter',
    'wordOperations',
    'wordPartOperations',
  ],
})

and the relevant updates in the component would be:

  • update imports
// OLD
// import * as monaco from 'monaco-editor'
// NEW
import { editor, languages } from 'monaco-editor/esm/vs/editor/editor.api';

  • update editor creation and typings
// OLD
// codeEditorInstance!: monaco.editor.IStandaloneCodeEditor;
// this.codeEditorInstance = monaco.editor.create(...
// NEW
codeEditorInstance!: editor.IStandaloneCodeEditor;
this.codeEditorInstance = editor.create(...

6) Bonus: Troubleshooting Jest Unit Testing

If like me, youre using NX which comes with Jest configured out of the box, you may need to add transformIgnorePatterns to jest.config.js per this answer

transformIgnorePatterns: ['node_modules/(?!monaco-editor/esm/.*)'],

like image 36
Chris Newman Avatar answered Dec 15 '25 16:12

Chris Newman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!