I’ve been meaning to document this for a while now, so here goes:
@stimulus/mutation-observers
The @stimulus/mutation-observers
package provides a tower of classes for handling mutation events at various levels of abstraction. Each class in the tower wraps another observer class, acts as that observer’s delegate, performs some kind of filtering or indexing, and finally exposes the results through its own delegate interface.
The ElementObserver
class sits at the base of the tower. Construct it with a root DOM element and an object which implements the ElementObserverDelegate
interface. Each instance has its own DOM MutationObserver
configured to observe the root and its child elements. The methods of the delegate interface control which elements are matched (matchElement()
, matchElementsInTree()
) and what to do in response (elementMatched()
, elementUnmatched()
, elementAttributeChanged()
).
ElementObserver
instances have stop()
and start()
methods which let you pause and resume matching (during performance-critical code, for example). After a call to start()
, the ElementObserver
notifies its delegate of all matching elements which have changed since the last call to stop()
.
On top of ElementObserver
sits AttributeObserver
, which monitors a DOM tree for mutations to attributes with a given name. The AttributeObserverDelegate
interface allows delegates to respond when an attribute is matched or unmatched on an element, or when an already matched attribute’s value has changed.
TokenListObserver
specializes AttributeObserver
to monitor a DOM tree for changes to token list attributes. (A token list is an attribute whose value is a space-separated list of zero or more string tokens, such as the HTML class
attribute.) The TokenListObserverDelegate interface exposes Token objects through tokenMatched()
and tokenUnmatched()
methods. Each token represents a unique string token belonging to an element and attribute at a particular index.
At the top of the tower is ValueListObserver
, whose ValueListObserverDelegate
interface allows delegates to specify a parseValueForToken()
method which converts a token to an application-defined value type. Then the elementMatchedValue()
and elementUnmatchedValue()
methods notify the delegate when a value appears in or disappears from the document.
The @stimulus/core
package has two observer classes of its own, each which sits atop ValueListObserver
from @stimulus/mutation-observers
. The first, BindingObserver
, watches data-action
attributes and creates bindings from action descriptor tokens. The second, ScopeObserver
, watches data-controller
attributes and creates scopes from identifier tokens. (More on the role of those classes below.)
Why do we organize the mutation observer classes this way? It turns out that loosely coupled filter classes are easy to conceive, rearrange, and discard as needs change. Parts of Stimulus were originally designed around a SelectorObserver
class, which is no longer present; an in-development branch adds a new StringMapObserver
class for watching changes to prefixed data attributes.
@stimulus/core
The @stimulus/core
package provides all the classes used to implement the Stimulus runtime environment.
At the top of the hierarchy is the Application
class, constructed with a root DOM element and a Schema
. The application creates and manages a router and a dispatcher. It also has methods for associating controller classes with identifiers, and for starting and stopping observation.
The Router
class creates a ScopeObserver
and acts as its delegate. The role of the router is to connect scopes to modules. A Scope
represents an element-identifier pair; a Module
represents a Stimulus controller class definition and all of its contexts, indexed by scope.
You can think of a Context
as the private backing store of a Controller
. There is a 1:1 mapping between instances of the two classes. Each context has a BindingObserver
, which watches the context’s scope for bindings that match the scope’s identifier. A Binding
represents a context-action pair.
The application’s shared Dispatcher
instance acts as the delegate for every binding observer in the application. The dispatcher installs and uninstalls DOM event listeners for bindings’ actions, and is responsible for invoking those actions in the order they appear in the data-action
attribute.
The purpose of the Scope
class is to query and filter the tree of elements relevant to a controller. As with contexts, there is a 1:1 mapping between controller and scope instances. Scopes know how to filter out subtrees of elements belonging to other scopes with the same identifier. They also provide target set and data map objects to the controller.
The TargetSet
class provides a Set
-like interface to a scope’s target elements. The targets
property on a controller points to its scope’s target set. Target properties declared in a controller’s static targets
array delegate to the target set’s find()
and findAll()
methods. Similarly, the DataMap
class provides a Map
-like interface to a scope’s namespaced data attributes.
I think this should cover most of the important classes in Stimulus as of version 1.1.1. Thanks for digging in, and please let me know if something above isn’t clear, or if you’re curious about anything else!