lazui

Introduction

lazui is a JavaScript library that allows you to create interactive websites and single page apps with less work.

It extends the attribute space of typical HTML to provide a rich set of functionality.

It provides the JavaScript, so you don't have to.

lazui draws its inspiration from the varied capabilities of htmx, lighterHTML, Turbo, Stimulus, Vue, and Lit. It also provides a number of features not found in these libraries.

The first major release of lazui is feature complete; however it needs more testing, optimization, improved documentation, and some functionality to round out the automatic form generation. In this case, don't be lazy ... join us and help out by creating issues on GitHub.

Documentation Conventions

It is possible to completely modify the lazui namespace. Throughout the documentation, references to attribute directives will take the form lz:<directive> and sample code will use the standards compliant form data-lz:<directive>, but they could just as well be data-myapp:<directive> or even myapp:<directive>.

Attribute Directive
A custom attribute that is used to make relatively minor modifications to the behavior of an element. For instance, lz:src is an attribute directive that loads content into an element. Elements can have multiple attribute directives.

In some cases, the documentation will use TypeScript notation to make APIs clear, however, lazui is not written in TypeScript.

Most of the JSON in this document is in JSON5 format. This makes JSON easier to write and less error prone. See Relaxed JSON Parser for more information. Unless you configure your version of lazui to use JSON5, you will need to modify the JSON in the examples to be valid JSON.

Any time you see a CDN URL for lazui you could use just /lazui if you are running the basic lazui server.

If there are known issues, the document will describe the issue and provide a link to the details on GitHub.

If you view the documentation from a locally installed lazui server, it has realtime integration with GitHub. Any issues that have been logged and assigned where an anchor id in this document is referenced in the issue description will be highlighted in the document along with a link back to the issue on GitHub. This is accomplished with the lazui component git-issue-ref. See Single Page Components

Installation

CDN

Unless you need your own attribute directives, you can use the CDN version of lazui. Place the following script tag in the head of your HTML:

<script src='https://www.unpkg.com/@anywhichway/lazui' autofocus></script>

lazui has a core size of less that 8K minimized and Brotli compressed and can be used without a build process, but enhanced with one.

Unless you are willing to write your own JavaScript, you should always provide the autofocus attribute. It tells lazui to process all the custom attribute directives and template substitutions in the document.

Local

You can also install lazui from npm:

npm install @anywhichway/lazui

Then run:

npm run serve

Access lazui and this documentation at http://localhost:3000/.

See Basic Server for more information on the lazui server you can customize.

https://lazui.org uses this basic server and is hosted on Render.

How To Be Lazui

You can put template literals directly in your HTML. Here is the first simple example:

<html>
    <head><script src="https://www.unpkg.com/@anywhichway/lazui" autofocus></head>
    <body>
        Hello, the date and time is ${new Date().toLocaleTimeString()}
    </body>
</html>

The above will render as:

Hello, the date and time is ${new Date().toLocaleTimeString()}

Working With Markdown

You can even include template literals in your Markdown.

Hello, the date and time is ${new Date().toLocaleTimeString()}

renders as:

Additional Options

You can also configure lazui to:

<script src="https://www.unpkg.com/@anywhichway/"
      autofocus
      data-lz:usejson="https://esm.sh/json5"
      data-lz:usehighlighter="https://esm.sh/highlight.js"
      data-lz:userouter="https://esm.sh/hono"
      data-lz:options="{
         userouter: {
            importName:'Hono',
            isClass:true,
            allowRemote:true,
            markdownProcessor: {
               src:'https://esm.sh/markdown-it',
               call:'render',
               isClass:true,
               options: {
                  html:true,
                  linkify:true
               }
            }
         },
         usehighlighter:{
            style:'/styles/default.css'
         }
      }">
</script>

A bit overwhelming? Use this shorthand:

<script src="https://www.unpkg.com/@anywhichway/lazui" data-lz:usedefaults></script>

Choose Your Development Paradigm

Although lazui can be used as a powerful JavaScript rendering engine, it really shines at reducing the amount of JavaScript required to create an interactive website or single page app. This shininess is provided through a set of attribute directives and JavaScript controller files.

If you are a fan of Turbo or htmx, you probably want to write less JavaScript. Since writing HTML is easier than JavaScript, the documentation uses the lazui (lazy) approach and covers the use of directives and controllers before the more JavaScript focused html and render functions.

Leveraging Attribute Directives

Attribute directives take the form <namespace>:<directive>=<value>. The default lazui namespace is lz.

Attribute directives are each stored in their own JavaScript file using the name of the directive as the file name. By convention, the files are in a directory called directives with subdirectories for each namespace. Hence, lz:src should be in /directives/lz/src.js.

If this convention is followed, lazui will lazy load only those directives that are used. If you want to preload directives or use a different naming/filesystem convention, you can load the directives directly using JavaScript.

Attribute directives can be provided configuration values with another directive called lz:options. The value of lz:options is a JSON object. The lz:options directive can be placed on any element and will apply to all directives. The configuration data for a specific directive is provided through a key of the same name as the directive. For instance,

<div data-lz:controller="/controllers/lz/chart.js" 
     data-lz:options="{controller:{type:'BarChart'}}" 
     data-lz:usestate="pizza">
</div>

Relaxed JSON Parser

The directive lz:usejson can be used to configure the JSON parser for more flexibility. This can dramatically simplify using embedded JSON by eliminating the need for quotes around keys and allowing the use of single quotes for strings. In short, you can be lazy about your JSON.

You can do this:

{
    name: "John",
    age: 30
}

instead of this:

{
    "name": "John",
    "age": 30
}

The attribute takes as its value a URL pointing to the parser. JSON5 is a good choice.

<div data-lz:usejson="https://esm.sh/json5"></div>

or, if you have a local copy of JSON5:

<div data-lz:usejson="/json5.js"></div>

Using State

Being able to insert a date and time or do inline math may with templates in HTML be useful, but you will probably want to include general data.

The data-lz:state attribute can be used to define a state/model. The value of the attribute is the name of the state/model. The state/model is defined as a JSON object inside an element (typically a <template>) or loaded from a file:

<template data-lz:state="person">
{
    name: "John",
    age: 30
}
</template>

Now you can do this:

${name} is ${age} years old.

You can also set a state as the default state for a document or globally using global (stored on the window object):

<template data-lz:state:document="person">
{
    "name": "John",
    "age": 30
}
</template>

