A Tutorial on Stapes JS - Reusable HTML Editor

Credit: CodeProject.com, Used under: fair use/non-profit/educational purpose.

This tutorial was published on www.codeproject.com on Jun 16, 2020.

Introduction

When I was writing my tutorial on require.js, I promised I will write a tutorial on stapes.js. I had several reasons for this:

  • I am always searching for a JavaScript framework that I can write classes like I do in Java or C#. Stapes JS is the closest one that match my expectation.
  • It would be considerable easier to create reusable components with Stapes JS and Require JS than using JQuery along.
  • This framework is encapsulated in just one JS file, adding and configuring for an application is very easy.
  • This framework is very easy to learn, and integrates with JQuery with minimum effort, it is an ideal wrapper for JQuery.

These are the reasons based on my experience with Stapes JS. It is one framework that can/should be used when you starts a brand new application development instead of using JQuery. As long as Stapes JS is used properly, it would greatly improve the code quality of the application.

For this tutorial, I have designed a JavaScript based HTML editor, just to show how to create an component with Stapes JS, then another component to utilize this HTML editor. Let's get started.

The Architecture

The screenshot of the application looks like this:

The application, as shown above, has two sections. The top side is the preview section. The bottom side is where a user can enter the HTML content, then the change would be reflected on the top side.

This is a single page web application. The page index.html is wrapped inside a Spring Boot application. This Spring Boot application does only one thing, starting up an web application and serve the index page to the user's browser, along with the CSS files and JavaScript files.

I used Bootstrap for the CSS mark-ups, with JQuery and StapesJS for the client side application development. JQuery is used for initializing Bootstrap, and closely integrated with Stapes JS. The function of querying HTML elements and setting up the event handling are all done with JQuery. It is just unrecognizable the way it was used, as you will see later.

The HTML Mark-up

First, let's take a look at the HTML markup of the web page. The mark up of the web page is in a file called "index.html".

The HTML content preview section is defined as the following:

<div class="row">
   <div class="col-xs-12 col-sm-offset-1 col-sm-10">
      <div id="htmlContent" class="html-content"></div>
   </div>
</div>

This code snippet defines a placeholder, where additional html code can be attached to, this is how the user's input HTML will be visible in the preview area.

In order to decorate the text entered in the text area with html tags by clicking some button, I have to create a drop down menu. The drop down menu looks like this:

The HTML markup for this drop down menu will be as this:

<div class="row combo-box">
   <div class="col-xs-12 col-sm-6 col-md-3">
      <div class="btn-group">
         <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            HTML Formatting <span class="caret"></span>
         </button>
         <ul class="dropdown-menu">
            <li><a id="h1Action">Header 1</a></li>
            <li><a id="h2Action">Header 2</a></li>
            <li><a id="h3Action">Header 3</a></li>
            <li><a id="h4Action">Header 4</a></li>
            <li><a id="h5Action">Header 5</a></li>
            <li role="separator" class="divider"></li>
            <li><a id="pAction">Paragraph</a></li>
            <li><a id="bAction">Bold</a></li>
            <li><a id="iAction">Italic</a></li>
            <li><a id="uAction">Underline</a></li>
            <li><a id="stAction">Strike Through</a></li>
         </ul>
      </div>
   </div>
</div>

The text area for entering the HTML content is as the following:

<textarea class="form-control" id="textEditor" rows="6" cols="40">
</textarea>

The red button for clear out the HTML content in the text area is defined as the following:

<div class="col-xs-12 col-sm-offset-1 col-sm-10">
   <div class="row">
      <div class="col-xs-12 col-sm-offset-7 col-sm-5">
          <button id="clearBtn" class="form-control btn btn-danger app-btn">Clear</button>
      </div>
   </div>
</div>

Lastly, I have added another button in a separated section (by itself). When user click this button, it will write to console the HTML content in the HTML editor.

<div class="container" id="otherServices">
   <div class="row">
      <div class="col-xs-4">
         <button class="btn btn-default" id="getHtml">Get HTML</button>
      </div>
   </div>
