Building a UML editor in JavaScript - part 1
This is part 1 of a series exploring how to build a full-pledged UML editor in pure JavaScript.
Why JavaScript
I really love Python & its wonderful frameworks - they brought me pure joy & satisfaction for the past 6 years.
However, I'm forced to acknowledge that the times they are a-changin: logic moves back to the client-side, leaving the server-side to do almost nothing (just data access, usually). Further more, the server side turned into a series of disporate API's, which the client consumes, provided by different vendors - so now even the data access moves to the client, which talks directly with datastore providers.
The lingua-franca of the client-side, available anywhere there's a browser, is JavaScript. And, when I needed to write some very scalable server-side service, I also found JavaScript (node.js) doing a really great job even there.
With HTML5, there's no limitation to the abilities of JavaScript on the client-side.
In addition, the platform of computing is going thru an even larger change: mobile devices talking with cloud back-ends. I don't know whether the mobile-specific platforms will remain the mobile client-side technology, or the open-stack of the Web will arrive to the client. Time will tell.
The application
So, in this series I will develop an application in pure JavaScript, for Web & Mobile platforms.
I do design very often, but don't have a UML editor that works for me: either they're not cloud-based & don't support collaboration, or they're too unusable, or just costing too much money.
So, I'll try to develop in this series a simple UML editor, which is
- Cloud-based
- Supports real-time collaboration
- Works well on tablet & mobile
- Free & open-source
Mock-up of the application:
Regarding the application name: the main consideration for a name should obviously be SEO - how easy it will be to find it in Google. So, given this imperative consideration, I came up with the name: model
It's going to be pure client-side app - all static files downloaded from CDN to the client The app will consume remote 3rd-party API's, for example, a remote storage for persistence. Using a tool such as PhoneGap, I'll try to make it usable also as a Mobile app. Of course a pure MVC will be enforced: clear separations between data, business logic & presentation .
If you were given the task of writing a UML editor, which languages would you use?
Initial survey
Before starting out I've sent a couple of colleagues a survey, asking them about their activities & needs in respect to UML. The idea is to try understand who will be the 1st users of the editor (except for myself) & how to make it useful for them.
You can see (& fill out) the survey here.
I received some 7 responses, from which I've learnt that:
- Usage: A third of the responders use it often (every week). A third uses it once a month or few. Another third don't use UML at all.
- Tools: Half of the users use desktop tools (StarUML, Sparx, OpenOffice, Violet). Another hald uses online tools (WebSequenceDiagrams, Google Docs). Many said they also use paper & whiteboard.
- Diagram types: Most use Class diagram, Sequence diagram & Use-case. Also: Activity, State, Relationship, Work-flow.
- Activities: Almost everyone: Embed diagrams in design documents, Present them in design reviews, Collaborate on them with others. Also: Save them in the repository, Revise them when design changes.
- Problems & needs: People expressed the need for online editor. Another requested feature is reverse engineering from existing source repository
Decisions
Possible frameworks & technologies
I made a check-list of frameworks & technologies that I'd like to use in this app:
- CoffeeScript - a high-level language that compiles into JavaScript, & abstracts some of the difficult & bad parts of the language
- BrowserID - an email-based authentication mechanism by Mozilla
- RemoteStorage - a mechanism to store data from JavaScript on remote providers
- Datastore providers: IrisCouch
- Backbone.js - a popular MVC framework for javascript
- Backbone.dualStorage - for Backbone storage on both local (HTML5) & remote storage
- Backbone.boilerplate - template utilities for Backbone
- Underscore.js - clean & feature-rich JavaScript framework
- d3.js - data-driven visualization framework
- Bootstrap2 - HTML5 template
- Uijet - UI widgets framework
- Faye & Kue- for real-time collaboration
- Selenium2 - unit-tests framework for JavaScript * Sauce Labs - hosted service for cross-browser testing using Selenium
- PhoneGap - framework for generatubg mobile editions of a Web app
- Google Page Speed- for optimizing loading & performance
- Google Analytics - Provide the analytics on a client-side app
- CloudBees - Continuous Integration & Testing
-
??? - Hosting & continuous deployment
Code hosting & IDE
BitBucket - source hosting
Since I prefer mercurial, I usually host my projects on BitBucket. The source code can be found in: https://bitbucket.org/dibau/model
WebStorm - IDE
This full-pledged JavaScript IDE is based on JetBrains' Java IDE: IntelliJ IDEA.
The main advantage of using it is the amazing intelligence it has in understanding code, detecting errors & enabling refactoring
I prefer it on any other IDE on this sole ground: especially with dynamic languages, it's crucial to have an editor that understands your code, & can validate it intelligently
WebStorm supports debugging of JavaScript ran on the browser from the IDE & comes with many plugins, such as CSS-X-Fire which enables you to update CSS attributes in your browser & automatically update them in your source files. For example, WebStorm can infer the type of function parameters (which aren't typed of course) & detect when a call passes parameters of a different type!
Debug & inspection
Browsers debug console
All modern browser have an interactive console, crucial for debugging & exploratory development
JSFiddle
An online tool to experiment with JavaScript & share it with others
JSLint
This tool by Douglas Corkford (the main guru & reviver of modern JavaScript) to validate the quality & correctness of your JavaScript code
Templates
The project can be started with several templates:
Boilerplate
Many nice features for HTML5 support. Comes with Google Analytics code & also script to install Chrome Frame
Initializr
Service that let's you customize the template, which is based on Boilerplate. Customizing it is a bit annoying - because there are incompatibilities between possible elements, & it takes time to succeed resolving the conflicts. Uses the Modernizr framework for detecting & leveraging HTML5 & CSS3
Bootstrap2
Best practices template by Twitter. Includes both resources & widgets.
All of these templates support the infra to make the app responsive - adapt to different devices (Computer, Tablet, Mobile, TV)
I decided to start with Bootstrap2. We'd also take stuff from Initializr/Biolerplate, such as: Chrome Frame & Google Analytics
Design & code
The initial version of the product is very simple - it let's you describe classes in text, and draws the corresponding class diagram. Here's the back-of-the-envelope design:
Main page (app.html)
The application has 1 main page, using Bootstrap2 as templae. The code is straight-forward:
<body> <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container-fluid"> <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </a> <a class="brand" href="#">Model</a> <div class="nav-collapse"> <ul class="nav"> <li class="active"><a href="#">Home</a></li> <li><a href="#about">About</a></li> <li><a href="#contact">Contact</a></li> </ul> <p class="navbar-text pull-right">Logged in as <a href="#">username</a></p> </div><!--/.nav-collapse --> </div> </div> </div> <div class="container-fluid"> <div class="row-fluid"> <div class="span3"> <div class="well sidebar-nav"> <ul class="nav nav-list"> <li class="nav-header">Project 1</li> <li class="active"><a href="#">Class Diagram 1</a></li> <li><a href="#">Diagram 2</a></li> <li><a href="#">Diagram 3</a></li> <li><a href="#">Diagram 4</a></li> <li class="nav-header">Project 2</li> <li><a href="#">Diagram 1</a></li> <li><a href="#">Diagram 2</a></li> <li><a href="#">Diagram 3</a></li> <li><a href="#">Diagram 4</a></li> <li><a href="#">Diagram 5</a></li> <li><a href="#">Diagram 6</a></li> <li class="nav-header">Project 3</li> <li><a href="#">Diagram 1</a></li> <li><a href="#">Diagram 2</a></li> <li><a href="#">Diagram 3</a></li> </ul> </div><!--/.well --> </div><!--/span--> <div class="span9"> <div class="hero-unit"> <h2>Class Diagram 1</h2> <p>This is the diagram description.</p> </div> <ul id="tab" class="nav nav-tabs"> <li class="active"><a href="#diagram" data-toggle="tab">Diagram</a></li> <li class=""><a href="#source" data-toggle="tab">Source</a></li> </ul> <div id="myTabContent" class="tab-content"> <div class="tab-pane fade active in" id="diagram"> <div id="chart" style="width: 675px; height: 360px;"></div> </div> <div class="tab-pane fade" id="source"> <div> <textarea id="source_input" rows="20" style="width: 675px; height: 341px;"></textarea> <p class="muted" style="float: right;"> For example:<br/><br/> Class1 : BaseClass1<br/> +method1<br/> +method2<br/> -method3<br/> <br/> Class2 : BaseClass2<br/> +method1<br/> +method2<br/> -method3<br/> </p> </div> </div> </div> </div><!--/span--> </div><!--/row--> <hr> <footer> <p>(cc) Model UML editor 2012</p> </footer> </div><!--/.fluid-container-->
JavaScript models (model-models.js)
The 1st interesting place is the JavaScript in which the Backbone.js models are defined. There's a simple base class (DiagramElement), a class that extend it (Class) for representing classes, & a collection of classes (ClassDiagram).
var DiagramElement = Backbone.Model.extend({ defaults: { "x": 0, "y": 0, "width": 40, "height": 100 } }); var Class = DiagramElement.extend({ defaults: { "name": "", "super_classes": [], "public_methods": [], "private_methods": [] }, get_all_members: function () { var result = []; var name = this.get('name'); if (this.get('super_classes').length > 0) { name += " : "; name += this.get('super_classes').join(",") } result.push(name); _.each(this.get('public_methods'), function(n) { result.push(n); }); _.each(this.get('private_methods'), function(n) { result.push(n); }); return result; } }); var ClassDiagram = Backbone.Collection.extend({ model: Class, source: "", get_classes: function() { var result = []; _.each(this.models, function(model) { result.push(model.get("name")); }); return result; } }); var current_diagram = new ClassDiagram();
JavaScript controllers (model-controllers.js)
The contoller file contains the parser of the text in which you describe classes. The parser goes over the text & creates Backbone models out of it. Then the controller creates the Drawer, that renders the class diagram, & binds it to the add event of the diagram (triggered whenever a new Class is added to the collection).
var SourceParser = { parse: function() { var previous_source = "", current_class = "", current_super_classes = []; var members_map = { "+": [], "-": [] }; var add_class = function() { var cls = new Class({ "name": current_class, "super_classes": _.clone(current_super_classes), "public_methods": _.clone(members_map["+"]), "private_methods": _.clone(members_map["-"]) }); var existing_class = current_diagram.find(function(c) { return c.get('name') == cls.name}); if (existing_class) { existing_class.set(cls); } else { current_diagram.add(cls); } current_class = ""; current_super_classes = []; members_map["+"] = []; members_map["-"] = []; }; return function() { var source = $("#source_input").val(); if (source != previous_source) { previous_source = source; var lines = source.split("\n"); _.each(lines, function(line) { if (line.trim().length == 0) return; var first_char = line.charAt(0); if (members_map[first_char]) { members_map[first_char].push(line.substr(1)); } else { if (current_class != "") { add_class(); } if (line.indexOf(":") >= 0) { var parts = line.split(":"); current_class = parts[0].trim(); current_super_classes = parts[1].trim().split(","); } else { current_class = line; } } }); add_class(); } } } }; $(function () { drawer.init(); var parse = SourceParser.parse(); $('a[data-toggle="tab"]').on('shown', function (e) { parse(); }); current_diagram.on("add", drawer.draw_class, drawer); });
JavaScript widgets (model-widgets.js)
The final source file defines the ClassDiagramDrawer, which uses the d3.js library to tramsform the Backbone models into SVG elements. d3.js is a very powerful library, which we'll explore more deeply in following posts. The main idea is to associate a data array with DOM elements, & create DOM elements for new data.
var ClassDiagramDrawer = { svg: null, current_offset: 0, default_class_width: 150, default_member_height: 20, default_member_spacing: 20, class_containers: {}, init: function() { var w = 675, h = 360; var pack = d3.layout.pack() .size([w - 4, h - 4]) .value(function (d) { return d.size; }); this.svg = d3.select("#chart").append("svg") .attr("width", w) .attr("height", h) .attr("class", "pack") .append("g") .attr("transform", "translate(2, 2)"); }, draw_class: function(model) { var data = model.get_all_members(), x = this.current_offset, y = 50, margin = 10; var g = this.svg.append('g') .attr("x", x) .attr("y", y); this.class_containers[model.get('name')] = g; g.append('rect') .attr("x", x) .attr("y", y) .attr("width", this.default_class_width) .attr("height", data.length * this.default_member_height); var that = this; g.selectAll("text") .data(data) .enter().append("text") .attr("x", x + margin) .attr("y", function(d, i) { return y + margin + i * that.default_member_height; }) .attr("dy", ".35em") // vertical-align: middle .text(String); g.append("line") .attr("x1", x) .attr("y1", y + this.default_member_height) .attr("x2", x + this.default_class_width) .attr("y2", y + this.default_member_height); this.current_offset += this.default_class_width + this.default_member_spacing; } }; var drawer = ClassDiagramDrawer;
Screenshots
Finally, here's how the 1st version looks like:
Next..
In the next parts, I'll try to make a real application out of this prototype, & alsp adopt better frameworks & technologies, such as CoffeeScript.
We will contact you as soon as possible.