<script>

A single place to write component logic. Here, define things either for use inside the component template or to be exposed on the custom element.

The <script> section is executed when a custom element is constructed. In other words, it runs once for every element instance.

Syntax

<script>…</script>

Attributes

The <script> tag, when used inside of a component definition file, does not take any attributes. If any are defined, they are ignored; even if they are normally valid on script tags. For example, type="text/plain", or defer, do nothing, and the <script> is treated as if it does not have any attributes.

In the script

Inside the component logic, a handful of variables are exposed;

The availability of all the above makes it easy to write component logic code with minimal boilerplate.

Examples

Component logic

Let's create a simple custom-greeting component that displays a greeting given by the greeting attribute and a name from an input element inside the component. In addition, we'll define a .setName() method that overwrites the current input value with the given value.

<title>custom-greeting</title> <meta attribute="greeting" type="string" default="Hello"> <meta method="setName"> <template mode="closed"> <label> Name: <input type="text"> </label> <div id="message"> {{ $.greeting }}, {{ $.name }}! </div> </template> <script> const input = query('input'); $.name = 'Max'; connected(() => { input.value = $.name; live.link($.$name, input); }); $.setName = (name) => { $.name = name; }; </script>

Before we step through the different parts of the <script>, note that we define the greeting attribute through <meta attribute=…> and the .setName() method using <meta method=…>. Since we provided a type (it's a string), the attribute is reflected by the .greeting property and it's bound to the state variable under $.greeting. The method is looked up under $.setName(), so we'll define that in the <script> section as well.

The <script> section runs when our custom element is constructed, meaning this happens once per created element. In this case, we first get a reference to the <input> element from our template using the query() helper. Then, we set an initial value for the live variable $.$name. Inside the script, we have access to lifecycle callbacks, and in this case we go for connected(). In theory, we don't need this lifecycle callback, but if we omit it then the component could be doing unnecessary work while disconnected. In general, using connected() for things that involve the template is a bit more efficient, so doing it here is moreso about building a habit than about performance benefits (which are minimal in this case). Inside the lifecycle callback, we set the input's value to whatever the live variable $.$name contains (this is our source of truth) and then we link the $.$name variable to the input using live.link(). This causes the input to always match $.$name and vice versa. Note that since we set it up inside the connected() callback, this live link will be taken down whenever the component disconnects.

Lastly, we need to define the .setName() method. We already declared it in the <meta method=…> tag, so we must define it in our <script>. In this case, the logic is simple; we set $.name to the function's argument. Since $.$name is a live variable linked to the input, this'll automatically update that as well.

Imports

Sometimes, a third-party or just seperate script needs to be imported into component logic. Both static import statements and top-level await are not permitted in <script> sections, since the component logic represents a constructor, which should be synchronous. This is because elements should be available and usable right away when they're created. In other words, the <script> in component definitions is not a type="module" script (and adding that attribute does not turn it into one). However, dynamic import() statements work as usual and are the recommended way to load a module into a component.

Warning: A pitfall of using import() statements is when using relative URLs. Since Yozo parses and compiles component definitions client-side, it cannot respect the original URL that your script is located at with regards to relative URLs. In other words, relative URLs are resolved against wherever Yozo's script lives. Similarly, import.meta.url always resolves to Yozo's script's location.

So, how does importing work in module scripts? The recommended way is to use a dynamic import() to load the external module or script and assign it to a property on the state variable $ once ready. Then, write logic for both the case where that key is not yet defined and for when it is defined. For example:

<title>db-status</title> <template mode="closed"> <div #if="!$.database">loading…</div> <div #else> {{ $.database.getStatus() }} </div> </template> <script> import('/scripts/database.js').then(exports => { $.database = exports; }); </script>

An alternative method is to set up an effect() or live.link() handling both the loading case and the after-loading case:

<title>db-status</title> <template mode="closed"> <div>{{ $.status }}</div> </template> <script> import('/scripts/database.js').then(exports => { $.database = exports; }); live.link($.$status, () => { if(!$.database){ return 'loading…'; } return $.database.getStatus(); }); </script>

The dynamic import is called once for every custom element instance, but this is okay; after the script has loaded once, subsequent import() statements resolve immediately, as the browser has cached the modules.

Usage notes

While it is possible to use top-level return statements inside the <script> section, this must not be done as it may break in future versions of Yozo.

Inside the <script>, all top-level goodies Yozo provides are available by default without the yozo. namespace, with an exception for register(). This exception exists mainly because it is somewhat vaguely named without the yozo namespace, and this is also consistent with what happens when assigning all Yozo helpers to the global scope (through Object.assign(window, yozo)), where register is also excluded. Either way, it is not generally advised to register components from the constructor of other components; if a certain component is needed in another, load them both at the same time for better performance. The non-namespaced Yozo helpers available inside the <script> should not (and cannot) be overwritten.

See also