This HTML editor is created as a AngularJS directive, so it can be placed in any web application written in AngularJS. This tutorial will first discuss the design of this HTML editor control, and then discuss how it can be used in your application.
Introduction
This tutorial is the best one I have ever created for Code Project. A while ago, I wrote a similar tutorial using requirejs and stapesjs. In that, I promised that I will follow up with one using AngularJS. I started working on the sample program right after that one was done. When I completed the application, it was way better than I expected. When you check out the code, and get an understanding of this, you will agree this is pretty awesome.
The HTML editor is created as a AngularJS directive, so it can be placed in any web application written in AngularJS. This tutorial will first discuss the design of this HTML editor control. Then it discusses how it can be used in your application. The biggest challenge with this control, with any control that was created with AngularJS directive is data exchange. That is, it is kinda easy passing data from the host to the directive, but kinda hard passing data back to the host. I know there are broadcast
and emit
. You don't want to use them because they can be unpredictable and painfully slow. This tutorial will show you the way of two-way variable binding, so that value changes can be exchanged between the host and the directive. In order to do this correctly, I had to get creative. This tutorial will show you the trick I have used. In this tutorial, I also discuss how to get the text value from the editor textarea
, convert it to HTML and append to the div that is the preview area. The trick here is that every time user type in a character, the change will be reflected in the preview area. It will be explained here.
What this Application Looks Like
Before we venture any further, let's see what this sample application looks like:
The two buttons on the top are part of the host area. They are used for testing getting the value from the directive. Don't worry about them for now. There is a small button (marked as "+") under the two buttons. When you click on it, you can expand and show the preview area. Initially, the preview area will show nothing.
Just above the text area, where you enter the HTML content, there are two drop down button, each has an extended list of options that can be chosen to add HTML tags to the text area. You can also highlight some text in the textarea
, and click on one of the options in the drop downs, the HTML tags will be added to enclose the highlighted text.
Yep. This is not a "what you see is what you get" HTML editor. It is an old fashion code editor where you have to type in the HTML code and text, and hope it will come out all right. I just made this a little better with a hide-able preview area. What you type in should immediately show up so that you can fix the error if you see some.
If you don't think this is cool. Then please stop reading and go somewhere else. We don't want to waste your time. If you are interested in continuing, great! Let me just show you the last two screenshots. Here is the HTML content I would put in the text area:
The preview area will display this:
These two lists shows all the HTML tags available from the drop downs:
As you can see from the screenshot and some guess, you know the application and HTML editor are using bootstrap for mark-up. So the two lists of selectable HTML tags are also using bootstrap CSS classes. If you want to use a different set of CSS classes, please modify the application yourself. Good luck with it.
I hope the screenshots are convincing enough that you want to read ahead and enjoy all the secrets. Let's go check out the first secret.
Interaction between AngularJS Code and textarea
This is a pretty complicated sample application. All the complications are with in the definition of the directive. To spare you from all the boring tidbits, I will only show the most important parts of the directive. If you want to get the whole picture, please review the full source code of the HTML editor directive.
Getting the Text Content from TextArea
We all know how to get a reference of an HTML element through plain JavaScript, or JQuery. If you have read some of my old tutorials, you probably know it is done through AngularJS. This is just a refresh.
The editor has a textarea
defined as the following (see file htmlEditor.html):
<textarea id="contentSource" class="form-control editor-textarea" rows="12">
</textarea>
In the directive definition (htmlEditor.js or htmlEditor.sj file), I used super helper function angular.element()
to get the reference to this textarea
. This reference does not change because it is a static
element on the page. Once I have the reference, I don't have to get it again. Then getting and setting its value would be easy. Here are the codes that get the reference, and get/set the value of the textarea
:
...
vm.htmlEditorTextArea = angular.element("#editorArea #contentSource");
...
...
vm.htmlEditorTextArea.val(fmtText);
...
...
$scope.htmlText = vm.htmlEditorTextArea.value;
...
This is the easy part. Next comes something slightly harder.
Data Exchange between TextArea and Another HTML Element
Next, I will discuss how to handle the situation where a user types in some characters, the change will immediately show up on the preview area. For this to work, I have to use a scope variable, get the value from the textarea
, then bind this scope variable to the preview area. The advantage of this setup is that anything changes in the textarea
, the change should be passed to this scoped variable. Then the $scope.$apply()
will automatically apply the change to the scoped variable.
The trick is that any change in the textarea
must notify the directive to update the value for the scoped variable. It took a little research. Basically, any HTML input element can have some event handler attached. That is, any of these events can trigger some customized functions to be called. In this case, there are four different events that I can attach event handlers:
- "
input
", this event is triggered when the value of the textarea
is changed. - "
change
", this event is triggered when the value of the textarea
is about to be changed. The value that you can see has changed, but the change value has not been set to the textarea
. It is an in-between event. - "
focus
", this event is triggered when the textarea
gains the focus. - "
blur
", this event is triggered when the textarea
loses the focus.
I added the same event handler for all these. As soon as they are triggered, the value on the textarea
will be updated to the scoped variable. Here is how I do it:
...
vm.htmlEditorTextArea = angular.element("#editorArea #contentSource");
...
vm.htmlEditorTextArea.on("input", function () {
updateHtmlText(this);
});
vm.htmlEditorTextArea.on("change", function () {
updateHtmlText(this);
});
vm.htmlEditorTextArea.on("focus", function () {
updateHtmlText(this);
});
vm.htmlEditorTextArea.on("blur", function () {
updateHtmlText(this);
});
...
...
function updateHtmlText(ctrlRef) {
if (ctrlRef != null) {
$scope.htmlText = ctrlRef.value;
$scope.$apply();
}
}
It is necessary to call $scope.$apply()
because these event handling functions are outside of the realm of AngularJS code, so even if I assigned the value back to the scope variable $scope.htmlText
, it does not have the value yet. The $scope.$apply()
is like a commit of a transaction. It completes the transaction.
When you view the source code for the directive, you will see the directive is defined as this:
(function () {
"use strict";
var mod = angular.module("htmlEditorModule", [ ]);
mod.directive("htmlEditor", [ function () {
var htmlEditorController = ["$scope", "$sce", "htmlEditService",
function ($scope, $sce, htmlEditService) {
...
return {
restrict: "EA",
templateUrl: "/assets/app/pages/directives/htmlEditor/htmlEditor.html",
scope: {
htmlText: "="
},
controller: htmlEditorController,
controllerAs: "vm"
};
}]);
})();
Can you spot where the scoped variable $scope.htmlText
is defined? Here it is:
...
scope: {
htmlText: "="
},
...
The "scope
" is the isolated scope, and the htmlText
is used for two-way binding. It is bounded to a data model variable on the host, meaning the initial value from the host will be passed to directive. And the value change of $scope.htmlText
will be passed back to the host. With this setup, any change in the textarea
, will propagate all the way to the host. And there is no need to use the $broadcast()
or $emit()
.
Once you reviewed the code, understood all these, great! You have got the hardest part. From this point on, it will get easier. The next part, not as tricky as this, is how to display the text in the text area as HTML.
Display Text as HTML Elements
Displaying text as HTML with AngularJS is easy. AngularJS provided $sce
and ngBindHtml
(or ng-bind-html
) to take care this. In order to get to this, I have to use a data model variable. I have to pass the value from the scoped variable to this data model variable. Then the data model variable is bound to the view. Here is how the preview area is defined:
<div class="row html-preview-outer collapse" id="previewArea">
<div class="col-xs-12">
<div class="alert alert-info info-display">
You are preview the content.
</div>
</div>
<div class="col-xs-12 html-preview">
<div ng-bind-html="vm.htmlContent"></div>
</div>
</div>
The data model variable used is vm.htmlContent
. The problem is that this data model does not get the value directly. The data exchange between the host and the directive is the directive's scoped variable $scope.htmlText
. In order to also get the value to this data model variable, I have to use a watcher (via $scope.$watch()
). Here it is:
$scope.$watch("htmlText", function(newVal, oldVal) {
if (newVal != null && newVal.length >= 0 && newVal !== oldVal) {
vm.htmlEditorTextArea.val(newVal);
vm.htmlContent = $sce.trustAsHtml(newVal);
$scope.htmlText = newVal;
}
});
This watcher setup does a few things, It check the new value is not null
, not empty and is different from the old value. If this is true
, then:
- Set the value of
textarea
to this new value. - Set the value of the data model variable
vm.htmlContent
. This will show the update HTML content in the preview area. - Set the scoped variable to the new value.
This seems to be excessive. Why is this necessary? It is necessary is that when I did the testing, the initial design was not working. The HTML text value in the textarea
changed, the scoped variable also changed. But somehow, the change never made to the host. So I used this three-ways data exchange, and it solved the issue for me. Don't mistake that there were three copies of the same HTML text content. The value is passed by reference so there should be only one copy.
The next fun part is how to decorate the value in the text area with HTML tags. That means I need to get the highlighted text (the index positions of the first and the last characters of the selected text), and insert the HTML tags before or after the selected text, which is explained in the next section.
Slicing the Text Content Then Decorate
One must-have feature is selecting some text in the textarea
by highlighting the text. Then the user can add a beginning HTML tag and an end HTML tag. To do this, I have to cut the text into three pieces, the first piece is the text from the beginning to the last character just before the highlighted text starts. The second piece is the highlighted text. The last piece is from the first character right after the highlighted text to the end of the text. Then adding the beginning and end tags would be easy.
To get the highlighted text from the textarea
, we can use the properties selectionStart
and selectionEnd
. The first one returns the position of the first character of the highlighted text. The second one returns the position of the last character of the selection. With these two positions, I can slice the text into three parts.
What if there is no highlighted text? Well, it is simple, in this case, the start position and end position are the same position. So the second piece of the three pieces is an empty string. And we still have three pieces.
Here is the code that captures the start index and end index of the selection, and the whole text string of the textarea
:
function getSelectedTextSegment() {
var startPos = vm.htmlEditorTextArea.prop("selectionStart");
var endPos = vm.htmlEditorTextArea.prop("selectionEnd");
var wholeText = vm.htmlEditorTextArea.val();
if (startPos == null) {
startPos = 0;
}
if (endPos == null) {
endPos = 0;
}
if (wholeText != null && startPos > wholeText.length) {
startPos = wholeText.length-1;
}
if (wholeText != null && endPos > wholeText.length) {
endPos = wholeText.length-1;
}
return {
wholeText: wholeText,
startPos: startPos,
endPos: endPos
};
}
This function gets the start and end index of the highlighted text, and the whole text string. Then wrap them as an object and return the object. This object will be used by the AngularJS service to decorate the HTML tags. This function also deals with the unexpected. When the string is empty, the two indexes will be set to 0
. And if any of the indexes are exceeded the length of the string, it will be set to the last character of the string.
Here is the code that cuts the HTML content into three pieces, then adds the tags, finally all pieces are joined together:
function addHtmlTagsToText (wholeText, selStart, selEnd, startTag, endTag) {
if (wholeText != null && wholeText.length > 0) {
if (selStart < 0) {
selStart = 0;
}
if (selEnd < 0) {
selEnd = 0;
}
if (selStart >= wholeText.length) {
selStart = wholeText.length - 1;
}
if (selEnd >= wholeText.length) {
selEnd = wholeText.length - 1;
}
if (selStart == selEnd) {
var startText = wholeText.substring(0, selStart);
var endText = wholeText.substring(selStart);
return startText + startTag + endTag + endText;
} else if (selStart >= 0 && selStart < selEnd && selEnd <= wholeText.length) {
var startText = wholeText.substring(0, selStart);
var middleText = wholeText.substring(selStart, selEnd);
var endText = wholeText.substring(selEnd);
return startText + startTag + middleText + endTag + endText;
} else {
return wholeText;
}
} else {
return "" + startTag + endTag;
}
}
The function is pretty straight forward. First, it checks if the text from the textarea
is not null
. Then it does the same check of the start index and end index of the highlighted portion, and does the "re-positioning" if needed. When the check is done, I use JavaScript's substring()
to slice the whole string into three pieces (two if there is no highlighted portion). Finally, the string is put back after the start tag and end tag was added to the highlighted part, then the first part, second part and third part are added together as one.
These are all the vital parts of this reusable component. They are only pieces of a complex component. If you don't want to design something similar, you don't need to know all these details. Next, I will show you how this reusable component can be used.
Using this HTML Editor
To demonstrate how this HTML editor can be used, I wrote a simple application. All this application has is this HTML editor and two buttons. One button labelled "Load", when clicked, will call a backend RESTFul API to load the HTML content and display in the textarea
of the HTML editor, as well as in the preview area. The other button "Save" will push the HTML content from the editor to the back end via RESTFul API. Both buttons are part of the host, not part of the directive. The intention here is to demonstrate the data exchanges between the directive and the host.
You can see the directive being used on the index page (index.html) of this application. Here is how it is declared:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
...
<div html-editor html-text="vm.htmlText"></div>
...
...
</body>
</html>
This (app.js file) is how you inject and defined the host data model variable to be bound to the HTML editor directive:
(function () {
"use strict";
var mod = angular.module("testSampleApp",
[ "ngResource", "infoDisplayModule", "htmlEditorModule" ]);
mod.controller("testSampleController",
[ "$scope", "$sce", "$resource", "infoDisplayService",
function ($scope, $sce, $resource, infoDisplayService) {
var vm = this;
vm.htmlText = "";
...
}
]);
})();
I have highlighted the import parts in this. First, in order to get to the directive, I must inject the module which has the directive to the application module. Then I define the data model variable, which is vm.htmlText
. These two and the HTML element are all you need to use this HTML editor directive.
On the HTML page of the application, the directive is injected as an attribute called html-editor
to a <div>
. The application scope's variable htmlText
is passed into the directive via two-way binding by the attribute html-text
. This is how the two way binding works. With this mark up and the injection of the module into the application, the whole application is complete.
Next, I will describe how it can be tested.
How to Test the Application
To build the sample application, use command line prompt, go to the folder where you find the pom.xml file. Then run the following command:
mvn clean install
To run the application, after building it, type in the following command and hit Enter:
java -jar target\hanbo-agular-htmleditor-1.0.1.jar
Wait for the startup run completes, then you will see something at the very end, like this:
...
...
2020-10-28 23:20:13.998 INFO 3460 --- [ main]
o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-28 23:20:14.129 INFO 3460 --- [ main]
o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page:
class path resource [static/index.html]
2020-10-28 23:20:14.273 INFO 3460 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on
port(s): 8080 (http) with context path ''
2020-10-28 23:20:14.289 INFO 3460 --- [ main] org.hanbo.boot.rest.App
: Started App in 3.942 seconds (JVM running for 4.586)
If you get any errors, most likely the port 8080 is being used by something else. Check that and re-run it again.
If you can successfully run it, you can now use the browser to navigate to the URL at:
http://localhost:8080
You will see the index page with the HTML editor. Try the Load button. And see if you can see the HTML content populated into the editor textarea
. And try all the tag commands in the drop down and see what they would display. Have fun with this. Finally, hit the Save button, the content will be send to the back end and you can see it displayed in the command line prompt log output.
Summary
The subject of this tutorial is really complex. It is a reusable HTML editor written in AngularJS and is suited for AngularJS and Bootstrap based applications only. It is probably the first reusable component I have offered to the general public. I have tested this component as much as I can. And I believe there might be issues, use it at your own risk.
Unlike some of my tutorials from the past, this one is not as detailed. I only described some key areas abut this component, such as how to get the text content of the textarea
, how data exchange between textarea
and the preview area, and how the data exchange is done between the directive and the host. These are the important aspects of the design. For end users, these don't matter. The section "Using This HTML Editor"describes how it can be used in your application (as long as it is based on Bootstrap and AngularJS). It is best used in the scenario where it occupies the entire page because it takes a lot of page spaces.
Enjoy! I will see you again in 2021!
History
- 28th October, 2020 - Initial draft