There are more advanced use of state to support storing it in a database or remote server synchronization documented later.

With Markdown

Setting state at the document level can be useful with Markdown. Below is the content of the file using-state-with-markdown.md, followed by the HTML loading the file into an <iframe> (which is optional).

*${name}* is *${age}* years old.
<template data-lz:state:document="person">
{
    name: "Mary",
    age: 21
}
</template>

Due to issues with some Markdown parsers, you MUST put state template at the end.

<div data-lz:src="/using-state-with-markdown.md" 
     data-lz:mode="frame" 
     title="Lazui: Markdown Example"></div>

Reactive State

Modifying a state will cause any elements using the state to be updated.

<div data-lz:state="{clickCount:0}" onclick="this.getState().clickCount++">
    Click Count: ${clickCount}
</div>
Click Count: ${clickCount}

lazui does not use a virtual DOM to manage changes, it uses direct dependency tracking. A special updating function wrapped around the normal browser screen refresh handler tracks state access. When a state changes, the nodes that depend on that state are updated. The nodes are typically text nodes, but can also be attributes. This is automatic when working at the no JavaScript level. You can take a more functional approach and manage reactivity yourself by using the html tagged template literal and render functions if you write JavaScript.

Inline State

You can also define state inline by providing JSON as the value of the data-lz:state attribute. An element id will be generated automatically if the element does not have an id.

<div data-lz:state="{name:'John',age:30}">
${name} is ${age} years old.
</div>
${name} is ${age} years old.

State Inheritance

States are inherited down the DOM and shadow the values of properties with the same name in ancestor element states.

<template data-lz:state="person">
  {
  name: "John",
  age: 30
  }
</template>
<div data-lz:usestate="person">
  ${name} is ${age} years old.
  <div data-lz:state="{age:21}">
    ${name} is ${age} years old.
  </div>
</div>
${name} is ${age} years old.
${name} is ${age} years old.

Loading and Submitting Content

<div data-lz:src="./path/to/somefile.html"></div>

By default, this will load the contents of somefile.html as the innerHTML of the div.

So long as a router has been enabled, if the lz:src value starts with a # it is treated as an element id and the innerHTML of the element is used as the content. Typically, these will be <templates>, but they do not have to be.

<template id="myelement">
    Content stored in a template
</template>
<div data-lz:src="#myelement"></div>

Markdown Hint: Except for your main .md file, you do not have to add the lazui.js script to your markdown files, it will be added automatically used for any content loaded using lz-src.

Targets

Anything with a src, action (forms), or data-lz:src, attribute can have a data-lz:target attribute. The value can be beforeBegin, previousSibling, afterBegin, beforeEnd, nextSibling, afterEnd, inner, outer, firstChild, lastChild, body, parent, _top, _blank or a CSS selectable target.

The targets are case-insensitive. The camelCase is used for legibility.

If data-lz:target is missing on elements other than anchors and forms, it defaults to inner.

The targets inner, outer, parent and body can also have a !<css-selector> suffix. This means you can update multiple elements with a single anchor or form submission.

<div id="somecontent" hidden>Hello, World!</div>
<div id="myparent">
   <span class="someclass"></span>
   <span id="child2" 
      data-lz:src="#somecontent" 
      data-lz:target="parent!.someclass" 
      data-lz:trigger="mouseover 
      dispatch:load">Mouse over me!</span>
   <span class="someclass"></span>
</div>
Mouse over me!

State and Loaded Content

If lz:usestate is used with or in a parent element of one with lz:src, the state will be applied to the loaded content.

Markdown Hint: Super useful! No need to put HTML into your Markdown files:

The source of markdowntemplate.md is:

${name} is ${age} years old.
<div data-lz:src="./markdowntemplate.md" data-lz:usestate="person"></div>

Single Page Components

If the attribute data-lz:mode has the value open, content will load into a shadowDOM without the need for a custom element. Hence, the HTML can include <style> tags that will be isolated from the rest of the page.

Consider a file with these contents:

<html>
    <head>
        <meta http-equiv="my-custom-header" content="my-custom-value">
        <style>
            p {
                color: red;
                font-weight: bold;
            }
        </style>
    </head>
    <body>
        <p>
            element.html contents  
        </p>
        <script>
             (document.currentScript||currentScript).insertAdjacentText("afterEnd","This was inserted by a script");
        </script>
    </body>
</html>

If the file has the same origin as the requesting document, scripts will be processed; otherwise, they will be ignored. In the case above, they are ignored. However, if we mount a similar file locally, they will be executed.

<template data-lz:url:get="./element.html" data-lz:mode="document">
    <style>
        p {
            color: red;
            font-weight: bold;
        }
    </style>
    <p>
        element.html contents  
    </p>
    <script>
        (document.currentScript||currentScript).insertAdjacentText("afterEnd","This was inserted by a script")
    </script>
</template>
<div data-lz:src="./element.html" data-lz:mode="open"></div>

Note: data-lz:mode="closed" is not supported.

If you are prepared to potentially write a lot JavaScript and want custom HTML tags, you can create a custom element with its own tag.

Using Forms

To bind form elements to state, use the lz:bind attribute. The value of the attribute is the name of the property in the state.

If lz:bind has no value, but the name attribute is provided, the value of the name attribute is used as the property in the state.

If lz:bind has a value and the name attribute is missing, the name attribute is added to the element.

The lz:bind directive supports dot notation for nested properties.

The form controller will add reasonable values for the attributes placeholder and title if they are missing.

By default, the state is updated on keyboard or mouse input, it can be set to change for when a value if fully changed, or 'submit' for forms that have a submit button by using lz:options="{controller:{bind:'change'}}".

Note: Processing forms requires the use of a directive not yet covered, lz:controller. See Pre-Built Controllers for more information. Forms are covered here because it is likely the next thing you will want to use after understanding the above.

With No Submit
<template data-lz:state="formexamplestate">
{
   name: "Tom",
   age: 21,
   married: false,
   address: {
      city: "New York",
      state: "NY"
   }
}
</template>
<div data-lz:usestate="formexamplestate">
   <form data-lz:controller="/controllers/lz/form.js" data-lz:options="{controller:{bind:'change'}}">
      <input name="name" data-lz:bind type="text" placeholder="name">
      <input name="age"data-lz:bind="age" type="number" title="user age">
      <input name="city" data-lz:bind="address.city" type="text" placeholder="city">
      <input data-lz:bind="married" type="checkbox"> Married
   </form>
   <div>${name}'s age is ${age}${married ? ", married, " :""} and lives in ${address.city}.</div>
