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?

Jacob Stamm
  • 1,660
  • 1
  • 29
  • 53
Tsvetelin
  • 221
  • 1
  • 3
  • 7
  • 1
    I am looking for the same thing. I tried following this article: https://ngohungphuc.wordpress.com/2019/01/08/integrate-monaco-editor-with-angular/ but I am getting an argument error on the window.require function – Jiren Feb 13 '22 at 16:14
  • 1
    For me it's working as expect - following just steps from here https://github.com/atularen/ngx-monaco-editor#readme , but concern for feature support – Tsvetelin Feb 14 '22 at 17:11
  • @jiren please check my proposed solution. Maybe this works for you. – moritz.vieli Feb 22 '22 at 07:19
  • @Tsvetelin maybe you can change your question to be more general for any Angular version. – moritz.vieli Feb 22 '22 at 07:19

5 Answers5

14

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>
moritz.vieli
  • 1,747
  • 1
  • 14
  • 17
  • 1
    I've been trying to implement this and have been unable to get it to work. I copied the library almost verbatim (I used real types instead of shoving `any` everywhere) _except_ where we load the actual Monaco code. I ended up using `import * as monacoRuntime from 'monaco-editor'; export var monaco: typeof import('monaco-editor') = monacoRuntime; ` to load it in a type-safe way. It completely breaks Monaco's layout engine. I don't want to rely on _another_ package just to load Monaco. I think the only acceptable answer here is one which doesn't rely on a third party package. – b4ux1t3 Jul 08 '22 at 14:55
  • Not sure, whether I understood your comment. This solution does not rely on a third party package, only monaco-editor. Not sure, however, why your layout is completely broken. The only difference to the presented code is the typesafe way to load it? – moritz.vieli Jul 11 '22 at 12:33
  • The file that Monaco vendors, `loader.js`, uses `require.js` (as far as I can tell) to dynamically `require` the requisite Monaco files, which get delivered as separate assets/downloads. The thing is, that defeats the purpose of a bundled application, _and_ it adds another (implicit and unmanageable) dependency (`require.js`). Didn't mean to be too negative, I just feel like any solution that doesn't result in a single, coherent Angular bundle is, at best, incomplete. Monaco is embedded in VSCode, which means there has to be a way to use it without requiring separate requests to a back-end. – b4ux1t3 Jul 21 '22 at 18:31
  • @b4ux1t3 ah, now I see what you mean. This is a fair point, I agree. Not sure, how this could be solved. The Monaco editor for VSCode seems to be built completely different (not even based on JS?), so maybe they don't have those kinds of issues. – moritz.vieli Jul 24 '22 at 11:54
  • 3
    Great solution overall! I just needed to add few things to get it to work. In your example you never call `monacoEditorService.load()` which is needed to initiate the loading. Also, `initMonaco` needs to be called (duh). The editor appears but looks strange so it was necessary to add the monaco styles to the `angular.json` as well. – a-ctor Jul 25 '22 at 20:20
  • @a-ctor true, I added initMonaco in the afterViewInit() method to the post. monacoEditorService.load() is called in the app.component.ts, as mentioned. Strange, I don't have to add the styles in angular.json. What did you add exactly? – moritz.vieli Jul 27 '22 at 06:17
  • So, @a-ctor probably added the `node_modules/monaco-editor/min/vs/editor/editor.main.css` file. I just tried doing exactly that. You can use the type-safe import method I described in a previous comment, and everything works just fine. No more calling back to the back-end to load Monaco! It balloons the assembly size, but, given that issue is just hidden behind a back-end call with the loader method, that's a reasonable tradeoff. – b4ux1t3 Jul 27 '22 at 15:44
  • @moritz.vieli as @b4ux1t3 suspected I added the `editor.main.css`. Maybe my css setup messed up monacos setup. But adding the file fixed it for me. – a-ctor Jul 28 '22 at 11:17
  • 1
    Great answer. To get this working with Angular 14 follow this https://github.com/angular/angular-cli/issues/23273. Just remove dependency from package.json and add the package via assets [] in angular.json – rossco Aug 17 '22 at 01:07
7

Posting an answer here that uses a custom webpack configuration with the Monaco Editor Webpack Loader Plugin instead of a 3rd party wrapper lib. Not tested in Angular 15+, but I'm not aware of any reasons why this approach would not work. I'll be migrating our app to v15 in the next few months and I'll update this post to confirm the approach works at that time.

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)

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'],
    },
    {
      test: /\.ttf$/,
      use: ['file-loader'],
    }
  );
  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
this.codeEditorInstance = editor.create(...
codeEditorInstance!: editor.IStandaloneCodeEditor;

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/.*)'],

schankam
  • 10,778
  • 2
  • 15
  • 26
Chris Newman
  • 3,152
  • 1
  • 16
  • 17
  • 1
    I've tested your solution on Angular 15 and I updated the proposed answer with the tweaks I had to do to be able to run it. Most of the changes have been added to the custom webpack builder. – schankam Apr 07 '23 at 11:56
  • Was able to integrate and run monaco-yaml but getting error for codicon.ttf, rather that refering url its using file path to access the codicon.ttf(Security Error: Content at http://localhost:4200/ may not load or link to file:///home/kprasad/kpworkspace/kp_17042023/angular-monaco-example/node_modules/monaco-editor/esm/vs/base/browser/ui/codicons/codicon/codicon.ttf.). the sample application with issue is pushed to https://github.com/kprasad99/angular-monaco-example – Karthik Prasad May 25 '23 at 15:37
3

Monaco editor for angular

In project directory:

npm i --legacy-peer-deps ngx-monaco-editor
npm i --legacy-peer-deps monaco-editor

update angular.json

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

component.html

<ngx-monaco-editor [options]="codeEditorOptions" [(ngModel)]="code"></ngx-monaco-editor>

component.ts

Component {
    code: string = '';
    codeEditorOptions = {
      theme: 'vs-dark',
      language: 'json',
      automaticLayout: true
    };
    ...
}

module.ts

import {MonacoEditorModule} from 'ngx-monaco-editor';

...

@NgModule({
  ...
  imports: [
    ...,
    MonacoEditorModule.forRoot()
    ...
  ],
  ...
})

It worked for me :)

rrkjonnapalli
  • 357
  • 1
  • 3
  • 7
1

Currently the original project does not support Angular 13.

On its Github Issues page though there is a fork that does work with Angular 13.

Issue:

https://github.com/atularen/ngx-monaco-editor/issues/248

Post author:

dmlukichev

Post author's forked npm package:

https://www.npmjs.com/package/@dmlukichev/ngx-monaco-editor

downvoteit
  • 588
  • 2
  • 7
  • 12
0

I was using ngx-monaco-editor in older versions of Angular. The lib was not exactly what I wanted to use the monaco editor lib for even though the lib is very good. So I wrote an alternative implementation for Angular 13 with the latest releases of monaco-editor https://github.com/cisstech/nge

mcisse
  • 1