Reputation: 261
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
Reputation: 92347
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
<user-editor>
tag and by add class UserEditorComponent to proper imports section of you module or other standalone componentwindows
object)Caution
Motivation
customTheme
and customLanguage
NG2012: Component imports contains a ModuleWithProviders value, likely the result of a 'Module.forRoot()'-style call
. When we use .forRoot()
import in standalone compoent.NullInjectorError: NullInjectorError: No provider for InjectionToken NGX_MONACO_EDITOR_CONFIG!
at runtime when .forRoot()
is not used at allUpvotes: 2
Reputation: 3312
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-loader
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;
};
"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/.*)'],
Upvotes: 10
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
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
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
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