</div>
Married
${name}'s age is ${age}${married ? ", married, " :""} and lives in ${address.city}.
With Standard Submit

Form submissions are intercepted and processed by lazui if the attribute lz:controller="/controllers/lz/form.js" has been applied to the form. When submitted, the event is trapped and fetch is used to get the response for updating the target(s) of the form. A template to format the results can be controlled via lz:options.

Encoding and method are handled by the standard method and enctype attributes.

The standard enctypes are:

lazui supports an additional type for forms to facilitate template completion and database operations on the server.

The example below just returns the body it was sent.

<div data-lz:state="{name:'Dick',age:25}">
   Name: ${name} Age: ${age}
   <form action="/reflectbody" 
      data-lz:controller="/controllers/lz/form.js" 
      data-lz:target="nextSibling" 
      enctype="multipart/form-data"
      data-lz:options="{controller:{bind:'submit'}}">
         <input data-lz:bind="name" type="text" placeholder="name">
         <input data-lz:bind="age" type="number" placeholder="age">
         <button type="submit">Submit</button><br>
   </form>
</div>
Name: ${name} Age: ${age}

With No Inner HTML

If a form has no innerHTML, the state local to the form is used to generate one based on the types of the property values.

Note: The state must be local to the forms. Forms do not support inherited state.

<template data-lz:state="formexample">
{
   name: "Joe",
   age: 20,
   address: {
      city: "Seattle",
      state: "WA"
   }
}
</template>
<form data-lz:controller="/controllers/lz/form.js" data-lz:usestate="formexample">
</form>
<div data-lz:usestate="formexample">
Name: ${name} Age: ${age} City: ${address.city} State: ${address.state}
</div>
Name: ${name} Age: ${age} City: ${address.city} State: ${address.state}

Generated forms both read from and write to their state.

If the form has an action, a sumbit button will be added.

If lz:options="{controller:{useLabels:true}}" is set, labels will be provided.

<form data-lz:controller="/controllers/lz/form.js" data-lz:usestate="formexample" data-lz:options="{controller:{useLabels:true}}">
</form>
With Template Responses

If lz:options provides a template, the response is treated as JSON and the template is used to format the response.

<template id="formresponse">
    <div>Thank you for letting us know ${name}'s age, ${age}.</div>
</template>
<form action="/reflectbody" enctype="application/json" 
   data-lz:state="{name:'Harry',age:22}" 
   data-lz:controller="/controllers/lz/form.js" 
   data-lz:target="nextSibling" 
   data-lz:options="{controller:{format:'json',template:'#formresponse'}}">
   <input data-lz:bind="name" type="text" placeholder="name">
   <input data-lz:bind="age" type="number" placeholder="age">
   <button type="submit">Submit</button>
</form>

If a template is provided, then expect:"json" is assumed for the lz:options controller configuration, other expect types will throw an error.

If no template is provided, then the response is treated as text unless expect:"html" or expect:"template" is provided in the options.

If expect:"html" is provided, the response is parsed as HTML, scripts are not run and only the body is used to place at the data-lz:target or target.

If expect:"template" is provided, then the server is expected to provide a template for formatting. Hence, the returned HTML is treated as a template and the state context of the form augmented by the form contents is used for resolution. Any scripts in the template are executed. Note: Although the form contents are available to the template, the state is not updated unless lz:bind has been used.

Assume the server returns this when a post is made to /form-template-example.

<form action="/form-template-example"  
   data-lz:state="{name:'Harry',age:22}" 
   data-lz:controller="/controllers/lz/form.js" 
   data-lz:target="nextSibling" 
   data-lz:options="{controller:{expect:'template'}}">
   <input data-lz:bind="name" type="text" placeholder="name">
   <input data-lz:bind="age"  type="number" placeholder="age">
   <button type="submit">Submit</button>
</form>

Using Frames

The attribute lz:mode can also take the value frame. This will load the content into an iframe and unlike the open mode, scripts will be executed regardless of origin, so long as a content security policy has not been applied.

<div data-lz:src="https://lazui.org/path/to/element.html" data-lz:mode="frame" title="My Frame"></div>

Note, if you are viewing the JavaScript console you may see a warning like this:

An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing 

When lazui inserts content into an <iframe> it also sets the parent and global scope of the <iframe>'s content to itself to prevent navigation out of the iframe.

Client Side Routing

The directive and value lz:userouter="<urltorouter>" will let you create a router without writing any Javascript. The only router that has been well tested for lazui in the browser is Hono, so use lz:userouter="https://esm.sh/hono" for now.

<template data-lz:userouter="/controllers/lz/router.js">
{
   importName: "Hono", // name of import from module file
   isClass: true, // optional, provide only if creating router requires a call to "new"
   options: {}, // options to pass to router
   allowRemote": true // optional, default is false, use true to allow spoofing of remote content,
   allowRemote:true,
   markdownProcessor: { // optional, only required if you expect to have untranslated Markdown delivered to the browser
      src:'https://esm.sh/markdown-it',
      call:'render',
      isClass:true,
      options: {
         html:true,
         linkify:true
      }
   }
}
</template>

Alternatively, see Specifying A Router, which requires a few lines of JavaScript.

With a lazui router in place, <templates>s with a lz:url attribute can be treated as files. This is useful for creating single page apps, documentation, demonstrations or testing the client with stubbed out responses when a server capability is not available.

The lz:url attribute always has a second component of get, put, post, or delete to indicate the HTTP method for which a response is supported. The value of the attribute is the URL of the file. The path must always be a full URL or an absolute path on the current origin. Relative paths are not supported.

The lz:url attribute should only be associated with a <template>. It will be ignored elsewhere.

When associated with a <template> having a lz:url attribute, lz:mode="document" can be used to tell the router to never forward requests to a server. If the mode is not document, the local copy will be treated like a cache entry and all requests will also be forwarded to a server. Currently, cache control headers will not be respected.

Except for examples currently requiring server interaction, e.g. Server Sent Events and Web Sockets, all the examples in this document depend on files simulated by <template>s with a lz:url attribute and a client side router.

Get
<template data-lz:url:get="./path/to/somefile.html" data-lz:mode="document">
    <style>
        p {
            color: red;
            font-weight: bold;
        }
    </style>
    <p>
        element.html contents  
    </p>