</div>

The intention of this button is to demonstrate how one component can access the data of another component, and the JavaScript section will show how this interaction is done.

Before I get to the JavaScript section, I will show you the scripts declaration section:

<script src="/assets/jquery/js/jquery.min.js"></script>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/underscore/underscore-min-1.9.2.js"></script>
<script src="/assets/stapes/stapes-min-1.0.0.js"></script>
<script src="/assets/app/app.js"></script>

As shown, all the HTL mark ups had no event handler attached. The event handling are all attached when the app.js is loaded and executed. app.js is where main javascript code is implemented. I will show it to you next.

The JavaScript application

I have mentioned before, using Stapes JS, I can define classes similar to classes defined in Java or C#. I will show you why this is. As shown at the end of last section, the last file included in the html file is the app.js, where all the UI interactions are implemented. In this JS file, you will see what I have claimed, class definition that is similar to the ones defined in Java or C#. Let me post the source code of the object class for the HTML editor. Here it is:

var testApp = Stapes.subclass({
   htmlContentView: null,
   textEditor: null,
   textFormat: null,
   updateBtn: null,
   clearBtn: null,
   
   h1Action: null,
   h2Action: null,
   h3Action: null,
   h4Action: null,
   h5Action: null,

   pAction: null,
   bAction: null,
   iAction: null,
   uAction: null,
   stAction: null,
   
   getHtmlVal: function () {
      var self = this;
      var retVal = self.textEditor.val();
      return retVal;
   },
   
   constructor : function() {
      var self = this;
      self.$el = $("#htmlEditing");
      
      self.htmlContentView = self.$el.find("#htmlContent");
      self.textEditor = self.$el.find("#htmlEditorDiv #textEditor");
      self.updateBtn = self.$el.find("#updateBtn");
      self.clearBtn = self.$el.find("#clearBtn");
      
      self.h1Action = self.$el.find(".btn-group #h1Action");
      self.h2Action = self.$el.find(".btn-group #h2Action");
      self.h3Action = self.$el.find(".btn-group #h3Action");
      self.h4Action = self.$el.find(".btn-group #h4Action");
      self.h5Action = self.$el.find(".btn-group #h5Action");

      self.pAction = self.$el.find(".btn-group #pAction");
      self.bAction = self.$el.find(".btn-group #bAction");
      self.iAction = self.$el.find(".btn-group #iAction");
      self.uAction = self.$el.find(".btn-group #uAction");
      self.stAction = self.$el.find(".btn-group #stAction");

      self.textEditor.on("input", function (e) {
     	   var htmlText = self.textEditor.val();
     	   self.htmlContentView.html(htmlText);
      });
      
      self.clearBtn.on("click", function(e) {
         self.textEditor.val("");
      });

      self.$el.on("submit", function(e) {
         e.preventDefault();
      });
      
      self.h1Action.on("click", function (e) {
         formatHtml("<h1>", "</h1>");
      });
      self.h2Action.on("click", function (e) {
         formatHtml("<h2>", "</h2>");
      });
      self.h3Action.on("click", function (e) {
         formatHtml("<h3>", "</h3>");
      });
      self.h4Action.on("click", function (e) {
         formatHtml("<h4>", "</h4>");
      });
      self.h5Action.on("click", function (e) {
         formatHtml("<h5>", "</h5>");
      });

      self.pAction.on("click", function (e) {
         formatHtml("<p>", "</p>");         
      });
      self.bAction.on("click", function (e) {
         formatHtml("<strong>", "</strong>");
      });
      self.iAction.on("click", function (e) {
         formatHtml("<span style='font-style: italic'>", "</span>");
      });
      self.uAction.on("click", function (e) {
         formatHtml("<span style='text-decoration: underline'>", "</span>");
      });
      self.stAction.on("click", function (e) {
         formatHtml("<span style='text-decoration: line-through'>", "</span>");
      });
      
      var formatHtml = function (beginTag, endTag) {
         var htmltext = self.textEditor.val();
         if (htmltext != null && htmltext.length >= 0) {
            var selStart = self.textEditor[0].selectionStart;
            var selEnd = self.textEditor[0].selectionEnd;
            
            if (selStart >= 0 && selEnd >= 0) {
               var seg1 = htmltext.substring(0, selStart);
               var seg2 = htmltext.substring(selStart, selEnd);
               var seg3 = htmltext.substring(selEnd);
               
               if (seg1 == null) {
                  seg1 = "";
               }
               
               if (seg2 == null) {
                  seg2 = "";
               }
               
               if (seg3 == null) {
                  seg3 = "";
               }
               
               var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
               self.textEditor.val(htmltext2);
            }
         }
      };
   }
});

