Tsvetelin
Tsvetelin

Reputation: 261

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?

Upvotes: 16

Views: 22577

Answers (6)

Kamil Kiełczewski
Kamil Kiełczewski

Reputation: 92347

Angular 17 - standalone component

Following standalone solution is based on pure js monaco lib

npm install monaco-editor

copy lib files from node_modules/monako-editor to e.g. /assets/lib/monaco-editor, by adding to angular.json key ...architect.build.options.assets following code:

{ 
    "glob": "**/*", 
    "input": "node_modules/monaco-editor", 
    "output": "/assets/lib/monaco-editor/"
}

And create user-editor.component.ts file with standalone component

import { filter, interval, Observable, ReplaySubject, take } from 'rxjs';
import { Component, ElementRef, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
    template: '<div #editorContainer class="editorContainer"></div>',
    styles: ['.editorContainer {width: 500px; height: 200px }'],
    selector: 'user-editor',
    imports: [CommonModule],
    standalone: true,
})
export class UserEditorComponent {
    private editor?: any; // editor instance to e.g. read value by this.editor.getValue()

    @ViewChild('editorContainer', { static: true }) set editorContainer(container: ElementRef) {
        this.loadMonacoScripts().subscribe((monaco) => {
            monaco.editor.defineTheme('customTheme', {
                rules: [{ token: 'custom-number', foreground: '#ff0000' }],
                colors: { 'editor.foreground': '#ffff00' },
                base: 'vs-dark',
                inherit: true,
            });

            monaco.languages.register({ id: 'customLanguage' });
            monaco.languages.setMonarchTokensProvider('customLanguage', {
                tokenizer: { root: [[/\b\d+\b/, 'custom-number']] },
            });

            this.editor = monaco.editor.create(container.nativeElement, {
                value: 'Alice has 666 keys\n',
                language: 'customLanguage',
                theme: 'customTheme',
            });
        });
    }

    private loadMonacoScripts(): Observable<any> {
        const loader = new ReplaySubject<any>(1);

        if ((window as any).monacoEditorLoading) {
            interval(200)
                .pipe(filter((_) => (window as any).monaco), take(1))
                .subscribe((_) => {
                    loader.next((window as any).monaco);
                    loader.complete();
                });
            return loader;
        }

        (window as any).monacoEditorLoading = true;

        const script = document.createElement('script');
        script.src = '/assets/lib/monaco-editor/min/vs/loader.js';
        script.type = 'text/javascript';
        script.async = true;

        script.onload = () => {
            (window as any).require.config({ paths: { vs: '/assets/lib/monaco-editor/min/vs' } });
            (window as any).require(['vs/editor/editor.main'], () => {
                loader.next((window as any).monaco);
                loader.complete();
            });
        };

        document.body.appendChild(script);

        return loader;
    }
}

Usage and How it works

  • use it in your template by <user-editor> tag and by add class UserEditorComponent to proper imports section of you module or other standalone component
  • you can define custom template and custom language
  • you can use many instances of compoennt at the same time, but monaco lib will loaded only once (we use global flag monacoEditorLoading in browser windows object)
  • no additional dependencies other than monaco-editor needed

Caution

  • for muliti-instances of editor on one page, the customTheme and customLanguage will be shared between instances (and you actually unable to have at the same time two editors with different custom langage and theme) - this is probably monaco-editor bug
  • maybe wrapping the js editor in a web-component (in similar way like here) will solve this problem - this is something to check

Motivation

  • angular dropp support for webpack (or plan it)
  • ngx-monaco-editor and v2 not allow (at least in easy strightforwad way) to use customTheme and customLanguage
  • v2 has cause problem NG2012: Component imports contains a ModuleWithProviders value, likely the result of a 'Module.forRoot()'-style call. When we use .forRoot() import in standalone compoent.
  • v2 cause NullInjectorError: NullInjectorError: No provider for InjectionToken NGX_MONACO_EDITOR_CONFIG! at runtime when .forRoot() is not used at all
  • @materia-ui/ngx-monaco-editor has no updates since 3 years
  • there is not standalone solution in other answers yet

Upvotes: 2

Chris Newman
Chris Newman

Reputation: 3312

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

Upvotes: 10

mcisse
mcisse

Reputation: 1

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

Upvotes: 0

moritz.vieli
moritz.vieli

Reputation: 1807

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>

Upvotes: 17

J011195
J011195

Reputation: 733

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

Upvotes: 1

rrkjonnapalli
rrkjonnapalli

Reputation: 437

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 :)

Upvotes: 3

Related Questions