Why We Ditched Angular.js and Wrote Our Own UI Library

https://pixabay.com/en/technology-computer-black-code-1283624/We recently announced SmartDraw Cloud, a browser-based version of our native Windows diagramming application SmartDraw 2016. SmartDraw Cloud, built entirely in JavaScript using the HTML 5 stack, was more than three years in the making and has the full feature set of our Windows Business Edition.

Ask most people how they feel about browser-based apps and they’ll tell you that there’s a trade-off between the convenience the cloud offers, and the power they get from the desktop versions of the same programs. Even the browser versions of Microsoft Office applications do not have the full feature sets of Microsoft’s desktop versions of the same. When we designed SmartDraw Cloud our goal was to eliminate this tradeoff and we’ve succeeded.

An earlier post summarizes the techniques we used to achieve this goal. This article focuses in depth on our unique approach to handling the complex UI of an app like SmartDraw, and why we initially adopted and then rejected Angular.js.

Framework Hell

The most common approach to developing a JavaScript app is to decide on a “framework” that isolates you from the common tasks of developing a UI, getting user input from it and showing the user the state of their document.

This is the sort of thing a framework is supposed to do for you: create and handle a simple control like this for the text font. The control shows the font of the currently selected shape or text, and allows you to change it.

sdcloud_text_ui

We looked at a wide range of such frameworks, including Dojo and React before setting on Angular.js. As we began development we quickly realized three things:

  1. Angular wouldn’t scale to the extent our sophisticated app required, and it obfuscated what was going on.
  2. The whole approach of wrapping the UI in a code-based framework made it impossible to separate the UI from the code. This is an important principle to us. The UI in SmartDraw is very rich and it needs to be coded by experts in HTML and CSS. The calls made to the app from the UI need to be specified in the UI HTML in manner easy enough for a non-JavaScript programmer to handle. We needed to be able to add commands by merely updating the HTML. Angular (and the other frameworks) just don’t work this way.
  3. Once you pick a framework, you become dependent on it. It becomes integral to your code. If support for it wanes, or your want to move to the latest “cool” framework, you have to pull your app apart. Our code base for Windows has been around for 20 years. We expect our JavaScript code base to last for many years too. We wanted no part of this “framework hell.”

How Angular.js failed us

Originally, when we set out to write SmartDraw Cloud we followed the typical pattern for app building: find an existing framework or libraries / modules and stitch them together into an app. After doing some research we decided on Angular.js because it looked like it offered us the power needed to make a snappy, responsive UI to what would necessarily become a very complicated drawing program. In particular it was the two way data-binding that looked cool. Hook up the UI element with your data model, and presto: Changes to the UI updates your model and changes to the model updates the UI. Nice.

However, once we got into the weeds of actually making the UI respond to changes in the drawing area, the performance of the app began to suffer.

The big challenge was UI idling, where we make the commands in the command bars at the top of the app (called ribbons) react to the user’s current shape selection, just like the text font  we used as an example earlier.

idle_text_ribbon

It seemed like precisely the task two-way data binding was designed to do. The problem was that as we added controls, this sort of idling began taking a noticeable amount of time – hundreds of milliseconds – and was causing the app to stutter and feel clunky.

And it’s not just the command buttons themselves that are idled: it’s also the rich menus that they call. For example:

fill_menu

These were all idled by two-way data binding.

Two-way Data Binding in Angular.js is Inefficient

We did additional research into the guts of how Angular.js does its two-way data binding, and the results were disturbing. Angular is based around the concept of “watches.” Watches bind a variable from a controller to a part of the DOM by making a copy of the value from the controller, “watching” for any change from either the controller or the DOM, and updating the in-memory value with whichever value was out of sync.

angular_digest_ex

These watches work well enough by themselves, but the real problem is in the way Angular solved the problem of when to have the watches sync themselves.

