Unpacking Typester: A New Open Source Text Editor

Fred Every
Type/Code
Published in
6 min readJun 14, 2018

By Fred Every, Lead Developer on the Typester project.

This article is the first in a series offering a look into the inner workings of the open-source project Typester, a WYSIWYG editor designed and developed at Type/Code to return standardised, sanitised, and predictable HTML. This article will cover the core components of Typester.

For more information on Typester, demos, or to get started with the editor, visit http://typester.io/.

Typester uses modules in order to decompose the complexity of the plugin into independent, cohesive, and interchangeable components. By doing so we leverage all the good stuff that comes from modular software design principles.

But, the modules in Typester have a twist. Where modules designed using classic module design patterns expose a public interface, that a developer can call directly in the code, Typester’s modules don’t expose anything. At least not directly.

Typester’s modules can’t be instantiated in isolation and then used via an exposed interface of public methods. Typester’s modular design works via a self contained ecosystem that leans heavily on nested layers of isolation and an hierarchy of mediators to facilitate interoperability.

There are three primary pieces to this:

  1. Containers
  2. Mediators
  3. Modules

Containers

The first part of this ecosystem is the Container. A container encapsulates, instantiates, and links modules using a mediator. Containers can do the same with other containers, offering container composition and nesting. These are created using a factory method that takes in a container definition and returns a constructor method of instantiating the container.

The container offers the following benefits:

  1. Domain scoping: where related modules are encapsulated and then linked via a shared mediator which allows for cross module communication to happen in the local scope.
  2. Inter-domain communication: where, when the local domain is unable to fulfill a request, the request is hoisted to the parent container which in turn checks with peer containers to see if any of them, or their modules, are capable of fulfilling the request. If none of the peers are capable off handling the request, it will be hoisted once again. This hoisting will continue until either the request is handled or the root container is reached. If the request hasn’t been handled by this point it will just fail silently.
  3. Module multi-instancing with isolation: by scoping and encapsulating the domains in containers, you are able to have multi-instanced modules existing in the same system, but used in different scopes. For example: in Typester there is a module that is responsible for handling selections, but we have two areas that require the module, the currently active editable node on the page as well as the canvas in which Typester does all the heavy lifting. By using containers to separate the editor and canvas domains, we are able to have the two instances of the selection module active without them cross interacting or fighting each other.

The library currently has 4 containers:

  1. AppContainer: This container holds modules responsible for handling the user’s interaction with the active editable container on the page. This is the only container that will have multiple instances, one for each editable container with Typester bound.
  2. UIContainer (Singleton): This container holds modules responsible for the display and interaction handling of the UI components such as the toolbar, the flyout that wraps and positions the toolbar, and a mouse handler.
  3. FormatterContainer (Singleton): This container holds all the formatter modules.
  4. CanvasContainer (Singleton): This container holds the modules related to the canvas and the handling of content into and out of the canvas.

Mediators

The mediators are the glue that holds everything together in Typester and facilitates the interoperability of the system. Containers and modules register themselves with the mediators and then use the mediator to dispatch commands, do requests, and bind to system events.

The mediators can also be chained, parent to child, to allow for a hierarchy of mediators. This is useful for propagating mediator dispatches that cannot be fulfilled by the current mediator. Mediators also allow for hot swapping their parents, which is used by Typester to attach the editor instances to the singleton instances of the UI, Formatter, and Canvas containers on focus of an editable DOM element.

The mediators offer three modes of communication within the system:

  1. Requests: Getting data or content from another part of the system.
  2. Commands: Triggering another part of the system to execute a method or routine.
  3. Events: Emit a system wide message to signal a noteworthy event.

Some of the benefits of using a mediator for inter-system communication include:

  1. Decoupling: Modules do not connect or interact with each other directly. All interaction is mediated by the mediator, so replacing a module or moving a handler to another module only requires declaring, or migrating, the correct handlers. So the rest of the system needs no changes. No need to grep the codebase and replace multiples of namespaced module method calls.
  2. Isolation: Modules are essentially blind to the inner workings of the rest of the system. They may assume that the system can do something, but have no knowledge of how or where the system will handle a request or execute a command. All they need to do is request something or issue a command. I need this done, and I don’t care who does it or how it gets done.
  3. Fault tolerance: Failures inside modules are not propagated to other parts of the system, and should not bring the entire system to a halt.

Module, and container, definitions can include a handler: {} object that declares the mapping of requests, commands, and events to handler methods using arbitrary call strings as keys. In Typester we opted for colon separated call strings that are declarative, describing the expected command outcome or the result from a request. In the code sample above, the handler mapper for the Canvas module, you can see that mediator.get('canvas:document') will be mapped to a method called getCanvasDocument and you can expect to get the document node of the canvas iframe in response.

Modules

The next piece to the system, and the real workhorses of it all, are the Modules. These are created using a factory method that takes in a module definition and returns a constructor method of instantiating the module.

As mentioned earlier, once instantiated, a module does not expose any kind of public interface. Instead, modules are passed a mediator to self-register to during setup.

The modules offer two hooks that are called when they are instantiated. A setup() hook and an init() hook, inside which relevant setup or post-init code can be placed.

Other than that the developer is free to populate the methods:{} object with all the methods they need. All the methods declared will be bound with the module context

Contexts

Used by modules and containers, contexts gave us a way of setting the this context of child methods to an extendable object. The system populates the context with references to the mediator, available methods from the module or container, the module props, and any declared DOM references.

Fin

For more info check out the technical docs for typester here: https://github.com/typecode/typester/tree/devel/docs

--

--