</template>
<div data-lz:src="./path/to/somefile.html" data-lz:mode="open"></div>

You can even simulate headers and status codes by adding data-lz:header, data-lz:headers and data-lz:status attributes to the source elements.

<template data-lz:url:get="/404.html"
   data-lz:status="404"
   data-lz:mode="document">
      Not Found
</template>
<div data-lz:src="/404.html"></div>

You can include head and body sections. Any meta http-equiv content in the head section will be treated as a header and included in the router response headers.

<template data-lz:url="https://lazui.org/path/to/element.html" data-lz:mode="document">
    <head>
        <meta http-equiv="my-custom-header" content="my-custom-value">
        <style>
            p {
                color: red;
                font-weight: bold;
            }
        </style>
    </head>
    <body>
        <p>
            element.html contents  
        </p>
        <script>
            document.currentScript.insertAdjacentText("afterEnd","This was inserted by a script")
        </script>
    </body>
</template>
Put and Post

Elements with lz:url:put and lz:url-post are used to indicate to the router that it is should update the content of the <template> with the corresponding lz:url:get. If it does not exist, the <template> will be created with the attribute lz:url:get="<someurl>". If the put or post <element> has content and the lz:mode is document, then the content will used ad the response body; otherwise, the body of the request is reflected back.

See the next section Enhanced Requests for an example.

Delete

Removes the content from the element with the corresponding lz:url:get URL and sets its lz:status to 404.

Remote State

With a router in place you can access and store remote state. The lz:state attribute supports the use of lz:src and lz:options.

Below, it is used to store the state in localStorage. The state will prefer the data in the storage over that originally specified as part of the innerHTML.

The below example uses some JavaScript, but that is just to show you that the state is being stored in localStorage. You can use localStorage without needing to write JavaScript.

<template id="someuniqueid"
   data-lz:state
   data-lz:src="lz://localStorage/someuniqueid"
   data-lz:options="{state:{put:true,delete:true}}">
{
   name: "Johnathan",
   "^": { }
}
</template>
<div id="localstorage">localStorage: </div>
<div id="state">inDocument: </div>
<script>
document.addEventListener("lz:loaded",() => {
   const state = lazui.getState("someuniqueid");
   state.addEventListener("state:put",() => {
     const el = document.getElementById("localstorage");
     el.insertAdjacentText("beforeEnd",localStorage.getItem("someuniqueid")); // {"name":"John"}
     const el2 = document.getElementById("state");
     el2.insertAdjacentText("beforeEnd",JSON.stringify(lazui.getState("someuniqueid"))); // {"name":"John"}
     state.delete();
   });
   state.name = "John";
})
</script>

localStorage:
inDocument:

The prefix lz:// on the src URL is a special protocol that tells lazui to handle the URL in a unique way. Note: If you chage the lazui namespace, lz:// will change to the new namespace, e.g. mynamespace://.

You can replace lz://localStorage with lz://sessionStorage to use sessionStorage.

You can replace lz://localStorage/someuniqueid with https://somedataserver.com/somepath/someuniqueid to use a remote server.

Advanced Routing

If you wish to use more sophisticated client side routing, you can use the lz:options attribute to specify handlers that will be loaded. The value of the attribute is the path to a JavaScript file. See Advanced Client Side Routing for more information.

Enhanced Requests

lz:src can be used to specify a request object that will be used to load/process content,e.g.

<div data-lz:src='{"url":"/path/to/element.html","method":"POST","body":"name=John","mode":"document"}'></div>

The lz:mode attribute can have the value document in addition to cors and no-cors. This tells the router not to forward a request to a server if the resource does not exist locally in the form of a <template> element with a lz:url attribute.

As a demonstration, this series tries to load a path that does not exist, i.e. the content is empty, then creates the content using POST, then loads it again. The delay on the second GET request is because it may take a moment for asynchronous updates of the page to occur.

<template data-lz:url:post="/path/to/newelement.html"></template>
<div data-lz:src="/path/to/newelement.html"></div>
<div data-lz:src='{"url":"/path/to/newelement.html","method":"POST","body":"name=John","mode":"document"}'></div>
<div data-lz:src="/path/to/newelement.html" data-lz:trigger="load delay:1000"></div>

The GET, DELETE, PUT, PATCH, HEAD methods are respected:

As you can see, the basic lazui router is content, not functionally, focused. If you need a functional focus, then you must work with the router at the JavaScript level.

Handling Events

In some cases, a file should only be loaded when a particular event occurs, e.g. a click. This can be done by adding the data-lz:trigger attribute to the element, e.g.

<div data-lz:src="https://lazui.org/path/to/element.html" data-lz:trigger="click dispatch:load" data-lz:mode="open">Click Me</div>

Try clicking on the below:

Click Me

Note, unlike htmx triggers, lazui triggers do not automatically load content, you must dispatch a load event to get the load to occur.

Events can be separated by commas, e.g. data-lz:trigger="click dispatch:load, mouseover dispatch:load once".

User Responsiveness

The event modifiers debounce:<ms> and throttle:<ms> can be used to control responsiveness to user interaction. The below will effectively ignore clicks at less than 2 second increments.

<div data-lz:src="./thanks.html"
   data-lz:trigger="click debounce:2000 dispatch:load"
   data-lz:target="nextSibling">
      Click Me
</div>
<div></div>
Click Me
Loading Just Once

once can be used to process an event only the first time it occurs. load once is the same as having just a data-lz:src attribute and no data-lz:trigger attribute. However, click once call:window.alert("clicked") will only display the alert once.

Delaying and Repeating Loads

Processing of events and subsequent loading of the content can be delayed by adding delay:<ms> to the event, e.g. data-lz:trigger="click dispatch:load delay:1000".

A repeating load can be established with every, e.g. data-lz:trigger="load every:1000" will start updating the content every second.

<template id="clock" data-lz:url:get="/clock" data-lz:mode="document">
    <p>
        The time is ${new Date().toLocaleTimeString()}
    </p>
</template>
<div data-lz:src="/clock"
   data-lz:trigger="click once delay:2000 dispatch:load placeholder:Loading clock ...,load every:1000">
      Click Me
</div>
Click Me
Alternative Actions

