documentationfor yFiles for HTML 2.6

Details of the Class Framework

This chapter contains background information about the yFiles for HTML class framework. The knowledge presented here is not required in order to use the vast majority of the APIs. Be sure to read Working with yFiles before continuing below.

The concepts presented in this chapter only apply to the use of yFiles for HTML in ECMAScript Level 5.In ES2015 and newer, we recommend using the new language feature of classes and their inheritance concepts, although you could still use the constructs of this chapter as well.

$class and isInstance apply to all classes that inherit from yFiles for HTML classes regardless of the implementation, e.g. the yFiles class framework or the ES2015 class features.

Modules and Namespaces

The yFiles Class Framework provides the function yfiles.lang.module to encapsulate the logic of creating a new namespace object, or appending to an existing one. This function creates the specified namespace if it does not yet exist, and invokes the callback function. The callback function can then attach functions, types, properties or fields to that object and they will be available in the specified namespace object.

The return value of the function is the innermost namespace container object.

Creating and using modules
var module = yfiles.lang.module('my.module', function(exports) {
  exports.doSomething = function() { return 'Hello World'; };
});
my.module.doSomething() === 'Hello World'; // true
my.module === module; // true

While not strictly required, using modules helps to reduce boilerplate code and prevent pollution of the global namespace.

Now that you have an understanding of modules, it’s time to learn about the yFiles Class Framework’s type system.

Type System Overview

The yFiles Class Framework uses constructor functions in combination with prototypal inheritance and type augmentation. It is made up of five different types which can be combined to create robust objects that can be type checked and reflected at runtime: classes, interfaces, structures, enumerations, and attributes.

It supports the new operator as well as the instanceof operator where possible.

The standard JavaScript instanceof operator does not work for interfaces and enums in all browser. Therefore, we recommend to always use the isInstance(object) static method instead of the instanceof operator for checking for interface implementation. Thi method is described below.

Every type defined using the yFiles Class Framework has the following static functions and properties:

isInstance(object)
Returns true if the object is an instance of the type.
$class
Provides access to the yfiles.lang.Class instance that describes the type and can be used for reflective access and meta-programming. The following section introduces each of the five types that the class framework support.

The static isInstance method and $class property should only be used with types defined with the yFiles Class Framework. We strongly advise to use the instanceof operator in conjunction with ES2015 classes.

Classes

Classes are the most powerful types in a type system. They act as prototypes for their instances, can inherit from other classes and implement interfaces.

A class defined with the yFiles Class Framework consists of a (possibly) named constructor function with an appropriate prototype. By providing named constructors, you can have more than one constructor function for a class without having to write them differently from normal constructor functions.

The yFiles for HTML API does not make use of the named constructors feature. All constructors in the yFiles API are regular constructors without a specific name.

Classes can have both static and instance functions, fields, and properties.

Every class has an additional static $super property which points to its parent class. $super can be used to invoke an overridden method definition:

MyClass.$super.doSomething.call(this, arg1, arg2);

All classes in the yFiles Class Framework inherit from at least yfiles.lang.Object, which provides useful methods like memberwiseClone and getClass. The memberwiseClone method returns a shallow copy of the object, and getClass returns the yfiles.lang.Class that represents the object’s type for meta-programming purposes.

Interfaces

At the most fundamental level, an interface is a description of the functionality of a type and does not come with an implementation for that functionality. yFiles interfaces can optionally provide default implementations for certain functionality that does not need to be implemented explicitly by implementing classes.

A class can implement multiple interfaces. There are rules in place to prevent the “diamond inheritance problem” known from C++. Interface inheritance is explained in more detail in Interface Inheritance.

Interfaces cannot be trivially instantiated, they can only be implemented by classes. With the quick interface definition feature, creating an instance of an anonymous type that implements that interface can be done nevertheless.

Structures

Structures are similar to ordinary classes but mimic value types, or primitives, like the native Number, Boolean, etc. Technically of course these are still classes and instances are passed by reference, but they use different semantics than classes and behave differently when cloned using the memberwiseClone function.

When an instance of a structure is cloned, its values are cloned rather than referenced, so that when a primitive field of the cloned object is modified, the original object is not changed. Similarly, when an instance of a regular class is cloned using memberwiseClone, all of its structure members are also cloned recursively.

