lightview
Version:
A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation
394 lines (339 loc) • 18.3 kB
HTML
<!-- SEO-friendly SPA Shim -->
<script src="/lightview-router.js?base=/index.html"></script>
<div class="docs-layout">
<aside class="docs-sidebar" src="./nav.html" data-preserve-scroll="docs-nav"></aside>
<main class="docs-content">
<h1>Hypermedia</h1>
<p>
HTM-style patterns, built right in. Load HTML fragments, fetch vDOM/Object DOM, clone templates - all with
the
<code>src</code> and <code>href</code> attributes.
<em>Requires lightview-x.js</em>
</p>
<p>
If you look at the source for the navigation to the left, you will see that this page is built using a
hypermedia approach.
</p>
<h2 id="fetching-content">Fetching Content</h2>
<h3 id="html">HTML</h3>
<p>
Point <code>src</code> at an HTML file and Lightview loads it as children:
</p>
<pre><code>const { tags } = Lightview;
const { div, header, main } = tags;
// Load HTML partials
const app = div(
header({ src: '/partials/nav.html' }),
main({ src: '/partials/content.html' })
);
// The HTML is fetched, parsed, and made reactive automatically!</code></pre>
<p>
By default, Lightview enforces a <strong>Same-Domain security policy</strong> for all remote fetches.
Content can only be fetched from the same domain or its subdomains. External domains are blocked to prevent
Cross-Site Scripting (XSS) and unauthorized data ingestion. Different ports on the same host (e.g.,
localhost:3000 and localhost:4000) are <strong>allowed</strong> by default.
</p>
<p>
<strong>Relative Paths:</strong> Any URL that does not specify a protocol (e.g., <code>./data.html</code>,
<code>/api/user</code>) is automatically considered valid. This ensures reliability in sandboxed
environments (like iframes) where the global origin might be reported as <code>null</code>.
</p>
<h4>Controlling Access with validateUrl</h4>
<p>
You can customize or disable this security policy using the <code>validateUrl</code> hook. This hook is
checked before any <code>src</code> fetch or non-standard <code>href</code> navigation.
</p>
<pre><code>// Allow specific external CDNs
Lightview.hooks.validateUrl = (url) => {
const target = new URL(url, location.origin);
const allowed = ['trusted-api.com', 'cdn.content.org', 'localhost'];
return allowed.includes(target.hostname) || target.hostname.endsWith('.mysite.com');
};
// Disable security (Not recommended for production)
Lightview.hooks.validateUrl = () => true;</code></pre>
<h4>Dangerous Protocol Blocking</h4>
<p>
Lightview automatically blocks dangerous URI protocols in <code>src</code> and <code>href</code> attributes,
including <code>javascript:</code>, <code>vbscript:</code>, and certain <code>data:</code> types. If a
dangerous protocol is detected, the action is cancelled and a warning is logged to the console.
</p>
<h3 id="vdom-object-dom">vDOM and Object DOM</h3>
<p>
Files with <code>.<a id="vdom" href="/docs/api/elements#vdom">vdom</a></code>,
<code>.<a id="odom" href="/docs/api/elements#object-dom">odom</a></code> or
<code>.<a id="cdom" href="/docs/cdom">cdom</a></code> extensions are parsed as JSON and converted to
elements.
Any <a href="#template-literals">Template Literals</a> within the JSON values are automatically resolved.
</p>
<pre><code>// /api/cards.vdom
[
{ "tag": "div", "attributes": { "class": "card" }, "children": ["Card 1"] },
{ "tag": "div", "attributes": { "class": "card" }, "children": ["Card 2"] }
]
// Load vDOM
div({ src: '/api/cards.vdom' })
// Load safe, reactive HTML
div({ src: '/api/ai-generated.cdom' })
</code></pre>
<h2 id="advanced-fetches">Advanced Fetches (data-method & data-body)</h2>
<p>
You can customize the HTTP request method and body for any <code>src</code> or <code>href</code> action
using <code>data-method</code> and <code>data-body</code>.
</p>
<h3>1. Request Methods</h3>
<p>Use <code>data-method</code> to specify an HTTP verb. Defaults to <code>GET</code>.</p>
<pre><code>// Send a DELETE request
button({
href: '/api/items/123',
'data-method': 'DELETE',
target: '#status-log'
}, 'Delete Item')</code></pre>
<h3>2. Request Bodies</h3>
<p>Use <code>data-body</code> to send data. If no prefix is used, it is treated as a CSS selector.</p>
<h4>CSS Selectors (Default)</h4>
<p>Grabs data from the DOM based on the element type:</p>
<ul>
<li><strong>Forms:</strong> If the selector points to a <code><form></code>, the entire form is
serialized as <code>FormData</code>.
</li>
<li><strong>Checkboxes/Radios:</strong> Only sends the <code>value</code> if the element is
<code>checked</code>. Otherwise, no data is sent.
</li>
<li><strong>Input/Select/Textarea:</strong> Sends the current <code>value</code>.</li>
<li><strong>Other Elements:</strong> Sends the <code>innerText</code> (e.g., from a <code><div></code>
or <code><span></code>).
</li>
</ul>
<pre><code><!-- Send the value of an input (Query param search?body=...) -->
<button href="/api/search" data-body="#search-input" target="#results">Search</button>
<input id="search-input" type="text">
<!-- Send an entire form as POST -->
<button href="/api/user/update" data-method="POST" data-body="#user-form">Save</button>
<form id="user-form">
<input name="email" value="user@example.com">
</form></code></pre>
<h4>Prefixes</h4>
<p>You can use prefixes to control exactly how the body is constructed:</p>
<ul>
<li><code>javascript:</code> - Evaluates an expression (has access to <code>state</code> and
<code>signal</code>).
</li>
<li><code>json:</code> - Sends a literal JSON string (sets <code>Content-Type: application/json</code>).
</li>
<li><code>text:</code> - Sends raw text (sets <code>Content-Type: text/plain</code>).</li>
</ul>
<pre><code><!-- Dynamic calculation -->
<button href="/api/log" data-body="javascript:state.user.id">Log ID</button>
<!-- Literal JSON -->
<button href="/api/config" data-method="POST" data-body='json:{"theme": "dark"}'>Set Dark Mode</button></code></pre>
<h4>GET Request Handling</h4>
<p>If the method is <code>GET</code>, the data provided via <code>data-body</code> is automatically converted
into <strong>URL Query Parameters</strong>. If multiple values are found (like in a form), all are
appended to the URL.</p>
<h2 id="cloning-dom-elements">Cloning DOM Elements</h2>
<p>
Use CSS selectors to clone existing elements:
</p>
<pre><code>// Clone a template
div({ src: '#my-template' })
// Clone multiple elements
div({ src: '.card-template' })</code></pre>
<pre><code><!-- Hidden template in HTML -->
<template id="my-template">
<div class="modal">
<h2>Modal Title</h2>
<p>Modal content here</p>
</div>
</template></code></pre>
<h2 id="href-navigation">HREF Navigation</h2>
<p>
Add <code>href</code> to any element to make it interactive. The behavior depends on the <code>target</code>
attribute:
</p>
<p><strong>Note:</strong> The same <a href="#fetching-content">src Fetch Security</a> controls (same-domain
policy
and protocol
blocking) apply to all interactive <code>href</code> navigations.</p>
<h3>1. Self-Loading (Default)</h3>
<p>If no target is specified, clicking sets the element's own <code>src</code> to the <code>href</code> value:
</p>
<pre><code>// Loads content into itself on click
button({ href: '/partials/data.html' }, 'Load Data')</code></pre>
<h3>2. Browser Navigation</h3>
<p>Use standard underscore targets for window navigation:</p>
<pre><code>// Opens new tab
button({ href: 'https://example.com', target: '_blank' }, 'Open External')
// Navigates current page
div({ href: '/home', target: '_self' }, 'Go Home')</code></pre>
<h3>3. Targeting Other Elements</h3>
<p>Use a CSS selector as the target to load content into other elements:</p>
<pre><code>// Loads content into element with id="main"
button({ href: '/pages/about.html', target: '#main' }, 'Load About Page')
div({ id: 'main' }) // Content appears here</code></pre>
<h3>4. Positioning Content</h3>
<p>Control where content is inserted using the <code>location</code> attribute or a target suffix.</p>
<p><strong>Supported locations:</strong> <code>innerhtml</code> (default), <code>outerhtml</code>,
<code>beforebegin</code>, <code>afterbegin</code>, <code>beforeend</code>, <code>afterend</code>,
<code>shadow</code>.
</p>
<pre><code>// Option A: Suffix syntax for href (Target Selector:Location)
button({
href: '/partials/item.html',
target: '#list:beforeend' // Append to list
}, 'Add Item')
// Option B: Explicit attribute on target for src
div({
src: '/partials/banner.html',
location: 'afterbegin'
})</code></pre>
<p><strong>Smart Replacement:</strong> Lightview tracks inserted content. Fetching the same content to the same
location is a no-op. Fetching different content replaces the previous content at that specific location.</p>
<h3>5. Hash Scrolling</h3>
<p>If an <code>href</code> or <code>src</code> includes a hash (e.g., <code>/page.html#section-1</code>),
Lightview will automatically
attempt to scroll to the element with that ID after the content is loaded and injected. This works for both
standard document targets and Shadow DOM targets (where it searches within the specific
<code>shadowRoot</code>).
</p>
<pre><code>// Scrolls to #details within the loaded content
button({ href: '/api/items.html#details', target: '#content' }, 'View Details')</code></pre>
<h2 id="event-gating">Declarative Event Gating (lv-before)</h2>
<p>
The <code>lv-before</code> attribute provides a declarative way to intercept, filter, and modify events
<em>before</em> they reach your standard handlers (like <code>onclick</code>) or trigger native browser
behavior.
</p>
<div class="alert alert-info">
<p>
<strong>Important:</strong> Custom gating functions must be defined in the <code>globalThis</code> scope
(e.g., <code>window.myGate = ...</code>) to be accessible to the declarative parser.
</p>
</div>
<h3>1. Selection & Exclusions</h3>
<p>You must specify which events to gate by name, or use <code>*</code> to gate all common (sensible) UI and
form events. You can
also exclude specific events:</p>
<pre><code><!-- Gate only clicks -->
<button lv-before="click myGate()">Click Me</button>
<!-- Gate all common events EXCEPT keyboard events -->
<div lv-before="* !keydown !keyup logInteraction()">...</div></code></pre>
<h3>2. Rate Limiting (Throttle & Debounce)</h3>
<p>Lightview-X provides built-in "Gate Modifiers" for common timing patterns. These return <code>true</code>
only when the timing condition is met, effectively filtering the event stream.</p>
<pre><code><!-- Prevent button spam (only 1 click per second) -->
<button lv-before="click throttle(1000)" onclick="saveData()">Save</button>
<!-- Wait for 500ms of silence before processing input -->
<input lv-before="input debounce(500)" oninput="search(this.value)"></code></pre>
<h3>3. Sequence & Async Pipelines</h3>
<p>You can chain multiple gates in a space-separated list. They execute sequentially and can be asynchronous.
If any function returns <code>false</code>, <code>null</code>, or <code>undefined</code>, the entire event
is aborted.</p>
<p><strong>Parsing Note:</strong> Expressions are fundamentally space-separated. However, spaces within single
quotes (<code>'</code>), double quotes (<code>"</code>), or parentheses (<code>()</code>) are preserved.
This allows you to pass strings with spaces to your gate functions, such as confirmation messages.</p>
<pre><code><!-- Chained: Throttled first, then a confirmation -->
<button lv-before="click throttle(2000) confirm('Really delete?')"
onclick="doDelete()">Delete</button></code></pre>
<h3>4. Context & Arguments</h3>
<p>Inside an <code>lv-before</code> expression, <code>this</code> refers to the current element. You can pass
the
native
<code>event</code> object, as well as <code>state</code> and <code>signal</code> registries, to your
custom gate functions.
</p>
<pre><code><!-- Pass event to a custom logic function -->
<button lv-before="click validateClick(event)" onclick="proceed()">...</button></code></pre>
<h2 id="template-literals">Template Literals</h2>
<p>
External HTML, <code>.vdom</code> and <code>.odom</code> files, can reference named signals or state with
template syntax:
</p>
<pre><code>// main.js - Register named signals and state
const count = signal(0, 'count');
const userName = signal('Guest', 'userName');
const userPrefs = state({ theme: 'dark', lang: 'en' }, 'userPrefs');
// Load template that uses them
div({ src: '/partials/dashboard.html' })
div({ src: '/partials/dashboard.vdom' })
div({ src: '/partials/dashboard.odom' })</code></pre>
<pre><code><!-- /partials/dashboard.html -->
<div class="dashboard">
<h1>Welcome, ${signal.get('userName').value}!</h1>
<p>You have ${signal.get('count').value} notifications.</p>
<p>Theme: ${state.get('userPrefs').theme}</p>
</div></code></pre>
<pre><code>// /partials/dashboard.vdom (JSON Array)
[
{
"tag": "div",
"attributes": { "class": "dashboard" },
"children": [
{ "tag": "h1", "children": ["Welcome, ${signal.get('userName').value}!"] },
{ "tag": "p", "children": ["Theme: ${state.get('userPrefs').theme}"] }
]
}
]</code></pre>
<pre><code>// /partials/dashboard.odom (Object DOM)
{
"div": {
"class": "dashboard",
"children": [
{ "h1": { "children": ["Welcome, ${signal.get('userName').value}!"] } },
{ "p": { "children": ["Theme: ${state.get('userPrefs').theme}"] } }
]
}
}</code></pre>
<h2 id="shadow-dom">Shadow DOM</h2>
<p>
Load content into shadow DOM for style isolation using <code>location="shadow"</code> or the
<code>:shadow</code> suffix:
</p>
<pre><code>// Option A: location attribute
div({ src: '/components/widget.html', location: 'shadow' })
// Option B: target suffix
button({ href: '/components/widget.html', target: '#container:shadow' }, 'Load Widget')</code></pre>
<h2 id="htmx-style-apps">HTMX-style Apps</h2>
<p>
Combine <code>src</code> and <code>href</code> for hypermedia-driven UIs. You can use <code>lv-before</code>
for confirmation or validation before navigation.
</p>
<h3>Using Tag Functions</h3>
<pre><code>const { tags } = Lightview;
const { div, nav, button, main } = tags;
const app = div({ class: 'app' },
nav({ class: 'sidebar' },
button({
href: '/pages/dashboard.html',
target: '#content'
}, '📊 Dashboard'),
button({
href: '/pages/settings.html',
target: '#content',
'lv-before': "click confirm('Go to settings?')" // Event gate
}, '⚙️ Settings')
),
main({
id: 'content',
src: '/pages/dashboard.html' // Initial content
})
);</code></pre>
<h3>Using Lightview Syntax</h3>
<pre><code><div class="app">
<nav class="sidebar">
<button href="/pages/dashboard.html" target="#content">📊 Dashboard</button>
<button href="/pages/settings.html" target="#content"
lv-before="click confirm('Go to settings?')">⚙️ Settings</button>
</nav>
<main id="content" src="/pages/dashboard.html"></main>
</div></code></pre>
<h3>vs. Using HTMX</h3>
<pre><code><div class="app">
<nav class="sidebar">
<button hx-get="/pages/dashboard.html" hx-target="#content">📊 Dashboard</button>
<button hx-get="/pages/settings.html" hx-target="#content"
hx-confirm="Go to settings?">⚙️ Settings</button>
</nav>
<main id="content" hx-get="/pages/dashboard.html" hx-trigger="load"></main>
</div></code></pre>
</main>
</div>