Because getters and setters are (currently) rarely used in JavaScript, there often isn’t a good way to tell when a value has been updated in either the DOM (excluding the DOM’s native eventing) or the in-memory object model of the app. So, Angular uses a blanket approach: re-evaluate every single watch every time any Angular event (ng-click, ng-mouseover, ng-keydown, ng-mousemove, etc) is raised. For example, if you have 100 data bound elements in your application, any time any event is raised (such as clicking on something) all 100 get re-evaluated and synced.  The result is a seriously inefficient – and ultimately un-scalable – application framework.

No wonder the app was having problems, we had hundreds of data bound values in our complex UI and every time a key was pressed each and every one of them were re-evaluated, and there is no good way in Angular to filter out watches that do not need to be re-evaluated (like watches whose elements have “display: none” on them).

We Tried to Make it More Efficient

In an effort to improve performance and tame the beast by controlling the flow of watch re-evaluations, we manually took control of what is called the $digest()/$apply() loop. $digest()sends a message down from a parent controller recursively to its child controllers telling them all to re-evaluate their watches, while $apply() sends a message recursively up the controller inheritance chain and ultimately causes every single watch in the entire application to be re-evaluated. We set it up so that $apply() was never called (except in a few instances where it made sense to do so) and only relevant parts of the UI would be digested in response to user events.  While this strategy dramatically improved performance, it was still noticeably slow and not satisfactory for our requirements and usability standards.

Finally – We Just Gave Up

We decided to put the final nail in the coffin when we called $digest() and $apply() at the same time, and the two commands “collided” in the Angular framework and caused a crash that effectively broke the app. We debugged the Angular code to see if we could find a quick fix to the problem, and discovered it was choking on a massive if statement that had at least a dozen separate clauses cobbled together, including a variable assignment INSIDE the if statement:

if ((x > y && hackyCode = true) === true)

This statement which is syntactically valid is nonetheless an awful corruption of what is supposed to be a vessel of pure Boolean logic and simply not the place for variable assignments.

Combined with the overall slow performance of the Angular framework, this kind of sloppy coding was enough for us to decide to ditch Angular completely and re-start our development effort using a different system.

Removing Angular.js had other benefits too:

  1. We were no longer dependent on a framework we had no control over that seriously affected the design of our app.
  2. We could develop a system that had much cleaner separation of UI and code.
  3. We could make a much lighter weight system that could perform well in a large complex app

The SmartDraw UI Library: Bantm.js

The problem we were trying to solve was not that hard. We needed a library that would:

  1. Organize the application codebase so that it was easy to write and maintain
  2. Get events from the UI in a way that allowed UI coders to assign functionality and values to user interface elements in HTML without touching the JavaScript.
  3. Idle the UI elements based on the state of the model (the drawing in our case) in an efficient and fast way.
  4. Make the model respond to changes in the UI in a clean and simple way.

So we decided to bite the bullet and just manually write the JavaScript required to power our app without a third-party framework. We call this library bantm.js (because its lightweight).

As we thought more about how to replace the two-way data binding provided by Angular.js it became clear that we didn’t need some bloated catch-all solution to try and alleviate ourselves the terrible and arduous task of writing actual code. Since the DOM gives us an event every time something in the UI has changed, we as the programmer always know when an in-memory value is changing – because we’re the one changing it!

Organizing the Application

One of the major problems we experienced with Angular is that applications written using the framework tended to become very messy very quickly which made it difficult to determine where things were happening, even for experienced developers. Perhaps there’s a way to write an Angular app that is organized and intuitive, but in following the examples found in various books, documentation, and samples we found online all led to very messy, convoluted code that was difficult to follow and debug. We clearly needed a cleaner solution.

The first part of the solution was to use object literals (i.e. myObject = {}) as namespaces and constants tables. We decided to make a root namespace by creating the single global variable that the entire SmartDraw UI application would live under, called “SDUI”, and then made sub-namespaces for different logical areas of the application. We made the constants tables for application settings or values that wouldn’t be changed between runs, for example:

SDUI.Constants =
{
    ConstantA: "apple",
    ConstantB: "banana"
};

