There are several design patterns that are repeated throughout the repository. This guide is meant to supplement the TypeScript Style Guide.
TypeScript is used in all of the source code. TypeScript is used because it provides features from the most recent EMCAScript 6 standards, while providing type safety. The TypeScript compiler eliminates an entire class of bugs, while making it much easier to refactor code.
Objects will typically have an IOptions
interface for
initializing the widget. The use of this interface allows options
to be later added while preserving backward compatibility.
A common option for a widget is a IContentFactory
, which is used to
customize the child content in the widget.
If not given, a defaultRenderer
instance is used if no arguments are
required. In this way, widgets can be customized
without subclassing them, and widgets can support customization
of their nested content.
An object class will typically have an exported static namespace sharing the same name as the object. The namespace is used to declutter the class definition.
The "Private" module namespace is used to group variables and functions that are not intended to be exported and may have otherwise existed as module-level variables and functions. The use of the namespace also makes it clear when a variable access is to an imported name or from the module itself. Finally, the namespace allows the entire section to be collapsed in an editor if desired.
JavaScript does not support "destructors", so the IDisposable
pattern is used to ensure resources are freed and can be claimed by the
Garbage Collector when no longer needed. It should always be safe to
dispose()
of an object more than once. Typically the object that
creates another object is responsible for calling the dispose method
of that object unless explicitly stated otherwise.
To mirror the pattern of construction, super.dispose()
should be called
last in the dispose()
method if there is a parent class.
Make sure any signal connections are cleared in either the local or parent
dispose()
method. Use a sentinel value to guard against reentry, typically
by checking if an internal value is null, and then immediately setting the
value to null. A subclass should never override the isDisposed
getter,
because it short-circuits the parent class getter. The object should not
be considered disposed until the base class dispose()
method is called.
Messages are intended for many-to-one communication where outside objects influence another object. Messages can be conflated and processed as a single message. They can be posted and handled on the next animation frame.
Signals are intended for one-to-many communication where outside objects
react to changes on another object. Signals are always emitted with
the sender as the first argument, and contain a single second argument
with the payload. Signals should generally not be used to trigger the
"default" behavior for an action, but to allow others to trigger additional
behavior. If a "default" behavior is intended to be provided by another
object, then a callback should be provided by that object. Wherever possible
as signal connection should be made with the pattern
.connect(this._onFoo, this)
. Providing the this
context allows the
connection to be properly cleared by clearSignalData(this)
. Using a
private method avoids allocating a closure for each connection.
Some of the more advanced widgets have a model associated with them.
The common pattern used is that the model is settable and must be set
outside of the constructor. This means that any consumer of the widget
must account for a model that may be null
, and may change at any time.
The widget should emit a modelChanged
signal to allow consumers to
handle a change in model. The reason to allow a model to swap is that
the same widget could be used to display different model content
while preserving the widget's location in the application. The reason
the model cannot be provided in the constructor is the initialization
required for a model may have to call methods that are subclassed.
The subclassed methods would be called before the subclass constructor has
finished evaluating, resulting in undefined state.
Prefer a method when the return value must be computed each time. Prefer a getter for simple attribute lookup. A getter should yield the same value every time.
For public API, we have three options: JavaScript Array
,
IIterator
, and ReadonlyArray
(an interface defined by TypeScript).
Prefer an Array
for:
Prefer a ReadonlyArray
Prefer an IIterator
for:
If an object instance should respond to DOM events, create a handleEvent
method for the class and register the object instance as the event handler. The
handleEvent
method should switch on the event type and could call private
methods to carry out the actions. Often a widget class will add itself as an
event listener to its own node in the onAfterAttach
method with something like
this.node.addEventListener('mousedown', this)
and unregister itself in the
onBeforeDetach
method with this.node.removeEventListener('mousedown', this)
Dispatching events from the handleEvent
method makes it easier to trace, log,
and debug event handling. For more information about the handleEvent
method,
see the EventListener
API.
We use Promises for asynchronous function calls, and a shim
for browsers that do not support them. When handling a resolved or
rejected Promise, make sure to check for the current state (typically
by checking an .isDisposed
property) before proceeding.
Commands used in the application command registry should be formatted as follows: package-name:verb-noun
. They are typically grouped into a
CommandIDs
namespace in the extension that is not exported.