Text Angular with Angular Material 1.x

Update 12/2017: Too bad there seems not to be a version of this for the new Angular. I liked this one, but had to use CKEditor in my projects. Would be so much easier if Google just did a minimalistic rich editor for Material.

I was doing some front-end coding recently using AngularJS, hence I thought I'd share my struggles a bit. One of those was about finding a good WYSIWYG editor for rich user input that would be playing nicely with Angular Material library. I did some research to evaluate what's available, and as usual in front-end world, everything had it's quirks. I finally stayed with textAngular for couple of reasons, but as it was written with Bootstrap in mind it needed some alterations. The documentation however was not really helpful here, so I'm sharing what I did.

Why textAngular

There's CKEditor and TinyMCE, but I needed something minimalistic with mobile in mind. Trix, Quill, ProseMirror would probably fit the bill, however I have certain reservations about quality/maturity of 'glue' modules available that are linking them to AngularJS. They seem to work, but try using them within ng-repeat blocks, components with ng-if, etc. Whereas textAngular was build for Angular, no glue needed at all. The only problem is it's wasn't made for Angular Material, but exposes enough guts to make it run ok with it. I also had a hidden agenda of having implemented a file upload tool for it before, so that finally did it.

Installation and inclusions

Assuming we're using NPM to install things:

npm install angular-material textangular

Then we need to include or webpack some dependencies, so here they go.

For AngularJS:

<script src="node_modules/angular/angular.js"/>
<script src="node_modules/angular/angular-aria.js"/>
<script src="node_modules/angular/angular-animate.js"/>
<script src="node_modules/angular/angular-messages.js"/>

Angular Material:

<script src="node_modules/angular-material/angular-material.js"/>

textAngular:

<script src="node_modules/textangular/dist/textAngular-rangy.min.js"/>
<script src="node_modules/textangular/dist/textAngular-sanitize.js"/>
<script src="node_modules/textangular/dist/textAngular.js"/>
<script src="node_modules/textangular/dist/textAngularSetup.js"/>

For looks we need Google's Material Icons and Angular Material CSS:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" type="text/css" rel="stylesheet" media="screen,print">
<link href="node_modules/angular-material/angular-material.css" type="text/css" rel="stylesheet" media="screen,print">

Note I'm intentionally skipping both bootstrap and textAngular CSS, that's entirely the point here.

Angular module and textAngular configuration

In angular module that we're going to use textAngular we need to configure it. Cutomizations involve:

  • In taOptions.classes I'm setting ta-toolbar, ta-button-group, ta-text-editor and md-input classes to toolbar, buttonGroup, textEditor and htmlEditor UI elements respectively. The purpose is to customize looks.

  • For each tool in taTools I'm defining custom, angular-material friendly markup using Material Icons where relevant, i.e.:

      <md-button class="md-icon-button" aria-label="Bold">
          <md-icon md-font-set="material-icons">format_bold</md-icon>
      </md-button>
    

Ok, so here goes the whole config thing:

angular.module('app', ['textAngular', 'ngMaterial'])
    .config(['$provide', function ($provide) {
        $provide.decorator('taOptions', ['$delegate', function (taOptions) {
            taOptions.forceTextAngularSanitize = true;
            taOptions.keyMappings = [];
            taOptions.toolbar = [
                ['h1', 'h2', 'h3', 'p', 'pre', 'quote'],
                ['bold', 'italics', 'underline', 'ul', 'ol', 'redo', 'undo', 'clear'],
                ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
                ['html', 'insertImage', 'insertLink']
            ];
            taOptions.classes = {
                focussed: '',
                toolbar: 'ta-toolbar',
                toolbarGroup: 'ta-button-group',
                toolbarButton: '',
                toolbarButtonActive: 'active',
                disabled: 'disabled',
                textEditor: 'ta-text-editor',
                htmlEditor: 'md-input'
            };
            return taOptions; // whatever you return will be the taOptions
        }]);
        $provide.decorator('taTools', ['$delegate', function (taTools) {
            taTools.h1.display = '<md-button aria-label="Heading 1">H1</md-button>';
            taTools.h2.display = '<md-button aria-label="Heading 2">H2</md-button>';
            taTools.h3.display = '<md-button aria-label="Heading 3">H3</md-button>';
            taTools.p.display = '<md-button aria-label="Paragraph">P</md-button>';
            taTools.pre.display = '<md-button aria-label="Pre">pre</md-button>';
            taTools.quote.display = '<md-button class="md-icon-button" aria-label="Quote"><md-icon md-font-set="material-icons">format_quote</md-icon></md-button>';
            taTools.bold.display = '<md-button class="md-icon-button" aria-label="Bold"><md-icon md-font-set="material-icons">format_bold</md-icon></md-button>';
            taTools.italics.display = '<md-button class="md-icon-button" aria-label="Italic"><md-icon md-font-set="material-icons">format_italic</md-icon></md-button>';
            taTools.underline.display = '<md-button class="md-icon-button" aria-label="Underline"><md-icon md-font-set="material-icons">format_underlined</md-icon></md-button>';
            taTools.ul.display = '<md-button class="md-icon-button" aria-label="Buletted list"><md-icon md-font-set="material-icons">format_list_bulleted</md-icon></md-button>';
            taTools.ol.display = '<md-button class="md-icon-button" aria-label="Numbered list"><md-icon md-font-set="material-icons">format_list_numbered</md-icon></md-button>';
            taTools.undo.display = '<md-button class="md-icon-button" aria-label="Undo"><md-icon md-font-set="material-icons">undo</md-icon></md-button>';
            taTools.redo.display = '<md-button class="md-icon-button" aria-label="Redo"><md-icon md-font-set="material-icons">redo</md-icon></md-button>';
            taTools.justifyLeft.display = '<md-button class="md-icon-button" aria-label="Align left"><md-icon md-font-set="material-icons">format_align_left</md-icon></md-button>';
            taTools.justifyRight.display = '<md-button class="md-icon-button" aria-label="Align right"><md-icon md-font-set="material-icons">format_align_right</md-icon></md-button>';
            taTools.justifyCenter.display = '<md-button class="md-icon-button" aria-label="Align center"><md-icon md-font-set="material-icons">format_align_center</md-icon></md-button>';
            taTools.justifyFull.display = '<md-button class="md-icon-button" aria-label="Justify"><md-icon md-font-set="material-icons">format_align_justify</md-icon></md-button>';
            taTools.clear.display = '<md-button class="md-icon-button" aria-label="Clear formatting"><md-icon md-font-set="material-icons">format_clear</md-icon></md-button>';
            taTools.html.display = '<md-button class="md-icon-button" aria-label="Show HTML"><md-icon md-font-set="material-icons">code</md-icon></md-button>';
            taTools.insertLink.display = '<md-button class="md-icon-button" aria-label="Insert link"><md-icon md-font-set="material-icons">insert_link</md-icon></md-button>';
            taTools.insertImage.display = '<md-button class="md-icon-button" aria-label="Insert photo"><md-icon md-font-set="material-icons">insert_photo</md-icon></md-button>';
            return taTools;
        }]);
    }]);

Customize looks

I'll be using SCSS syntax here, with variables set to colors in material palette, default being something around:

$md-primary: #3f51b5;
$md-text-secondary: rgba(0, 0, 0, 0.54);
$md-background-100: #f5f5f5;

Originally textAngular is using Bootstrap layouts for toolbar, but material is using Flexbox, so:

.ta-toolbar {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  .ta-button-group {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    button {
      min-width: 0;
      min-height: 0;
      line-height: normal;
      margin-left: 0;
      color: $md-text-secondary;
      md-icon {
        color: $md-text-secondary;
      }
      &.active {
        background-color: $md-background-100;
      }

    }
  }
}

Last thing is to make html editor look more like standard angular-material input:

.ta-text-editor {
  .ta-bind {
    border-bottom: 1px solid rgba(0, 0, 0, 0.117647);
    &:focus {
      border-bottom: 2px solid $md-primary;
      outline: none;
    }
  }
}

The <textarea> holding HTML should already be taken care of, because it's got .md-input class in textAngular configuration.

HTML

In case anyone asks, I'm using it like this:

<md-input-container>
    <text-angular ng-model="foo"></text-angular>
</md-input-container>

Conclusion

Yes, I have working Pen here. Also feel free to take it further and don't forget to share it afterwards.

Polite Notice - if you have a questions concerning implementation details in your own projects then you're much better off asking them on Stack Overflow. More people to help you this way.