What is in this listing can be broken into serveral different sections. The first is the definition of the component class, which is this:

var testApp = Stapes.subclass({
...
...
});

Next I want to define some class properties. These properties hold the references to the HTML elements. With these references, I can attach event handlers to them. What I have described would look similar to C# WinForm programming. Here is the definition of the class properties:

...
   htmlContentView: null,
   textEditor: null,
   textFormat: null,
   updateBtn: null,
   clearBtn: null,
   
   h1Action: null,
   h2Action: null,
   h3Action: null,
   h4Action: null,
   h5Action: null,

   pAction: null,
   bAction: null,
   iAction: null,
   uAction: null,
   stAction: null,
...

To initialize these properties, I have defined an object constructor for this class. What this constructor does is use JQuery to find the elements on the HTML page. Then attach the event handling methods to them:

...
constructor : function() {
   var self = this;
   self.$el = $("#htmlEditing");
   
   self.htmlContentView = self.$el.find("#htmlContent");
   self.textEditor = self.$el.find("#htmlEditorDiv #textEditor");
   self.updateBtn = self.$el.find("#updateBtn");
   self.clearBtn = self.$el.find("#clearBtn");
   
   self.h1Action = self.$el.find(".btn-group #h1Action");
   self.h2Action = self.$el.find(".btn-group #h2Action");
   self.h3Action = self.$el.find(".btn-group #h3Action");
   self.h4Action = self.$el.find(".btn-group #h4Action");
   self.h5Action = self.$el.find(".btn-group #h5Action");

   self.pAction = self.$el.find(".btn-group #pAction");
   self.bAction = self.$el.find(".btn-group #bAction");
   self.iAction = self.$el.find(".btn-group #iAction");
   self.uAction = self.$el.find(".btn-group #uAction");
   self.stAction = self.$el.find(".btn-group #stAction");

   self.textEditor.on("input", function (e) {
      var htmlText = self.textEditor.val();
      self.htmlContentView.html(htmlText);
   });
   
   self.clearBtn.on("click", function(e) {
      self.textEditor.val("");
   });

   self.$el.on("submit", function(e) {
      e.preventDefault();
   });
   
   self.h1Action.on("click", function (e) {
      formatHtml("<h1>", "</h1>");
   });
   self.h2Action.on("click", function (e) {
      formatHtml("<h2>", "</h2>");
   });
   self.h3Action.on("click", function (e) {
      formatHtml("<h3>", "</h3>");
   });
   self.h4Action.on("click", function (e) {
      formatHtml("<h4>", "</h4>");
   });
   self.h5Action.on("click", function (e) {
      formatHtml("<h5>", "</h5>");
   });

   self.pAction.on("click", function (e) {
      formatHtml("<p>", "</p>");         
   });
   self.bAction.on("click", function (e) {
      formatHtml("<strong>", "</strong>");
   });
   self.iAction.on("click", function (e) {
      formatHtml("<span style='font-style: italic'>", "</span>");
   });
   self.uAction.on("click", function (e) {
      formatHtml("<span style='text-decoration: underline'>", "</span>");
   });
   self.stAction.on("click", function (e) {
      formatHtml("<span style='text-decoration: line-through'>", "</span>");
   });
   
   var formatHtml = function (beginTag, endTag) {
      var htmltext = self.textEditor.val();
      if (htmltext != null && htmltext.length >= 0) {
         var selStart = self.textEditor[0].selectionStart;
         var selEnd = self.textEditor[0].selectionEnd;
         
         if (selStart >= 0 && selEnd >= 0) {
            var seg1 = htmltext.substring(0, selStart);
            var seg2 = htmltext.substring(selStart, selEnd);
            var seg3 = htmltext.substring(selEnd);
            
            if (seg1 == null) {
               seg1 = "";
            }
            
            if (seg2 == null) {
               seg2 = "";
            }
            
            if (seg3 == null) {
               seg3 = "";
            }
            
            var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
            self.textEditor.val(htmltext2);
         }
      }
   };
}
...