Finally, call:<scope>...<functionName>(...args)? can be used to call a function in the specified scope when the event occurs instead of or in addition to loading. For convenience, if parentheses are not provided, it is assumed the function should be called wih the event, i.e. call:window.console.log is the same as call:window.console.log(event).

The specified scope can be:

Dotted access after the scope is permitted, e.g. window.console.log

An error will occur if the scope does not provide the dotted path or implement the functionName.

Content Control

if

The lz:if directive can be used to conditionally remove content. If the ultimate value of the attribute is not truthy, the element will be removed. If the original value is a boolean or number, it is used as is. If it is a string prefixed by a period, it is used as a property of the closest state. If the original value is of the form #<id>.<property>?, it is resolved using the state with the id. If the property portion is left off, then the state itself is used.

<div data-lz:if=".name" data-lz:usestate="person">Hello, ${name}!</div>
Hello, ${name}!
<div data-lz:if="${age < 21}">Hello, ${name}!</div>
Hello, ${name}!

does not render at all because age >= 21.

<div data-lz:if="${age >= 21}" data-lz:usestate="person">Welcome to the bar, ${name}!</div>
Welcome to the bar, ${name}!

foreach

The lz:foreach directive can be used to repeat content. It takes the form lz:foreach:what:itemAlias:indexAlias:arrayAlias. The what portion can be value, key, or entry. The itemAlias defaults to the value of what. The indexAlias, and arrayAlias default to index, and array.

The value of the attribute should be a JSON object or an #<id>.<property>? which is resolved to a state. If the property portion is left off, then the state itself is used.

The innerHTML will be cloned for each element in the array resulting from a call to Object.values, Object.keys, or Object.entries, i.e. Object[what](<resolvedAttributeValue>), and the array element will be used as the context for the cloned element.

<div data-lz:foreach:value:name:i='["Peter","Paul","Mary"]'><template><p>${i+1}: Hello, ${name}!</p></template></div>
<div data-lz:foreach:entry='["Peter","Paul","Mary"]'><template><p>${parseInt(entry[0])+1}: Hello, ${entry[1]}!</p></template></div>

As with other directives, the value between ${} can also be a literal.

<script>
var peterPaulMary = ["Peter","Paul","Mary"];
</script>
<div data-lz:foreach:value:name:i='${peterPaulMary}'><template><p>${i+1}: Hello, ${name}!</p></template></div>

render

The lz:render directive can be used to render content. It takes the form lz:render:fname. The fname should be a method available on the state or the window object. The innerHTML can optionally contain a <template>. The fname will be called as fname({el,template,state,lazui}) where el is the element and is expected to modify or populate the innerHTML of el. The template and state arguments can be ignored if not needed.

<script>
   var myRenderData = ["Peter","Paul","Mary"];

   var myRender = function({el,template,state,lazui}) {
      const {html} = lazui;
      while(el.lastChild) { el.removeChild(el.lastChild); };
        myRenderData.forEach(function(name,i) {
             const content = html`<p>${i+1}: Hello, ${name}!</p>`.nodes();
             el.append(...content);
        });
   }
</script>
<div data-lz:render="myRender"></div>

show

The lz:show directive can be used to conditionally show content. If works just like lz:if, but instead of removing the content, it sets or removes the hidden attribute.

examplify

If you have applied lz:usedefault, lz:userouter to your lazui script, are using a server based on the lazui basic server, or have custom integrated server using examplify you can use three ` followed by !html to replicate the content of a code block into the source.

This ensures that the example content is always in sync with the execution of the example.

```!html
<form>
   <input
      type="text"
      value="Hello, World!">
