The Modulo Tutorial: Part 3
In Part 3 of this tutorial, we'll dig deeper with two new CParts: State,
which allows individual components to store, modify, and link together data,
and Script, which allows custom JavaScript with complex behaviors.
Prerequisites: HTML, CSS, JavaScript, and Part 1 & 2.
#
State
So far, our components have been static and unchanging. They can't do a
lot. They might be useful for refactoring HTML into more DRY, reusable
components, but not much more than that. They can't validate form data, use
APIs, or form complete applications. In fact, they can't have any sort of
interaction whatsoever, or any dynamic or changing
content.
Examining a memory game's usage of State
The Symbolic Memory Game
Choose your difficulty:
Let's put on our "Holmes-style" detective hats and get out our detective
magnifying glasses, we're about to go sleuthing!
Examine this "Symbolic Memory Game" in the box here. Just like any other
component, this was placed on this HTML page here with the following code:
<eg-Memory></eg-Memory>
However, this demo component is doing much more than what we have covered in
the tutorial so far. It is changing, or mutating based on user
interaction. This is no longer a simple, static component, but a
dynamic component.
We can deduce two things about this little memory game component. Deduction
One: It used a State CPart. We know that it used a State
CPart because it changes or mutates. Without a State CPart, a
component cannot "change state", or mutate or have dynamic content, and instead
will be rendered the same way every time (given certain props).
In other words, if a component needs to have dynamic content or change over
time, then a State CPart is necessary to store this dynamic content.
Now on to Deduction Two: It used a Script CPart. Script CParts
allow for complicated interactions and new behavior to be developed. Memory
game logic is complicated behavior that requires custom JavaScript. We'll get
to Script later on.
Why keep State separate?
Earlier jQuery-style JS frameworks were more concerned with manipulating the
DOM directly. Now, the modern approach is to combine templating and/or DOM
building tools (e.g. JSX, virtual DOM) with "state management" (e.g. Redux,
useState). This is due to perceived flaws with the original approach of direct
DOM manipulation: Pretty soon as an app grows, you get a big tangled mess of
different code reaching in different spots. Frontend frameworks clearly needed
an "MVC Model"-like structure to "keep stuff separate".
Modulo closely follows this modern approach. This "detangles" the spaghetti
mess of DOM manipulation: Instead of one button inserting stuff over here, and
one input reaching in and sending data over there, the "state" creates a single
"choke-point" that keeps data "flowing" in one direction. No matter what you
want changed, you do one thing: Change state & rerender!
State is private
State allows each component instance to store data. Each component instance
has a separate state from every other instance. As a demonstration of that,
examine the behavior of the following "Hello" counting buttons when clicked,
once again taken from the demo page:
(Source code:
<eg-Hello></eg-Hello>
<eg-Hello></eg-Hello>
<eg-Hello></eg-Hello>
)
Once again, we can determine that state is being used, as the text on the
button changes when that button is clicked. Furthermore, this demonstrates how
state is not shared: Each button is a totally separate instance that keeps
track of it's own separate number.
By the end of this third part, we will have examined every detail that goes
into this counting button, so you will be able understand it's code in full.
Initializing the State CPart
Let's "peel back the layers" and examine out how these "stateful" or dynamic
components were written.
In order for a component to be able to "modify state", we
must define a State CPart, conventionally placed after the Template
but before the Script or Style tags in a component definition (such that the
order is Props, Template, State, Script, Style). State CParts
are defined much like Props, except that instead of just listing the
attribute names, initial values must be provided as defaults. A State
CPart might look like this:
<State
count="1"
color="blue"
></State>
Here we are defining two state variables: count
, which
we initialize to equal "1"
, and color
, which we
initialize to equal blue
. We can then use the state variables in
our Template, in a similar way to how we did with Props:
<Template>
<p style="color: {{ state.color }}">
You have {{ state.count }} bananas.
</p>
</Template>
Try it now
- Practice modifying the State CPart
(
<State>
) and re-running to see how that affects the
output. Note that the State CPart is traditionally placed
after the Template.
- Practice incorporating these CParts into your own components on a
real page by copying the code here and pasting it within your component
definition (that is, the one that you created in the Part 1 of
this tutorial)
Nonsense poem:
Professor Toot who
tooted a kazoo,
taught tooting in
the City of Kazoo,
to two kazoos.
#
2. Directives
We'll get to more practice with the State CPart in a moment, but
first we need to take a little detour and learn about a few important built-in
Modulo "directives".
A directive is a type of HTML attribute. You can recognize
directive by spotting certain special characters in the attribute
name. For example, <input [state.bind] />
is an
input
HTML tag with a [state.bind]
directive. Some
directives will have a square-bracket syntax (e.g. [ ]
), while
others might use other special characters to set them apart from "normal"
attributes (e.g. @
or :
). While re-rendering,
Modulo scans the resulting DOM to set-up or "mount" any directives it
encounters.
Directives are useful for a variety of tasks, ranging connecting CParts to
each other, to more complicated modifications to DOM elements. We'll learn two
directives next: data prop and state.bind.
Data prop directive (:=)
Why don't we use "strings" for numbers? Using
strings of digits (i.e. in quotes) instead of the numbers themselves means that
things like arithmetic won't work as intended. Example: If state variable
count="1"
, then state.count + 10
will result in "110"
instead of the desired 11
, since
it's a string of digits, so "1" + "10" = "110"
Typically, when we add attributes to anything, whether it is a CPart or even
just in regular HTML, the attribute value can only be a string. This means when
we did <State count="1" ... >
previously, we made a mistake:
The count
variable didn't get assigned to 1
the
Number, but rather "1"
the String. To fix it, we
do the following:
<State
count:=1
color="blue"
></State>
This is called a data prop directive. You can identify a data
prop directive (:=
), by spotting an attribute name that is
suffixed with a colon right before the equal sign, like this:
attributeName:=value
.
The term directives in Modulo refers to special attributes that you
add to HTML to add extra functionality. We'll explore more directives in this
section, but to learn more on directives in general, including how to
author your own directives, see the section on
Lifecycle & Directives. However, most component developers will
have no need to use directives outside of the built-in directives that come
with Modulo, one of which we'll explore next: [state.bind]
Binding state data with [state.bind] directive
State and predictability The purpose of
State is to separate out everything that changes about a component
into it's own isolated data structure. It should be the case that for a
well-written component, if anything changes visually, that "visual" change
should always start with changing state. There should never be a "mismatch", or
a way for visual changes to occur without state changes. If such a thing were
possible, it would imply a component that is non-deterministic, or renders
unpredictably. In other words, given a particular state (and props), a
component should be predictable or deterministic in that it renders the same
way every time.
State comes with a directive that helps "bind" it to form data. What does
this mean? You can attach a [state.bind]
directive to any
<input>
, and the State CPart will "sync up" the
input with the state after every keystroke. The binding is "two-way", or it
goes in both directions: The input gets the initial state value, and if the
state ever changes, the input will be updated to reflect that, and if the input
ever changes, the state gets updated.
It's best practice to bind all of your form inputs that are in components to
state variables. This is because in order to get the benefits of separating out
state, all visual changes, including something as simple as typing a
single character in an input, should be reflected in state changes.
To bind an input to state, use something like the following:
<Template>
<input [state.bind] name="subject" />
</Template>
<State
subject="Testing message..."
></State>
It's important that you always include a "name" attribute when binding. This
should contain the name of the State variable to be kept in sync with
that input.
Try it now
- Try modifying the "Username" input box in the preview below. Do you
see how it "quickly reacts" or re-renders the username text in
lower-case as you type?
- Try also adjusting the "Opacity" input to see how it updates the
transparency of the text, and the "Color" input, which only supports
"blue" or "green" (anything else turns red).
- Examine the code in the Template CPart. Examine each
input, and how it uses the
[state.bind]
directive to keep
it in-sync with state. The name="username"
,
name="color"
, and name="opacity"
attributes
are what the State CPart uses to figure out which state variable should
be "linked".
- Now, practice the link in "the other way": That is, see how
State populates the inputs' values. You can do this by
changing the initial values of State and then re-running the
program. See how by changing the initial values of State it
will also update the initial values of the linked inputs?
Clarification: The other attributes, (e.g. name
, along with
type
, max
, min
, and
step
), are not Modulo directives, but are instead
plain HTML attributes. The State CPart reuses name
, but will
ignore the others.
Further practice: Practice incorporating these CParts
into your own components on a real page by copying the code here and
pasting it within your component definition (that is, the one that you
created in the Part 1 of this tutorial)
Final notes on directives
- If you are still scratching your head over the use of
[state.bind]
but are familiar with vanilla JS, it's
all about reducing the need to "manually reach" into the DOM. It
simplifies code like this var inputData =
document.getElementsByName('myinput')[0].value
(or the similar
$('[name=myinput]').val()
in jQuery), with more readable code
like state.myinput
, and similarly untangles code for
validation, API requests, etc.
- Where do these "directives" come from? All directives
are "provided" by a CPart. That is, including CParts in your component
definition may "enable" more directives in your HTML. Data-prop is a built-in
feature of Components, which means it's always available (technically,
the
:=
syntax is in fact syntactic sugar for it's full name,
[component.dataProp]
).
- Keep in mind that data props directives are not the same as
the Props CPart. They are, however, related, in that the
Props CPart looks for both regular attributes and data props: You
can in fact set any Props attributes with the
:=
syntax, for data other than Strings.
- Data props support any JSON literal type. Technically,
data props values can have double quotes just like normal
String-based attributes. However, as a stylistic convention, you may omit
the double quotes for any one-word value, and should use only single quotes
for complex types, such as JSON-formatted Arrays or Objects. See below for
stylistic examples of data props with different types:
#
3. Script
While Modulo is designed to be useful even without JavaScript, sometimes you
just need access to that extra power of custom JavaScript code. Using
JavaScript, Modulo can be even used to develop more complicated interactive web
applications.
The Script CPart
To add JavaScript, use the Script CPart. This CPart will execute
the JS code contained within once, immediately upon loading the component. See
this example:
<Script>
console.log("Hello JavaScript world!");
</Script>
In this above example, the Script CPart will execute that JS code
once, as soon as it's loaded, causing the "console.log" to log that message to
the web browser's Developer Tools JavaScript console exactly once.
Embedded components and script tags
Generally speaking, it's always desirable to put components in a separate
file, as was demonstrated in Part 1 with the -src=
style
attribute. This becomes even more necessary if you want to use a
Script CPart. This is because of a limitation with HTML: It does not
support "nested" script tags. This means that the </Script>
tag will end up closing off the outer script
tag early and
"interrupting" your component definition.
However, there is an alternative syntax to still allow a Script
CPart embedded in your <script Modulo ...
definition,
even without splitting it off. This messier syntax is far from ideal, but can
do in a pinch:
<cpart Script>
console.log("Hello JavaScript world!");
</cpart>
Note: Only use this alternative cpart Script
syntax within
a <script Modulo ...
tag. There is no reason to use
it when writing code in a separate file!
Event directives Let's break down that event
directive: @click:=script.sayHello
. First, note the at-sign:
@
. This is "syntactic sugar" for the
[component.event]
directive. This will attach a "click" event
listener to the given element when that element is first mounted (i.e.
displayed on the screen), and remove the listener if it leaves. In this case,
we are using a :=
style "data prop" style assignment, to assign
the click event to point to the sayHello function of the Script CPart.
All functions defined in a Script CPart will automatically be
"exported" and available to click events, or in dataprops in general.
Attaching click events
Typically, it's more useful to execute code when a user performs an action.
To do this, we must place the "console.log" into a function:
<Script>
function sayHello() {
console.log("Hello JavaScript world!");
}
</Script>
Then, attach a "click" event directive to a HTML tag, such as, for example,
a button element:
<button @click:=script.sayHello>Click me</button>
Now, whenever a user clicks on the button, it will run the "sayHello"
function, logging the text into the JavaScript console.
Try it now
- Bring up the console: Press
Control+Shift+J
(Linux,
Windows) or Command+Option+J
(macOS) on your keyboard to
open the Console. Alternatively, you can right-click with your mouse
and select "Inspect", and then go to the Console tab.
- Do you see the "COMPONENT GOT LOADED!" text displayed in the
console? It is displayed once every time the component is loaded. By
clicking "RUN", you can "reload" the component, causing that message to
show (or count up) again.
- Try now clicking on the button in the preview on the right. Do you
see how every time you click it shows (or counts) the text of the
console.log in the console?
- Extra: Any number of functions can be defined in a
Script CPart. Practice writing your own function that console
logs a different message, and then attaching it to a new button (or the
existing button).
- Extra: The "event" directive supports any event. Try
changing "@click" to "@mouseover", and then move your mouse over the
button (without clicking). (For the curious:
MDN has a list of all events)
Interacting with state
The Script CPart is like the Template CPart in one way:
You get variables referencing the other CParts. Within functions defined in
the Script CPart, variables will be available representing the other
CParts that have been defined in the Component. As with the
Template CPart, the most useful variables are state
, with
the current data in State CPart (the "data" Object), and
props
, with the value of the attributes that were passed to this
component.
Remember our "Holmes-style" detective work we did in the beginning? We
looked at a button which incremented a value when clicked. The JavaScript code
to increment a variable is num++
, and for state data it can be:
state.num++
. Also, by default, components will rerender after
every event that you are listening to. With that in mind, examine the code of
the Hello button:
By clicking on the button, it will increment the state value. Since the
component will rerender after the click, it will then change the DOM to show
the new number.
With the power of JavaScript, you can do all manner of things with the
Script tag. The Example page has all sorts of examples of
more complicated apps and applications. The typical use of a Script tag, thus,
is to create custom logic that manipulates or "puppets" the state, which in
turn is what controls the rendering of the component's HTML code by the
Template CPart.
Keep in mind that the Script CPart is intended to be limited. Serious
JavaScript development should be split into separate JS files, or defined as
custom CParts. Thus, think of the Script CPart as more "filling in the gaps"
between CParts, which should do most of the heavy lifting (e.g. asynchronous
code, complicated API calls or data transformation, etc).
Key terms
- State - A CPart used to include changing data, which is
used to render the HTML of the component
- Directive - A special type of HTML attribute that "hooks
in" functionality to otherwise plain HTML elements. Three built-in directives
include:
[state.bind]
- two-way binds inputs to state variables, so
modifying the input modifies the variable, and vice-versa
:=
- "data-prop" assignment, allowing for assigning to
JavaScript values such as functions and/or primitive JavaScript data types in JSON syntax
@click
- attach event listener to element
-
Script - CPart that enables embedding of arbitrary JavaScript
code, and allowing for easy access to CPart interface, and exposing JS
functions to be attached as events
Next step
That's all for the Modulo tutorial! Modulo is still in early development, so
it's likely the tutorial will expand in the future to cover more aspects of
Modulo development.