The way Stapes JS interact with HTML element is done by first finding a container element. A container element is a parent element to a number of child elements. Once the Stapes JS object has a reference to this parent element, it can be used to find all the child elements in it. Here is how to get the reference of the parent element:

constructor : function() {
   var self = this;
   self.$el = $("#htmlEditing");
   
   ...
}

In this example, the container element is something with an id: "#htmlEditing".

Note, the local variable self is a reference to the object of the Stapes JS class. Sometimes, the keyword this is a reference to the function. So it is important to find what the keyword this is really referencing to. In this case, since we passed in an object to the Stapes.subclass(), the keyword this is referencing this object. This allows me to initialize all the properties of the class, like this:

...
   self.htmlContentView = self.$el.find("#htmlContent");
   self.textEditor = self.$el.find("#htmlEditorDiv #textEditor");
   self.updateBtn = self.$el.find("#updateBtn");
   self.clearBtn = self.$el.find("#clearBtn");
   
   self.h1Action = self.$el.find(".btn-group #h1Action");
   self.h2Action = self.$el.find(".btn-group #h2Action");
   self.h3Action = self.$el.find(".btn-group #h3Action");
   self.h4Action = self.$el.find(".btn-group #h4Action");
   self.h5Action = self.$el.find(".btn-group #h5Action");

   self.pAction = self.$el.find(".btn-group #pAction");
   self.bAction = self.$el.find(".btn-group #bAction");
   self.iAction = self.$el.find(".btn-group #iAction");
   self.uAction = self.$el.find(".btn-group #uAction");
   self.stAction = self.$el.find(".btn-group #stAction");
...

This and the code snippet before all used JQuery to find the references of the child elements in the container. Once the references are acquired, I can attach the event handling methods to all these elements:

self.textEditor.on("input", function (e) {
   var htmlText = self.textEditor.val();
   self.htmlContentView.html(htmlText);
});

self.clearBtn.on("click", function(e) {
   self.textEditor.val("");
});

self.$el.on("submit", function(e) {
   e.preventDefault();
});

self.h1Action.on("click", function (e) {
   formatHtml("<h1>", "</h1>");
});
self.h2Action.on("click", function (e) {
   formatHtml("<h2>", "</h2>");
});
self.h3Action.on("click", function (e) {
   formatHtml("<h3>", "</h3>");
});
self.h4Action.on("click", function (e) {
   formatHtml("<h4>", "</h4>");
});
self.h5Action.on("click", function (e) {
   formatHtml("<h5>", "</h5>");
});

self.pAction.on("click", function (e) {
   formatHtml("<p>", "</p>");         
});
self.bAction.on("click", function (e) {
   formatHtml("<strong>", "</strong>");
});
self.iAction.on("click", function (e) {
   formatHtml("<span style='font-style: italic'>", "</span>");
});
self.uAction.on("click", function (e) {
   formatHtml("<span style='text-decoration: underline'>", "</span>");
});
self.stAction.on("click", function (e) {
   formatHtml("<span style='text-decoration: line-through'>", "</span>");
});

There are a few thing I want to call out here. First, the text area element is inside a form element. There is also a button in the same form. So anytime this button is clicked, the form will be "submitted". The first thing I had to do is disabling such action. Here it is:

...
self.$el.on("submit", function(e) {
   e.preventDefault();
});
...