Below is the basic structure of SmartDraw Cloud’s UI namespace/constants table hierarchy:

sdui_structure

The next part of organizing the app was setting up a few simple rules that, if followed, would keep the app from becoming the incomprehensible tangled mess that is all too common with JavaScript apps.

Define Constructors for Objects

The first rule is that every object used anywhere in the application has a defining constructor in the Resources namespace for reference. Every time that object needs to be used, it is instantiated using the fully namespaced constructor so any other programmer reading the code can tell exactly what object is being used and how to find out what the properties of that object are.

Define a Hierarchy of Controllers

A controller is a JavaScript object that responds to UI commands. Each grouping of UI has its own controller. For example, each Dialog has a controller, the Ribbons have a controller, the SmartPanel has a controller and so on. All commands are routed to these controllers via the MainController.

The second rule is that, in almost all cases, one controller cannot directly reference another controller, they must instead talk to each other somewhere outside themselves (in our case, this was the main controller).  This rule prevents dependency nightmares where controllers get tangled together into an unworkable mess. While this does present its own challenge of how to elegantly coordinate the controllers when they need to work together, this is solvable through careful planning. An awesome side benefit of having controllers effectively be silos is that they are reusable and can usually be ported from application to application without issue.

All of our controllers are first defined as functions (in the root namespace) with their own properties and methods, and each exists as single instance of the function in the MainController. For example:

/** Controller for the symbol browser dialog. */
SDUI.SymbolLibraryBrowser = function ()
{
    /*jQuery reference to the root gallery node of symbol icons.*/
    this.Gallery = null;
    /*HTML template of a symbol icon.*/
    this.GalleryItemTemplate = null;
    /*HTML template for an item in the tree view.*/
    this.TreeViewItemTemplate = null;
    /*jQuery reference to the root node of the tree view.*/
    this.TreeRoot = null;
    /*The TreeView on the left hand side of the browser.*/
    this.TreeView = new SDUI.TreeView2();
    /*Array of AppendedItems representing all the symbol icons currently displayed.*/
    this.GalleryItems = [];
    /*The PreviewList representing the symbol library that is currently being viewed.*/
    this.CurrentLibrary = null;
    /*Whether or not the library browser has been initialized.*/
    this.Initialized = false;
    /*The current 'file path' to the current library for the windows client to use.*/
    this.CurrentLibraryPath = null;
    /*The 'file path' of the library to open the symbol browser up to on first start up.*/
    this.LibraryPathTarget = null;

    /**Initializes the symbol library browser.
    @method Initialize
    @param {Object} galleryRoot: jQuery reference to the root gallery node for symbol icons.
    @param {String} galleryTemplate: The HTML string to serve as the template for symbol gallery items.
    @param {Object} treeViewRoot: jQuery reference to the root node for the tree view.
    @param {String} treeViewItemTemplate: The HTML string to serve as the template for items in the tree view.
    @param {String} treeViewListTemplate: The HTML string to serve as the template for a list of items in the tree view.*/
    this.Initialize = function (galleryRoot, galleryTemplate, treeViewRoot, treeViewItemTemplate, treeViewListTemplate) {…};

    /**Gets a library from the internal content hierarchy of the symbol browser.
    @method GetLibrary
    @param {String} libraryId: The ID of the library to find.*/
    this.GetLibrary = function (libraryId) {…};

    /**Loads a library from the server.
    @method LoadLibrary
    @param {String} libraryId: The ID of the library to load the contents of.
    @param {Function} callback: A callback function that takes the result JSON as a parameter to call asynchronously when the request ends. If left null the operation is synchronous.*/
    this.LoadLibrary = function (libraryId, callback) {…};

    …
};

Getting Events from the UI

The next problem to solve was how to get events from the DOM into the core of the application. As an added challenge, the system of event routing also had to be useable by someone who doesn’t know JavaScript. The system we decided on was simple: have a registry of command names that would be written into the markup of the application’s HTML and use attributes to hold values that would be parameters to pass into the functions that would be called.