</form>
```
<form>
   <input
      type="text" 
      value="Hello, World!">
</form>

Dataset Management

dataset

The lz:dataset directive can be used to set data- attributes. The value of the attribute is a JSON object with the names of the data- attributes as keys, e.g.

<div id="datasetexample" data-lz:dataset='{"mydata":"myvalue"}'></div>

Once resolved, the lz:dataset attribute is removed.

Styling and Accessibility

ARIA

The lz:aria directive can be used to set ARIA attributes. The value of the attribute is a JSON object with the names of the ARIA attributes as keys and the values as values, e.g.

<div data-lz:aria='{"role":"button","aria-label":"Click Me"}'>Click Me</div>

Once resolved, the lz:aria attribute is removed.

Code Highlighting

The lz:usehighlighter directive is typically attached to the <script> that loads lazui. Currently, only highlight.js is supported.

<script src='https://www.unpkg.com/@anywhichway/lazui@0.0.16-a'  autofocus
    data-lz:usehighlighter="https://esm.sh/highlight.js"
   data-lz:options="{usehighlighter:{style:'/styles/default.css'}}">
</script>

Style

The lz:style directive can be used to set style attributes. The value of the attribute is a JSON object with the names of the style attributes as keys and the values as values. The keys can be in either camelCase or dashed format, e.g.

I am red and bold

Once resolved, the lz:style attribute is removed.

Pre-Built Controllers

Except for State and Forms, the above sections have covered the use of pre-built attribute directives. If you need more attribute directives, see Creating Custom Attribute Directives. This section covers the use of pre-built controllers.

Controller
A JavaScript file that is loaded to provide additional sophisticated functionality to an element. For instance, chart.js can be loaded by the attribute lz:controller to support rendering of charts and graphs with configuration data provided by the element. Elements can only have one controller.

There are a number of pre-built controllers; however, you can also create your own controllers. You can use custom controllers, even with the CDN version of lazui.

Controllers are always loaded using the attribute directive lz:controller=<location>. The location can be relative to the current document or an absolute URL including starting with a /, http: or https:.

Like attribute directives, controllers can accept configuration data through the attribute lz:options. Since there can only be one controller per element, the key in the options object is controller not the name of the controller. This key should contain an object with the configuration data, which will be different for each controller.

Charts

Currently supported chart types are those in the Google library, just some of which are:

You can see examples in the Google chart gallery. The core library is automatically loaded. You can add special packages with the lz:options attribute, e.g. lz:options="{controller:{packages:['wordtree']}}".

The chart definitions always use a type, options, and data property in the state defining the chart.

<template data-lz:state="pizza">
{
    type: 'PieChart',
    options:{
       title:'How Much Pizza I Ate Last Night',
       width:400,
       height:300
    },
    data: [
        ["Topping","Slices"],
        ["Mushrooms",3],
        ["Onions",1],
        ["Olives",1],
        ["Zucchini",1],
        ["Pepperoni",2]
    ]
}
</template>
<div data-lz:controller="/controllers/lz/chart.js" data-lz:usestate="pizza"></div>

You can optionally provide a type to the controller options to override the chart type:

<div data-lz:controller="/controllers/lz/chart.js" data-lz:options="{controller:{type:'BarChart'}}" data-lz:usestate="pizza"></div>

You could also load the state from a remote source:

<template id="remotedonuts" data-lz:state data-lz:src="./donuts.json">
</template>
<div data-lz:controller="/controllers/lz/chart.js" data-lz:usestate="remotedonuts"></div>

Pushed Content

Although content can be polled using lz:trigger="load interval:<ms>", it is often more efficient to use pushed content.

Three types of pushed content are supported:

PubSub

Typically, you will want to subscribe to a channel. The lz:config attribute can be used to provide configuration data to the controller. If the channel property in the configuration starts with a #, then it is treated as the target element identifier for the message. Otherwise, you can just use lz-target to specify the target element for all content.

A template with a {message} block can also be provided to format the messages. The template can be at the scope of the pubsub enabled element, or at the scope of a channel element.

For convenience, elements enhanced with a pubsub controller have subscribeand unsubscribe methods added that can be called from JavaScript and also respond to subscribe and unsubscribe events.

<div id="pubsub-example" data-lz:controller="/controllers/lz/pubsub" data-lz:options="{controller:{src:'/docs/hello-pubsub.js',channel:'#joe',target:'beforeEnd',subscribe:true}}">
  <template><div>${message}</div></template>
</div>
<div>Joe's Messages
<div id="joe"></div>
</div>
<script>
document.addEventListener("lz:loaded", () => {
  setTimeout(() => {
    const el = document.querySelector("#pubsub-example");
    el.unsubscribe();
    }, 10000);
})
</script>
Joe's Messages

The service should expose subscribe and unsubscribe methods that accept an element as an argument. In should publish messages with the form {channel:<channel name>,message:<message>}, for example:

let interval;
const subscribe = (element,channel="*") => {
    interval = setInterval(() => {
        element.dispatchEvent(new CustomEvent("message",{detail:{channel,message:`${channel.slice(1)} the datetime is: ` + new Date().toLocaleTimeString()}}));
    },10000);
};
const unsubscribe = (element,channel="*") => {
    if(interval) clearInterval(interval)
}
export {subscribe,unsubscribe}

Server Sent Events

Loading...

If you want to log each server sent event separately you can use the lz:target attribute and provide a template with a message block. If the message was parsable as JSON, nested properties will be accessible.

For convenience, elements with an sse controller have subscribeand unsubscribe methods added that can be called from JavaScript and also respond to subscribe and unsubscribe events.

<div id="sse-example" data-lz:controller="/controllers/lz/sse.js" data-lz:options="{controller:{src:'/datetime'}}" data-lz:target="beforeEnd">
  <template><div>${message}</div></template>
</div>
<script>
document.addEventListener("lz:loaded", () => {
  setTimeout(() => {
    const element = document.getElementById("sse-example");
    element.unsubscribe();
  }, 10000);
});
</script>

Web Sockets

For convenience, elements enhanced with lz:ws have publish and subscribe methods added that can be called from JavaScript and also respond to publish and subscribe events.

Note: The directive will automatically detect and adjust for connecting with a secure protocol, i.e. wss, if the page hosting it is served over https.

<div data-lz:controller="/controllers/lz/ws.js" data-lz:target="beforeEnd">
  <template><div>${(new Date()).toLocaleTimeString()}: ${message}</div></template>
  <div>Peter
    <div id="peter"></div>
  </div>
  <div>Paul
    <div id="paul"></div>
  </div>
</div>
Peter
Paul
To:
Message:

Pre-Built Components

Components are loaded into <template> elements via the lz:src attribute with an HTML file as the value and a lz:tagname attribute to specify the custom tag name to use.

Document Table of Contents

<template data-lz:src="lz://toc.html" data-lz:tagname="lz-toc"></template>

GitHub Issue Reference

<template data-lz:src="lz://git-issue-ref.html" data-lz:tagname="git-issue-ref"></template>
<git-issue-ref data-lz:src=":account/:repository/:issue-number"></git-issue-ref>

For example:

<git-issue-ref data-lz:src="anywhichway/lazui/1"></git-issue-ref>

Start Up Options

If you want to simplify your JSON, you can call enable with an alternate JSON parser:

import {enable} from "./lazy-elments.js";
import JSON5 from "https://esm.sh/json5";
const {getModel} = enable({JSONParser:JSON5});
<template lz:model="mymodel">
{
    name: "John",
    age: 30
}
</template>

Models are bound to elements using lz:usesemodel. The value of the attribute is the name of the model. Whenever properties in the model that are referenced in the interpolated source of an element change, the element will be updated.

<div data-lz:usesemodel="mymodel">
    <span>Hello, ${name}!</span>
    <span>You are ${age} years old.</span>
</div>
const model = getModel("mymodel");
model.name = "Jane"; // the div will be updated

The model name can be followed by a space and the word eager to force updates regardless of what property changes.

<div data-lz:usesemodel="mymodel eager">
    <span>Hello, ${name}!</span>
    <span>You are ${age} years old.</span>
</div>

Using JavaScript

If you plan to write custom JavaScript, you will probably want to install lazui locally:

npm install lazui

This will install a local server you can run using:

npm run serve

Unless otherwise noted, all the JavaScript examples below assume that lazui.js has already been loaded.

If you are using the local server the script will be:

<script src="/lazui"></script>

Otherwise, unpkg can be used to load the script:

<script src="https://www.unpkg.com/lazui"></script>

html

Like that in lighterHTML and lit, the html template literal processes a template string with a context to use for JavaScript execution and data substitution.

const {html} = lazui;

const greeting = (person) => html`Hello, ${person.name}!`;

const interpolation = greeting({name:"John"});

The interpolation value above is an enhancement of an object capturing the arguments generated when JavaScript processes a tagged template. Tagged template functions take a fairly standard form:

const myfunction = (strings, ...values) => { return {strings,values} };

const person = {name:"John"};

const interplolation = myfunction`Hello, ${person.name}!`;

const {strings,values} = interpolation;

console.log(strings); // ["Hello, ","!"]
console.log(values); // ["John"]

See the MDN documentation on tagged templates for more information.

The lazui implementation of html is a bit more sophisticated than the above, it returns an object with three additional functions:

{
    strings: string[];
    values: any[];
    raw(): string;
    toDocumentFragment(): DocumentFragment;
    nodes(): NodeList;
}

However, you will do not need to call the functions directly unless you need specialized sanitation processing, instead use the following:

html.raw

Returns raw HTML and ensures that nested templates are processed correctly along the way. Also note, toString() is an alias for raw().

This allows the expedient but potentially unsafe processing of templates to deliver HTML to a browser:

const {html} = lazui;

const list = ['some', '<b>nasty</b>', 'list'];

const content = html` // or add a .raw before the backtick
 <ul>${list.map(text => html`
 <li>${text}</li>
 `)}
 </ul>`;

html.documentFragment

Returns a DocumentFragment where the strings and values have been inserted at appropriate points. This takes a little more computational effort, but is far safer than just html or html.raw. During this processing, any strings passed in will be inserted as text not HTML, <template> placement and boolean attributes are normalized, and functions assigned to event handlers, attributes starting with on, e.g. onclick are bound to the DOM nodes properly.

const {html} = lazui;

const list = ['some', '<b>nasty</b>', 'list'];

const content = html.documentFragment`<ul>${list.map(text => html`<li>${text}</li>`)}</ul>`;

As you can see <b>nasty</b> is now text not HTML:

toDocumentFragment is pretty safe as is, however; if you want more sanitation, you can call toDocumentFragment with a sanitizer function. In Chrome you can enable the Sanitizer API by enabling the Experimental Web Platform features flag in chrome://flags. Then you pass in a bound sanitize function:

const {html} = lazui;

const list = ['some', '<b>nasty</b>', 'list'];

const content = html`
  <ul>${list.map(text => html`
  <li>${text}</li>
  `)}
  </ul>`;

const sanitizer = new Sanitizer();
  
const fragment = content.toDocumentFragment(sanitizer.sanitize.bind(sanitizer));

Note: Technically <style> and <template> tags are not allowed outside a document <head> element. However, lazui explicitly supports this, so lazui creates a fragment internally with both a <head> and <body>. The sanitize function is called with <style> and <template> elements in a <head> section, then they are moved and just the <body> child nodes are returned as the child nodes of the fragment.

html.nodes

nodes is just a convenience wrapper around html.documentFragment. It returns the child node list instead of the fragment.

const {html} = lazui;

const list = ['some', '<b>nasty</b>', 'list'];

const content = html`
<ul>${list.map(text => html`
<li>${text}</li>
`)}
</ul>`;

Once again, <b>nasty</b> is now text not HTML:

Template Hooks

It is also possible to insert hooks into templates. Hooks run after the template has been processed and allow updating of the node at the location of the hook.

<script>
const myhook = (node) => {
         return new Date().toLocaleTimeString();
     },
     content = lazui.html`<div>Timestamp: <span>${{hook:myhook,interval:1000}}</span></div>`;
document.currentScript.after(...content.nodes());
</script>

The properties of a hook are:

render

render(node:Node,content:Interpolation|string|Node|NodeList|DocumentFragment|null,{where="inner",state:object})

render is the multi-talented workhorse of lazui. It can be used to replace, update or augment the contents of a Node with the content from a string, Interpolation, Node, or DocumentFragment. The content can contain string literals that get resolved using the state object. If content is null, then it is assumed that the node contains string literals which are resolved using the state object.

The where argument can be one of inner, outer, beforeBegin, afterBegin, beforeEnd, afterEnd, firstChild, lastChild, parent, top, or blank. The default is inner.

When where is anything other than inner, the node operates as an anchor point from which to find a target location to put the content. In most implementations of render by other libraries one might interpret render to mean:

Insert the provided content into the DOM as a replacement for the inner contents of node.

With lazui the interpretation is:

Insert the provided content into the DOM at the location specified by where using node as a reference point or default value for the content.

Classic Use Of Render

The classic use of render takes an interpolation and replaces the inner contents of a node with the result of the interpolation.

const {render,html} = lazui;
let count = 0;
const clicked = (event) => {
    count++;
    event.target.innerText = `Click count: ${count}`;
};
render(document.getElementById('classic-render'), html`<div onclick=${clicked}>Click count: ${count}</div>`);

With Strings

With Nodes and NodeLists

With DocumentFragments

Creating Custom Elements

Custom Elements are just templates with the directive lz:tagname.

<template data-lz:tagname="my-custom-element">
    <style>
        p {
            color: red;
            font-weight: bold;
        }
    </style>
    <p>
        I am a custom element!
    </p>
    <script>
      self.observedAttributes = ["title"];
      self.connected = function() {
          this.shadowRoot.querySelector("p").innerText = "I am a custom element!  I am connected!";
      };
      self.attributeChanged = function(name,oldValue,newValue) {
          this.shadowRoot.querySelector("p").innerText = `I am a custom element!  I am connected!  My title is ${newValue}`;
      };
    </script>
</template>
<my-custom-element id="custom-element" title=""></my-custom-element>
<script type="module">
document.addEventListener("lz:loaded",() => { // Note: if you are using a custom namespace you will need to replace "lz"
   document.getElementById("custom-element").setAttribute("title","My Title");
 });
</script>

You can provide an lz:src attribute and keep the contents in a separate file without the <template> wrapper.

The table of contents for this document is managed via a custom element in the file /components/toc.html.

Creating Custom Attribute Directives

Creating Custom Controllers

If you need to drop down into JavaScript, you can add a controller to an element. The controller will be loaded and executed after the element content is loaded. A controller is simply a module that exports:

  1. event handlers like onclick
  2. functions that can be bound to event handlers declared in HTML, e.g. onclick="this.greet"
  3. A targets object that maps the names of targets to CSS selectors. These will typically correspond to the value of an attribute like lz:property on an element, but they can actually match anything that querySelector can match. The keys of the targets object are used to create properties on the controlled HTML element that are bound to the matching elements.
<div data-controller="hello.js">
    <input name="name" type="text">
    <button onclick="this.greet">Submit</button>
    <span lz:property="greeting"></span>
</div>
const imports = {
    greeting: '[lz:property="greeting"]',
    name: 'input[name="name"]',
};

function greet() {
    this.greeting.textContent = `Hello, ${this.name.value}!`;
}

function onclick(event) {
    console.log(event)
}

export {
    imports,
    greet,
    onclick
}

Custom controllers can be added to forms to support validation. However, submission handling will still be done by lazy-elments unless no submit button is provided or the controller implements its own fetch approach.

<form lz:controller="hello.js">
    <input name="name" type="text">
    <button type="submit">Submit</button>
    <span data-property="greeting"></span>
</form>

It is even possible to add controllers to specific input elements within a form. In this case the CSS selectors in the targets will be relative to parent of the input element.

<form>
    <span><input name="name" type="text" lz:controller="./validate-name.js"><span id="#input-error"></span></span>
    <button>Submit</button>
    <span data-property="greeting"></span>
</form>
const targets = {
    "nameError": "#name-error"
}
function onchange(){
    var name = this.value;
    if(name.length < 5){
        this.nameError.innerHTML = "Name must be at least 5 characters long";
    }else{
        this.nameError.innerHTML = "";
    }
}

export {
    onchange,
    targets
}

The above is somewhat blunt, but it demonstrates the ability to add controllers to specific elements within a form. See the MDN documentation on form validation for more information on how to do this properly.

Advanced Client Side Routing

Most server routers let you add routes by using something like app.get("/some/path",handler). With lazui you can use <template data-lz:url:get="/some/path" data-lz:options="{url:{handler:"myhandler.js"}}> to add a JavaScript handler for a route. The handler should export a function that takes a Request object. If the handler returns a Response object, it will be sent to the client. Otherwise, the router will continue to its next option.

Assume the file docs/helloworld.js contains:

const handlers = {
    get(request) {
        return new Response("Hello world!",{status:200,headers:{"Content-Type":"text/plain"}})
    }
}
export {handlers as default};

It is also possible to avoid going to a server by using a global variable to store the handlers.

The declarative router does not support the wild card or app.any(...) method. However, you can add a route that matches any behavior by implementing a handler that exports an object with all the possible handlers as properties, e.g.

const handlers = {
    get: (request) => {
        return new Response("Hello, World!");
    },
    post: (request) => {
        return new Response("Hello, World!");
    },
    put: (request) => {
        return new Response("Hello, World!");
    },
    delete: (request) => {
        return new Response("Hello, World!");
    },
    patch: (request) => {
        return new Response("Hello, World!");
    },
    head: (request) => {
        return new Response("Hello, World!");
    },
    options: (request) => {
        return new Response("Hello, World!");
    }
};

Advanced State

States support addEventLister, remoteEventListener, dispatchEvent, and delete.

When a state is created, it posts the event state:created with the {detail:{state}}.

When a state is loaded from a URL, it posts the event state:loaded with {detail:{state,src}}.

When a state changes, it posts the event state:change with {detail:{state,property,value,oldValue,path,ancestors}}}.

When a state is deleted, in posts the event state:deleted with {detail:{state}}.

Advanced Configuration

The first place you may wish to use JavaScript is for the configuration of lazui. In a module script immediately after the script that loads lazui you can do things like define which attribute directives, JSON parser and router to use.

Setting The Attribute Namespace

Loading Attribute Directives

Specifying A Router

Specifying A JSON Parser

Creating A Custom Bundle

This section assumes you have read Advanced Configuration.

If you prefer a single http connection to get all or most of the lazui capability loaded when a web page loads, creating a custom bundle is easy if you know how to use webpack.

Create a file that loads and configures lazui (my-custom-lazui.js):

await import("/lazui.js"); // set's gloablThis.lazui as a side-effect
import json5 from "https://esm.sh/json5";
import {state} from "/directives/lz/state.js";
import {usestate} from "/directives/lz/usestate.js";
lazui.useJSON(json5);
lazui.useDirectives("lz",state,usestate);
export {lazui as myLazui}

Configure your bundle (my-custom-lazui.config.cjs):

const path = require('path'),
        webpack = require('webpack'),
        TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  mode: 'production',
  entry: './my-custom-lazui.js',
  output: {
    path: path.resolve(__dirname, 'docs'),
    filename: 'my-custom-lazui.js',
  },
  plugins: [
    new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1})
  ],
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
      terserOptions: {
        mangle: {
          reserved: ['state'], // <-- list directive names here
        },
      }
    })]
  }
}

The line new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}) is necessary because lazui already uses dynamic imports. If you don't include this line, webpack will create dynamic chunk files that will be loaded on demand. As a result, other than minification, you will not see any benefit from bundling. In fact, it will make debugging obscure.

The line reserved: ['state'] is necessary because in my-custom-lazui.js you are telling lazui to configure for use of state. Webpack mangles the names of functions and variables to make them shorter. If you don't tell it to leave state alone, it will rename it and lazui will not be able to find it. Alternatively, you can turn all function name mangling off by setting mangle: false in the terserOptions object. However, this will increase the bundle size.

Build your bundle:

npx webpack build --config ./my-custom-lazui.config.cjs

Create a file that uses your bundle (my-custom-lazui.html):

<html>
  <head>
      <meta charset="UTF-8">
      <title>Custom Lazui Bundle</title>
      <script src="./my-custom-lazui.js" autofocus></script>
  </head>
  <body>
  <div data-lz:state="{name:'John'}">
      Name: ${name}
  </div>
  </body>
</html>

And, access the file!.

Basic Server

A server supporting Markdown, web sockets and server side events, had to be written to support this documentation, so it is included to serve as a foundation for your use.

Inspiration

lazui draws from many other UI toolkits:

htmx

lighterHTML and lit

html

render

template hooks

Turbo and Stimulus

Riot

Single file components

Vue

FAQs

My web page flickers with unresolved templates when first loaded. How can I avoid this?
If you have a page with a lot of templates in HTML or custom elements, you may see a flicker as the page loads. This is because the browser will render the page before lazui has a chance to process all the templates and custom elements. You can avoid this by setting the attribute hidden in the <html> tag in your file using document.documentElement.setAttribute("hidden"). lazui will remove the hidden attribute when it is done processing.
Why isn't lazui.js a module?
Modules do not fully resolve until a page is fully loaded, which makes it harder to intercept display of the page to prevent flicker on initial rendering. Providing autofocus to a standard script, will cause lazui to start processing the page for template literals embedded in HTML and process many lazui directives as soon as the DOMContent is loaded, but before it is displayed to the user. However, all lazui directives and controllers are modules.
Why itty-router for the server?
It is small, fast, platform agnostic (can run on Bun, Cloudflare, Node, etc.), and far easier to use than Express.