Overview
The Template Designer and E-Signer viewers follow a modular design. This section provides information regarding the modular design and the principles we follow when implementing the Viewer modules.
Modules are generalized, single-feature, and reusable. Modules should implement their feature as generically as possible, making the least amount of assumptions about the external code consuming that module. If you find that you must use "and" to describe what the module does, you should probably be writing two modules.
Modules should also be instance-safe and viewer-safe. Each viewer should be able to use multiples of the same module without adverse effects, and multiple viewers should be able to initialize the same module without adverse effects. When writing a module, each module will need to initialize itself in such a way that allows multiple instances of that module to run at the same time. This means that things like global state variables are not allowed; any global variables need to be static.
Core Module
Each viewer will need its own core module. The purpose of the core is to set up some common API, initialize modules, and provide a basic page structure and module containers:
- Core will set up both the StateStore and EventStore (available as
stateStore
andeventStore
on the Viewer object). These APIs are expected by all modules and need to be created by the code initializing the modules. - Core will also create the general page layout (think large containers), and will size and position all modules. It will need to provide parent containers as part of initializing UI modules.
- Core will also attach all exported members of common-core.js, including
parseIcons
andparseComponents
functions, to the Viewer object. These are auxiliary functions that are expected by all modules.
JS-Only Modules
JS-only modules are encouraged when there is specific business logic to take care of. This includes, but is not limited to, client-server communication, controller modules that translate one thing to another, and modules that manage a state in the background. These modules should consist of a single JavaScript file, or a main JavaScript file that calls out to one or more sub-modules.
These modules can listen to and trigger any EventStore events, and can listen to any ViewerControl events. These modules must never listen to DOM events.
UI Modules
UI modules present a specific user interface, and should be entirely contained inside one container. If you find that you need two containers in your implementation, you should probably be writing two modules. These modules should always take up 100% of their specified container. It is up to the code creating the module to size and position it, and not up to the module itself.
These modules can listen to and trigger EventStore events. It can also listen to DOM events of elements that are located inside the module’s container. They must never listen to DOM events for elements that are located outside of the module’s container. Though it is not encouraged, it could be okay at times to listen to ViewerControl events, although such logic should most likely be extracted to a separate JS-only module.
UI modules should take care of updating their own UI, and staying up-to-date with any viewer logic (such as changes in relevant state values).
Initializing
Each module should take up to two parameters when initializing. The first will be an instance of a viewer (such as that defined in a "core" module). The second parameter is an optional options
parameter, which provides extra settings and values to the module, such as the DOM element that the module should use as a parent container. Modules should register all events that they need in order to complete their tasks. Modules should not rely on external triggers that are not expressly defined as events.
Destroying
Modules should provide a mechanism to allow them to be destroyed. Typically, this will be a destroy method available as the module’s API.
Any event that is registered during initialization or throughout the lifespan of the module must be removed during the destroy. This includes, but is not limited to, event store events, ViewerControl
events, all DOM events, and events and functions that are temporarily registered to perform a transient task, such as animation frame optimizations or timeouts. In the case of the last example, the developer should never assume that a transient event was already disconnected, as a module could be destroyed before the transient task has completed.
Any resources created during initialization or throughout the lifespan of the module must be removed and cleaned up during the destroy. This includes, but is not limited to, DOM elements, CSS classes used on the parent container element, any amounts of global data being stored, pending web requests or other asynchronous tasks, and functions that exist as part of events. The last is very important, as we need to avoid memory leaks due to data staying in scope indefinitely.
Components
Components are special types of modules that provide some widely reusable canned behavior for a single conceptual thing. Components will most likely be needed for, but not necessarily confined to, polyfilling native browser controls and components. Examples of this are the TextInput
, CheckboxCollection
, and Dropdown
components. For example, browsers provide a native dropdown through the select
tag; however, these are not all that pretty and have very limited styling and extensibility options. In order to provide flexible, extensible, and beautiful dropdowns, we have implemented the Dropdown component to polyfill the parts that a native select
element does not provide.
When implementing components, we should provide an experience as close as possible to the native browser ability. For example, in dropdown, we need to provide a similar developer experience to using the native select
tag. In this case, the developer using Dropdowns should provide a parent tag defining the component, as well as a list of elements to use as the options. Code to handle selecting options, including the label and dropdown arrow, as well as any extra markup, should be created by the component code.
Initializing Components
Components are initialized by providing the component parent element to the component. In the Dropdown example, this is the element equivalent to the select
tag. In the case of sets, such as CheckboxCollection
and ButtonSet
, each element from the set is initialized separately, and it is up to the component to group them together in order to add functionality. Components should use the "Data Dash" DOM API (e.g., data-pcc-something) in order to define properties of that component.
Destroying Components
Similar to modules, components need to expose a destroy method which cleans up all resources used by that component.
Component API
Components should expose similar functionality to the native ability that they are polyfilling. This includes, but is not limited to, useful events (such as the "change" event for values), ways of getting and setting the value (in the case of input components), and a way to access the list of values. This should be standard throughout all components, as much as possible.