One of the most important parts of this scheme was constructing a wall to separate the direct UI event handling from the business logic. For each command there was a UI handing method that would be directly called by the DOM which would take the calling element, extract whatever parameters had been added to it as attributes, and then pass those attributes as parameters into the business logic method.  The benefit of this strategy is that you can write simple, parameterized methods that can be called either as a response to an event, or invoked manually when you want to call the functionality outside of an eventing context.

For example, this is the Arrowhead menu.

arrowhead_menu_ex

This is the HTML for the Arrowhead Menu:

<ul id="dd-arrowheads" class="dropdown-menu">
	<li id="dd-arrowheads-none" onclick="SD_Click(event, 'SD_Line_SetArrowhead')" arrowheadLocation="none" arrowheadId="0">
		<button><i class="icon-arrowhead-none"></i> None</button>
	</li>

	<li id="dd-arrowheads-right" onclick="SD_Click(event, 'SD_Line_SetArrowhead')" arrowheadLocation="end" arrowheadId="1">
		<button><i class="icon-long-arrow-right"></i> Right</button>
	</li>

	<li id="dd-arrowheads-left" onclick="SD_Click(event, 'SD_Line_SetArrowhead')" arrowheadLocation="start" arrowheadId="1">
		<button><i class="icon-long-arrow-left"></i> Left</button>
	</li>

	<li id="dd-arrowheads-both" onclick="SD_Click(event, 'SD_Line_SetArrowhead')" arrowheadLocation="both" arrowheadId="1">
		<button><i class="icon-resize-horizontal"></i> Both</button>
	</li>

	<li><hr></li>
	<li id="dd-arrowheads-custom"  class="dropdown-submenu" onclick="SD_Click(event, 'SD_ShowModal')" modalId="m-arrowheads">
		<button>Custom<i class="icon-external-link icon-1x pull-right"></i></button>
	</li>
</ul>

Note each item in the menu has an onclick method that specifies a method in EventCommands.js. The first four are the same command (SD_Line_SetArrowhead()) but with different arrowheadLocation and  arrowheadId values. The final item calls ShowModal with a modalId of “m_arrowheads”. This shows the custom arrowhead dialog:

code_example_2

Again, the first thing that happens is our global event handler (SD_Click) is invoked, and SD_Click looks in the “SDUI.Commands” lookup table for a function with the same name as the second argument passed into SD_Click (the first argument is always the browser’s event arguments), which in this case is “SD_ShowModal”. SD_Click then invokes SDUI.Commands.SD_ShowModal function and passes in the browser’s event augments.

SD_ShowModal then takes the element that raised the event and looks for a modalid and contextId attribute on that element and extracts whatever values were present. Note that the names of the attributes we are looking for both live in SDUI.Constants and every time we need to reference either of those constants we refer to the constants table (SDUI.Constants.Whatever) rather than writing out the raw value (so if we change it, we only have to change it in one place).

Once the attribute values have been extracted from the target element, we pass them into the MainController’s ShowModal function that actually does the work of loading, building, and displaying the modal dialog. ShowModal can be called from anywhere and isn’t hard-wired to a browser event, so we get the flexibility of being able to call it in response to an event or in the middle of our business/display logic as needed.

Making the UI Respond to User Actions

The final challenge was to efficiently solve the problem of making all the buttons on the ribbons, menus and SmartPanels light up or grey out based on user actions. Angular.js had struggled with accomplishing this due to its lack of scalability, but using the namespace hierarchy with constants tables made this much easier.

One of the most important constants tables we used was the SDUI.Resources.Controls table, which contained a special object reference for every piece of UI we programmatically manipulated in the application (each control we manipulate is given a unique three-part ID based on its name and location in the UI, so “r-home-save” would be the save button on the home ribbon).