A structure always extends yfiles.lang.Struct, which extends yfiles.lang.Object and can implement any number of interfaces, but a structure cannot be inherited from.

Enumerations

Enumerations are an alternative to static number fields which allow easier usage of an API by grouping related values, making it clear exactly what options are available, and by helping to catch invalid or mistyped values.

The Enum implementation of the yFiles Class Framework does not try to do anything special or smart. It simply contains a number of fields with primitive values, that can be used instead of magic constants or unrelated static fields.

Enumeration types can have additional static members, but since instances are technically just JavaScript numbers, they do not provide specific instance members.

Attributes

Attributes provide meta information about types and their properties. They are essentially classes but their constructor is only called when an attribute is actually used by the class framework, not when it is invoked. This lazy construction allows use of attributes with very little performance hit.

Access to attributes is provided by the reflection API of Class<T>.

The main purpose of attributes is to provide meta information about classes and properties during (de-)serialization of GraphML files.

Unless you need to customize GraphML IO, you probably never need to use Attributes.

Lazy Type Definitions

The types defined by the yFiles Class Framework need references to base classes and implemented interfaces at definition time to arrange the prototype chain, check the type, and reference default implementations for non-abstract interface members.

To prevent circular references during the process of class definitions, it is possible to declare lazy type definitions. Type definitions declared as lazy are evaluated at first access to their constructor. They must be contained in a yfiles.lang.module block and must have an associated namespace.

Now that you know the basic differences between the five types, let’s take a closer look at how to define and use each type.

Classes

Defining a Class in ES5

Classes are created by calling yfiles.lang.Class as a function, and storing the result in a variable.

Basic class definition
var Person = yfiles.lang.Class('Person', {
  constructor: function(name) {
    this.name = name;
  },

  toString: function() {
    return this.name;
  }
});

// use:
var john = new Person('John Doe');
console.log(john.toString()); // -> John Doe

The constructor must be named constructor. Custom methods can be defined in the same way.

The following identifiers are reserved and have a special meaning for the yFiles Class Framework:

constructor
The constructor function of the class or an object that defines multiple named constructor functions.
'default'
Denotes the default constructor function of a class, when there are multiple named constructors defined. Note that the quotation marks are part of the reserved name. Property in the constructor object.
$static
An object that describes static functions, properties, and fields that should be available on the class, rather than on the instance.
$clinit
A static function that is called by the yFiles Class Framework to perform additional class initialization. This is an optional property in the $static object, which is only supported for lazily defined types.
$meta
An array, or a function that returns an array, of yfiles.lang.Attribute objects.
$extends
The class from which to inherit.
$with
An array of interfaces that the class implements.
$abstract
A boolean value that specifies whether the class is abstract and does not define all required methods, fields, or properties.

Static members
var CookieFactory = yfiles.lang.Class('CookieFactory', {
  $static: {
    createCookie: function() {
      return 'A fresh chocolate chip cookie!';
    }
  }
});
var cookie = CookieFactory.createCookie();
console.log(cookie); // -> A fresh chocolate chip cookie!

Static members are not stored in the prototype object, rather they are directly accessible from the class. The yFiles Class Framework adds them to the class’s native JavaScript constructor.

Named constructors
const Point = yfiles.lang.Class('Point', {
  x: 0, y: 0, // some fields
  constructor: {
    default: function(x, y) {
      this.x = x
      this.y = y
    },
    /**
     * @param {Point} p
     */
    CopyOf: function(p) {
      this.x = p.x
      this.y = p.y
    },
    /**
     * @param {Rect} r
     */
    CenterOf: function(r) {
      // invokes default constructor instead of manual assignment
      Point.call(this, r.x + r.width / 2, r.y + r.height / 2)
    }
  },

  size: function() {
    return Math.sqrt(this.x * this.x + this.y * this.y)
  }
})
// usage
const p = new Point(10, 10)
const p2 = new Point.CopyOf(p)
const p3 = new Point.CenterOf(new Rect(10, 10, 10, 10))

Named constructors are a good alternative to using a single constructor that evaluates the arguments to conditionally handle multiple types of input. A big benefit of using the yFiles Class Framework for this type of thing is that there is no difference in how you write a named or a default/single constructor. If using multiple constructors, simply mark the default one as 'default' including quotation marks. Additionally, this is set correctly and invoking another constructor has the syntax that you’d expect: ClassName.ConstructorName.call(this, arguments).