The event handling method for the text area is for updating the HTML preview area whenever characters are typed in. I did some research. the "change" event does not work with characters typed into the text area. After some search, turned out the event that needs call back to reflect change to the text area is called "input". So I added a callback to take whatever is in the text area and set it as the html content to the preview area. Here it is:

...
self.textEditor.on("input", function (e) {
   var htmlText = self.textEditor.val();
   self.htmlContentView.html(htmlText);
});
...

See how simple it is? I take the text value in the text area, and attach to the preview area as inner HTML. The cool thing about this is that whatever user entered in the text area, is unescaped text. Without any change to the text, it can be attached as an HTML element to the <div></div> of the preview area.

As mentioned earlier, the HTML editor has a drop down menu with a bunch of options to click on. These options will add decorations to the text in the text area. The user can either highlight a range of text, then click on these options to add HTML tags to the beginning and end of the highlighted text; or put the enclosed HTML tags at the text caret position if no text has been highlighted. Please try it when you run the sample application. There are 10 options, each has an event handling method associated with it. Here is one of them:

...
self.iAction.on("click", function (e) {
   formatHtml("<span style='font-style: italic'>", "</span>");
});
...

This event handling method is for adding the text decoration to the highlighted text so that it can be displayed as italic format. We use the JQuery's on("event_name", function (...) { ... }) method to attach the event handling method. All this is calling a separated function called formatHtml(). I will discuss what this does next.

Handling Text Highlighting and Caret Position

The hardest part of this application, is the functionality to get the start and end positions of the highlighted text in the text area. For any text based input, there are special properties attached to these input HTML elements. As long as you use these special properties, you can determine the start and end positions of the highlighted text. If there is no highlighted text, then the start position and end position are the same.

As long as we know the start and end positions, we can cut the entire text into three different pieces:

  • The first piece is the text from the beginning of the entire content to the position just before the highlighted text starts.
  • The second piece is the highlighted text from beginning to end. If no text is highlighted, this will be an empty string.
  • The last piece is the text from the end of the selection to the end of the entire content.

Once I have these three pieces, I can add the HTML tag decorations at the beginning of the second piece and the end of it. Then add the first piece, decorated second piece and third piece all together. This is is how to add HTML tag to the beginning and end of highlighted text in text area. This is what the function formatHtml() does:

var formatHtml = function (beginTag, endTag) {
   var htmltext = self.textEditor.val();
   if (htmltext != null && htmltext.length >= 0) {
      var selStart = self.textEditor[0].selectionStart;
      var selEnd = self.textEditor[0].selectionEnd;
      
      if (selStart >= 0 && selEnd >= 0) {
         var seg1 = htmltext.substring(0, selStart);
         var seg2 = htmltext.substring(selStart, selEnd);
         var seg3 = htmltext.substring(selEnd);
         
         if (seg1 == null) {
            seg1 = "";
         }
         
         if (seg2 == null) {
            seg2 = "";
         }
         
         if (seg3 == null) {
            seg3 = "";
         }
         
         var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
         self.textEditor.val(htmltext2);
      }
   }
};

From the above code snippet, here is how I get the start position of selected text and end of it:

...
if (htmltext != null && htmltext.length >= 0) {
   var selStart = self.textEditor[0].selectionStart;
   var selEnd = self.textEditor[0].selectionEnd;
   ...
}
...

The text area has properties selectionStart and selectionEnd. There is one interesting twist here. The reference to the text area is a JQuery reference, which is always an array of matching element. I am pretty sure there is only one text area in this page. So the first element of the matching array of elements will be the text area I need.

Next, I need to cut the entire text value in text area into three pieces, here is how I did it:

...
if (selStart >= 0 && selEnd >= 0) {
   var seg1 = htmltext.substring(0, selStart);
   var seg2 = htmltext.substring(selStart, selEnd);
   var seg3 = htmltext.substring(selEnd);
   ...
}
...

It is exactly what it looks like, the first piece starts at 0, and ends at the beginning of the highlighted text. The second piece starts at the beginning of the highlighted text and ends at the end of it. The last piece starts at the end of the highlighted text and ends at the very end of the entire text value.

