2.48.0 • Published 4 months ago

@memberjunction/ng-code-editor v2.48.0

Weekly downloads
-
License
ISC
Repository
-
Last release
4 months ago

@memberjunction/ng-code-editor

A powerful and flexible code editor component for Angular applications in the MemberJunction framework, built on top of CodeMirror 6.

Overview

The @memberjunction/ng-code-editor package provides a feature-rich code editing experience with syntax highlighting, form integration, and extensive customization options. It seamlessly integrates with Angular's reactive forms and template-driven forms, making it ideal for applications that require code editing capabilities.

Features

  • Syntax Highlighting: Support for 150+ programming languages via @codemirror/language-data
  • Form Integration: Full support for ngModel, formControlName, and ControlValueAccessor
  • Reactive Configuration: All editor properties can be changed dynamically at runtime
  • Accessibility: Built-in keyboard navigation and screen reader support
  • Line Wrapping: Optional automatic line wrapping for long lines
  • Whitespace Visualization: Option to highlight tabs, spaces, and line breaks
  • Customizable Indentation: Configure tab behavior and indent units
  • Multiple Setup Modes: Choose between basic, minimal, or custom editor setups
  • Extension Support: Add custom CodeMirror extensions for advanced functionality
  • TypeScript Support: Full type safety with TypeScript declarations

Installation

npm install @memberjunction/ng-code-editor

Peer Dependencies

This package requires Angular 18.0.2 or higher:

  • @angular/common: ^18.0.2
  • @angular/core: ^18.0.2

Getting Started

1. Import the Module

import { CodeEditorModule } from '@memberjunction/ng-code-editor';

@NgModule({
  imports: [
    CodeEditorModule,
    // other imports
  ],
  // ...
})
export class AppModule { }

2. Basic Usage

<mj-code-editor
  [value]="code"
  [language]="'javascript'"
  [placeholder]="'// Enter your code here...'"
  (change)="onCodeChange($event)">
</mj-code-editor>
export class MyComponent {
  code = 'console.log("Hello, world!");';
  
  onCodeChange(newCode: string) {
    console.log('Code changed:', newCode);
  }
}

Usage Examples

Template-Driven Forms with NgModel

import { Component } from '@angular/core';
import { languages } from '@codemirror/language-data';

@Component({
  selector: 'app-template-form',
  template: `
    <div class="editor-container">
      <mj-code-editor
        [(ngModel)]="code"
        name="code"
        [languages]="languages"
        [language]="'typescript'"
        [lineWrapping]="true"
        [highlightWhitespace]="showWhitespace"
        [indentWithTab]="true"
        [placeholder]="'// Start typing your TypeScript code...'"
        (focus)="onFocus()"
        (blur)="onBlur()">
      </mj-code-editor>
      
      <div class="controls">
        <label>
          <input type="checkbox" [(ngModel)]="showWhitespace">
          Show whitespace
        </label>
      </div>
    </div>
  `
})
export class TemplateFormComponent {
  code = '';
  languages = languages;
  showWhitespace = false;
  
  onFocus() {
    console.log('Editor focused');
  }
  
  onBlur() {
    console.log('Editor blurred');
  }
}

Reactive Forms

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { languages } from '@codemirror/language-data';