From the user’s perspective, named constructors resemble factory methods but with an explicit new. This feature was inspired by Google Dart.

Calling the parent class
const Employee = yfiles.lang.Class('Employee', {
  $extends: Person, // inherits from Person defined earlier

  constructor: function(name, company) {
    // calls default parent class constructor
    Employee.$super.constructor.call(this, name)
    this.company = company
  },

  toString: function() {
    // calls parent toString implementation
    return Employee.$super.toString.call(this) + ', ' + this.company
  }
})

To call the parent class, simply use Classname.$super.methodName.call.

Defining properties
const Manager = yfiles.lang.Class('Manager', {
  $extends: Employee,

  constructor: function(name, company, employees) {
    // shorter way to call super constructor
    Employee.call(this, name, company)
    this._employees = employees
  },

  // uses native JavaScript syntax for properties
  get allEmployees() {
    return this._employees
  },

  // uses alternative class framework specific syntax
  hasEmployees: {
    get: function() {
      return this._employees.length > 0
    }
  }
})

There are two ways of defining properties, but both syntactic variants do the same thing: create a read-only property and define and implement an accessor. Properties must have at least a setter or a getter, but can, and usually do, have both.

The second, non-native, syntactic form is required when adding yfiles.lang.Attribute objects to a class member.

To get or set the value of a property of the parent class, use the following:

Working with properties of the parent class
ClassName.$super.getOwnProperty("propertyName", this).get()
ClassName.$super.getOwnProperty("propertyName", this).set(value)

Class Inheritance

Inherit from another class by adding an $extends member to the class definition.

A class inherits all non-static, non-constructor members of the class it extends. That is, subclasses do not automatically get the parent class’s constructors or static members.

If a non-abstract class inherits from an abstract class, it must implement all abstract members.

Interface Inheritance

Interface inheritance allows a class to state that it conforms to a specific interface. In contrast with traditional interface inheritance, it allows the reuse of standard implementations of interface methods.

A class which inherits from an interface with abstract members must implement all of the abstract members or be declared as abstract. Otherwise an error is thrown when the debug library version is used and the behavior is undefined when the production version is used.

If a class inherits from an interface with a non-abstract method and provides a method with the same name, the class implementation always has precedence. The same is true for members that are inherited from a base class, that is, the subclass implementation always has precedence.

To inherit from an interface, use the $with member of a class definition.

To inherit from multiple interfaces, use the $with member of a class definition with an array of interface references.

Inheriting from multiple interfaces
const List = yfiles.lang.Class('List', {
  $extends: AbstractList,
  $with: [ICollection, IEnumerable, IList]
  // ...
})

This class states that it implements three interfaces: ICollection, IEnumerable, and IList. It also extends the AbstractList class.

JavaScript only allows for a single prototype value to be tested using the instanceof operator. Since the prototype is already used for class inheritance, it is not possible to test whether an object implements a certain interface using the instanceof operator. Instead we use the isInstance method for this purpose.

Type checking an object for interface inheritance
if (ICollection.isInstance(obj)) {
  // obj inherits the ICollection interface
}

The tested object must be an instance of at least yfiles.lang.Object for this test to work. Also, it is not sufficient to simply implement all methods defined by the interface; it must inherit from the interface using $with. The implementation does not check for the existence of the members on the instance but relies on type information stored in the object’s associated yfiles.lang.Class object. This approach is significantly faster however requires the use of the class framework. Instances that implement the interface by convention, only, are not considered instances in the sense of the class framework. If such “duck-typed” objects are passed to the yFiles library, they will not be recognized correctly and their implementations will not always be called properly.

You can use the isInstance method on class, enumeration, structure, and annotation types, too.

Lazy Classes

Lazy classes are evaluated when they are first used or referenced. This offers two major benefits:

  • Unused types are never evaluated
  • The load order of the classes doesn’t matter

Using lazy classes can significantly improve your application’s load time, since most of your files can be loaded in parallel and the amount of code that runs during loading is much smaller, as only module definitions need to be processed.

