Directives

Directives are one of the core features of Modulo. It allows for callbacks to be triggered when a particular DOM element is "mounted" or first appears in the DOM. It allows for your custom code to access references to DOM nodes created after rendering.

Introduction to directives

Every directive has a name. Every directive is specified as an attribute on the DOM element that you wish to gain access to, by enclosing the name in square brackets. For example, the State CPart has the directive named bind, making the full attribute be [state.bind]. Some directives have shortcut names. For example, [component.event] can be shortened to only a single at-sign, @.

Directives also have callback functions. Just like lifecycle callbacks, a directive callback name is created by suffixing a string to the end of the name of the directive. Unlike lifecycle callback functions, directive callback functions end with Mount and Unmount.

Built-in directives vs custom directives

Modulo ships with a total of 3 built-in directives, defined by the built-in CParts (1 from State and 2 from Component). Typically, the built-in directives are all you need. However, just like with lifecycle functions, the Script CPart also exposes the directive interface to component developers. This is so that you can create custom directives in a component to access the DOM after rendering.

Built-in directives

Important directive facts: Directives are discovered during the reconcile lifecycle phase when DOM reconciliation is occurring, and invoked during the update phase. Note that they are independent of the Template CPart: You can have a component that has no Template but still may employ directives, e.g. if it generates HTML contents some other way.

Custom directives

Custom directives are like "refs" - Directives have the same uses as Refs in React: “Managing focus, text selection, or media playback, triggering imperative animations, or integrating with third-party DOM libraries.”

Custom directives are used for direct access to the DOM. They are invoked when a particular element is first rendered on the screen (Mount), invoked when that attribute has any changes (e.g. the value gets changed, also Mount), and also invoked when about to be removed from the document (Unmount). This allows you to do custom set-up or tear-down code for particular elements, such as to attach third-party JavaScript frameworks in a convenient manner.

Registering Mount callback

Custom directives can be easily registered in Script CParts simply by defining a function with a certain name. For example, for a directive called myinput directive, you can create a function called function myinputMount(opts) {. Similarly, If you want to register custom code when an element leaves the screen, such as to clean up references, you can register an Unmount callback: function myinputUnmount(opts) {.

A full, working example is below, which uses a custom directive to focus on an input when a button is clicked:

Example custom directives:

Multiple directives

Multiple directives can be attached to the same attribute. For example, @click: has both a component.event directive (@) and a component.dataProp (:) directive. Similarly, multiple custom directives can be applied to the same attribute. For example: [script.hook][script.setup]text="Hello" would be a valid way to register two directives(the imaginary script.hook and script.setup directives, in that order).

Mount and Unmount parameters

Frequently used

Infrequently used

Mount and Unmount and multiple directive demonstration

The following demonstration shows both Mount and Unmount callbacks in a script tag. Note that in this demonstration, the "Mount count" and "Unmount count" values visually displayed in the <p> tags appear to "lag" behind the actual value — the number of times a Mount or Unmount callback was actually called. This is because the Mount and Unmount happen after the rendering is complete, and thus the rendering can only "report" on the previous value.

Uncommenting the "console.log" statements will reveal what arguments and information are sent along with the callback.

Custom directive pitfalls

A quick word of caution on custom directives: If you find yourself using custom directives to do vanilla JS DOM manipulation often, you are probably doing something wrong! They are meant as an "emergency escape hatch" to gain access to the DOM underneath, and typically you only use them to integrate with other libraries. The most common usage is mixing in older jQuery-style libraries that require a reference to a DOM element. In general, it's when you run into the limits of what Modulo is capable of doing.

Directives vs. Templates for manipulating DOM

Not sure which to use to manipulate the DOM and generate HTML? Short answer: Templates! Long answer: Directives deal with direct DOM references, and thus are almost always messier to use. Modulo's templating system is designed to generate a string of HTML code as a purposeful limitation during the render phase, and thus prevents this messiness with a stricter structure.

So, when you can, try to make the DOM the "source of truth", and attach data to DOM elements via regular HTML properties or :=-style dataProp properties. In other words, avoid using direct DOM manipulation as your first approach, instead only using this "escape hatch" to vanilla JS as a last resort.

Directives and template variables

Don't get confused when attempting to mix Template CPart variables with directives. Directives cannot access template variables, since directives are only are applied after the template is fully rendered and all template variables are already forgotten. As an example, consider the following code:

Why "payload"? Why does Modulo take this approach, vs something like React, that allows direct attachment of anonymous JavaScript functions? As any React developer knows by now, there are a lot of "footguns" (common mistakes) with attaching events like this, specifically because complexities with "this" context, anonymous functions, and bound functions with arguments can make introspection (e.g. interactive debugging) hard.

The Modulo approach is always "DOM determines behavior". Just by using "Inspect" in your browser's Developer Tools, you can examine or even modify the "payload" attribute while debugging event behavior. In other words, Modulo tends to treat the DOM as the "source of truth", and thus derives it's behavior from properties on DOM elements.

The first attempt (HTML Attributes) uses a directive ([component.dataProp], in this case using the colon : shortcut), in an attempt to "directly" attach the URL to href. The second attempt correctly uses the template variable with double curly braces to embed it as an actual HTML property. The first attempt will fail because it fails to take into account that the directives will happen after rendering the template. That is, the resulting HTML will literally resemble something like this: “<a href:=athlete.url>Steph Curry </a> <a href:=athlete.url>Megan Rapinoe</a> <a href:=athlete.url>Devante Adams</a>”.

In other words, the "athlete" variable is a temporary Template variable in the for loop (that is to say, not a variable in the renderObj), and will only be used at the templating step, and forgotten immediately after. Since directives are only invoked once templating is fully completed, there is no way to resolve the variable from the for loop.

What is the proper use of the [component.dataProp] directive, you might ask? The proper use is for direct assignment or attachment of values that are not strings or numbers at the top level of the renderObj. Basically, anything that can't be conveniently serialized into a string attribute. The common usage is passing down complex nested data types as Props (i.e. without having to clutter the DOM and waste memory with a massive JSON object serialized as an attribute), or for attaching callbacks from the renderObj.

We see such correct usage in the second (Events) "correct" example: It references script.selectAthlete which is at the global level. We can tell that it's at the global renderObj level since it starts with script., referencing the contribution to the renderObj that was provided by the Script CPart. The issue with this, however, is that we won't know which button was clicked, since it references one universal selectAthlete function. This is solved by attaching some sort of ID reference to the DOM element as a "payload" so that the callback function knows which athlete was selected. This is the correct approach: It uses the DOM as a "source of truth" and is predictable in behavior, with no "hidden" functions getting attached.