Search code examples
javascriptangularjsangularsystemjs

Integrate the ng2-ace library into a freshly created angular-cli (angular2) project using SystemJS


I just created an angular2 project with the latest angular-cli tool. I now want to get the ace editor up and running using the ng2-ace library. I want to do it in a clean approach using SystemJS as the module loader.

I did

npm install --save ng2-ace

then I added the following two lines to angular-cli-builds.js to the vendorNpmFiles array

'ng2-ace/index.js',
'brace/**/*.js

then I added the following to system-config.ts

 const map: any = {
   'ng2-ace': 'vendor/ng2-ace',
   'brace': 'vendor/brace'
 };

 /** User packages configuration. */
 const packages: any = {
   'brace': {
     format: 'cjs',
     defaultExtension: 'js',
     main: 'index.js'
   },
   'ng2-ace': {
     format: 'cjs',
     defaultExtension: 'js',
     main: 'index.js'
   }
 };

Now I tried importing the directive from a component

import { AceEditorDirective } from 'ng2-ace';

This makes the compiler ng serve aborting with the following error:

The Broccoli Plugin: [BroccoliTypeScriptCompiler] failed with:
Error: Typescript found the following errors:
Cannot find module 'ng2-ace'.

I tried to follow the Readme from angular-cli and got the google material design library working. However, I don't know what I do wrong when trying to load the ng2-ace library.


Solution

  • I think the reason this is so hard is that there's no typings library provided. I was able to get the rough equivalent of this working by adding a few things. My version has a pretty static configuration but you can enhance it.

    system-config needs this:

    const map:any = {
      'brace': 'vendor/brace',
      'w3c-blob': 'vendor/w3c-blob',
      'buffer': 'vendor/buffer-shims'
    };
    

    it may also need:

    const packages:any = {
      'w3c-blob': {
        format: 'cjs',
        defaultExtension: 'js',
        main: 'index.js'
      },
    
      'brace': {
        format: 'cjs',
        defaultExtension: 'js',
        main: 'index.js'
      },
    
      'buffer': {
        format: 'cjs',
        defaultExtension: 'js',
        main: 'index.js'
      }
    };
    

    Then you also need to add these things as npm dependencies in angular-cli-build.js:

    module.exports = function(defaults) {
      return new Angular2App(defaults, {
        vendorNpmFiles: [
          /* your stuff goes here, then add: */
          'brace/**/*.js',
          'w3c-blob/index.js',
          'buffer-shims/index.js'
     ]
    });
    

    That pretty much gets you everything you need as far as dependencies are concerned. At this point I added my own directive. The important parts are here:

    import { Directive, ElementRef, EventEmitter } from '@angular/core';
    

    Now import brace itself plus whatever modes and themes you will be using:

    import 'brace';
    declare let ace;
    
    import 'vendor/brace/mode/javascript';
    import 'vendor/brace/theme/monokai';
    

    The 'declare let ace' lets you have access to brace even though there's no typings and it's not a real typescript module.

    I named my directive 'js-editor' and you attach it to a tag of an appropriate height and width. The docs for brace say to apply a 'block' style to the div it as well. Then declare the directive:

    @Directive({
      selector: '[js-editor]',
      inputs: ['text'],
      outputs: ['textChanged', 'editorRef']
    })
    export class JsEditor {
    
      editor : any;
      textChanged : EventEmitter<any>;
      editorRef : EventEmitter<any>;
      value : string;
    
      set text(value) {
        // if(value === this.oldVal) return;
        // this.editor.setValue(value);
        // this.editor.clearSelection();
        this.editor.focus();
      }
    
      constructor(elementRef : ElementRef) {
        this.textChanged = new EventEmitter();
        this.editorRef = new EventEmitter();
    
        const el = elementRef.nativeElement;
        el.classList.add('editor');
    

    Setting the base path is the key element to brace being able to find modes and themes. This is really the wrong place to set it - it should be done globally, and ONLY ONCE ... but this was just an experiment to see if it would work so I did it here:

        ace.config.set('basePath', 'vendor/brace');
    

    Finally, create the editor:

        this.editor = ace.edit(el);
    

    and then set your mode and theme. Note that these modes/themes LOOK like paths, but they really are not. Ace (or perhaps Brace) will use these strings to generate the path using the basePath above:

        this.editor.getSession().setMode('ace/mode/javascript');
        this.editor.setTheme('ace/theme/monokai');
    
        setTimeout(() => {
          this.editorRef.next(this.editor);
        });
    
        this.editor.on('change', () => {
            /* do whatever you want here */
        });
      }
    }
    

    That's the general idea. It really needs to be wrapped up into a nice configurable directive along the lines of ng2-ace but I'm not the right guy to do that, I just wanted to get you headed in the right direction.

    --Chris