Lazy class definition
yfiles.lang.module('myapp.model', function(exports) {
  exports.Person = new yfiles.ClassDefinition(function() {
    // this part is not evaluated until necessary
    return {
      constructor: function(name) {
        this.name = name
      },

      toString: function() {
        return this.name
      },

      $static: {
        $clinit: function() {
          console.log('Person class has been created!')
        }
      }
    }
  })
})
// at this point the class definition is evaluated and the type is created
const pete = new myapp.model.Person('Pete')

Lazy classes are evaluated as soon as they are used. In the above example, any access to myapp.model.Person triggers evaluation of the class.

After a lazy class has been evaluated and created, the yFiles Class Framework will invoke the static $clinit class initialization function, if defined. This callback can be useful if you need to perform complex calculation for static class members or to reduce the number of dependencies on other types during construction, which can further help to reduce cyclic references between types.

Interfaces

Defining an Interface

Interfaces are defined using the yfiles.lang.Interface function.

Defining a simple interface
const IIterable = yfiles.lang.Interface('IIterable', {
  iterator: yfiles.lang.AbstractMethod
});

This example defines an interface with a single, abstract method. The yFiles Class Framework provides yfiles.lang.Abstract, yfiles.lang.AbstractMethod and yfiles.lang.AbstractProperty for documentation purposes, but doesn’t differentiate between them.

The primary difference between an interface in the class framework and most interfaces used by other languages and technologies is that yFiles interfaces can define implementations of the members they require.

Defining an interface with a concrete method
const IIterable = yfiles.lang.Interface('IIterable', {
  iterator: yfiles.lang.AbstractMethod,

  forEach: function(fn, ctx) {
    const it = this.iterator()
    while (it.hasNext()) {
      fn.call(ctx, it.next)
    }
  }
})

In this example, the interface IIterable requires its implementers to provide an iterator method. Every class that implements IIterable will get a free forEach method in return but can optionally define its own optimized version of it.

Defining an interface with a static method
const IList = yfiles.lang.Interface('IList', {
  $with: [ICollection],

  add: yfiles.lang.AbstractMethod,

  $static: {
    of: function(arguments) {
      const list = new List() // choose default implementation
      for (let i = 0, n = arguments.length; i < n; i++) {
        list.add(arguments[i])
      }
      return list
    }
  }
})
const mylist = IList.of('hello', 'world')

While interfaces can have static members, these members are never inherited by classes or other interfaces.

Interface Inheritance

An interface can implement any number of other interfaces.

Since interfaces effectively allow for multiple inheritance of member definitions, the primary concern is how to resolve a conflict when a class implements two interfaces that each define the a member with the same name. This problem is known as the diamond inheritance problem.

Diamond inheritance resolution
const IShape = yfiles.lang.Interface('IShape', {
  area: yfiles.lang.AbstractProperty
})

const IRectangle = yfiles.lang.Interface('IRectangle', {
  $with: [IShape], // inherit from IShape
  width: yfiles.lang.AbstractProperty,
  height: yfiles.lang.AbstractProperty,

  get area() {
    return this.width * this.height
  }
})

const ISquare = yfiles.lang.Interface('ISquare', {
  $with: [IShape], // inherit from IShape

  size: yfiles.lang.AbstractProperty,
  get area() {
    return this.size * this.size
  }
})

const Square = yfiles.lang.Class('Square', {
  $with: [IRectangle, ISquare]
})

The question is: which version of the area property is used in the Square class? The yFiles Class Framework follows a simple rule to resolve this kind of problem: If in doubt, it’s abstract.

Because IRectangle and ISquare both implement IShape's abstract area property, Square is responsible for defining its own implementation so that there is no ambiguity. Since it doesn’t, trying to access a Square instance’s area would result in a runtime error.

The implementation of an interface is always preferred over implementations of its base interfaces. Thus, if Square were an interface and had an implementation of area, then its implementation would be used instead of any of the ones defined by its base interfaces.

To summarize the inheritance rules of interfaces: in cases where two interfaces with the same member are implemented by a subtype:

  • If both method definitions are abstract, then the method is abstract.
  • If one method definition is abstract and the other concrete, then the concrete method is used.
  • If both method definitions are concrete:
  • If the interfaces are implemented at the same level, then the method is abstract.
  • If one of the interfaces is a sub-type of the other, then its method definition will be used.

Lazy Interfaces

