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?
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>
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
npm i -D @angular-builders/custom-webpack monaco-editor-webpack-plugin style-loader css-loaderthere'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;
};
"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"
}
},
...
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,
});
}
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:
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:
// OLD
// import * as monaco from 'monaco-editor'
// NEW
import { editor, languages } from 'monaco-editor/esm/vs/editor/editor.api';
// OLD
// codeEditorInstance!: monaco.editor.IStandaloneCodeEditor;
// this.codeEditorInstance = monaco.editor.create(...
// NEW
codeEditorInstance!: editor.IStandaloneCodeEditor;
this.codeEditorInstance = editor.create(...
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/.*)'],
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With