UNPKG

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
<!-- 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>&lt;form&gt;</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>&lt;div&gt;</code> or <code>&lt;span&gt;</code>). </li> </ul> <pre><code>&lt;!-- Send the value of an input (Query param search?body=...) --&gt; &lt;button href="/api/search" data-body="#search-input" target="#results"&gt;Search&lt;/button&gt; &lt;input id="search-input" type="text"&gt; &lt;!-- Send an entire form as POST --&gt; &lt;button href="/api/user/update" data-method="POST" data-body="#user-form"&gt;Save&lt;/button&gt; &lt;form id="user-form"&gt; &lt;input name="email" value="user@example.com"&gt; &lt;/form&gt;</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>&lt;!-- Dynamic calculation --&gt; &lt;button href="/api/log" data-body="javascript:state.user.id"&gt;Log ID&lt;/button&gt; &lt;!-- Literal JSON --&gt; &lt;button href="/api/config" data-method="POST" data-body='json:{"theme": "dark"}'&gt;Set Dark Mode&lt;/button&gt;</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>&lt;!-- Hidden template in HTML --&gt; &lt;template id="my-template"&gt; &lt;div class="modal"&gt; &lt;h2&gt;Modal Title&lt;/h2&gt; &lt;p&gt;Modal content here&lt;/p&gt; &lt;/div&gt; &lt;/template&gt;</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>&lt;!-- Gate only clicks --&gt; &lt;button lv-before="click myGate()"&gt;Click Me&lt;/button&gt; &lt;!-- Gate all common events EXCEPT keyboard events --&gt; &lt;div lv-before="* !keydown !keyup logInteraction()"&gt;...&lt;/div&gt;</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>&lt;!-- Prevent button spam (only 1 click per second) --&gt; &lt;button lv-before="click throttle(1000)" onclick="saveData()"&gt;Save&lt;/button&gt; &lt;!-- Wait for 500ms of silence before processing input --&gt; &lt;input lv-before="input debounce(500)" oninput="search(this.value)"&gt;</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>&lt;!-- Chained: Throttled first, then a confirmation --&gt; &lt;button lv-before="click throttle(2000) confirm('Really delete?')" onclick="doDelete()"&gt;Delete&lt;/button&gt;</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>&lt;!-- Pass event to a custom logic function --&gt; &lt;button lv-before="click validateClick(event)" onclick="proceed()"&gt;...&lt;/button&gt;</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>&lt;!-- /partials/dashboard.html --&gt; &lt;div class="dashboard"&gt; &lt;h1&gt;Welcome, ${signal.get('userName').value}!&lt;/h1&gt; &lt;p&gt;You have ${signal.get('count').value} notifications.&lt;/p&gt; &lt;p&gt;Theme: ${state.get('userPrefs').theme}&lt;/p&gt; &lt;/div&gt;</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>&lt;div class="app"&gt; &lt;nav class="sidebar"&gt; &lt;button href="/pages/dashboard.html" target="#content"&gt;📊 Dashboard&lt;/button&gt; &lt;button href="/pages/settings.html" target="#content" lv-before="click confirm('Go to settings?')"&gt;⚙️ Settings&lt;/button&gt; &lt;/nav&gt; &lt;main id="content" src="/pages/dashboard.html"&gt;&lt;/main&gt; &lt;/div&gt;</code></pre> <h3>vs. Using HTMX</h3> <pre><code>&lt;div class="app"&gt; &lt;nav class="sidebar"&gt; &lt;button hx-get="/pages/dashboard.html" hx-target="#content"&gt;📊 Dashboard&lt;/button&gt; &lt;button hx-get="/pages/settings.html" hx-target="#content" hx-confirm="Go to settings?"&gt;⚙️ Settings&lt;/button&gt; &lt;/nav&gt; &lt;main id="content" hx-get="/pages/dashboard.html" hx-trigger="load"&gt;&lt;/main&gt; &lt;/div&gt;</code></pre> </main> </div>