Interfaces can also be defined as lazy types with the same semantics as the other types, such as being namespaced to a module. They are only created when they’re used. This feature allows for better startup time and helps to prevent cyclic dependencies.

Lazy interface definition
yfiles.lang.module('myapp.api', function(exports) {
  exports.CookieFactory = new yfiles.InterfaceDefinition(function() {
    return {
      $with: [myapp.api.Factory],
      makeCookie: yfiles.lang.AbstractMethod
    }
  })
})

The CookieFactory, an interface which itself implements the Factory interface, is defined when it is first used.

Structures

Defining a Structure

Structures are value types. They have the same definition as classes, except that they always directly extend yfiles.lang.Struct. To create a new structure type, use yfiles.lang.Struct as a function.

Structure definition
const Rect = yfiles.lang.Struct('Rect', {
  $with: [IRectangle, ICloneable],
  $topLeft: null,
  width: 0,
  height: 0,

  constructor: function(x, y, w, h) {
    this.$topLeft = new Point(x, y)
    this.width = w
    this.height = h
  },

  get topLeft() {
    return this.$topLeft.clone()
  }
})

Structures are defined just like classes. They have the same semantics and work the same way with one major difference:

If a class or structure is cloned using memberwiseClone, all members that are structures are cloned as well.

In the above example, $topLeft is a Point, which is a structure. If we clone an instance of Rect, then the copy will have a cloned $topLeft member. Note that while structures have a built-in clone method, you are responsible for using it to implement structure semantics. That is, you must clone structures before passing them to a method or returning them from one.

Structures also have more specialized versions of the equals and hashCode methods. Two structure instances with the same values are equal and have the same hash code. This differs from the yfiles.lang.Object definitions for equals and hashCode, which ensure that only identical objects are the same.

Lazy Structures

A lazy structure has the same properties as a lazy class. It must be namespaced to a yfiles.lang.module and is evaluated at first use.

Defining a lazy structure
yfiles.lang.module('myapp.model', function(exports) {
  exports.Name = new yfiles.StructDefinition(function() {
    return {
      firstName: '',
      lastName: '',

      constructor: function(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
      },

      toString: function() {
        return this.firstName + ' ' + this.lastName
      }
    }
  })
})

Enumerations

Defining an Enumeration Type

Enums are enumerations of flags. They are easier to use than prefixed integer fields and the yFiles Class Framework supports a simple syntax to define such an enumeration type. Their usage makes code more readable and makes it easier to catch typos and invalid values.

Basic Enum definition
const Priority = yfiles.lang.Enum('Priority', {
  LOW: 0,
  MEDIUM_LOW: 1,
  MEDIUM: 2,
  MEDIUM_HIGH: 3,
  HIGH: 4,
  VERY_HIGH: 5
})

switch(bug.priority) {
  case Priority.LOW: // ...
    break;
  case Priority.MEDIUM_LOW: // ...
    break;
  // ...
}

All members of an enumeration are automatically static.

Automatically generate enumeration values
const _ = void 0 // "defines" another name for undefined
const Priority = yfiles.lang.Enum('Priority', {
  LOW: _,
  MEDIUM_LOW: _,
  MEDIUM: _,
  MEDIUM_HIGH: _,
  HIGH: _,
  VERY_HIGH: _
})

In this example, we have defined a shorter name for the undefined value in JavaScript: _. When the value of an enumeration member is undefined, it will be set to the next logical value. This will only work for integer fields and should not be mixed.

Since most of the time an enumeration value is only a integer value, you can use “bitwise or” and “bitwise and” to create flagged enumerations. In this case, you must define the value of the fields yourself.

The enumeration type also offers a few useful methods that can be used to convert values to strings and strings to values.

Using an enumeration
const priority = yfiles.lang.Enum.parse(Priority.$class, 'LOW')
if (priority === Priority.LOW) {
  // will be true
  console.log('ok') // will be printed
}
console.log(yfiles.lang.Enum.getName(Priority.$class, priority)) // -> "LOW"

The yfiles.lang.Enum.parse method takes parameters for the enumeration and value, with an optional third parameter: ignoreCase. If this parameter is set to true, then the strings 'LOW', 'Low', 'low', etc. will all provide the same value.

The yfiles.lang.Enum.getName method returns the string matching the specified value.

Lazy Enumerations

