Home

The General Structure of the Application

Conceptually, I have divided the logic of the Syntree application into three (ish) layers: sanitization, delegation, and action. Sanitization means getting user input and interpreting it, i.e. taking a keydown event and turning it into a request to navigate downwards. Delegation means receiving a such a request -- say, to edit the selected node -- and deciding what Element needs to be manipulated, or what other operation needs to be performed. Action means just that -- actually altering the data of the Elements (or occasionally doing something else). There is also a fourth pseudo-layer: the representation of the data as graphics. The delineation between these layers is not always perfect, but I have nonetheless found it useful to use this structure as a guide.

The sanitization layer, existing at the conceptual “top” of the application, is composed of the Workspace object. Therefore, Workspace is in charge of listening for relevant user input and passing it to the appropriate control structure of the delegation layer.

The delegation layer is composed of a Page instance. Page has a broad range of functionality, acting in many cases as a way to separate complex control logic from the user input sanitation logic in Workspace. As such, Page is responsible for a variety of tasks, including navigating between nodes and adding new elements. Methods of Page are often closely bound to Workspace methods (and thus user input).

The bread and butter class of Syntree is Node. Nodes are the primary data object. Each Node stores a reference to its parent and to its children, as well as the branches which connect it to each of these. The Tree class is, in fact, nothing but a wrapper around a group of linked Nodes. Any given instance of Tree only stores one reference to a Node -- the root Node. All methods of Tree access descendant Nodes through this reference to root. This pattern is very similar to the basic implementation of a link list. It allows us to wrap any arbitrary subtree in a new Tree object, and have access to the methods of Tree in relation to that subtree.

So far we’ve described user input flowing down from Workspace, through Page, and finally to a Tree or an Element. Each Element then updates its own graphical display (utilizing its own instance of the Graphic class). I have tried as much as possible to enforce a strict separation between data and graphical representation -- this has not always been successful.

There are some properties of Elements which are “data”, but are only used for graphical display; while other properties are data data, necessary for the underlying representation of the tree itself. For example, a Node’s links to parents and children is data that is absolutely essential to the underlying representation of the tree. However, a Node’s x and y coordinates are not even related to this underlying representation. Node coordinates are not even stored when we save a Tree as a “.tree” file (which can be uploaded and turned back into a tree). In fact, they are generated by the code based on the relationships of Nodes to one another. Because of such difficulties, it proved ultimately impractical to enforce a perfect separation of data and graphical representation; and in fact, such enforcement, were it to be implemented, would be more likely to confuse matters than simplify them.

Specific Features and Quirks

Class System Implementation

JavaScript does not natively support a class system like you would find in most server side languages. ES6 does add syntactical sugar to allow for code like:

class Bear extends class Animal { definition }

I elected not to use any ES6 syntax, for classes and elsewhere. Partially this decision was made because ES6 isn’t yet fully supported, and often needs to be backwards transpiled into ES5. Mostly, though, I relished the challenge of implementing a class system with basic JavaScript. Working through this exercise helped me to understand JavaScript better as a language, and understand class systems better conceptually.

Syntree uses two methods to define classes: constructor functions and object literals. Regular classes are constructor functions, and are intended to be instantiated multiple times, dynamically throughout the course of user interaction. Classes defined with object literals are meant to be an implementation of the “singleton” pattern, where a given class is restricted to a single instance. Singleton classes include Workspace, Lib, and Tutorial.

These singleton classes are basically synonymous with their object instantiations. I found it expedient to conceptualize this structure as a compromise between regular classes and static classes (which, as far as I am aware, do not exist in JavaScript).

One might think that, because of its prototype system, it would be easy to implement class inheritance in JavaScript. As it turns out, this is very much not the case. There are solutions, though -- the one I settled on was to have a function that manually created a prototype chain based on constructor functions passed to it. The function takes those constructors, and an object instance, and inserts the proper prototypes into the instance’s prototype chain. For more information on this admittedly confusing process, see Lib.extend.

Config Maps and Matrices and checkArg

As I built each class, I noticed a repetitive pattern, wherein I go through each argument passed to the constructor, check it for a certain type, and then implement a default value if the type check was not passed. In order to streamline this process, each constructor takes a single argument, that argument being an object containing the actual arguments proper (the so-called config_matrix). Each class that implements this pattern must also have a listing in the global config_maps object, which contains data about the required type of each argument and what default value to use if the argument is not that type.

I then wrote a Lib function which takes as arguments a config_matrix and a class instance. It retrieves the relevant config_map for the target class instance, and checks each config_matrix value against that map.

A related pattern can be seen in the Lib function checkArg, which takes any value, a type string, and another value to be the default. In essence, checkArg is an inline version of a single config_map check. It is used in regular functions, instead of in constructors, and had a similar origin to config_maps -- namely, I noticed a repetitive pattern (checking the type of an argument) and wanted to modularize that pattern.