2.48.0 • Published 5 months ago

@memberjunction/ng-code-editor v2.48.0

Weekly downloads
-
License
ISC
Repository
-
Last release
5 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

9 months ago

2.46.0

5 months ago

2.23.1

9 months ago

2.27.0

8 months ago

2.34.0

7 months ago

2.30.0

8 months ago

2.19.4

10 months ago

2.19.5

10 months ago

2.19.2

10 months ago

2.19.3

10 months ago

2.19.0

10 months ago

2.19.1

10 months ago

2.15.2

10 months ago

2.34.2

7 months ago

2.34.1

7 months ago

2.15.1

10 months ago

2.38.0

6 months ago

2.45.0

5 months ago

2.22.1

9 months ago

2.22.0

9 months ago

2.41.0

6 months ago

2.22.2

9 months ago

2.26.1

9 months ago

2.26.0

9 months ago

2.33.0

7 months ago

2.18.3

10 months ago

2.18.1

10 months ago

2.18.2

10 months ago

2.18.0

10 months ago

2.37.1

6 months ago

2.37.0

6 months ago

2.14.0

10 months ago

2.21.0

9 months ago

2.44.0

5 months ago

2.40.0

6 months ago

2.29.0

8 months ago

2.29.2

8 months ago

2.29.1

8 months ago

2.25.0

9 months ago

2.48.0

5 months ago

2.32.0

7 months ago

2.32.2

7 months ago

2.32.1

7 months ago

2.17.0

10 months ago

2.13.4

11 months ago

2.36.0

6 months ago

2.13.2

11 months ago

2.13.3

11 months ago

2.13.0

12 months ago

2.36.1

6 months ago

2.13.1

12 months ago

2.43.0

6 months ago

2.20.2

9 months ago

2.20.3

9 months ago

2.20.0

10 months ago

2.20.1

10 months ago

2.28.0

8 months ago

2.47.0

5 months ago

2.24.1

9 months ago

2.24.0

9 months ago

2.31.0

7 months ago

2.12.0

1 year ago

2.39.0

6 months ago

2.16.1

10 months ago

2.35.1

6 months ago

2.35.0

7 months ago

2.16.0

10 months ago

2.42.1

6 months ago

2.42.0

6 months ago

2.23.0

9 months ago

2.11.0

1 year ago

2.10.0

1 year ago

2.9.0

1 year 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