Lazy enumerations function similarly to lazy instantiation of other types. They must be namespaced to a module, and provide benefits at startup time and in preventing circular references.

Lazy enum definition
yfiles.lang.module('myapp.model', function(exports) {
  exports.Priority = new yfiles.EnumDefinition(function() {
    return {
      LOW: 0,
      MEDIUM_LOW: 1,
      MEDIUM: 2,
      MEDIUM_HIGH: 3,
      HIGH: 4,
      VERY_HIGH: 5
    }
  })
})

Attributes

Defining an Attribute

Attributes contain meta information about namespaces, types, and type members. yFiles for HTML uses meta information primarily for reading and writing GraphML file format, but other use cases exist.

yFiles Class Framework attributes are closer to C# attributes than to Java annotations. The major difference between both concepts is that attributes are actual classes which can extend other attributes, implement interfaces and have methods and properties.

Defining a basic attribute
const FormTypeAttribute = yfiles.lang.Attribute('FormTypeAttribute', {
  type: '',
  defaultValue: '',
  constructor: function (type) {
    this.type = type
  }
})

This attribute could be used to annotate a data model so that you could generate a form for the model automatically.

class LoginFormModel {
  constructor() {
    this.firstName = ''
    this.lastName = ''
    this.stayLoggedIn = true
  }

  // provide a static $meta property that returns an object with meta properties for each field
  static get $meta() {
    return {
      firstName: [FormTypeAttribute('text')],
      lastName: [FormTypeAttribute('text')],
      stayLoggedIn: [FormTypeAttribute('checkbox').init({ defaultValue: true })] // defines arbitrary additional members
    }
  }
}

Annotating ES2015 classes like this is possible since yFiles for HTML 2.3. It is also possible to annotate ECMAScript Level 5 yFiles classes with Attributes like this:

const LoginFormModel = yfiles.lang.Class('LoginFormModel', {
  firstName: {
    // uses object with $meta instead of field
    $meta: [FormTypeAttribute('text')], // applies attribute
    value: '' // follows PropertyDescriptor semantics
  },
  lastName: {
    $meta: [FormTypeAttribute('text')],
    value: ''
  },
  stayLoggedIn: {
    $meta: [FormTypeAttribute('checkbox').init({ defaultValue: true })], // defines arbitrary additional members
    value: true
  }
})

Even though attributes are class types, they are used like functions, i.e. without the new keyword. This helps to differentiate them from a normal class constructor, since they behave differently.

While it may look like the previous sample created three instances of our FormTypeAttribute, it didn’t create any instances. Attribute constructors are only invoked when attributes are read using reflection.

Lazy Attributes

Defining a lazy attribute
yfiles.lang.module('myapp.attributes', function(exports) {
  exports.CanBeNullAttribute = new yfiles.AttributeDefinition(function() {
    return {
      constructor: function() {}
    }
  })
})

As with other types, lazy attributes must be namespaced to a module. This attribute will only be constructed when it is first used.

Additional Support

Events

The yFiles Class Framework is complemented by further support that provides convenience functionality around event handling.

Events can be easily subscribed to and unsubscribed from through associated methods for adding and removing listener callbacks. These methods take an event handler function as their parameter.

For example, the QueryItemToolTip event of class GraphViewerInputMode can be easily subscribed to and unsubscribed from through the addQueryItemToolTipListener and removeQueryItemToolTipListener method, respectively.

In the yFiles for HTML API documentation, the associated methods to add and remove listener callbacks to events are currently listed along with the corresponding event.

By means of the yfiles.lang.delegate factory method, appropriate event handler functions can be conveniently registered as implicit closures. The yfiles.lang.delegate method wraps a given function together with a target object which serves as the caller context for when the given function actually gets invoked.

The following code snippet from the Graph Viewer tutorial demo application shows how an event handler function is registered as a closure with the current this context:

Registering an event listener
const graphViewerInputMode = new yfiles.input.GraphViewerInputMode()

graphViewerInputMode.addQueryItemToolTipListener(
  yfiles.lang.delegate(this.$onQueryItemToolTip, this)
)

When, in response to the QueryItemToolTip event, the $onQueryItemToolTip listener callback gets invoked, all uses of this within the callback’s body actually access the object that the this referenced at invocation time of the yfiles.lang.delegate factory method.