UNPKG

@anywhichway/lazui

Version:

Single page apps and lazy loading sites with minimal JavaScript or client build processes.

1,315 lines (1,008 loc) 78.1 kB
<script src="/lazui" data-lz:usedefaults></script> <script> document.addEventListener("lz:loaded", async (lazui) => { const issues = await fetch("https://api.github.com/repos/anywhichway/lazui/issues").then(r => r.json()); issues.filter((issue) => issue.assignee).forEach((issue) => { const parts = issue.body ? issue.body.split(" ") : [], ids = parts.filter((item) => item.startsWith("#")); ids.forEach((id) => { const el = document.getElementById(id.slice(1)); if(el) { const ref = document.createElement("git-issue-ref"); ref.setAttribute("src",`/anywhichway/lazui/${issue.number}`); el.after(ref); } }); }); }); </script> <title>lazui Documentation</title> <style> .issue { margin-left: 20px; margin-bottom: 10px; color: rgba(255,0,0,0.85); } .issue::before { content: "Issue: "; } </style> <a href="../index.md">lazui</a> <template data-lz:src="lz://components/git-issue-ref.html" data-lz:tagname="git-issue-ref"></template> <template data-lz:src="lz://components/toc.html" data-lz:tagname="lz-toc"></template> <lz-toc></lz-toc> ## 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](https://htmx.org/), [lighterHTML](https://github.com/WebReflection/lighterhtml), [Turbo](https://turbo.hotwired.dev/), [Stimulus](https://stimulus.hotwired.dev/), [Vue](https://vuejs.org/), and [Lit](https://lit.dev). 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](https://www.github.com/anywhichway/lazui/issues). ## 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](https://json5.org/) format. This makes JSON easier to write and less error prone. See [Relaxed JSON Parser](#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](#basic-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](#basic-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](#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: ```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](#creating-a-custom-bundle). Unless you are willing to [write your own JavaScript](#using-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`: ```bash npm install @anywhichway/lazui ``` Then run: ```bash npm run serve ``` Access `lazui` and this documentation at `http://localhost:3000/`. See [Basic Server](#basic-server) for more information on the `lazui` server you can customize. [https://lazui.org](https://lazui.org) uses this basic server and is hosted on [Render](https://render.com/). ## How To Be Lazui You can put template literals directly in your HTML. Here is the first simple example: ```html <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: <div>Hello, the date and time is ${new Date().toLocaleTimeString()}</div> ### Working With Markdown You can even include template literals in your Markdown. ```markdown Hello, the date and time is ${new Date().toLocaleTimeString()} ``` renders as: <template data-lz:url:get="./working-with-markdown.md" data-lz:mode="document"> Hello, the date and time is ${new Date().toLocaleTimeString()} </template> <div data-lz:src="./working-with-markdown.md" data-lz:mode="open"></div> ### Additional Options You can also configure `lazui` to: - use a [relaxed JSON parser](#relaxed-json-parser), - highlight code [highlighter](#code-highlighting), - handle [client side routes](#client-side-routing) with optional Markdown transformation. ```html <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: ```html <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](#using-javascript) [html](#html) and [render](#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](#loading-attribute-directives). 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, ```html <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: ```json { name: "John", age: 30 } ``` instead of this: ```json { "name": "John", "age": 30 } ``` The attribute takes as its value a URL pointing to the parser. [JSON5](https://json5.org/) is a good choice. ```html <div data-lz:usejson="https://esm.sh/json5"></div> ``` or, if you have a local copy of JSON5: ```html <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](#loading-content): ```!html <template data-lz:state="person"> { name: "John", age: 30 } </template> ``` Now you can do this: <div data-lz:usestate="person" data-lz:showsource="beforeBegin">${name} is ${age} years old.</div> You can also set a state as the default state for a `document` or globally using `global` (stored on the `window` object): ```html <template data-lz:state:document="person"> { "name": "John", "age": 30 } </template> ``` There are more [advanced use of state](#advanced-use-of-state) to support storing it in a database or remote server synchronization [documented later](#advanced-use-of-state). #### 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). ```html *${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. ```html <div data-lz:src="/using-state-with-markdown.md" data-lz:mode="frame" title="Lazui: Markdown Example"></div> ``` <template data-lz:url:get="./using-state-with-markdown.md" data-lz:mode="document"> *${name}* is *${age}* years old. <template data-lz:state:document="person"> { "name": "Mary", "age": 21 } </template> </template> <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. ```!html <div data-lz:state="{clickCount:0}" onclick="this.getState().clickCount++"> Click Count: ${clickCount} </div> ``` `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](#html) tagged template literal and [render](#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. ```!html <div data-lz:state="{name:'John',age:30}"> ${name} is ${age} years old. </div> ``` #### State Inheritance States are inherited down the DOM and shadow the values of properties with the same name in ancestor element states. ```html <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> ``` <div data-lz:usestate="person"> ${name} is ${age} years old. <div data-lz:state="{age:21}"> ${name} is ${age} years old. </div> </div> ### Loading and Submitting Content ```html <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](#client-side-routing), 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. ```!html <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. - `outer!<css>` and `inner!<css>` are effectively `this.querySelectorAll(<css>)` but one replaces the inside and the other outside - `parent!<css>` is effectively `this.parentElement.querySelectorAll(<css>)` and replaces the innerHTML of the selected elements - `body!<css>` is effectively `document.body.querySelectorAll(<css>)` and replaces the innerHTML of the selected elements ```!html <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> ``` #### 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: ```markdown ${name} is ${age} years old. ``` <template data-lz:url:get="./markdowntemplate.md" data-lz:mode="document"> ${name} is ${age} years old. </template> ```!html <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 <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> ``` <template id="element" data-lz:url:get="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||currentScript).insertAdjacentText("afterEnd","This was inserted by a script"); </script> </body> </template> <div data-lz:src="https://lazui.org/path/to/element.html" data-lz:mode="open" data-lz:showsource="beforeBegin"></div> 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. ```!html <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](#creating-custom-elements) 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](#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 ```!html <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> ``` ##### 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: - `application/x-www-form-urlencoded` - `multipart/form-data` - `text/plain` `lazui` supports an additional type for forms to facilitate template completion and database operations on the server. - `application/json` The example below just returns the body it was sent. <template data-lz:url:post="/reflectbody" data-lz:mode="document"></template> ```!html <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> ``` ##### 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. ```!html <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> ``` 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. ```!html <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. ```!html <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`. <template data-lz:url:post="/form-template-example" data-lz:mode="document" data-lz:showsource:inner="beforeBegin"> <div>${name}'s age is ${age}.</div> </template> ```!html <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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) has not been applied. ```html <div data-lz:src="https://lazui.org/path/to/element.html" data-lz:mode="frame" title="My Frame"></div> ``` <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](https://hono.dev), so use `lz:userouter="https://esm.sh/hono"` for now. ```html <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](#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](#server-sent-events) and [Web Sockets](#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 ```!html <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. ```!html <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. ```html <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](#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. ```!html <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> ``` 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](#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. ```html <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. ```html <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> ``` <template data-lz:url:post="/path/to/newelement.html" data-lz:mode="document"></template> <div data-lz:src='{url:"/path/to/newelement.html",method:"GET",mode:"document"}'></div> <div data-lz:src='{url:"/path/to/newelement.html",method:"POST",body:"name=John"}'></div> <div data-lz:src='{url:"/path/to/newelement.html",method:"GET",mode:"document"}' data-lz:trigger="load delay:1000"></div> The `GET`, `DELETE`, `PUT`, `PATCH`, `HEAD` methods are respected: - `GET` (default) will get the content. If `mode` is not document, then any local copy will be treated like a cache - `DELETE` will remove the element. - `PUT` will replace the element's content with the body. A `Response` with a status code `200` with a response body of "Ok" will be returned. - `PATCH` if the element exists, will patch the JSON in the element's `innerHTML` if it can be parsed as JSON and the body is JSON; otherwise the content is replaced. If the element also happens to be a state, i.e. has the `lz:state` attribute, then the state will be updated with the patched or new JSON. If the element does not exist forwards to the server. - `POST` will create a new element on the client if it does not exist, otherwise it behaves like `PUT`. - `HEAD` if the element exists, will get the `<head>` `innerHTML` if the element is a `template`; otherwise nothing. If the element does not exist, forwards to the server. 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. ```html <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: <div data-lz:src="https://lazui.org/path/to/element.html" data-lz:trigger="click dispatch:load" data-lz:mode="open">Click Me</div> 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. <template data-lz:url:get="./thanks.html" data-lz:mode="document">Thanks for clicking!</template> ```!html <div data-lz:src="./thanks.html" data-lz:trigger="click debounce:2000 dispatch:load" data-lz:target="nextSibling"> Click Me </div> <div></div> ``` ##### 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. ```!html <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> ``` ##### 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: - `window` or `globalThis`, the window object - `document`, the document object - `controller`, the closest ancestor element that has a `data-lz:controller` attribute - `src`, the closest ancestor element that has a `data-lz:src` attribute - `closest`, the closest ancestor element that implements the function - `state`, the state bound to the element or its ancestors - `this`, the element on which the event occurred 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. ```!html <div data-lz:if=".name" data-lz:usestate="person">Hello, ${name}!</div> ``` ```!html <div data-lz:if="${age < 21}">Hello, ${name}!</div> ``` does not render at all because `age >= 21`. ```!html <div data-lz:if="${age >= 21}" data-lz:usestate="person">Welcome to the bar, ${name}!</div> ``` #### 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. ```!html <div data-lz:foreach:value:name:i='["Peter","Paul","Mary"]'><template><p>${i+1}: Hello, ${name}!</p></template></div> ``` ```!html <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. ```!html <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. ```!html <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](#basic-server), or have custom integrated server using [examplify](https://github.com/anywhichway/examplify) you can use three &grave; 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. <pre class="hljs"> &grave;&grave;&grave;!html &lt;form&gt; &lt;input type="text" value="Hello, World!"&gt; &lt;/form&gt; &grave;&grave;&grave; </pre> ```!html <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. ```!html <div id="datasetexample" data-lz:dataset='{"mydata":"myvalue"}'></div> ``` <script> (() => { const script = document.currentScript||currentScript; document.addEventListener("lz:loaded",function(event) { setTimeout(function() { const el = document.getElementById("datasetexample"); script.insertAdjacentText("afterEnd",el.outerHTML.replaceAll(/&quot;/g,"'")); },1000); }); })(); </script> 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. ```html <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](https://highlightjs.org/) is supported. ```html <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. <div data-lz:style='{"color":"red","fontWeight":"bold"}' data-lz:showsource="beforeBegin">I am red and bold</div> Once resolved, the `lz:style` attribute is removed. ## Pre-Built Controllers Except for [State and Forms](#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](#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](#charts) 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](#creating-custom-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: - bar, - column, - donut, - combo You can see examples in the Google [chart gallery](https://developers.google.com/chart/interactive/docs/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. ```html <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> ``` <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: ```html <div data-lz:controller="/controllers/lz/chart.js" data-lz:options="{controller:{type:'BarChart'}}" data-lz:usestate="pizza"></div> ``` <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 data-lz:url:get="./donuts.json" data-lz:header="{'content-type':'application/json'}" data-lz:mode="document"> { type: 'PieChart', options:{ title:'How Many Donuts We Ate Today', width:400, height:300, pieHole: 0.4 }, data: [ ["Type","Number"], ["Chocolate",3], ["Blueberry",1], ["Plain",5], ["Whole Wheat",1] ] } </template> ```!html <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 - Server Sent Events - Web Sockets #### PubSub <div data-lz:controller="/controllers/lz/pubsub" data-lz:options="{controller:{src:'/docs/hello-pubsub.js'}}" data-lz:showsource="beforeBegin"></div> 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 `subscribe`and `unsubscribe` methods added that can be called from JavaScript and also respond to `subscribe` and `unsubscribe` events. ```!html <div id="pubsub-example" data-lz:controller="/controllers/lz/pubsub" data-lz:opti