Unpacking Typester: A New Open Source Text Editor

Fred Every
Jun 14, 2018 · 6 min read

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 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 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

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

Fin

Type/Code

Type/Code is an interaction design studio in Brooklyn NY.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store