The method formatHtml has two input parameters. The first is the stat HTML tag, which can be added to the beginning of the highlighted text. The second parameter will be added at the end of the highlighted text. Then I stitch all three pieces together to reconstruct the entire text value. Here it is:

...
   if (seg1 == null) {
      seg1 = "";
   }
   
   if (seg2 == null) {
      seg2 = "";
   }
   
   if (seg3 == null) {
      seg3 = "";
   }
   
   var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
   self.textEditor.val(htmltext2);
...

The last line re-assign the value as value of the text area. This is how I manipulate the text content of the text area using the drop down menu options.

HTML Editor As a Re-usable Component

This simple HTML editor is intended to be a reusable component. Think about it. An HTML editor like this can be reused in many different situations (as long as the HTML content can be safely consumed at the back end). So it makes no sense to duplicate the same code all over an application. Duplicating code and change some attributes to make the duplication work in different situations, it is a terrible practice anyways.

Since this tutorial didn't use RequireJS for dependency management (don't want to complicate the tutorial too much), I will define another component which references this HTML editor. The new component has only one control on it, it is the button:

When this button is clicked, the content of the text area will be output in the Developer Tools console. This new compoent is defined as the following:

var otherServiceApp =  Stapes.subclass({
   btnGetHtml: null,
   testAppObj: new testApp(),
   
   constructor: function() {
      var self = this;
      self.$el = $("#otherServices");
      
      self.btnGetHtml = self.$el.find("#getHtml");
      
      self.btnGetHtml.on("click", function(e) {
         var htmlContent = self.testAppObj.getHtmlVal();
         console.log(htmlContent);
      });
   }
});

new otherServiceApp();

Basically, I defined another class type called otherServiceApp. This class type has two properties, one is the button reference. The other is the reference to the HTML editor. In the constructor of this class type, it creates the event handling for the button, and the instance of the HTML Editor. For the event handling function for the button click, it will call the exposed method of the HTML Editor class, getHtmlVal(), to get the text value in the text area. The call to console.log() will dump the text value to the debugger console.

The last line of this code snippet will construct an anonymous instance of this second component. This, will start up the JavaScript application.

Running the Sample Application

Before you build and run the sample application, please find all the files which are named as *.sj in folder and sub folders of src/main/resources/static/assets, rename these files as *.js. If you don't do this, the sample application will not work.

To compile the sample application, please go to the base directory where you can find the maven POM.xml file, and run the following command:

mvn clean install

The first time when you run this, it will take a long while. Maven will download all the needed dependencies. Eventaully it will succeed. Once the build is successful, run the following command in the same directory (where the POM.xml file is) to start the application:

java -jar target/testapp-0.0.1-SNAPSHOT.jar

The application will bind to port 8080. And you can access he application at this location:

http://localhost:8080/

Once you get to this location with your browser, you will see the very first screenshot shown at the beginning of this tutorial.

Summary

This is the end of the tutorial. I hope you have enjoyed it as much as I enjoyed writing it. The goal of this tutorial is to show how to use Stappes JS with JQuery to create client side application. To demo, I created a simple HTML editor with real-time preview as the sample application. With this sample application, I was able to show that using Stapes JS, I can create a class definition similar to classes defined in Java or C# (with similar structures).

The sample application also shows how to define a container element, and to be assigned to the Stapes JS component. From there, child elements can be queried and attached with event handlers. The values which the elements contain can be retrieved or set with Jquery specific methods or properties. The sample application also show how to get the reference to the actual HTML element and use native JavaScript to interact with the HTMl element (how to get the highlighted positions of text in text area), and how to programmatically change the value held by the HTML elements.

What is cool about this sample application is that it can be easiy expanded to have better functionalities. So if you can get the application work, you can extend it by adding more editing capabilities to it, such as more HTML tags for decorations. And you can extract the component and used in your own application, as long as you don't mind use StapesJS.


Add Comment

Comments