UNPKG

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
<!DOCTYPE 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> ```