This special object was called “SDUI.Resources.ControlInfo” (below) and it served three purposes:

  1. Serve as the official lookup location of the element’s ID.
  2. Query the DOM once to get a jQuery reference to a DOM element and then keep it around forever so we don’t impact performance by re-querying the same few dozen elements every time the user clicked on a shape or dropped a dropdown.
  3. Hold custom metadata about each control so that making them respond to user actions could be parameterized and handled generically.
SDUI.Resources.ControlInfo = function (controlElementId, minItems, minShapes, minLines, notesEditEnable,HasTextOnly, noPolyLineContainer)
{
    /*The HTML Element ID of this control.*/
    this.Id = (controlElementId == null) ? null : controlElementId;
    /*The JQuery results for the HTML Element that corresponds to the Id.*/
    this.Control = null;
    /*The minimum number of items that must be selected in order for this control to become active.*/
    this.MinSelectedItems = (minItems == null) ? 0 : minItems;
    /*The minimum number of shapes that must be selected in order for this control to become active.*/
    this.MinSelectedShapes = (minShapes == null) ? 0 : minShapes;
    /*The minimum number of lines that must be selected in order for this control to become active.*/
    this.MinSelectedLines = (minLines == null) ? 0 : minLines;
    /*If this control should always be enable during notes editing. */
    this.NotesEditEnable = (notesEditEnable == null) ? false : notesEditEnable;
    /*Hilite this control only when an item with text is selected.*/
    this.HasTextOnly = (HasTextOnly == null) ? false : HasTextOnly;
    /*Do not hilite control if a polylinecontainer is selected.*/
    this.NoPolyLineContainer = (noPolyLineContainer == null) ? false : noPolyLineContainer;

    /*Runs a jQuery query based on the Id property.*/
    this.GetControl = function (forceReQuery)
    {
        if (this.Control != null && forceReQuery !== true) //if we have it and are not re-querying the control.
        {
            return this.Control;
        }

        var control = $("#" + this.Id);

        if (control != null && control.length > 0) //found the control
        {
            this.Control = control;
        }
        else
        {
            return null;
        }

        return this.Control;
    };
};

In addition to creating a ControlInfo for every control in the app, we categorized them based on where they appeared in the UI so that if, for example, we wanted to grab the controls for every button on the home ribbon, we had a function (SDUI.Resources.Controls.GetRibbonControls(ribbonID)) that would return an array of every ControlInfo contained by that ribbon.

Our organized approach also allowed us to easily keep track of what UI elements were visible (such as which ribbon the user was looking at), so when the time came to make the UI reflect the current selection state, we just looked up what was visible, used that data to grab all the ControlInfo objects belonging to the visible portion of the UI and tossed them into the generic UI idling function which would enable/disable them based on their metadata.

Here’s the definition of the controls for ribbons and then the design ribbon:

Ribbons:
{
    Design: new SDUI.Resources.ControlInfo("r-design"),
    Help: new SDUI.Resources.ControlInfo("r-help"),
    Home: new SDUI.Resources.ControlInfo("r-home"),
    Insert: new SDUI.Resources.ControlInfo("r-insert"),
    Page: new SDUI.Resources.ControlInfo("r-page"),
    Table: new SDUI.Resources.ControlInfo("r-table"),
    Review: new SDUI.Resources.ControlInfo("r-review"),
    File: new SDUI.Resources.ControlInfo("r-file"),
    Options: new SDUI.Resources.ControlInfo("r-options")
},
Ribbon_Home:
{
    Paste: new SDUI.Resources.ControlInfo("r-home-paste"),
    Copy: new SDUI.Resources.ControlInfo("r-home-copy"),
    Cut: new SDUI.Resources.ControlInfo("r-home-cut", 1),
    FormatPainter: new SDUI.Resources.ControlInfo("r-home-formatPainter", 1),
    Undo: new SDUI.Resources.ControlInfo("r-home-undo"),
    ReDo: new SDUI.Resources.ControlInfo("r-home-redo"),
    Select: new SDUI.Resources.ControlInfo("r-home-select"),
    LineTool: new SDUI.Resources.ControlInfo("r-home-lineTool"),
    LineToolDD: new SDUI.Resources.ControlInfo("r-home-lineToolDD"),
    ShapeTool: new SDUI.Resources.ControlInfo("r-home-shapeTool"),
    ShapeToolDD: new SDUI.Resources.ControlInfo("r-home-shapeToolDD"),
    Text: new SDUI.Resources.ControlInfo("r-home-text"),
    AddLink: new SDUI.Resources.ControlInfo("r-home-addLink", 1),
    AddNote: new SDUI.Resources.ControlInfo("r-home-addNote", 1),
    Theme: new SDUI.Resources.ControlInfo("r-home-theme"),
    QuickStyle: new SDUI.Resources.ControlInfo("r-home-quickStyle"),
    Fill: new SDUI.Resources.ControlInfo("r-home-fill"),
    Lines: new SDUI.Resources.ControlInfo("r-home-lines"),
    Effects: new SDUI.Resources.ControlInfo("r-home-effects"),
    Font: new SDUI.Resources.ControlInfo("r-home-font"),
    CurrentFontLabel: new SDUI.Resources.ControlInfo("r-home-currentFont"),
    TextSize: new SDUI.Resources.ControlInfo("r-home-textSize"),
    CurrentTextSizeLabel: new SDUI.Resources.ControlInfo("r-home-currentTextSize"),
    Bold: new SDUI.Resources.ControlInfo("r-home-bold", 0, 0, 0, true),
    Italic: new SDUI.Resources.ControlInfo("r-home-italic", 0, 0, 0, true),
    Underline: new SDUI.Resources.ControlInfo("r-home-underline", 0, 0, 0, true),
    Subscript:new SDUI.Resources.ControlInfo("r-home-subscript", 1, 0, 0, true),
    Superscript:new SDUI.Resources.ControlInfo("r-home-superscript", 1, 0, 0, true),
    TextColor:new SDUI.Resources.ControlInfo("r-home-textColor", 0, 0, 0, true),
    Align: new SDUI.Resources.ControlInfo("r-home-align",1),
    Bullets: new SDUI.Resources.ControlInfo("r-home-bullets", 1, 0, 0, true,true),
    Spacing: new SDUI.Resources.ControlInfo("r-home-spacing", 1, 0, 0, true,true),
    Direction: new SDUI.Resources.ControlInfo("r-home-direction",0,0,1),
    InsertSymbol: new SDUI.Resources.ControlInfo("r-home-insertSymbol",0,0,0,true)
},       

We are able to get all of the controls in the home ribbon because the Home member of the ribbon array tells us that all the home ribbon controls begin with “r-home”. In the same way we can get all of the controls for any ribbon, so if we know that the current ribbon displayed is “Home”, we need only idle the controls defined by Ribbon_Home.

Setting Values for UI Based on User Interaction

The controls in the UI change their state based on the selection of shapes in the drawing. The action of selecting (or unselecting) a shape is easily detected by the JavaScript code that does this, and it calls a function to aggregate the properties of every shape selected into a “current properties” object.

As the particular shapes selected in the drawing change, in response we need to set the states of only the controls that are visible at all times. After updating the “current properties” object we call the method to adjust the state of the visible controls. This is very fast because it is restricted to a small subset of the total number of controls, and we don’t have to traverse the DOM to get their references each time we do this. We get them once and store them.

The controls in dialogs and menus are not visible until they are displayed, and so we need set their states only as they are about to be displayed. This too is very fast because we are handling only a small subset of all of the controls and we get their references only once.

Conclusion

Off-the shelf frameworks and components can save time and effort if you want to develop a quick in-house line of business app, but they are absolutely not the way to go if your goal is to develop a reliable, scalable, maintainable commercial application. With a few person weeks of coding we developed the the bantm UI Library. It allowed us to build an app with a very rich UI that is easy to maintain, responsive and scales with no noticeable performance hit.

Editors Note: This is part 2 of a technical series about the making of SmartDraw Cloud. You can read part 1 here.