Let's face it, copy-and-paste of rich content in browsers is almost always terrible. All those sterling efforts by W3C and the like in getting everyone to agree on how a particular chunk of HTML should be rendered, have never extended to what should happen when you click-drag your mouse across a slice of screen real-estate and hit <ctrl>-c.

If you're really interested in getting an appreciation of the size of the problem, download a clipboard viewer (like one of these: http://bfy.tw/ASgC ) and be amazed at how the various browsers 'helpfully' process the HTML in completely different ways before dropping it into the clipboard memory.

And that's all before you start throwing copy-paste from other applications like word processors and email clients into the mix. It's a free-for-all out there, and that mess is why most attempts at adding WYSIWYG (What You See Is What You Get) will inevitably generate support requests.

The irony that this very blog post is being typed as markdown is certainly not lost on the author.

Introducing Textbox.io

I've tried a fair few WYSIWYG editors over the years and the product that seems to have made the best-attempt-yet at sorting through this complexity is Textbox.io by Ephox. It's pretty rare for me to consider a non-free option but I can only assume they got it to acceptably handle pasting from Microsoft Word into multiple browsers through sheer bloody-minded effort.
Try out the demo here: https://textbox.io/

Textbox.io is "free for non-commercial use" under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International license.
https://creativecommons.org/licenses/by-nc-nd/4.0/
WTF that means for you and your project is left as an exercise for the reader. From a technical point-of-view this means it's not free enough to be an npm package.

Introducing Angular cli

The rest of this post assumes your're familiar with Angular cli. If not, get started here: https://github.com/angular/angular-cli

Integrating Textbox.io with Angular cli

So here's how I recently added Textbox.io to an angular/cli project.

Install

As there's no npm package, the installation involved downloading from the website and extracting under the /assets folder.

Add script reference to angular-cli.json

This file has a scripts array that adds global-scoped javascript files.

"scripts": [
    "assets/textboxio-client/textboxio/textboxio.js"
],

Add some global configuration to index.html

After adding that scripts entry, Textbox.io is now available under the global object textboxio
The library won't be able 'find itself' under angular2-cli (it loads other resources from the textboxio folder when invoked). So lets give it some global configuration inside index.html

    <script type="text/javascript">
        var textboxioConfig = {
          basePath : [your-app-path]+'assets/textboxio-client/textboxio',
          images : {
            allowLocal : true,
            editing : {
               enabled : false
            }
          }
        };
    </script>

BasePath - is the crucial setting here.

Turning a div or textarea into a Textbox.io editor

Now you should be able to turn any div or <input type="text"> into a rich-text editor from any Angular component with:

declare var textboxio: any;
declare var textboxioConfig: any;

Figure: add this to the top of your component

textboxio.replace('#myDiv', textboxioConfig);

Figure: use this inside your component

OK, so far we've hopefully proved that we can get Textbox.io to run but we've not got much integration so far.

  • When we call textboxio.replace the div we're targeting needs to exist in the DOM, so we need to think about where in the lifecycle to invoke this line.
    • We've also got more to think about if we've placed that div under an *ngIf etc.
  • We've got none of that awesome data binding that makes Angular 2 rock. We'll pick up the initial content inside the element when we call textboxio.replace but nothing more.

To address this, we'll create a component.

A Textbox.io Component

So lets build a component. This will:

  • Initialize the Textbox.io editor at the right time (after the element we're targeting has been created in the DOM)
  • Support 2-way binding
    • Detect changes to the bound model and update the html display
    • Raise an appropriate event when the textbox is used to change the content (either through typing or using the editor's menu controls)
  • Hide any Textbox.io specific complexity so that the rest of the application can use this like any other Angular component.

The Template

    <div [id]="textboxId" class="textboxio">
    </div>

Possibly one of the simplest templates ever! You can see that I decided to data-bind the id property - this is in case anyone is crazy enough to want to load multiple instances of this on a page.

Initializing

To initialize after this view has been loaded, we'll use ngAfterViewInit
https://angular.io/docs/ts/latest/api/core/index/AfterViewInit-class.html

    export class TextboxIoComponent implements AfterViewInit {
        private _textbox: any = null;
        private _content: string;
    
        @Input() textboxId: string;

        ngAfterViewInit() {
            this.initTextbox();
       }

       initTextbox() {
           this._textbox = textboxio.replace('#' + this.textboxId, textboxioConfig);

        this.updateTextBox();
       }
    }

So textboxio.replace is what finds the element in our page and loads the textbox ui into our DOM. We assign the result of textboxio.replace to a variable so that we can tell that the editor has been initialized.

Data Binding In

Detecting incoming changes is easy - we just need to decorate a typescript getter with the @Input attribute.

    @Input() set content(content: string)
    {
        this._content = content;
        if (this._textbox) this.updateTextbox();
    }

This input will be called before initTextbox() has completed so we check this._textbox exists before updating.

updateTextBox() applies the current value of this._content to the editor using this._textbox.content.set

     updateTextbox() {
        if (! this._content) { 
          this._textbox.content.set('');
          return;
        }
        // only call _textbox.content.set if content is different. 
        if (this._content !== this._textbox.content.get()) { 
          this._textbox.content.set(this._content); 
        }    
      }

We've also got a bit of logic here to workaround a minor issue with textboxio - when this._textbox.content.set is called, the content is replaced and the cursor position is set to the start. This is fine when we are completely replacing the content, but once our @Output event is in place, change events will fire out every time we type and the new value will come back to us again via the @Input, triggering updateTextbox() and resetting the cursor for every key we press.

So this workaround means we only call this._textbox.content.set if the new incoming content is different from the current content.

Data Binding Out

Next we need send change events out when the content is changed. For this, the Textbox.io API gives us an event called 'dirty' which is raised when the content is first changed. This is only raised once so as soon as we catch it we need to clear the dirty state so that we catch the next change.
This next code block contains our whole initTextbox() function, including the change event listener as well as the @output EventEmitter.

    @Output() contentChanged = new EventEmitter<string>();

    initTextbox() {
        let component = this;
        component._textbox = textboxio.replace('#' + this.textboxId, textboxioConfig);
        component.updateTextbox();
        component._textbox.events.dirty.addListener(function(){
          component.change(component._textbox.content.get());  
          // always reset dirty so we catch the next change event - we should be tracking model changes in the parent component's model
          component._textbox.content.setDirty(false);
        });
      }

     change(newContent: string) {
        this._content = newContent;
        this.contentChanged.emit(this._content);
      }

Usage

Now you have a component, you can reuse this across your application. I typically maintain a Shared module declaring a library of UI components to be available everywhere.

Lets see this embedded in a bootstrap - styled form-group

    <div class="form-group">
        <label class="control-label col-md-3">
         Additional Information
       </label>
        <div class="col-md-9">
          <app-textbox-io [textboxId]="'textboxNotes'" [content]="externalContact.notes" (contentChanged)="textboxChanged($event)"></app-textbox-io>
        </div>  
      </div>  

Summary

Although a lot of the details here are specific to the Textbox.io API, the basic technique of wrapping third-party JavaScript libraries as reusable Angular components can be useful for many of your other favourite js widgets.