@Component({
  selector: 'app-reactive-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <mj-code-editor
        formControlName="query"
        [languages]="languages"
        [language]="'sql'"
        [placeholder]="'SELECT * FROM ...'"
        [readonly]="isExecuting"
        [setup]="'basic'"
        [lineWrapping]="true">
      </mj-code-editor>
      
      <div class="form-errors" *ngIf="form.get('query')?.errors && form.get('query')?.touched">
        <p *ngIf="form.get('query')?.errors?.['required']">Query is required</p>
      </div>
      
      <button type="submit" [disabled]="form.invalid || isExecuting">
        Execute Query
      </button>
    </form>
  `
})
export class ReactiveFormComponent implements OnInit {
  form!: FormGroup;
  languages = languages;
  isExecuting = false;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.form = this.fb.group({
      query: ['', [Validators.required, this.sqlValidator]]
    });
  }
  
  sqlValidator(control: any) {
    const value = control.value;
    if (value && !value.toLowerCase().includes('select')) {
      return { invalidSql: true };
    }
    return null;
  }
  
  onSubmit() {
    if (this.form.valid) {
      this.isExecuting = true;
      // Execute query...
      setTimeout(() => this.isExecuting = false, 2000);
    }
  }
}

Dynamic Language Switching

import { Component, OnInit } from '@angular/core';
import { LanguageDescription } from '@codemirror/language';
import { languages } from '@codemirror/language-data';

@Component({
  selector: 'app-multi-language-editor',
  template: `
    <div class="language-selector">
      <select [(ngModel)]="selectedLanguage" (change)="onLanguageChange()">
        <option *ngFor="let lang of availableLanguages" [value]="lang.name">
          {{ lang.name }}
        </option>
      </select>
    </div>
    
    <mj-code-editor
      [(ngModel)]="code"
      [languages]="languages"
      [language]="selectedLanguage"
      [setup]="'basic'"
      [indentUnit]="getIndentUnit()">
    </mj-code-editor>
  `
})
export class MultiLanguageEditorComponent implements OnInit {
  code = '';
  languages = languages;
  availableLanguages: LanguageDescription[] = [];
  selectedLanguage = 'JavaScript';
  
  ngOnInit() {
    // Filter to show only common languages
    this.availableLanguages = this.languages.filter(lang => 
      ['JavaScript', 'TypeScript', 'Python', 'Java', 'C++', 'HTML', 'CSS', 'SQL'].includes(lang.name)
    );
  }
  
  onLanguageChange() {
    // Update code sample based on language
    const samples: Record<string, string> = {
      'JavaScript': 'function greet(name) {\n  return `Hello, ${name}!`;\n}',
      'Python': 'def greet(name):\n    return f"Hello, {name}!"',
      'Java': 'public String greet(String name) {\n    return "Hello, " + name + "!";\n}',
      'SQL': 'SELECT name, email\nFROM users\nWHERE active = true;'
    };
    
    this.code = samples[this.selectedLanguage] || '// Start coding...';
  }
  
  getIndentUnit() {
    // Python uses 4 spaces, others use 2
    return this.selectedLanguage === 'Python' ? '    ' : '  ';
  }
}

Custom Extensions and Themes

import { Component } from '@angular/core';
import { Extension } from '@codemirror/state';
import { keymap } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { oneDark } from '@codemirror/theme-one-dark';

@Component({
  selector: 'app-custom-editor',
  template: `
    <mj-code-editor
      [(ngModel)]="code"
      [extensions]="customExtensions"
      [setup]="'minimal'"
      [placeholder]="'// Custom configured editor'">
    </mj-code-editor>
  `
})
export class CustomEditorComponent {
  code = '';
  
  customExtensions: Extension[] = [
    oneDark,  // Add One Dark theme
    keymap.of(defaultKeymap),  // Add default keybindings
    // Add more custom extensions as needed
  ];
}

Accessing the EditorView Instance

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { CodeEditorComponent } from '@memberjunction/ng-code-editor';

@Component({
  selector: 'app-advanced-editor',
  template: `
    <mj-code-editor
      #editor
      [(ngModel)]="code"
      [language]="'javascript'">
    </mj-code-editor>
    
    <button (click)="insertTextAtCursor()">Insert Comment</button>
    <button (click)="selectAll()">Select All</button>
  `
})
export class AdvancedEditorComponent implements AfterViewInit {
  @ViewChild('editor') editor!: CodeEditorComponent;
  code = '';
  
  ngAfterViewInit() {
    // Access the CodeMirror EditorView instance
    const view = this.editor.view;
    if (view) {
      console.log('Editor state:', view.state);
    }
  }
  
  insertTextAtCursor() {
    const view = this.editor.view;
    if (view) {
      const pos = view.state.selection.main.head;
      view.dispatch({
        changes: { from: pos, insert: '// TODO: ' }
      });
    }
  }
  
  selectAll() {
    const view = this.editor.view;
    if (view) {
      view.dispatch({
        selection: { anchor: 0, head: view.state.doc.length }
      });
    }
  }
}

API Reference

Component Selector

mj-code-editor

Inputs

InputTypeDefaultDescription
valuestring''The editor's content
disabledbooleanfalseDisables the editor (no editing allowed)
readonlybooleanfalseMakes the editor read-only (selection allowed)
placeholderstring''Placeholder text shown when editor is empty
indentWithTabbooleanfalseWhether Tab key indents instead of focusing next element
indentUnitstring''String used for indentation (e.g., ' ' for 2 spaces)
lineWrappingbooleanfalseWhether long lines wrap to next line
highlightWhitespacebooleanfalseWhether to visually highlight whitespace characters
languagesLanguageDescription[][]Array of available language descriptions (static)
languagestring''Current language for syntax highlighting
setup'basic' \| 'minimal' \| null'basic'Built-in editor setup configuration
extensionsExtension[][]Additional CodeMirror extensions
autoFocusbooleanfalseWhether to focus editor on initialization (static)
rootDocument \| ShadowRootundefinedCustom root for the editor (static)

Note: Inputs marked as "static" cannot be changed after initialization.

Outputs

OutputTypeDescription
changeEventEmitter<string>Emits when editor content changes
focusEventEmitter<void>Emits when editor gains focus
blurEventEmitter<void>Emits when editor loses focus

Public Methods

MethodParametersDescription
setValuevalue: stringProgrammatically set editor content
setExtensionsvalue: Extension[]Replace all editor extensions
setEditablevalue: booleanToggle editor's editable state
setReadonlyvalue: booleanToggle editor's readonly state
setPlaceholdervalue: stringUpdate placeholder text
setIndentWithTabvalue: booleanToggle Tab key indentation
setIndentUnitvalue: stringSet indentation string
setLineWrappingvalue: booleanToggle line wrapping
setHighlightWhitespacevalue: booleanToggle whitespace highlighting
setLanguagelang: stringChange syntax highlighting language

Public Properties

PropertyTypeDescription
viewEditorView \| undefinedThe underlying CodeMirror EditorView instance

Setup Modes

The setup input accepts three values:

  • 'basic' (default): Includes the full CodeMirror basic setup with line numbers, fold gutters, search, brackets matching, etc.
  • 'minimal': Includes only essential features like undo/redo history and basic keymaps
  • null: No built-in setup, allowing complete customization via extensions

Working with Languages

To enable syntax highlighting, you must:

  1. Import language descriptions from @codemirror/language-data
  2. Pass them to the languages input
  3. Set the desired language via the language input
import { languages } from '@codemirror/language-data';

// In your component
languages = languages;  // All 150+ languages
// or filter to specific languages
languages = languages.filter(lang => 
  ['JavaScript', 'TypeScript', 'Python'].includes(lang.name)
);

Language matching is case-insensitive and supports aliases. For example, "javascript", "JavaScript", "js" all work for JavaScript.

Integration with MemberJunction

This component integrates seamlessly with other MemberJunction packages:

  • Form Integration: Works with MJ's form generation and validation systems
  • Container Directives: Uses @memberjunction/ng-container-directives for advanced template composition
  • Entity Management: Can be used in entity forms for code-type fields

Example: Using in Entity Forms

import { BaseFormComponent } from '@memberjunction/ng-base-forms';

@Component({
  template: `
    <mj-code-editor
      [value]="record.CodeField"
      (change)="updateRecord('CodeField', $event)"
      [language]="'sql'"
      [readonly]="!this.EditMode">
    </mj-code-editor>
  `
})
export class CustomEntityFormComponent extends BaseFormComponent {
  updateRecord(field: string, value: any) {
    this.record[field] = value;
    this.RecordChanged = true;
  }
}

Performance Considerations

  • The editor uses CodeMirror 6's efficient document model for handling large files
  • Language loading is lazy - languages are only loaded when selected
  • Use 'minimal' setup for better performance with many editor instances
  • Consider virtual scrolling for very large documents (via custom extensions)

Build and Development

This package uses Angular CLI for building:

# Build the package
npm run build

# The built package will be in ./dist

Browser Support

Supports all modern browsers that CodeMirror 6 supports:

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)

License

ISC

Contributing

This component is part of the MemberJunction framework. For contribution guidelines, please refer to the main MemberJunction repository.

Troubleshooting

Common Issues

  1. No syntax highlighting: Ensure you've provided the languages input and set a valid language name
  2. Form validation not working: Make sure to import FormsModule or ReactiveFormsModule in your module
  3. Custom extensions not applying: Extensions are applied in order - ensure no conflicts with the setup mode
  4. Performance issues with large files: Consider using 'minimal' setup and adding only necessary extensions
2.27.1

8 months ago

2.23.2

8 months ago

2.46.0

4 months ago

2.23.1

8 months ago

2.27.0

8 months ago

2.34.0

6 months ago

2.30.0

7 months ago

2.19.4

9 months ago

2.19.5

9 months ago

2.19.2

9 months ago

2.19.3

9 months ago

2.19.0

9 months ago

2.19.1

9 months ago

2.15.2

9 months ago

2.34.2

6 months ago

2.34.1

6 months ago

2.15.1

9 months ago

2.38.0

5 months ago

2.45.0

5 months ago

2.22.1

8 months ago

2.22.0

8 months ago

2.41.0

5 months ago

2.22.2

8 months ago

2.26.1

8 months ago

2.26.0

8 months ago

2.33.0

6 months ago

2.18.3

9 months ago

2.18.1

9 months ago

2.18.2

9 months ago

2.18.0

9 months ago

2.37.1

5 months ago

2.37.0

5 months ago

2.14.0

10 months ago

2.21.0

9 months ago

2.44.0

5 months ago

2.40.0

5 months ago

2.29.0

7 months ago

2.29.2

7 months ago

2.29.1

7 months ago

2.25.0

8 months ago

2.48.0

4 months ago

2.32.0

7 months ago

2.32.2

7 months ago

2.32.1

7 months ago

2.17.0

9 months ago

2.13.4

10 months ago

2.36.0

6 months ago

2.13.2

11 months ago

2.13.3

10 months ago

2.13.0

11 months ago

2.36.1

6 months ago

2.13.1

11 months ago

2.43.0

5 months ago

2.20.2

9 months ago

2.20.3

9 months ago

2.20.0

9 months ago

2.20.1

9 months ago

2.28.0

8 months ago

2.47.0

4 months ago

2.24.1

8 months ago

2.24.0

8 months ago

2.31.0

7 months ago

2.12.0

12 months ago

2.39.0

5 months ago

2.16.1

9 months ago

2.35.1

6 months ago

2.35.0

6 months ago

2.16.0

9 months ago

2.42.1

5 months ago

2.42.0

5 months ago

2.23.0

8 months ago

2.11.0

12 months ago

2.10.0

12 months ago

2.9.0

12 months ago

2.8.0

1 year ago

2.7.0

1 year ago

2.6.1

1 year ago

2.5.2

1 year ago

2.6.0

1 year ago

2.7.1

1 year ago

2.5.1

1 year ago

2.5.0

1 year ago

2.4.1

1 year ago

2.4.0

1 year ago

2.3.3

1 year ago

2.3.2

1 year ago

2.3.1

1 year ago

2.3.0

1 year ago

2.2.1

1 year ago

2.2.0

1 year ago

2.1.5

1 year ago

2.1.4

1 year ago

2.1.3

1 year ago

2.1.2

1 year ago

2.1.1

1 year ago

2.0.0

1 year ago

1.8.1

1 year ago

1.8.0

1 year ago

1.7.1

1 year ago

1.7.0

1 year ago

1.6.1

1 year ago

1.6.0

1 year ago

1.5.3

1 year ago

1.5.2

1 year ago

1.5.1

1 year ago

1.5.0

1 year ago

1.4.1

1 year ago