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.
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
[component.dataProp]
(shortcut: suffix:
) - Attach data to a DOM element's.dataProp
object, which can be used to directly passrenderObj
values as Props or events- Symbols:
ev:=script.gotClicked
,data:=state.dataArray
,blogs:=props.blogArray
etc - Data-props can attach any values that are exposed in therenderObj
by any CPart in your component. Use JavaScript reference notation, e.g.state.objdata.prop1
would access something like<State objdata:='{"prop1": "my data"}'>
- Primitives:
arr:=[]
,obj:={}
boo:=true
,num:=3
, etc - The data-prop can also be used to attach any data in JSON format (whatever "JSON.parse" can parse).
- Symbols:
[component.event]
(shortcut: prefix@
) - Attach event listeners to DOM elements, and remove them when the DOM elements are removed. (For jQuery users, this is used for similar purposes as "live" (delegated) events, but is faster.)payload=...
- By assigning to the "payload" property, either with a regular HTML attribute or a:=
data-prop, you can provide extra arguments to events.click.payload=...
,mouseover.payload=...
, etc - If you have multiple events attached, you can attach separate payloads by prefixing the "payload" attribute with the name of the event followed by a dot.
[state.bind]
- Two-way binding with State data, with the key determined by thename=
property of whatever it is attached to.
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 like such:
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:
Example 1: Mount variables
For a simple example that shows the variables avaialble:
Example 2: Storing Ref
A full, working example is below, which uses a custom directive to focus on an input when a button is clicked:
Example custom directives:
Notes on custom directives
- Note that the functions must be named exactly like this, including capitalization. The attributes, however, are technically not case-sensitive, but the JavaScript portion is.
- DOM manipulation is what Templates, and many other features of Modulo, are supposed to "save you from". In other words, it gets messy. This is why the it is recommended to try to use the "Modulo" way of State, Templating, and re-rendering, to keep things from getting messy.
- Only do direct DOM manipulation as a last resort, such as when you are following a tutorial that gives instructions this way, or are combining Modulo with another framework that operates with direct DOM references
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
el
will contain a reference to the HTML elementvalue
will contain the value you are assigning to the attribute, with any previous directives resolved (e.g. if you combine your directive with a:=
dataProp directive, you will already have the "true value" be passed in.)attrName
will contain the bare attribute name, e.g. the portion of the attribute name (on the left side of the assignment) without the directive syntax
Infrequently used
name
contains contain the entirety of the attribute name, directive syntax includedrawName
will typically be the same asname
. However, if shortcuts were applied,rawName
will show the previous, "unexpanded" verison of the name (e.g. without the regular expression substitution applied).directiveName
will contain the name of the current directive being applied — typically not useful since we already know that, it's the function's name!
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.
Directive pitfalls
A quick word of caution on custom directives: If you find yourself using custom directives in Script CParts 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 while writing a CPart library, or when you are doing something unusual such as a situational or unique performacne enhancement in mind that requires direct DOM manipulation.
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.
Rewriting direct DOM manipulation
In this section, the goal is to show how direct DOM manipulation might be more cumbersome than using Template and other built-in Modulo features. If you are familiar with vanilla JavaScript, observer how these two approaches differ in the next section.
Using direct DOM manipulation
Examine this odd little example of directly attaching click events using the
onclick
property, without using the Modulo helper "@click", and then directly
using the "textContent" property to modify content.
Rewritten
Here is the previous code rewritten to be a normal click event. Using Modulo's built-in functionality the code becomes cleaner.