Stephan Miller

Creating an AngularJS 2 contenteditable Directive the Wrong Way

The contenteditable Attribute

It was the discovery of the contenteditable attribute in HTML that set me off on my development of Zen Notebook. I was lazy. I didn’t want to shoehorn magical CSS into my application to hide all the indication that you were typing in a textarea. I know from making websites cross browser compatible that fucking with forms was a big nope. They just not going to look the same across browsers. Browsers are very opinionated about their forms. This attribute was the answer even though it sometimes gets a bad wrap.

Unfortunately, AngularJS 1 didn’t make the contenteditable attribute two-way data bindable. So I ended up writing a directive to do that.

AngularJS 1 contenteditable Directive

app.directive("contenteditable", ['$rootScope', function ($rootScope) {
    return {
        restrict: "A",
        link: function (scope, element, attrs) {
            var content = '';

            element.bind("blur keyup change focus", function (event) {
                content = element.html();
                $rootScope.editorContent = content;
            });
        }
    };
}]);

AngularJS2 contenteditable Directive

So when I upgraded the application to AngularJS 2, I had to rewrite the directive along with just about everything else. So I am learning Typescript now, which is the standard for Angular 2 and I am liking it. It forces you to think more about what you are creating. And a linter will tell you if it will compile even before it does.

Here is that directive:

import {Input, Output, Directive, ElementRef, Renderer, EventEmitter, OnInit} from '@angular/core'

@Directive({
    selector: '[contenteditable]',
    host: {
        '(input)': 'update($event)'
    }
})
export class contenteditableDirective implements OnInit {
    @Input() myContent;
    @Output() myContentChange: EventEmitter<any> = new EventEmitter();

    constructor(private el: ElementRef, private renderer: Renderer) { }

    update(event) {
        this.myContentChange.emit(this.el.nativeElement.innerText);
    }

    ngOnInit() {
        this.el.nativeElement.innerText = this.myContent;
    }
}

The @Directive above is a decorator. We put it above the class to declare that it is a Directive and add some metadata to it.

The selector key is the CSS selector we are targeting. By using brackets, we are saying we want to target an attribute of the element, the contenteditable attribute.

The host key, well, that’s the cart before the horse.

@Input declares an input property, myContent which we can update via a property binding which we will see in our component template below. This is one direction in the two way binding we are creating.

@Output declares an output property, myContentChange, that fires events that you can subscribe to with an event binding. And this event binding is what is in the host key of the decorator.

Okay, so the host key, this is basically what I am using to fire the actual event defined with @Output. Went for some research, came back and decided, yes, this is the wrong way to do things, but not trashing the post, explained in the ‘Although This Works…’ section below.

Using the Directive in Another Component

Now lets put the directive to use.

AngularJS 2 Template

<body>
    <app>
      ...loading
    </app>
</body>

Not much to be said about this, except we are going to have our component we are creating target the app element.

AngularJS 2 Component

import {Component} from '@angular/core';
import {contenteditableDirective} from './contenteditableDirective';

@Component({
    template: `<div class="content" contenteditable [(myContent)]="editorContent">
      </div>
      <h2>Result</h2>
      <div class="result"></div>`,
    directives: [contenteditableDirective],
    selector: 'app'
})
export class App {
    public editorContent: string;

    constructor() {
        this.editorContent = '';
    }
}

Here we have another decorator for a Component class. We don’t do much in the class itself except setting the editorContent variable to an empty string. The decorator does a lot more work in this example than the last.

First, it targets the app element, like we said it would, so we use the name of the element without an extra characters to signify we are targeting an element.

We also do our dependency injection here. We are injecting contenteditableDirective so it can do it’s job on our element.

And finally we have the template that will be applied to the app element, which has the event binding for the directive.

Plunker Example

And then I thought, why not, create a live example. It won’t take that long and it adds value. Well, that was so much of a pain in the ass to do for the first time that I took a break for a day and came back, only because I was unsure of how to use Typescript on Plunker. I figured that out. Then it took me a while to figure out the config.js file. I got that worked out and then it took me a while to discover I had to get the files for my version of Angular 2 from npm rather than the CDN in the examples I was working from.

You can find the working example here

Although This Works…

So I wrote the code part, then I went about explaining myself and realized I that made more than one mistake in the code above. So look for the sequel, coming to my blog soon…when I have the time, with newly factored logic, code and explaining myself. But it works for now, and that is a problem when you are learning something new. Too easy too break it and spending hours only to roll back to the original.

But for now I know I have to research local variables like <div contenteditable #myContent></div> and using @HostBinding and @HostListener. I thought I’d throw that in just in case I don’t revisit this for a while. It’s what happens when you write your code and a few weeks later, you write a post about it and things change. Who knows, by the time I get back to this, something totally different might be the right way.

Stephan Miller

Written by

Kansas City Software Engineer and Author

Twitter | Github | LinkedIn

Updated