create-modulo
Version:
Starter projects for Modulo.html - Ready for all uses - Markdown-SSG / SSR / API-backed SPA
334 lines (266 loc) • 10.4 kB
HTML
<script src=../static/Modulo.html></script><script type=md>---
title: Script - Component Parts
---
> **Custom vs Script Component Part** - The general rule of thumb is that
> _Script_ tags are for custom, component-specific behavior, or to fill in gaps
> in stitching together other Component Parts, while writing Custom Component
> Parts is for interfacing with async, APIs, or any time that a Script
> Component Part gets too cumbersome. Modulo's philosophy is to allow
> separation of work between _component development_ (HTML, CSS, templating,
> and high-level behavior), and _Component Part development_ (complex
> JavaScript and APIs).
# Script
_Script_ Component Parts allow you to define custom behavior for components using the
full power of JavaScript. The most common use of _Script_ tags is to add more
sophisticated interactive behavior. Instead of just relying on premade Component Parts,
with a Script tag you can program any custom behavior into your component.
## Typical use
The most common use of a _Script_ Component Part is to specify custom JS code.
See below for a simple example:
```
<Component name="ButtonExample">
<Template>...</Template>
<State>...</State>
<Script>
function initializedCallback() {
console.log("Executes every time an instance of ButtonExample is created");
}
function updateCallback() {
console.log("Executes after every rerender of the component");
}
/Script>
</Component>
```
## Event callbacks
The most common purpose of a Script Component Part is to add custom behavior
when certain "events" occur to your component. Consider the following example
of 3 click events:
```modulo
edit:demo=modulo
<Template>
<button on.click=script.doConsole>Console Log</button>
<button on.click=script.doAlert>Show Alert</button>
<button on.click=script.countUp>Counter {{ state.num }}</button>
</Template>
<State
num:=42
></State>
<Script>
function doConsole() {
console.log("Event callback. State.num is:", state.num);
}
function doAlert() {
alert("Event callback. State.num is: " + state.num);
}
function countUp() {
state.num++;
}
<-Script>
```
In this, the _Script_ Component Part defines a function named `countUp`. The `on.click=`
attribute on the button utilizes directives to attach a "click event" to the
button, such that when a user clicks on that button it will invoke the
`countUp` function.
From within event callbacks, the _Script_ Component Part exposes the current renderObj as variables. So, `state` by itself is equivalent to `renderObj.state`. This enables us to directly modify the state by simply doing `state.count++`. By default, components rerender after every event, so after the event the component will rerender and visually reflect the state changes.
This means that all renderObj variables will be available here, in a similar way to how _Template_ exposes them: For example, you can use code like `props.XYZ` to access data from a _Props_ Component Part.
You can also access the JavaScript Object instances of the Component Part Class. To access those, you use the `cparts`
Finally, the variable `element` will refer to the HTML element of the current component. This is useful for direct DOM manipulation or interfacing with other frameworks or "vanilla" JavaScript. Generally, however, you should avoid direct DOM manipulation whenever you can, instead using a _Template_ Component Part to render the component (otherwise, the _Template_ will override whatever direct manipulation you do!).
## DOM references
### script.ref
Using `script.ref`, we can get easy access to DOM elements. See below for
examples:
##### Example 1: Using script.ref to focus input
```modulo
edit:demo=modulo
<Template>
<button on.click=script.focusOnInput>Set focus to input</button>
<input script.ref>
</Template>
<Script>
function focusOnInput() {
ref.input.focus()
console.log(ref.input)
}
<-Script>
```
##### Example 2: Script.ref naming and drilling down
Using a suffix we can provide a custom name. For example, the directive
`script.ref.firstname` becomes `ref.firstname`.
By specifying a value, we can "drill down" to properties of the element. For
example, to access the built in `style` attribute, you can do
`script.ref=style`.
```modulo
edit:demo=modulo
<Template>
<button on.click=script.scrollMe>Scroll to bottom</button>
<div script.ref=style>Styled directly...</div>
<h1 script.ref.target>BOTTOM!</h1>
</Template>
<Script>
function scrollMe() {
ref.target.scrollIntoView()
}
function updateCallback() {
if (ref.div) {
ref.div.backgroundColor = 'pink'
ref.div.padding = '10px'
ref.div.height = '400px'
}
}
<-Script>
```
# Lifecycle callbacks
By naming functions with certain names, you can "hook into" the component
rendering lifecycle with callbacks.
You can define a function with any of the following names and expect it to be
invoked during it's namesake lifecycle phase: `initializedCallback`,
`mountCallback`, `prepareCallback`, `renderCallback`, `domCallback`,
`reconcileCallback`, and finally `updateCallback`.
See below for an example of defining a custom `prepareCallback` in order to
"hook into" the component rendering lifecycle to execute custom code. The
return value of this function is available to the _Template_ Component Part when it
renders during the `render` lifecycle.
##### Example 1: prepareCallback
```modulo
edit:demo=modulo
<Template>
{% for item in script.data %}<p>{{ item }}</p>{% endfor %}
<p>(C) {{ script.year }} All Rights Reserved</p>
</Template>
<Script>
function prepareCallback() {
return {
data: ["a", "b", "c"],
year: (new Date()).getFullYear(),
};
}
<-Script>
```
##### Example 2: All callbacks
```modulo
edit:demo=modulo
<Template>
<label>
<input state.bind name="enabled" type="checkbox" />
Show messages in console
</label>
</Template>
<State
enabled:=false
></State>
<Script>
function prepareCallback(renderObj) {
_logInfo("prepare", renderObj, { });
}
function renderCallback(renderObj) {
_logInfo("render", renderObj);
}
function domCallback(renderObj) {
_logInfo("dom", renderObj);
}
function reconcileCallback(renderObj) {
_logInfo("reconcile", renderObj);
}
function updateCallback(renderObj) {
_logInfo("update", renderObj, true);
}
let _table
function _logInfo(phase, renderObj, newVal) {
_table = _table || newVal
if (state.enabled) {
for (const key of Object.keys(renderObj)) {
_table[key] = _table[key] || {}
_table[key][phase] = renderObj[key]
}
}
if (newVal === true && Object.keys(_table).length) {
console.group()
console.table(_table)
console.groupEnd()
_table = { }
}
}
<-Script>
```
> **Template literals?** The back-tick syntax is [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals).
## Manual HTML creation with Script
> **Template vs renderCallback**
> Before replacing a _Template_ with a `renderCallback`, first consider using
> prepareCallback or extending the templating language with a custom filter.
> The _Templating_ language is intentionally limited and result in more
> readable code and a cleaner separation of concerns of logic (JavaScript) from
> presentation (HTML templates).
You can use _Script_ as a _Template_ replacement, if necessary. For
fine-grained control over a component's rendered HTML, you can hook into the
`renderCallback`. While not recommended for most usage, it remains an option
to use the `renderCallback` to write JS that constructs the HTML in a custom
fashion, such as using a third party templating system, or simply using
template literals.
For even greater control, you can simply replace the `innerHTML` of the entire
element. This will skip any virtual DOM or reconciler. Thus, we don't have
access to click events or binding, making many features less useful. Also note
that we can't use "onkeyup" but instead use "onchange" -- using the built-in
`element.innerHTML` causes the input to lose focus, otherwise. Nonetheless,
it's still usable, and might be a useful example for fine-grained control.
See below for both examples:
###### Example 1: Script without Template
```modulo
edit:demo=modulo
<Script>/* The classic To-Do App in Modulo (without using Template) */
function renderCallback() {
component.innerHTML = `<ol>${
state.list.map(title => `
<li>${ title }</li>
`).join('')
}<li>
<input state.bind name="text" />
<button on.click=script.addItem>Add</button>
</li>
</ol>`
}
function addItem() {
state.list.push(state.text); // add to list
state.text = ""; // clear input
}
<-Script>
<State
list:='["Milk", "Bread", "Candy"]'
text="Coffee"
></State>
<Style>
li { color: indigo }
</Style>
```
###### Example 2: Script without Virtual DOM
```modulo
edit:demo=modulo_embed
<Component name=App>
<Script> /* To-Do App (without using Template or a DOM reconciler) */
function updateCallback() {
const P3 = 'this.parentNode.parentNode.parentNode.cparts.'; // parent is 3 nodes up
element.innerHTML = `<ol>${
state.list.map(title => `
<li>${ title }</li>
`).join('')
}<li>
<input value="${ state.text }"
onchange="${ P3 }state.propagate('text', this.value, this)">
<button onclick="${ P3 }script.addItemCallback()">Add</button>
</li>
</ol>`;
}
function addItemCallback() {
state.list.push(state.text); // add to list
state.text = ""; // clear input
element.rerender(); // Ensure rerender
}
<-Script>
<State
list:='["Milk", "Bread", "Candy"]'
text="Coffee"
></State>
<Style>
li { color: indigo }
</Style>
</Component>
```