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
[component.dataProp]
(shortcut: suffix :
) -
Attach data to a DOM element's `.dataProp` object, which can be used to
directly pass renderObj
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 the
renderObj
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).
[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 the name=
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 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
el
will contain a reference to the HTML element
value
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 included
rawName
will typically be the same as name
.
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.
Example H1
Mount count: 0
Unmount count: 0
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.