├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.routes.ts │ ├── code-editor-demo │ │ ├── file-node.ts │ │ ├── code-editor-demo.component.spec.ts │ │ ├── code-editor-demo.component.html │ │ ├── code-editor-demo.component.scss │ │ ├── file-database.ts │ │ └── code-editor-demo.component.ts │ ├── app.component.ts │ ├── app.component.spec.ts │ └── app.config.ts ├── favicon.ico ├── tsconfig.app.json ├── main.ts ├── tsconfig.spec.json ├── styles.css ├── .browserslistrc └── index.html ├── projects ├── multiple-editors │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── styles.css │ │ ├── favicon.ico │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ └── app.config.ts │ │ ├── main.ts │ │ └── index.html │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── .eslintrc.json └── code-editor │ ├── src │ ├── lib │ │ ├── code-editor │ │ │ ├── code-editor.component.html │ │ │ ├── code-editor.component.css │ │ │ ├── code-editor.component.spec.ts │ │ │ └── code-editor.component.ts │ │ ├── editor-settings.ts │ │ ├── models │ │ │ └── code.model.ts │ │ ├── services │ │ │ ├── code-editor.service.spec.ts │ │ │ ├── json-defaults.service.ts │ │ │ ├── javascript-defaults.service.ts │ │ │ ├── typescript-defaults.service.ts │ │ │ └── code-editor.service.ts │ │ ├── code-editor.module.ts │ │ └── workers │ │ │ └── typings-worker.js │ └── public_api.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── .eslintrc.json │ ├── LICENSE │ └── README.md ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── pull-request.yml ├── .prettierrc ├── .vscode └── settings.json ├── .editorconfig ├── .gitignore ├── cspell.json ├── .eslintrc.json ├── LICENSE ├── tsconfig.json ├── package.json ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngstack/code-editor/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: DenysVuika 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 150 5 | } 6 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/code-editor/code-editor.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngstack/code-editor/HEAD/projects/multiple-editors/src/favicon.ico -------------------------------------------------------------------------------- /projects/multiple-editors/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | } 4 | 5 | .container > * { 6 | flex: 1; 7 | } 8 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/code-editor/code-editor.component.css: -------------------------------------------------------------------------------- 1 | .editor { 2 | width: 100%; 3 | height: inherit; 4 | min-height: 200px; 5 | } 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/editor-settings.ts: -------------------------------------------------------------------------------- 1 | export interface CodeEditorSettings { 2 | editorVersion?: string; 3 | baseUrl?: string; 4 | typingsWorkerUrl?: string; 5 | } 6 | -------------------------------------------------------------------------------- /projects/code-editor/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/@ngstack/code-editor", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/code-editor/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[javascript]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[markdown]": { 9 | "editor.formatOnSave": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts" 9 | ], 10 | "include": [ 11 | "src/**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); 6 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.spec.ts", 11 | "**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/models/code.model.ts: -------------------------------------------------------------------------------- 1 | export interface CodeModel { 2 | language: string; 3 | value: string; 4 | uri: string; 5 | 6 | dependencies?: Array; 7 | schemas?: Array<{ 8 | uri: string; 9 | schema: Object; 10 | }>; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { CodeEditorDemoComponent } from './code-editor-demo/code-editor-demo.component'; 3 | 4 | export const routes: Routes = [ 5 | { 6 | path: '', 7 | component: CodeEditorDemoComponent 8 | } 9 | ]; 10 | -------------------------------------------------------------------------------- /projects/code-editor/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.spec.ts", 11 | "**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Multiple Editors Demo

2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/code-editor-demo/file-node.ts: -------------------------------------------------------------------------------- 1 | import { CodeModel } from '@ngstack/code-editor'; 2 | 3 | export enum FileNodeType { 4 | file = 'file', 5 | folder = 'folder', 6 | } 7 | 8 | export class FileNode { 9 | children?: FileNode[]; 10 | name: string; 11 | type: FileNodeType; 12 | 13 | code?: CodeModel; 14 | } 15 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | body { margin: 0; } 4 | 5 | html, 6 | body, 7 | app-root, 8 | app-code-editor-demo { 9 | display: flex; 10 | flex-direction: column; 11 | flex: 1; 12 | height: 100%; 13 | overflow: hidden; 14 | min-height: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | standalone: true, 7 | imports: [RouterOutlet], 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.css'] 10 | }) 11 | export class AppComponent {} 12 | -------------------------------------------------------------------------------- /projects/code-editor/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/multiple-editors/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/multiple-editors/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MultipleEditors 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /projects/code-editor/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of code-editor 3 | */ 4 | 5 | export * from './lib/services/code-editor.service'; 6 | export * from './lib/services/typescript-defaults.service'; 7 | export * from './lib/services/javascript-defaults.service'; 8 | 9 | export * from './lib/models/code.model'; 10 | 11 | export * from './lib/code-editor/code-editor.component'; 12 | 13 | export * from './lib/code-editor.module'; 14 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Demo Application 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | cache: 'npm' 19 | node-version: 20 20 | - name: Test 21 | run: | 22 | npm ci && \ 23 | npm run lint && \ 24 | npm run test:ci && \ 25 | npm run build:prod 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(waitForAsync(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | declarations: [AppComponent] 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', waitForAsync(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | .nx 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.angular/cache 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CodeEditorComponent, CodeModel } from '@ngstack/code-editor'; 3 | import { RouterOutlet } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | standalone: true, 8 | imports: [RouterOutlet, CodeEditorComponent], 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.css'] 11 | }) 12 | export class AppComponent { 13 | file1: CodeModel = { 14 | language: 'text', 15 | value: 'left editor', 16 | uri: 'left.txt' 17 | }; 18 | 19 | file2: CodeModel = { 20 | language: 'text', 21 | value: 'right editor', 22 | uri: 'right.txt' 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /projects/multiple-editors/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideCodeEditor } from '@ngstack/code-editor'; 3 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 4 | 5 | export const appConfig: ApplicationConfig = { 6 | providers: [ 7 | provideZoneChangeDetection({ eventCoalescing: true }), 8 | // provideRouter(routes), 9 | provideAnimationsAsync(), 10 | provideCodeEditor({ 11 | // editorVersion: '0.46.0', 12 | // use local Monaco installation 13 | baseUrl: 'assets/monaco', 14 | // use local Typings Worker 15 | typingsWorkerUrl: 'assets/workers/typings-worker.js' 16 | }) 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/services/code-editor.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { CodeEditorService } from './code-editor.service'; 4 | import { TypescriptDefaultsService } from './typescript-defaults.service'; 5 | import { JavascriptDefaultsService } from './javascript-defaults.service'; 6 | 7 | describe('CodeEditorService', () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [ 11 | CodeEditorService, 12 | TypescriptDefaultsService, 13 | JavascriptDefaultsService 14 | ] 15 | }); 16 | }); 17 | 18 | it( 19 | 'should be created', 20 | inject([CodeEditorService], (service: CodeEditorService) => { 21 | expect(service).toBeTruthy(); 22 | }) 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "ignorePaths": ["src/app/code-editor-demo/file-database.ts"], 5 | "words": [ 6 | "classlist", 7 | "Denys", 8 | "guid", 9 | "hammerjs", 10 | "Helvetica", 11 | "Injectable", 12 | "jsonp", 13 | "Neue", 14 | "ngstack", 15 | "pdfjs", 16 | "polyfill", 17 | "polyfills", 18 | "Segoe", 19 | "tooltip", 20 | "tooltips", 21 | "Truthy", 22 | "uncomment", 23 | "unindent", 24 | "unshare", 25 | "validators", 26 | "Webstorm", 27 | "xpath", 28 | "UNPATCHED" 29 | ], 30 | "flagWords": [], 31 | "dictionaries": [ 32 | "en-gb", 33 | "html" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | import { provideCodeEditor } from '@ngstack/code-editor'; 4 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 5 | import { routes } from './app.routes'; 6 | 7 | export const appConfig: ApplicationConfig = { 8 | providers: [ 9 | provideZoneChangeDetection({ eventCoalescing: true }), 10 | provideRouter(routes), 11 | provideAnimationsAsync(), 12 | provideCodeEditor({ 13 | // editorVersion: '0.46.0', 14 | // use local Monaco installation 15 | baseUrl: 'assets/monaco', 16 | // use local Typings Worker 17 | typingsWorkerUrl: 'assets/workers/typings-worker.js' 18 | }) 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /projects/code-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngstack/code-editor", 3 | "description": "Code editor component for Angular applications.", 4 | "version": "9.0.0", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Denys Vuika", 8 | "email": "denys.vuika@gmail.com", 9 | "url": "https://denys.dev" 10 | }, 11 | "keywords": [ 12 | "angular", 13 | "monaco", 14 | "editor", 15 | "code" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ngstack/code-editor.git" 20 | }, 21 | "homepage": "https://github.com/ngstack/code-editor", 22 | "bugs": { 23 | "url": "https://github.com/ngstack/code-editor/issues" 24 | }, 25 | "peerDependencies": { 26 | "@angular/common": ">=17.1.1", 27 | "@angular/core": ">=17.1.1" 28 | }, 29 | "dependencies": { 30 | "tslib": "^2.5.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/code-editor/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "projects/code-editor/tsconfig.lib.json", 10 | "projects/code-editor/tsconfig.spec.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "rules": { 15 | "@angular-eslint/directive-selector": [ 16 | "error", 17 | { 18 | "type": "attribute", 19 | "prefix": "lib", 20 | "style": "camelCase" 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "lib", 28 | "style": "kebab-case" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "files": ["*.html"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /projects/multiple-editors/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "projects/multiple-editors/tsconfig.app.json", 10 | "projects/multiple-editors/tsconfig.spec.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "rules": { 15 | "@angular-eslint/directive-selector": [ 16 | "error", 17 | { 18 | "type": "attribute", 19 | "prefix": "app", 20 | "style": "camelCase" 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "app", 28 | "style": "kebab-case" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "files": ["*.html"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/app/code-editor-demo/code-editor-demo.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { CodeEditorDemoComponent } from './code-editor-demo.component'; 4 | import { provideCodeEditor } from '@ngstack/code-editor'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('CodeEditorDemoComponent', () => { 8 | let component: CodeEditorDemoComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [BrowserAnimationsModule, CodeEditorDemoComponent], 14 | providers: [provideCodeEditor()] 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(CodeEditorDemoComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/recommended", 13 | "plugin:@angular-eslint/template/process-inline-templates" 14 | ], 15 | "rules": { 16 | "@angular-eslint/component-selector": [ 17 | "error", 18 | { 19 | "prefix": "app", 20 | "style": "kebab-case", 21 | "type": "element" 22 | } 23 | ], 24 | "@angular-eslint/directive-selector": [ 25 | "error", 26 | { 27 | "prefix": "app", 28 | "style": "camelCase", 29 | "type": "attribute" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "files": ["*.html"], 36 | "extends": ["plugin:@angular-eslint/template/recommended"], 37 | "rules": {} 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 Denys Vuika 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "strict": false, 6 | "noImplicitOverride": true, 7 | "noPropertyAccessFromIndexSignature": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "skipLibCheck": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "baseUrl": "./", 14 | "module": "ES2022", 15 | "sourceMap": true, 16 | "declaration": false, 17 | "moduleResolution": "bundler", 18 | "experimentalDecorators": true, 19 | "target": "ES2022", 20 | "typeRoots": [ 21 | "node_modules/@types" 22 | ], 23 | "lib": [ 24 | "ES2022", 25 | "dom" 26 | ], 27 | "paths": { 28 | "@ngstack/code-editor": [ 29 | "projects/code-editor/src/public_api.ts" 30 | ] 31 | }, 32 | "useDefineForClassFields": false 33 | }, 34 | "angularCompilerOptions": { 35 | "enableI18nLegacyMessageIdFormat": false, 36 | "strictInjectionParameters": true, 37 | "strictInputAccessModifiers": true, 38 | "strictTemplates": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/code-editor/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Denys Vuika 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/code-editor/code-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; 2 | 3 | import { CodeEditorComponent } from './code-editor.component'; 4 | import { CodeEditorService } from '../services/code-editor.service'; 5 | import { provideCodeEditor } from '../code-editor.module'; 6 | 7 | describe('CodeEditorComponent', () => { 8 | let component: CodeEditorComponent; 9 | let fixture: ComponentFixture; 10 | let codeEditorService: CodeEditorService; 11 | 12 | beforeEach(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [CodeEditorComponent], 15 | providers: [provideCodeEditor({ baseUrl: 'assets/monaco' })] 16 | }); 17 | 18 | codeEditorService = TestBed.inject(CodeEditorService); 19 | spyOn(codeEditorService, 'createEditor').and.stub(); 20 | spyOn(codeEditorService, 'createModel').and.returnValue({ 21 | onDidChangeContent: () => {}, 22 | dispose: () => {} 23 | } as any); 24 | 25 | fixture = TestBed.createComponent(CodeEditorComponent); 26 | component = fixture.componentInstance; 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('should create', () => { 31 | expect(component).toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/code-editor-demo/code-editor-demo.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | Example 22 | 23 | @for (file of files; track file) { 24 | {{file.name}} 25 | } 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/app/code-editor-demo/code-editor-demo.component.scss: -------------------------------------------------------------------------------- 1 | .demo-layout-root { 2 | height: 100%; 3 | min-height: 100%; 4 | min-width: 100%; 5 | width: 100%; 6 | flex-direction: column; 7 | box-sizing: border-box; 8 | display: flex; 9 | } 10 | 11 | .page-layout { 12 | flex-direction: row; 13 | box-sizing: border-box; 14 | display: flex; 15 | flex: 1 1 1e-9px; 16 | } 17 | 18 | .page-layout-content { 19 | flex: 1 1 100%; 20 | box-sizing: border-box; 21 | } 22 | 23 | .ngs-code-editor { 24 | display: flex; 25 | flex-direction: column; 26 | flex: 1; 27 | height: 100%; 28 | max-height: 100%; 29 | overflow: hidden; 30 | min-height: 0; 31 | 32 | .editor { 33 | border: 1px solid grey; 34 | // min-height: 400px; 35 | } 36 | } 37 | 38 | .file-tree-container { 39 | height: calc(100vh - 64px); 40 | overflow: hidden; 41 | } 42 | 43 | .file-tree { 44 | display: flex; 45 | flex-direction: column; 46 | flex: 1; 47 | height: 100%; 48 | max-height: 100%; 49 | overflow: hidden; 50 | min-height: 0; 51 | overflow: auto; 52 | 53 | &-invisible { 54 | display: none; 55 | } 56 | ul, 57 | li { 58 | margin-top: 0; 59 | margin-bottom: 0; 60 | list-style-type: none; 61 | } 62 | 63 | .mat-icon-button[disabled] { 64 | display: none; 65 | } 66 | 67 | .file-node-label { 68 | display: flex; 69 | cursor: pointer; 70 | user-select: none; 71 | 72 | &__icon { 73 | float: left; 74 | } 75 | 76 | &__text { 77 | float: left; 78 | margin-top: 4px; 79 | margin-left: 4px; 80 | white-space: nowrap; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/code-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CodeEditorComponent } from './code-editor/code-editor.component'; 4 | import { CodeEditorService, EDITOR_SETTINGS } from './services/code-editor.service'; 5 | import { CodeEditorSettings } from './editor-settings'; 6 | import { TypescriptDefaultsService } from './services/typescript-defaults.service'; 7 | import { JavascriptDefaultsService } from './services/javascript-defaults.service'; 8 | import { JsonDefaultsService } from './services/json-defaults.service'; 9 | 10 | export function setupEditorService(service: CodeEditorService) { 11 | return () => service.loadEditor(); 12 | } 13 | 14 | export function provideCodeEditor(settings?: CodeEditorSettings): Provider[] { 15 | return [ 16 | { provide: EDITOR_SETTINGS, useValue: settings }, 17 | CodeEditorService, 18 | TypescriptDefaultsService, 19 | JavascriptDefaultsService, 20 | JsonDefaultsService, 21 | { 22 | provide: APP_INITIALIZER, 23 | useFactory: setupEditorService, 24 | deps: [CodeEditorService], 25 | multi: true, 26 | }, 27 | ]; 28 | } 29 | 30 | /** @deprecated use `provideCodeEditor(settings)` instead */ 31 | @NgModule({ 32 | imports: [CommonModule, CodeEditorComponent], 33 | exports: [CodeEditorComponent], 34 | }) 35 | export class CodeEditorModule { 36 | static forRoot( 37 | settings?: CodeEditorSettings 38 | ): ModuleWithProviders { 39 | return { 40 | ngModule: CodeEditorModule, 41 | providers: [ 42 | { provide: EDITOR_SETTINGS, useValue: settings }, 43 | CodeEditorService, 44 | { 45 | provide: APP_INITIALIZER, 46 | useFactory: setupEditorService, 47 | deps: [CodeEditorService], 48 | multi: true, 49 | }, 50 | ], 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngstack/code-editor", 3 | "description": "Code editor component for Angular applications. ", 4 | "version": "9.0.0", 5 | "license": "MIT", 6 | "author": "Denys Vuika ", 7 | "scripts": { 8 | "start": "ng serve app --open", 9 | "build": "ng build app", 10 | "test": "ng test code-editor --browsers=ChromeHeadless", 11 | "lint": "ng lint", 12 | "build:lib": "ng build code-editor && cp -R projects/code-editor/src/lib/workers dist/@ngstack/code-editor", 13 | "build:prod": "ng build code-editor --configuration production && cp -R projects/code-editor/src/lib/workers dist/@ngstack/code-editor", 14 | "test:ci": "ng test code-editor --watch=false --browsers=ChromeHeadless" 15 | }, 16 | "private": true, 17 | "dependencies": { 18 | "@angular/animations": "^18.2.10", 19 | "@angular/cdk": "^18.1.1", 20 | "@angular/common": "^18.2.10", 21 | "@angular/compiler": "^18.2.10", 22 | "@angular/core": "^18.2.10", 23 | "@angular/forms": "^18.2.10", 24 | "@angular/material": "^18.2.9", 25 | "@angular/platform-browser": "^18.2.10", 26 | "@angular/platform-browser-dynamic": "^18.2.10", 27 | "@angular/router": "^18.2.10", 28 | "monaco-editor": "^0.50.0", 29 | "rxjs": "^7.8.0", 30 | "tslib": "^2.5.0", 31 | "zone.js": "~0.14.10" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "^18.2.11", 35 | "@angular-eslint/builder": "18.1.0", 36 | "@angular-eslint/eslint-plugin": "18.1.0", 37 | "@angular-eslint/eslint-plugin-template": "18.1.0", 38 | "@angular-eslint/schematics": "18.1.0", 39 | "@angular-eslint/template-parser": "18.1.0", 40 | "@angular/cli": "^18.2.11", 41 | "@angular/compiler-cli": "^18.2.10", 42 | "@angular/language-service": "^18.2.10", 43 | "@types/jasmine": "~5.1.4", 44 | "@typescript-eslint/eslint-plugin": "^7.17.0", 45 | "@typescript-eslint/parser": "^7.17.0", 46 | "eslint": "^8.57.0", 47 | "jasmine-core": "~5.2.0", 48 | "karma": "~6.4.3", 49 | "karma-chrome-launcher": "~3.2.0", 50 | "karma-coverage": "~2.2.0", 51 | "karma-jasmine": "~5.1.0", 52 | "karma-jasmine-html-reporter": "^2.1.0", 53 | "ng-packagr": "^18.1.0", 54 | "prettier": "^3.3.3", 55 | "typescript": "5.5.4" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/services/json-defaults.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CodeEditorService } from './code-editor.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class JsonDefaultsService { 8 | private monaco: any; 9 | 10 | constructor(codeEditorService: CodeEditorService) { 11 | codeEditorService.loaded.subscribe(event => { 12 | this.setup(event.monaco); 13 | }); 14 | } 15 | 16 | setup(monaco: any): void { 17 | if (!monaco) { 18 | return; 19 | } 20 | 21 | this.monaco = monaco; 22 | 23 | const defaults = monaco.languages.json.jsonDefaults; 24 | 25 | defaults.setDiagnosticsOptions({ 26 | validate: true, 27 | allowComments: true, 28 | schemas: [ 29 | ...defaults._diagnosticsOptions.schemas, 30 | { 31 | uri: 'http://myserver/foo-schema.json', 32 | // fileMatch: [id], 33 | // fileMatch: ['*.json'], 34 | schema: { 35 | type: 'object', 36 | properties: { 37 | p1: { 38 | enum: ['v1', 'v2'] 39 | }, 40 | p2: { 41 | $ref: 'http://myserver/bar-schema.json' 42 | } 43 | } 44 | } 45 | }, 46 | { 47 | uri: 'http://myserver/bar-schema.json', 48 | // fileMatch: [id], 49 | // fileMatch: ['*.json'], 50 | schema: { 51 | type: 'object', 52 | properties: { 53 | q1: { 54 | enum: ['x1', 'x2'] 55 | } 56 | } 57 | } 58 | } 59 | ] 60 | }); 61 | } 62 | 63 | addSchemas( 64 | id: string, 65 | definitions: Array<{ uri: string; schema: Object }> = [] 66 | ) { 67 | const defaults = this.monaco.languages.json.jsonDefaults; 68 | const options = defaults.diagnosticsOptions; 69 | 70 | const schemas: { [key: string]: Object } = {}; 71 | 72 | if (options && options.schemas && options.schemas.length > 0) { 73 | options.schemas.forEach(schema => { 74 | schemas[schema.uri] = schema; 75 | }); 76 | } 77 | 78 | for (const { uri, schema } of definitions) { 79 | schemas[uri] = { 80 | uri, 81 | schema, 82 | fileMatch: [id || '*.json'] 83 | }; 84 | } 85 | 86 | // console.log(schemas); 87 | // console.log(Object.values(schemas)); 88 | 89 | options.schemas = Object.values(schemas); 90 | defaults.setDiagnosticsOptions(options); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/services/javascript-defaults.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CodeEditorService, TypingsInfo } from './code-editor.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class JavascriptDefaultsService { 8 | private monaco: any; 9 | 10 | constructor(codeEditorService: CodeEditorService) { 11 | codeEditorService.loaded.subscribe(event => { 12 | this.setup(event.monaco); 13 | }); 14 | codeEditorService.typingsLoaded.subscribe(typings => { 15 | this.updateTypings(typings); 16 | }); 17 | } 18 | 19 | setup(monaco: any): void { 20 | if (!monaco) { 21 | return; 22 | } 23 | 24 | this.monaco = monaco; 25 | 26 | const defaults = monaco.languages.typescript.javascriptDefaults; 27 | 28 | defaults.setCompilerOptions({ 29 | target: monaco.languages.typescript.ScriptTarget.ES6, 30 | module: 'commonjs', 31 | allowNonTsExtensions: true, 32 | baseUrl: '.', 33 | paths: {} 34 | }); 35 | 36 | defaults.setMaximumWorkerIdleTime(-1); 37 | defaults.setEagerModelSync(true); 38 | 39 | /* 40 | defaults.setDiagnosticsOptions({ 41 | noSemanticValidation: false, 42 | noSyntaxValidation: false 43 | }); 44 | */ 45 | } 46 | 47 | updateTypings(typings: TypingsInfo) { 48 | if (typings) { 49 | this.addExtraLibs(typings.files); 50 | this.addLibraryPaths(typings.entryPoints); 51 | } 52 | } 53 | 54 | addExtraLibs(libs: Array<{ path: string; content: string }> = []): void { 55 | if (!this.monaco || !libs || libs.length === 0) { 56 | return; 57 | } 58 | 59 | const defaults = this.monaco.languages.typescript.javascriptDefaults; 60 | 61 | // undocumented API 62 | const registeredLibs = defaults.getExtraLibs(); 63 | 64 | libs.forEach(lib => { 65 | if (!registeredLibs[lib.path]) { 66 | // needs performance improvements, recreates its worker each time 67 | // defaults.addExtraLib(lib.content, lib.path); 68 | // undocumented API 69 | defaults._extraLibs[lib.path] = lib.content; 70 | } 71 | }); 72 | 73 | // undocumented API 74 | defaults._onDidChange.fire(defaults); 75 | } 76 | 77 | addLibraryPaths(paths: { [key: string]: string } = {}): void { 78 | if (!this.monaco) { 79 | return; 80 | } 81 | 82 | const defaults = this.monaco.languages.typescript.javascriptDefaults; 83 | const compilerOptions = defaults.getCompilerOptions(); 84 | compilerOptions.paths = compilerOptions.paths || {}; 85 | 86 | Object.keys(paths).forEach(key => { 87 | compilerOptions.paths[key] = [paths[key]]; 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/services/typescript-defaults.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CodeEditorService, TypingsInfo } from './code-editor.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class TypescriptDefaultsService { 8 | private monaco: any; 9 | 10 | constructor(codeEditorService: CodeEditorService) { 11 | codeEditorService.loaded.subscribe(event => { 12 | this.setup(event.monaco); 13 | }); 14 | codeEditorService.typingsLoaded.subscribe(typings => { 15 | this.updateTypings(typings); 16 | }); 17 | } 18 | 19 | setup(monaco: any): void { 20 | if (!monaco) { 21 | return; 22 | } 23 | 24 | this.monaco = monaco; 25 | 26 | const defaults = monaco.languages.typescript.typescriptDefaults; 27 | 28 | defaults.setCompilerOptions({ 29 | target: monaco.languages.typescript.ScriptTarget.ES6, 30 | module: 'commonjs', 31 | noEmit: true, 32 | noLib: true, 33 | emitDecoratorMetadata: true, 34 | experimentalDecorators: true, 35 | allowNonTsExtensions: true, 36 | declaration: true, 37 | lib: ['es2017', 'dom'], 38 | baseUrl: '.', 39 | paths: {} 40 | }); 41 | 42 | defaults.setMaximumWorkerIdleTime(-1); 43 | defaults.setEagerModelSync(true); 44 | 45 | /* 46 | defaults.setDiagnosticsOptions({ 47 | noSemanticValidation: true, 48 | noSyntaxValidation: true 49 | }); 50 | */ 51 | } 52 | 53 | updateTypings(typings: TypingsInfo) { 54 | if (typings) { 55 | this.addExtraLibs(typings.files); 56 | this.addLibraryPaths(typings.entryPoints); 57 | } 58 | } 59 | 60 | addExtraLibs(libs: Array<{ path: string; content: string }> = []): void { 61 | if (!this.monaco || !libs || libs.length === 0) { 62 | return; 63 | } 64 | 65 | const defaults = this.monaco.languages.typescript.typescriptDefaults; 66 | 67 | // undocumented API 68 | const registeredLibs = defaults.getExtraLibs(); 69 | 70 | libs.forEach(lib => { 71 | if (!registeredLibs[lib.path]) { 72 | // needs performance improvements, recreates its worker each time 73 | // defaults.addExtraLib(lib.content, lib.path); 74 | // undocumented API 75 | defaults._extraLibs[lib.path] = lib.content; 76 | } 77 | }); 78 | 79 | // undocumented API 80 | defaults._onDidChange.fire(defaults); 81 | } 82 | 83 | addLibraryPaths(paths: { [key: string]: string } = {}): void { 84 | if (!this.monaco) { 85 | return; 86 | } 87 | 88 | const defaults = this.monaco.languages.typescript.typescriptDefaults; 89 | const compilerOptions = defaults.getCompilerOptions(); 90 | compilerOptions.paths = compilerOptions.paths || {}; 91 | 92 | Object.keys(paths).forEach(key => { 93 | compilerOptions.paths[key] = [paths[key]]; 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/code-editor-demo/file-database.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { FileNode, FileNodeType } from './file-node'; 4 | 5 | const FILES_DATA: FileNode[] = [ 6 | { 7 | name: 'schema.sql', 8 | type: FileNodeType.file, 9 | code: { 10 | language: 'sql', 11 | uri: 'schema.sql', 12 | value: [ 13 | 'CREATE TABLE dbo.EmployeePhoto (', 14 | ' EmployeeId INT NOT NULL PRIMARY KEY,', 15 | ' Photo VARBINARY(MAX) FILESTREAM NULL,', 16 | ' MyRowGuidColumn UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL UNIQUE DEFAULT NEWID()', 17 | ');' 18 | ].join('\n') 19 | } 20 | }, 21 | { 22 | name: 'component.style.css', 23 | type: FileNodeType.file, 24 | code: { 25 | language: 'css', 26 | uri: 'component.style.css', 27 | value: [ 28 | 'html {', 29 | ' background-color: #e2e2e2;', 30 | ' margin: 0;', 31 | ' padding: 0;', 32 | '}', 33 | '', 34 | 'body {', 35 | ' background-color: #fff;', 36 | ' border-top: solid 10px #000;', 37 | ' color: #333;', 38 | ' font-size: .85em;', 39 | ' font-family: "Segoe UI","HelveticaNeue-Light", sans-serif;', 40 | ' margin: 0;', 41 | ' padding: 0;', 42 | '}' 43 | ].join('\n') 44 | } 45 | }, 46 | { 47 | name: 'json.json', 48 | type: FileNodeType.file, 49 | code: { 50 | language: 'json', 51 | uri: 'main.json', 52 | value: [ 53 | '{', 54 | ' "$schema": "http://custom/schema.json",', 55 | ' "type": "button"', 56 | '}' 57 | ].join('\n'), 58 | schemas: [ 59 | { 60 | uri: 'http://custom/schema.json', 61 | schema: { 62 | type: 'object', 63 | properties: { 64 | type: { 65 | enum: ['button', 'textbox'] 66 | } 67 | } 68 | } 69 | } 70 | ] 71 | } 72 | }, 73 | { 74 | name: 'javascript.js', 75 | type: FileNodeType.file, 76 | code: { 77 | language: 'javascript', 78 | uri: 'main.js', 79 | dependencies: [/*'@types/node'*/], 80 | value: [ 81 | '// JavaScript Example', 82 | `import * as fs from 'fs';`, 83 | '', 84 | 'class Person {', 85 | ' greet() {', 86 | ` console.log('hello there');`, 87 | ` fs.mkdir('folder');`, 88 | ' }', 89 | '}' 90 | ].join('\n') 91 | } 92 | }, 93 | { 94 | name: 'typescript.ts', 95 | type: FileNodeType.file, 96 | code: { 97 | language: 'typescript', 98 | uri: 'main.ts', 99 | dependencies: [ 100 | // '@types/node', 101 | // '@ngstack/translate', 102 | // '@ngstack/code-editor' 103 | ], 104 | value: [ 105 | '// TypeScript Example', 106 | `import { TranslateModule, TranslateService } from '@ngstack/translate';`, 107 | `import { CodeEditorModule } from '@ngstack/code-editor';`, 108 | `import * as fs from 'fs';`, 109 | '', 110 | 'export class MyClass {', 111 | ' constructor(translate: TranslateService) {', 112 | ' ', 113 | ' }', 114 | '}' 115 | ].join('\n') 116 | } 117 | } 118 | ]; 119 | 120 | @Injectable() 121 | export class FileDatabase { 122 | dataChange = new BehaviorSubject([]); 123 | 124 | get data(): FileNode[] { 125 | return this.dataChange.value; 126 | } 127 | 128 | constructor() { 129 | this.initialize(); 130 | } 131 | 132 | initialize() { 133 | // Notify the change. 134 | this.dataChange.next(FILES_DATA); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/app/code-editor-demo/code-editor-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; 2 | import { CodeEditorComponent, CodeEditorService, CodeModel, CodeModelChangedEvent } from '@ngstack/code-editor'; 3 | import { Observable } from 'rxjs'; 4 | import { debounceTime } from 'rxjs/operators'; 5 | import { FileDatabase } from './file-database'; 6 | import { FileNode } from './file-node'; 7 | import { MatToolbarModule } from '@angular/material/toolbar'; 8 | import { MatMenuModule } from '@angular/material/menu'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { AsyncPipe, NgFor, NgIf } from '@angular/common'; 11 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 12 | import { MatButtonModule } from '@angular/material/button'; 13 | import { editor } from 'monaco-editor'; 14 | import { MatFormField, MatLabel } from '@angular/material/form-field'; 15 | import { MatOption, MatSelect, MatSelectChange } from '@angular/material/select'; 16 | 17 | @Component({ 18 | standalone: true, 19 | imports: [ 20 | NgIf, 21 | NgFor, 22 | AsyncPipe, 23 | MatButtonModule, 24 | MatToolbarModule, 25 | MatMenuModule, 26 | MatIconModule, 27 | MatProgressBarModule, 28 | MatFormField, 29 | MatSelect, 30 | MatOption, 31 | MatLabel, 32 | CodeEditorComponent 33 | ], 34 | selector: 'app-code-editor-demo', 35 | templateUrl: './code-editor-demo.component.html', 36 | styleUrls: ['./code-editor-demo.component.scss'], 37 | encapsulation: ViewEncapsulation.None, 38 | providers: [FileDatabase] 39 | }) 40 | export class CodeEditorDemoComponent implements OnInit { 41 | themes = [ 42 | { name: 'Visual Studio', value: 'vs' }, 43 | { name: 'Visual Studio Dark', value: 'vs-dark' }, 44 | { name: 'High Contrast Dark', value: 'hc-black' } 45 | ]; 46 | 47 | selectedModel: CodeModel = null; 48 | activeTheme = 'vs'; 49 | readOnly = false; 50 | isLoading = false; 51 | isLoading$: Observable; 52 | 53 | private _codeEditor: CodeEditorComponent; 54 | 55 | @ViewChild(CodeEditorComponent, { static: false }) 56 | set codeEditor(value: CodeEditorComponent) { 57 | this._codeEditor = value; 58 | } 59 | 60 | get codeEditor(): CodeEditorComponent { 61 | return this._codeEditor; 62 | } 63 | 64 | @HostBinding('class') 65 | class = 'app-code-editor-demo'; 66 | 67 | options: editor.IStandaloneEditorConstructionOptions = { 68 | contextmenu: true, 69 | minimap: { 70 | enabled: false 71 | } 72 | }; 73 | 74 | files: FileNode[]; 75 | selectedFile: FileNode; 76 | 77 | constructor(database: FileDatabase, editorService: CodeEditorService) { 78 | database.dataChange.subscribe((data) => { 79 | this.files = data; 80 | this.selectedFile = this.files[0]; 81 | this.selectNode(this.selectedFile); 82 | }); 83 | 84 | this.isLoading$ = editorService.loadingTypings.pipe(debounceTime(300)); 85 | } 86 | 87 | onCodeChanged(value) { 88 | // console.log('CODE', value); 89 | } 90 | 91 | selectNode(node: FileNode) { 92 | this.isLoading = false; 93 | console.log(node); 94 | this.selectedModel = node.code; 95 | } 96 | 97 | // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method 98 | ngOnInit() { 99 | /* 100 | this.selectedModel = { 101 | language: 'json', 102 | uri: 'main.json', 103 | value: '{}' 104 | }; 105 | */ 106 | } 107 | 108 | onEditorLoaded(editor: CodeEditorComponent) { 109 | console.log('loaded', editor); 110 | } 111 | 112 | onCodeModelChanged(event: CodeModelChangedEvent) { 113 | console.log('code model changed', event); 114 | 115 | setTimeout(() => { 116 | event.sender.formatDocument(); 117 | }, 100); 118 | } 119 | 120 | onSelectionChange($event: MatSelectChange) { 121 | const fileNode = $event.value as FileNode; 122 | this.selectNode(fileNode); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/workers/typings-worker.js: -------------------------------------------------------------------------------- 1 | // This worker resolves typings (.d.ts files) for the given list of dependencies. 2 | 3 | self.importScripts([ 4 | 'https://cdnjs.cloudflare.com/ajax/libs/typescript/5.3.3/typescript.min.js' 5 | ]); 6 | 7 | const PACKAGES_SOURCE = 'https://unpkg.com'; 8 | 9 | const resolved = {}; 10 | const downloaded = {}; 11 | 12 | function normalizeUrl(url) { 13 | if (url && url.startsWith('node:')) { 14 | return url.replace('node:', 'https://unpkg.com/@types/node/'); 15 | } 16 | 17 | return url; 18 | } 19 | 20 | const loadContent = async (url) => { 21 | try { 22 | url = normalizeUrl(url); 23 | const existing = downloaded[url]; 24 | if (existing) { 25 | return existing; 26 | } 27 | 28 | console.info('loading', url); 29 | const response = await fetch(url); 30 | 31 | if (response.status >= 200 && response.status < 300) { 32 | const text = await response.text(); 33 | downloaded[url] = text; 34 | return text; 35 | } 36 | } catch (err) { 37 | console.log('Error:', err); 38 | } 39 | 40 | return null; 41 | }; 42 | 43 | const getIndex = async (lib) => { 44 | let packageUrl = `${PACKAGES_SOURCE}/${lib}/package.json`; 45 | let content = await loadContent(packageUrl); 46 | 47 | if (content) { 48 | const json = JSON.parse(content); 49 | if (json.typings) { 50 | return getTypingsUrl(json.typings, packageUrl); 51 | } 52 | 53 | packageUrl = `${PACKAGES_SOURCE}/${lib}/index.d.ts`; 54 | content = await loadContent(packageUrl); 55 | if (content) { 56 | return getTypingsUrl(packageUrl); 57 | } 58 | } 59 | 60 | return null; 61 | }; 62 | 63 | function getTypingsUrl(url, base) { 64 | return new URL(normalizeUrl(url), normalizeUrl(base)).href; 65 | } 66 | 67 | const findReferences = (sourceFile) => { 68 | const result = []; 69 | 70 | /** 71 | * Resolve referenced files like: 72 | * /// 73 | */ 74 | if (sourceFile.referencedFiles.length > 0) { 75 | result.push(...sourceFile.referencedFiles.map((ref) => ref.fileName)); 76 | } 77 | 78 | function scanNode(node) { 79 | if ( 80 | node.kind === ts.SyntaxKind.ImportDeclaration || 81 | node.kind === ts.SyntaxKind.ExportDeclaration 82 | ) { 83 | if (node.moduleSpecifier && node.moduleSpecifier.text) { 84 | result.push(node.moduleSpecifier.text); 85 | } 86 | } 87 | ts.forEachChild(node, scanNode); 88 | } 89 | 90 | ts.forEachChild(sourceFile, scanNode); 91 | return result; 92 | }; 93 | 94 | const resolveLibs = async (url, cache = {}) => { 95 | if (cache[url]) { 96 | return []; 97 | } 98 | cache[url] = true; 99 | 100 | const result = []; 101 | const content = await loadContent(url); 102 | 103 | if (content) { 104 | result.push({ 105 | url: url, 106 | path: url.replace(PACKAGES_SOURCE, 'node_modules') 107 | }); 108 | 109 | const sourceFile = ts.createSourceFile( 110 | 'main.ts', 111 | content, 112 | ts.ScriptTarget.Latest, 113 | true, 114 | ts.ScriptKind.TS 115 | ); 116 | const references = findReferences(sourceFile); 117 | 118 | for (const ref of references) { 119 | const fileName = ref.endsWith('.d.ts') ? ref : `${ref}.d.ts`; 120 | 121 | try { 122 | const fileUrl = getTypingsUrl(fileName, url); 123 | const refLibs = await resolveLibs(fileUrl, cache); 124 | 125 | if (refLibs && refLibs.length > 0) { 126 | result.push(...refLibs); 127 | } 128 | } catch (err) { 129 | console.log('Oops'); 130 | console.error(err); 131 | } 132 | } 133 | } 134 | 135 | return result; 136 | }; 137 | 138 | const getPackageTypings = async (lib, entryPoints) => { 139 | const libUrl = `${PACKAGES_SOURCE}/${lib}`; 140 | 141 | const existing = resolved[libUrl]; 142 | if (existing) { 143 | return existing.files || []; 144 | } 145 | 146 | const indexUrl = await getIndex(lib); 147 | if (indexUrl) { 148 | entryPoints[lib] = indexUrl.replace(PACKAGES_SOURCE, 'node_modules'); 149 | 150 | const files = await resolveLibs(indexUrl); 151 | 152 | resolved[libUrl] = { files }; 153 | return files; 154 | } 155 | 156 | return []; 157 | }; 158 | 159 | self.addEventListener('message', async (e) => { 160 | const { dependencies } = e.data; 161 | 162 | if (dependencies && dependencies.length > 0) { 163 | const entryPoints = {}; 164 | 165 | const result = await Promise.all( 166 | dependencies.map((libName) => { 167 | return getPackageTypings(libName, entryPoints); 168 | }) 169 | ); 170 | 171 | const files = result 172 | .reduce((prev, curr) => prev.concat(curr), []) 173 | .map((t) => { 174 | return { 175 | ...t, 176 | content: downloaded[t.url] 177 | }; 178 | }); 179 | 180 | self.postMessage({ 181 | entryPoints: entryPoints, 182 | files: files 183 | }); 184 | } 185 | }); 186 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/services/code-editor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, InjectionToken, Optional, Inject } from '@angular/core'; 2 | import { Subject, BehaviorSubject } from 'rxjs'; 3 | import { CodeEditorSettings } from '../editor-settings'; 4 | import { editor } from 'monaco-editor'; 5 | 6 | export const EDITOR_SETTINGS = new InjectionToken( 7 | 'EDITOR_SETTINGS' 8 | ); 9 | 10 | export interface TypingsInfo { 11 | entryPoints: { [key: string]: string }; 12 | files: Array<{ 13 | content: string; 14 | name: string; 15 | url: string; 16 | path: string; 17 | }>; 18 | } 19 | 20 | @Injectable({ 21 | providedIn: 'root' 22 | }) 23 | export class CodeEditorService { 24 | readonly baseUrl: string; 25 | readonly typingsWorkerUrl: string; 26 | 27 | typingsLoaded = new Subject(); 28 | loaded = new BehaviorSubject<{ monaco: any } | null>({ monaco: null }); 29 | 30 | loadingTypings = new BehaviorSubject(false); 31 | 32 | private typingsWorker: Worker; 33 | 34 | private _monaco: any; 35 | 36 | /** 37 | * Returns the global `monaco` instance 38 | */ 39 | get monaco(): any { 40 | return this._monaco; 41 | } 42 | 43 | constructor( 44 | @Optional() 45 | @Inject(EDITOR_SETTINGS) 46 | settings: CodeEditorSettings 47 | ) { 48 | const editorVersion = settings?.editorVersion || 'latest'; 49 | 50 | this.baseUrl = 51 | settings?.baseUrl || 52 | `https://cdn.jsdelivr.net/npm/monaco-editor@${editorVersion}/min`; 53 | this.typingsWorkerUrl = settings?.typingsWorkerUrl || ``; 54 | } 55 | 56 | private loadTypingsWorker(): Worker { 57 | if (!this.typingsWorker && (window).Worker) { 58 | if (this.typingsWorkerUrl.startsWith('http')) { 59 | const proxyScript = `importScripts('${this.typingsWorkerUrl}');`; 60 | const proxy = URL.createObjectURL( 61 | new Blob([proxyScript], { type: 'text/javascript' }) 62 | ); 63 | this.typingsWorker = new Worker(proxy); 64 | } else { 65 | this.typingsWorker = new Worker(this.typingsWorkerUrl); 66 | } 67 | this.typingsWorker.addEventListener('message', (e) => { 68 | this.loadingTypings.next(false); 69 | this.typingsLoaded.next(e.data); 70 | }); 71 | } 72 | return this.typingsWorker; 73 | } 74 | 75 | loadTypings(dependencies: string[]) { 76 | if (dependencies && dependencies.length > 0) { 77 | const worker = this.loadTypingsWorker(); 78 | if (worker) { 79 | this.loadingTypings.next(true); 80 | worker.postMessage({ 81 | dependencies 82 | }); 83 | } 84 | } 85 | } 86 | 87 | loadEditor(): Promise { 88 | return new Promise((resolve) => { 89 | const onGotAmdLoader = () => { 90 | (window).require.config({ 91 | paths: { vs: `${this.baseUrl}/vs` } 92 | }); 93 | 94 | if (this.baseUrl.startsWith('http')) { 95 | const proxyScript = ` 96 | self.MonacoEnvironment = { 97 | baseUrl: "${this.baseUrl}" 98 | }; 99 | importScripts('${this.baseUrl}/vs/base/worker/workerMain.js'); 100 | `; 101 | const proxy = URL.createObjectURL( 102 | new Blob([proxyScript], { type: 'text/javascript' }) 103 | ); 104 | window['MonacoEnvironment'] = { 105 | getWorkerUrl: function () { 106 | return proxy; 107 | } 108 | }; 109 | } 110 | 111 | (window).require(['vs/editor/editor.main'], () => { 112 | this._monaco = window['monaco']; 113 | this.loaded.next({ monaco: this._monaco }); 114 | resolve(); 115 | }); 116 | }; 117 | 118 | if (!(window).require) { 119 | const loaderScript = document.createElement('script'); 120 | loaderScript.type = 'text/javascript'; 121 | loaderScript.src = `${this.baseUrl}/vs/loader.js`; 122 | loaderScript.addEventListener('load', onGotAmdLoader); 123 | document.body.appendChild(loaderScript); 124 | } else { 125 | onGotAmdLoader(); 126 | } 127 | }); 128 | } 129 | 130 | /** 131 | * Switches to a theme. 132 | * @param themeName name of the theme 133 | */ 134 | setTheme(themeName: string) { 135 | this.monaco.editor.setTheme(themeName); 136 | } 137 | 138 | createEditor( 139 | containerElement: HTMLElement, 140 | options?: editor.IEditorConstructionOptions 141 | ): editor.ICodeEditor { 142 | return this.monaco.editor.create(containerElement, options); 143 | } 144 | 145 | createModel( 146 | value: string, 147 | language?: string, 148 | uri?: string 149 | ): editor.ITextModel { 150 | return this.monaco.editor.createModel( 151 | value, 152 | language, 153 | this.monaco.Uri.file(uri) 154 | ); 155 | } 156 | 157 | setModelLanguage( 158 | model: editor.ITextModel, 159 | mimeTypeOrLanguageId: string 160 | ): void { 161 | if (this.monaco && model) { 162 | this.monaco.editor.setModelLanguage(model, mimeTypeOrLanguageId); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /projects/code-editor/README.md: -------------------------------------------------------------------------------- 1 | # @ngstack/code-editor 2 | 3 | Code editor component for Angular applications. 4 | 5 | Based on the [Monaco](https://www.npmjs.com/package/monaco-editor) editor 6 | that powers [VS Code](https://github.com/Microsoft/vscode). 7 | 8 | 9 | Buy Me A Coffee 10 | 11 | 12 | ## Installing 13 | 14 | ```sh 15 | npm install @ngstack/code-editor 16 | ``` 17 | 18 | ## Integrating with Angular CLI project 19 | 20 | Import `CodeEditorModule` into your main application module: 21 | 22 | ```ts 23 | import { CodeEditorModule } from '@ngstack/code-editor'; 24 | 25 | @NgModule({ 26 | imports: [CodeEditorModule.forRoot()] 27 | }) 28 | export class AppModule {} 29 | ``` 30 | 31 | If you want to use a specific version of the Monaco editor, use `editorVersion` parameter. 32 | If not provided, the component is always going to use the `latest` version. 33 | 34 | ```ts 35 | @NgModule({ 36 | imports: [ 37 | CodeEditorModule.forRoot({ 38 | editorVersion: '0.44.0' 39 | }) 40 | ] 41 | }) 42 | export class AppModule {} 43 | ``` 44 | 45 | Update template to use the `ngs-code-editor`: 46 | 47 | ```html 48 | 49 | ``` 50 | 51 | Update component controller class and provide corresponding properties and events: 52 | 53 | ```ts 54 | export class AppComponent { 55 | theme = 'vs-dark'; 56 | 57 | model: CodeModel = { 58 | language: 'json', 59 | uri: 'main.json', 60 | value: '{}' 61 | }; 62 | 63 | options = { 64 | contextmenu: true, 65 | minimap: { 66 | enabled: true 67 | } 68 | }; 69 | 70 | onCodeChanged(value) { 71 | console.log('CODE', value); 72 | } 73 | } 74 | ``` 75 | 76 | ## Input Properties 77 | 78 | | Name | Type | Default Value | Description | 79 | | --------- | --------- | ------------- | -------------------------------------------------------------- | 80 | | theme | string | vs | Editor theme. Supported values: `vs`, `vs-dark` or `hc-black`. | 81 | | options | Object | {...} | Editor options. | 82 | | readOnly | boolean | false | Toggles readonly state of the editor. | 83 | | codeModel | CodeModel | | Source code model. | 84 | 85 | The `codeModel` property holds the value that implements the `CodeModel` interface: 86 | 87 | ```ts 88 | export interface CodeModel { 89 | language: string; 90 | value: string; 91 | uri: string; 92 | 93 | dependencies?: Array; 94 | schemas?: Array<{ 95 | uri: string; 96 | schema: Object; 97 | }>; 98 | } 99 | ``` 100 | 101 | ### Editor Options 102 | 103 | For available options see [IEditorConstructionOptions](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorConstructionOptions.html) docs. 104 | 105 | The following options are used by default when Editor Component gets created: 106 | 107 | ```json 108 | { 109 | "lineNumbers": true, 110 | "contextmenu": false, 111 | "minimap": { 112 | "enabled": false 113 | } 114 | } 115 | ``` 116 | 117 | ## Output Events 118 | 119 | | Name | Argument Type | Description | 120 | | ------------ | ------------- | ------------------------------------------------------- | 121 | | loaded | | Raised when editor finished loading all its components. | 122 | | valueChanged | string | Raised after editor value gets changed. | 123 | 124 | ## Typings 125 | 126 | The editor is able to resolve typing libraries when set to the `Typescript` or `Javascript` language. 127 | 128 | Use `dependencies` property to provide a list of libraries to resolve 129 | 130 | ```html 131 | 132 | ``` 133 | 134 | And in the controller class: 135 | 136 | ```ts 137 | export class MyEditorComponent { 138 | codeModel: CodeModel = { 139 | language: 'typescript', 140 | uri: 'main.ts', 141 | value: '', 142 | dependencies: ['@types/node', '@ngstack/translate', '@ngstack/code-editor'] 143 | }; 144 | } 145 | ``` 146 | 147 | Run your application, it may take a few seconds to resolve dependencies. 148 | It is performed in the background (web worker), so you can type your code. 149 | 150 | Try pasting the following snippet at runtime: 151 | 152 | ```typescript 153 | import { TranslateModule, TranslateService } from '@ngstack/translate'; 154 | import { CodeEditorModule } from '@ngstack/code-editor'; 155 | import * as fs from 'fs'; 156 | 157 | export class MyClass { 158 | constructor(translate: TranslateService) {} 159 | } 160 | ``` 161 | 162 | You should have all the types resolved and auto-completion working. 163 | 164 | ## JSON schemas 165 | 166 | You can associate multiple schemas when working with JSON files. 167 | 168 | ```html 169 | 170 | ``` 171 | 172 | Provide the required schemas like in the example below. 173 | 174 | ```ts 175 | export class MyEditorComponent { 176 | codeModel: CodeModel = { 177 | language: 'json', 178 | uri: 'main.json', 179 | value: '{ "test": true }', 180 | schemas: [ 181 | { 182 | uri: 'http://custom/schema.json', 183 | schema: { 184 | type: 'object', 185 | properties: { 186 | type: { 187 | enum: ['button', 'textbox'] 188 | } 189 | } 190 | } 191 | } 192 | ] 193 | }; 194 | } 195 | ``` 196 | 197 | The schemas get automatically installed and associated with the corresponding file. 198 | 199 | ## Offline Setup 200 | 201 | ### Editor 202 | 203 | You can run the editor in the offline mode with your Angular CLI application using the following steps: 204 | 205 | Install the `monaco-editor`: 206 | 207 | ```sh 208 | npm install monaco-editor 209 | ``` 210 | 211 | Update the `angular.json` file and append the following asset rule: 212 | 213 | ```json 214 | { 215 | "glob": "**/*", 216 | "input": "../node_modules/monaco-editor/min", 217 | "output": "./assets/monaco" 218 | } 219 | ``` 220 | 221 | Update the main application module and setup the service to use the custom `baseUrl` when application starts: 222 | 223 | ```ts 224 | import { CodeEditorModule, CodeEditorService } from '@ngstack/code-editor'; 225 | 226 | @NgModule({ 227 | ..., 228 | imports: [ 229 | ..., 230 | CodeEditorModule.forRoot({ 231 | baseUrl: 'assets/monaco' 232 | }) 233 | ], 234 | ... 235 | }) 236 | export class AppModule {} 237 | ``` 238 | 239 | ### Typings Worker 240 | 241 | Update the `angular.json` file and append the following asset rule: 242 | 243 | ```ts 244 | { 245 | "glob": "**/*.js", 246 | "input": "../node_modules/@ngstack/code-editor/workers", 247 | "output": "./assets/workers" 248 | } 249 | ``` 250 | 251 | Then update the `CodeEditorService` configuration at the application startup: 252 | 253 | ```ts 254 | @NgModule({ 255 | ..., 256 | imports: [ 257 | ..., 258 | CodeEditorModule.forRoot({ 259 | typingsWorkerUrl: 'assets/workers/typings-worker.js' 260 | }) 261 | ], 262 | ... 263 | }) 264 | export class AppModule {} 265 | ``` 266 | 267 | ## Lazy Loading 268 | 269 | To enable Lazy Loading 270 | use `CodeEditorModule.forRoot()` in the main application, 271 | and `CodeEditorModule.forChild()` in all lazy-loaded feature modules. 272 | 273 | For more details please refer to [Lazy Loading Feature Modules](https://angular.io/guide/lazy-loading-ngmodules) 274 | -------------------------------------------------------------------------------- /projects/code-editor/src/lib/code-editor/code-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | ViewEncapsulation, 5 | OnChanges, 6 | OnDestroy, 7 | AfterViewInit, 8 | ViewChild, 9 | ElementRef, 10 | Input, 11 | Output, 12 | EventEmitter, 13 | SimpleChanges, 14 | HostListener, 15 | inject 16 | } from '@angular/core'; 17 | import { CodeEditorService } from '../services/code-editor.service'; 18 | import { TypescriptDefaultsService } from '../services/typescript-defaults.service'; 19 | import { JavascriptDefaultsService } from '../services/javascript-defaults.service'; 20 | import { JsonDefaultsService } from '../services/json-defaults.service'; 21 | import { CodeModel } from '../models/code.model'; 22 | import { editor } from 'monaco-editor'; 23 | 24 | export interface CodeModelChangedEvent { 25 | sender: CodeEditorComponent; 26 | value: CodeModel; 27 | } 28 | 29 | @Component({ 30 | // eslint-disable-next-line @angular-eslint/component-selector 31 | selector: 'ngs-code-editor', 32 | standalone: true, 33 | templateUrl: './code-editor.component.html', 34 | styleUrls: ['./code-editor.component.css'], 35 | changeDetection: ChangeDetectionStrategy.OnPush, 36 | encapsulation: ViewEncapsulation.None, 37 | // eslint-disable-next-line 38 | host: { class: 'ngs-code-editor' } 39 | }) 40 | export class CodeEditorComponent 41 | implements OnChanges, OnDestroy, AfterViewInit 42 | { 43 | private _editor: editor.ICodeEditor; 44 | private _model: editor.ITextModel; 45 | // private _value = ''; 46 | 47 | private defaultOptions: editor.IStandaloneEditorConstructionOptions = { 48 | lineNumbers: 'on', 49 | contextmenu: false, 50 | minimap: { 51 | enabled: false 52 | } 53 | }; 54 | 55 | /** 56 | * The instance of the editor. 57 | */ 58 | get editor(): editor.ICodeEditor { 59 | return this._editor; 60 | } 61 | 62 | protected set editor(value: editor.ICodeEditor) { 63 | this._editor = value; 64 | } 65 | 66 | @ViewChild('editor', { static: true }) 67 | editorContent: ElementRef; 68 | 69 | @Input() 70 | codeModel: CodeModel; 71 | 72 | // @Input() 73 | // set value(v: string) { 74 | // if (v !== this._value) { 75 | // this._value = v; 76 | // this.setEditorValue(v); 77 | // this.valueChanged.emit(v); 78 | // } 79 | // } 80 | 81 | // get value(): string { 82 | // return this._value; 83 | // } 84 | 85 | /** 86 | * Editor theme. Defaults to `vs`. 87 | * 88 | * Allowed values: `vs`, `vs-dark` or `hc-black`. 89 | * @memberof CodeEditorComponent 90 | */ 91 | @Input() 92 | theme = 'vs'; 93 | 94 | /** 95 | * Editor options. 96 | * 97 | * See https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html for more details. 98 | * 99 | * @memberof CodeEditorComponent 100 | */ 101 | @Input() 102 | options: editor.IStandaloneEditorConstructionOptions = {}; 103 | 104 | /** 105 | * Toggle readonly state of the editor. 106 | * 107 | * @memberof CodeEditorComponent 108 | */ 109 | @Input() 110 | readOnly = false; 111 | 112 | /** 113 | * An event emitted when the text content of the model have changed. 114 | */ 115 | @Output() 116 | valueChanged = new EventEmitter(); 117 | 118 | /** 119 | * An event emitted when the code model value is changed. 120 | */ 121 | @Output() 122 | codeModelChanged = new EventEmitter(); 123 | 124 | /** 125 | * An event emitted when the contents of the underlying editor model have changed. 126 | */ 127 | @Output() 128 | modelContentChanged = new EventEmitter(); 129 | 130 | /** 131 | * Raised when editor finished loading all its components. 132 | */ 133 | @Output() 134 | loaded = new EventEmitter(); 135 | 136 | protected editorService = inject(CodeEditorService); 137 | protected typescriptDefaults = inject(TypescriptDefaultsService); 138 | protected javascriptDefaults = inject(JavascriptDefaultsService); 139 | protected jsonDefaults = inject(JsonDefaultsService); 140 | 141 | ngOnDestroy() { 142 | if (this.editor) { 143 | this.editor.dispose(); 144 | this.editor = null; 145 | } 146 | 147 | if (this._model) { 148 | this._model.dispose(); 149 | this._model = null; 150 | } 151 | } 152 | 153 | ngOnChanges(changes: SimpleChanges) { 154 | const codeModel = changes['codeModel']; 155 | const readOnly = changes['readOnly']; 156 | const theme = changes['theme']; 157 | 158 | if (codeModel && !codeModel.firstChange) { 159 | this.updateModel(codeModel.currentValue); 160 | } 161 | 162 | if (readOnly && !readOnly.firstChange) { 163 | if (this.editor) { 164 | this.editor.updateOptions({ 165 | readOnly: readOnly.currentValue 166 | }); 167 | } 168 | } 169 | 170 | if (theme && !theme.firstChange) { 171 | this.editorService.setTheme(theme.currentValue); 172 | } 173 | } 174 | 175 | @HostListener('window:resize') 176 | onResize() { 177 | if (this.editor) { 178 | this.editor.layout(); 179 | } 180 | } 181 | 182 | async ngAfterViewInit() { 183 | this.setupEditor(); 184 | this.loaded.emit(this); 185 | } 186 | 187 | private setupEditor() { 188 | const domElement = this.editorContent.nativeElement; 189 | const settings = { 190 | value: '', 191 | language: 'text', 192 | uri: `code-${Date.now()}`, 193 | ...this.codeModel 194 | }; 195 | 196 | this._model = this.editorService.createModel( 197 | settings.value, 198 | settings.language, 199 | settings.uri 200 | ); 201 | 202 | const options = Object.assign({}, this.defaultOptions, this.options, { 203 | readOnly: this.readOnly, 204 | theme: this.theme, 205 | model: this._model 206 | }); 207 | 208 | this.editor = this.editorService.createEditor(domElement, options); 209 | 210 | this._model.onDidChangeContent((e: editor.IModelContentChangedEvent) => { 211 | this.modelContentChanged.emit(e); 212 | 213 | const newValue = this._model.getValue(); 214 | if (this.codeModel) { 215 | this.codeModel.value = newValue; 216 | } 217 | this.valueChanged.emit(newValue); 218 | }); 219 | 220 | this.setupDependencies(this.codeModel); 221 | this.codeModelChanged.emit({ sender: this, value: this.codeModel }); 222 | } 223 | 224 | runEditorAction(id: string, args?: unknown) { 225 | this.editor.getAction(id)?.run(args); 226 | } 227 | 228 | formatDocument() { 229 | this.runEditorAction('editor.action.formatDocument'); 230 | } 231 | 232 | private setupDependencies(model: CodeModel) { 233 | if (!model) { 234 | return; 235 | } 236 | 237 | const { language } = model; 238 | 239 | if (language) { 240 | const lang = language.toLowerCase(); 241 | 242 | switch (lang) { 243 | case 'typescript': 244 | if (model.dependencies) { 245 | this.editorService.loadTypings(model.dependencies); 246 | } 247 | break; 248 | case 'javascript': 249 | if (model.dependencies) { 250 | this.editorService.loadTypings(model.dependencies); 251 | } 252 | break; 253 | case 'json': 254 | if (model.schemas) { 255 | this.jsonDefaults.addSchemas(model.uri, model.schemas); 256 | } 257 | break; 258 | default: 259 | break; 260 | } 261 | } 262 | } 263 | 264 | private setEditorValue(value: any): void { 265 | // Fix for value change while dispose in process. 266 | setTimeout(() => { 267 | if (this._model) { 268 | this._model.setValue(value); 269 | } 270 | }); 271 | } 272 | 273 | private updateModel(model: CodeModel) { 274 | if (model) { 275 | this.setEditorValue(model.value); 276 | this.editorService.setModelLanguage(this._model, model.language); 277 | this.setupDependencies(model); 278 | this.codeModelChanged.emit({ sender: this, value: model }); 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "cli": { 6 | "packageManager": "npm", 7 | "schematicCollections": [ 8 | "@angular-eslint/schematics" 9 | ], 10 | "analytics": false 11 | }, 12 | "projects": { 13 | "app": { 14 | "root": "", 15 | "sourceRoot": "src", 16 | "projectType": "application", 17 | "prefix": "app", 18 | "schematics": {}, 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:application", 22 | "options": { 23 | "outputPath": "dist/app", 24 | "index": "src/index.html", 25 | "browser": "src/main.ts", 26 | "polyfills": ["zone.js"], 27 | "tsConfig": "src/tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets", 31 | { 32 | "glob": "**/*", 33 | "input": "node_modules/monaco-editor/min", 34 | "output": "./assets/monaco" 35 | }, 36 | { 37 | "glob": "**/*.js", 38 | "input": "projects/code-editor/src/lib/workers", 39 | "output": "./assets/workers" 40 | } 41 | ], 42 | "styles": [ 43 | { 44 | "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 45 | "inject": true 46 | }, 47 | "src/styles.css" 48 | ], 49 | "scripts": [], 50 | "extractLicenses": false, 51 | "sourceMap": true, 52 | "optimization": false, 53 | "namedChunks": true 54 | }, 55 | "configurations": { 56 | "production": { 57 | "budgets": [ 58 | { 59 | "type": "initial", 60 | "maximumWarning": "2.8MB", 61 | "maximumError": "3MB" 62 | }, 63 | { 64 | "type": "anyComponentStyle", 65 | "maximumWarning": "2kB", 66 | "maximumError": "4kB" 67 | } 68 | ], 69 | "fileReplacements": [ 70 | { 71 | "replace": "src/environments/environment.ts", 72 | "with": "src/environments/environment.prod.ts" 73 | } 74 | ], 75 | "outputHashing": "all" 76 | }, 77 | "development": { 78 | "optimization": false, 79 | "extractLicenses": false, 80 | "sourceMap": true 81 | } 82 | }, 83 | "defaultConfiguration": "production" 84 | }, 85 | "serve": { 86 | "builder": "@angular-devkit/build-angular:dev-server", 87 | "configurations": { 88 | "production": { 89 | "buildTarget": "app:build:production" 90 | }, 91 | "development": { 92 | "buildTarget": "app:build:development" 93 | } 94 | }, 95 | "defaultConfiguration": "development" 96 | }, 97 | "test": { 98 | "builder": "@angular-devkit/build-angular:karma", 99 | "options": { 100 | "polyfills": [ 101 | "zone.js", 102 | "zone.js/testing" 103 | ], 104 | "tsConfig": "src/tsconfig.spec.json", 105 | "styles": [ 106 | { 107 | "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 108 | "inject": true 109 | }, 110 | "src/styles.css" 111 | ], 112 | "scripts": [], 113 | "assets": [ 114 | "src/favicon.ico", 115 | "src/assets" 116 | ] 117 | } 118 | }, 119 | "lint": { 120 | "builder": "@angular-eslint/builder:lint", 121 | "options": { 122 | "lintFilePatterns": [ 123 | "src/**/*.ts", 124 | "src/**/*.html" 125 | ] 126 | } 127 | } 128 | } 129 | }, 130 | "code-editor": { 131 | "root": "projects/code-editor", 132 | "sourceRoot": "projects/code-editor/src", 133 | "projectType": "library", 134 | "prefix": "lib", 135 | "architect": { 136 | "build": { 137 | "builder": "@angular-devkit/build-angular:ng-packagr", 138 | "options": { 139 | "project": "projects/code-editor/ng-package.json" 140 | }, 141 | "configurations": { 142 | "production": { 143 | "tsConfig": "projects/code-editor/tsconfig.lib.prod.json" 144 | }, 145 | "development": { 146 | "tsConfig": "projects/code-editor/tsconfig.lib.json" 147 | } 148 | }, 149 | "defaultConfiguration": "production" 150 | }, 151 | "test": { 152 | "builder": "@angular-devkit/build-angular:karma", 153 | "options": { 154 | "tsConfig": "projects/code-editor/tsconfig.spec.json", 155 | "polyfills": [ 156 | "zone.js", 157 | "zone.js/testing" 158 | ] 159 | } 160 | }, 161 | "lint": { 162 | "builder": "@angular-eslint/builder:lint", 163 | "options": { 164 | "lintFilePatterns": [ 165 | "projects/code-editor/**/*.ts", 166 | "projects/code-editor/**/*.html" 167 | ] 168 | } 169 | } 170 | } 171 | }, 172 | "multiple-editors": { 173 | "projectType": "application", 174 | "schematics": {}, 175 | "root": "projects/multiple-editors", 176 | "sourceRoot": "projects/multiple-editors/src", 177 | "prefix": "app", 178 | "architect": { 179 | "build": { 180 | "builder": "@angular-devkit/build-angular:application", 181 | "options": { 182 | "outputPath": "dist/multiple-editors", 183 | "index": "projects/multiple-editors/src/index.html", 184 | "browser": "projects/multiple-editors/src/main.ts", 185 | "polyfills": ["zone.js"], 186 | "tsConfig": "projects/multiple-editors/tsconfig.app.json", 187 | "assets": [ 188 | "projects/multiple-editors/src/favicon.ico", 189 | "projects/multiple-editors/src/assets", 190 | { 191 | "glob": "**/*", 192 | "input": "node_modules/monaco-editor/min", 193 | "output": "./assets/monaco" 194 | }, 195 | { 196 | "glob": "**/*.js", 197 | "input": "dist/@ngstack/code-editor/workers", 198 | "output": "./assets/workers" 199 | } 200 | ], 201 | "styles": [ 202 | "projects/multiple-editors/src/styles.css" 203 | ], 204 | "scripts": [] 205 | }, 206 | "configurations": { 207 | "production": { 208 | "fileReplacements": [ 209 | { 210 | "replace": "projects/multiple-editors/src/environments/environment.ts", 211 | "with": "projects/multiple-editors/src/environments/environment.prod.ts" 212 | } 213 | ], 214 | "outputHashing": "all" 215 | }, 216 | "development": { 217 | "optimization": false, 218 | "extractLicenses": false, 219 | "sourceMap": true 220 | } 221 | }, 222 | "defaultConfiguration": "production" 223 | }, 224 | "serve": { 225 | "builder": "@angular-devkit/build-angular:dev-server", 226 | "configurations": { 227 | "production": { 228 | "buildTarget": "multiple-editors:build:production" 229 | }, 230 | "development": { 231 | "buildTarget": "multiple-editors:build:development" 232 | } 233 | }, 234 | "defaultConfiguration": "development" 235 | } 236 | } 237 | } 238 | }, 239 | "schematics": { 240 | "@schematics/angular:component": { 241 | "style": "scss" 242 | }, 243 | "@angular-eslint/schematics:application": { 244 | "setParserOptionsProject": true 245 | }, 246 | "@angular-eslint/schematics:library": { 247 | "setParserOptionsProject": true 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ngstack/code-editor 2 | 3 | Code editor component for Angular applications. 4 | 5 | Based on the [Monaco](https://www.npmjs.com/package/monaco-editor) editor 6 | that powers [VS Code](https://github.com/Microsoft/vscode). 7 | 8 | 9 | Buy Me A Coffee 10 | 11 | 12 | ## Installing 13 | 14 | ```sh 15 | npm install @ngstack/code-editor 16 | ``` 17 | 18 | ## Integrating with Standalone Angular Project 19 | 20 | Update the `app.config.ts` file to provide the code editor configuration: 21 | 22 | ```ts 23 | export const appConfig: ApplicationConfig = { 24 | providers: [ 25 | provideZoneChangeDetection({ eventCoalescing: true }), 26 | provideRouter(routes), 27 | provideAnimationsAsync(), 28 | 29 | // Configure Code Editor 30 | provideCodeEditor({ 31 | // editorVersion: '0.46.0', 32 | // use local Monaco installation 33 | baseUrl: 'assets/monaco', 34 | // use local Typings Worker 35 | typingsWorkerUrl: 'assets/workers/typings-worker.js' 36 | }) 37 | ] 38 | }; 39 | ``` 40 | 41 | ## Integrating with Modules-based Angular Project 42 | 43 | Import `CodeEditorModule` into your main application module: 44 | 45 | ```ts 46 | import { provideCodeEditor } from '@ngstack/code-editor'; 47 | 48 | @NgModule({ 49 | providers: [provideCodeEditor()] 50 | }) 51 | export class AppModule {} 52 | ``` 53 | 54 | If you want to use a specific version of the Monaco editor, use `editorVersion` parameter. 55 | If not provided, the component is always going to use the `latest` version. 56 | 57 | > For a full list of Monaco versions and changes, please refer to the official [CHANGELOG.md](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md) file 58 | 59 | ```ts 60 | import { provideCodeEditor } from '@ngstack/code-editor'; 61 | 62 | @NgModule({ 63 | providers: [ 64 | provideCodeEditor({ 65 | editorVersion: '0.44.0' 66 | }) 67 | ] 68 | }) 69 | export class AppModule {} 70 | ``` 71 | 72 | Update template to use the `ngs-code-editor`: 73 | 74 | ```html 75 | 76 | ``` 77 | 78 | Update component controller class and provide corresponding properties and events: 79 | 80 | ```ts 81 | export class AppComponent { 82 | theme = 'vs-dark'; 83 | 84 | model: CodeModel = { 85 | language: 'json', 86 | uri: 'main.json', 87 | value: '{}' 88 | }; 89 | 90 | options = { 91 | contextmenu: true, 92 | minimap: { 93 | enabled: true 94 | } 95 | }; 96 | 97 | onCodeChanged(value) { 98 | console.log('CODE', value); 99 | } 100 | } 101 | ``` 102 | 103 | ## Input Properties 104 | 105 | | Name | Type | Default Value | Description | 106 | |-----------|-----------|---------------|----------------------------------------------------------------| 107 | | theme | string | vs | Editor theme. Supported values: `vs`, `vs-dark` or `hc-black`. | 108 | | options | Object | {...} | Editor options. | 109 | | readOnly | boolean | false | Toggles readonly state of the editor. | 110 | | codeModel | CodeModel | | Source code model. | 111 | 112 | The `codeModel` property holds the value that implements the `CodeModel` interface: 113 | 114 | ```ts 115 | export interface CodeModel { 116 | language: string; 117 | value: string; 118 | uri: string; 119 | 120 | dependencies?: Array; 121 | schemas?: Array<{ 122 | uri: string; 123 | schema: Object; 124 | }>; 125 | } 126 | ``` 127 | 128 | ### Editor Options 129 | 130 | For available options see [IEditorConstructionOptions](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorConstructionOptions.html) docs. 131 | 132 | The following options are used by default when Editor Component gets created: 133 | 134 | ```json 135 | { 136 | "lineNumbers": true, 137 | "contextmenu": false, 138 | "minimap": { 139 | "enabled": false 140 | } 141 | } 142 | ``` 143 | 144 | ## Output Events 145 | 146 | | Name | Argument Type | Description | 147 | |---------------------|-----------------------------|--------------------------------------------------------------------------------| 148 | | loaded | | Raised when editor finished loading all its components. | 149 | | valueChanged | string | An event emitted when the text content of the model have changed. | 150 | | modelContentChanged | `IModelContentChangedEvent` | An event emitted when the contents of the underlying editor model have changed | 151 | | codeModelChanged | `CodeModelChangedEvent` | An event emitted when the code model value is changed. | 152 | 153 | ## Component API 154 | 155 | | Name | Description | 156 | |---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| 157 | | editor | returns the instance of the underlying Monaco [ICodeEditor](https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.ICodeEditor.html) | 158 | | runAction(id, args) | runs the editor actions, for example `editor.action.formatDocument` | 159 | | formatDocument() | shortcut function to format the document | 160 | 161 | ## Editor Service 162 | 163 | The component comes with a separate `CodeEditorService` service that provides additional APIs for the underlying `monaco` editor: 164 | 165 | | Name | Description | 166 | |---------------------|-------------------------------------------------------| 167 | | monaco | get the global monaco instance | 168 | | typingsLoaded | An event emitted when code typings are loaded | 169 | | loaded | An event emitted when the `monaco` instance is loaded | 170 | | setTheme(themeName) | Switches to a theme | 171 | 172 | ## Typings 173 | 174 | The editor is able to resolve typing libraries when set to the `Typescript` or `Javascript` language. 175 | 176 | Use `dependencies` property to provide a list of libraries to resolve 177 | 178 | ```html 179 | 180 | ``` 181 | 182 | And in the controller class: 183 | 184 | ```ts 185 | export class MyEditorComponent { 186 | codeModel: CodeModel = { 187 | language: 'typescript', 188 | uri: 'main.ts', 189 | value: '', 190 | dependencies: ['@types/node', '@ngstack/translate', '@ngstack/code-editor'] 191 | }; 192 | } 193 | ``` 194 | 195 | Run your application, it may take a few seconds to resolve dependencies. 196 | It is performed in the background (web worker), so you can type your code. 197 | 198 | Try pasting the following snippet at runtime: 199 | 200 | ```typescript 201 | import { TranslateModule, TranslateService } from '@ngstack/translate'; 202 | import { CodeEditorModule } from '@ngstack/code-editor'; 203 | import * as fs from 'fs'; 204 | 205 | export class MyClass { 206 | constructor(translate: TranslateService) {} 207 | } 208 | ``` 209 | 210 | You should have all the types resolved and auto-completion working. 211 | 212 | ## JSON schemas 213 | 214 | You can associate multiple schemas when working with JSON files. 215 | 216 | ```html 217 | 218 | ``` 219 | 220 | Provide the required schemas like in the example below. 221 | 222 | ```ts 223 | export class MyEditorComponent { 224 | codeModel: CodeModel = { 225 | language: 'json', 226 | uri: 'main.json', 227 | value: '{ "test": true }', 228 | schemas: [ 229 | { 230 | uri: 'http://custom/schema.json', 231 | schema: { 232 | type: 'object', 233 | properties: { 234 | type: { 235 | enum: ['button', 'textbox'] 236 | } 237 | } 238 | } 239 | } 240 | ] 241 | }; 242 | } 243 | ``` 244 | 245 | The schemas get automatically installed and associated with the corresponding file. 246 | 247 | ## Accessing Code Editor Instance 248 | 249 | You can access the Code Editor component instance API from other components when using with the `@ViewChild`: 250 | 251 | ```ts 252 | class MyComponent { 253 | private _codeEditor: CodeEditorComponent; 254 | 255 | @ViewChild(CodeEditorComponent, { static: false }) 256 | set codeEditor(value: CodeEditorComponent) { 257 | this._codeEditor = value; 258 | } 259 | 260 | get codeEditor(): CodeEditorComponent { 261 | return this._codeEditor; 262 | } 263 | } 264 | ``` 265 | 266 | The code above allows you to use the code editor within the `*ngIf`, for example: 267 | 268 | ```html 269 | 270 | 271 | 272 | ``` 273 | 274 | Other components can now have access to the editor instance: 275 | 276 | ```html 277 | 280 | ``` 281 | 282 | ### Example: auto-formatting on load 283 | 284 | ```html 285 | 286 | ``` 287 | 288 | ```ts 289 | import { CodeModelChangedEvent } from '@ngstack/code-editor'; 290 | 291 | class MyComponent { 292 | onCodeModelChanged(event: CodeModelChangedEvent) { 293 | setTimeout(() => { 294 | event.sender.formatDocument(); 295 | }, 100); 296 | } 297 | } 298 | ``` 299 | 300 | ## Offline Setup 301 | 302 | ### Editor 303 | 304 | You can run the editor in the offline mode with your Angular CLI application using the following steps: 305 | 306 | Install the `monaco-editor`: 307 | 308 | ```sh 309 | npm install monaco-editor 310 | ``` 311 | 312 | Update the `angular.json` file and append the following asset rule: 313 | 314 | ```json 315 | { 316 | "glob": "**/*", 317 | "input": "../node_modules/monaco-editor/min", 318 | "output": "./assets/monaco" 319 | } 320 | ``` 321 | 322 | Update the main application module and setup the service to use the custom `baseUrl` when application starts: 323 | 324 | ```ts 325 | import { provideCodeEditor } from '@ngstack/code-editor'; 326 | 327 | @NgModule({ 328 | providers: [ 329 | provideCodeEditor({ 330 | baseUrl: 'assets/monaco' 331 | }) 332 | ] 333 | }) 334 | export class AppModule {} 335 | ``` 336 | 337 | ### Typings Worker 338 | 339 | Update the `angular.json` file and append the following asset rule: 340 | 341 | ```json 342 | { 343 | "glob": "**/*.js", 344 | "input": "../node_modules/@ngstack/code-editor/workers", 345 | "output": "./assets/workers" 346 | } 347 | ``` 348 | 349 | Then update the `CodeEditorService` configuration at the application startup: 350 | 351 | ```ts 352 | import { provideCodeEditor } from '@ngstack/code-editor'; 353 | 354 | @NgModule({ 355 | providers: [ 356 | provideCodeEditor({ 357 | typingsWorkerUrl: 'assets/workers/typings-worker.js' 358 | }) 359 | ] 360 | }) 361 | export class AppModule {} 362 | ``` 363 | 364 | ## Lazy Loading 365 | 366 | To enable Lazy Loading 367 | use `CodeEditorModule.forRoot()` in the main application, 368 | and `CodeEditorModule` in all lazy-loaded feature modules. 369 | 370 | For more details please refer to [Lazy Loading Feature Modules](https://angular.io/guide/lazy-loading-ngmodules) 371 | --------------------------------------------------------------------------------