UNPKG

lightview

Version:

A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation

410 lines (366 loc) 17.5 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>State (Store)</h1> <p> State provides deep reactivity for objects and arrays. Unlike signals which only track reassignment, state tracks nested property changes automatically. </p> <h2 id="shortcomings">The Shortcomings of Signals</h2> <pre><code>// Signals only react to reassignment const user = signal({ name: 'Alice', age: 25 }); user.value.age = 26; // ❌ Won't trigger updates! user.value = { ...user.value, age: 26 }; // ✅ Works, but verbose // Arrays have the same issue const items = signal([1, 2, 3]); items.value.push(4); // ❌ Won't trigger updates! items.value = [...items.value, 4]; // ✅ Works, but tedious</code></pre> <h2 id="state-to-the-rescue">State to the Rescue</h2> <pre><code>const { state } = LightviewX; // Deep reactivity - mutations work! const user = state({ name: 'Alice', age: 25 }); user.age = 26; // ✅ Triggers updates! user.name = 'Bob'; // ✅ Triggers updates! // Arrays just work const items = state([1, 2, 3]); items.push(4); // ✅ Triggers updates! items[0] = 10; // ✅ Triggers updates! items.sort(); // ✅ Triggers updates!</code></pre> <h2 id="state-function">State Function</h2> <p>The <code>state</code> function is the primary initializer for reactive stores.</p> <div class="code-block"> <pre><code>state(initialValue, nameOrOptions?)</code></pre> </div> <h3>Parameters</h3> <ul> <li><code>initialValue</code>: The object or array to be wrapped in a reactive proxy.</li> <li><code>nameOrOptions</code> (Optional): <ul> <li>If a <strong>string</strong>: This becomes the <code>name</code> of the state for global registration.</li> <li>If an <strong>object</strong>: Supported keys include: <ul> <li><code>name</code>: A unique identifier for the state in the registry.</li> <li><code>storage</code>: An object implementing the Storage interface (e.g., <code>localStorage</code>) to enable persistence. </li> <li><code>scope</code>: A DOM element or object to bind the state's visibility for up-tree lookups.</li> <li><code>schema</code>: A validation behavior (<code>"auto"</code>, <code>"dynamic"</code>, <code>"polymorphic"</code>) or a formal registered JSON Schema name (see <a href="#json-schema-lite">JSON Schema Lite</a> below). </li> </ul> </li> </ul> </li> </ul> <p>See the examples in the sections below for detailed usage of these parameters.</p> <h2 id="nested-objects">Nested Objects</h2> <p>State tracks changes at any depth:</p> <pre><code>const app = state({ user: { profile: { name: 'Alice', settings: { theme: 'dark', notifications: true } } }, items: [] }); // All of these trigger updates: app.user.profile.name = 'Bob'; app.user.profile.settings.theme = 'light'; app.items.push({ id: 1, text: 'Hello' });</code></pre> <h2 id="in-the-ui">In the UI</h2> <div id="ui-example"> <pre><script> examplify(document.currentScript.nextElementSibling, { at: document.currentScript.parentElement, scripts: ['/lightview.js', '/lightview-x.js'], type: 'module', height: '200px', autoRun: true, controls: false }); </script><code>const { state } = LightviewX; const { tags, $ } = Lightview; const { div, ul, li, input, span, button } = tags; const todos = state([ { text: 'Learn Lightview', done: true }, { text: 'Build app', done: false } ]); const app = div({ style: 'padding: 1rem;' }, ul({ style: 'list-style: none; padding: 0; margin-bottom: 1rem;' }, () => todos.map((todo, i) => li({ style: 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;' }, input({ type: 'checkbox', checked: todo.done, onchange: () => todos[i].done = !todos[i].done, style: 'cursor: pointer;' }), span({ style: () => todo.done ? 'text-decoration: line-through; opacity: 0.6;' : '' }, todo.text) ) ) ), button({ onclick: () => todos.push({ text: `Task ${todos.length + 1}`, done: false }), }, '+ Add Task') ); $('#example').content(app);</code></pre> </div> <h2 id="array-methods">Array Methods</h2> <p>All mutating array methods are reactive:</p> <pre><code>const items = state([1, 2, 3]); items.push(4); // Add to end items.pop(); // Remove from end items.shift(); // Remove from start items.unshift(0); // Add to start items.splice(1, 1); // Remove at index items.sort(); // Sort in place items.reverse(); // Reverse in place items.fill(0); // Fill with value</code></pre> <h2 id="date-methods">Date Methods</h2> <p>When a property is a <code>Date</code> object, all mutating methods are reactive. This includes all <code>set*</code> methods (e.g. <code>setFullYear()</code>, <code>setDate()</code>, <code>setHours()</code>, etc.): </p> <pre><code>const event = state({ date: new Date() }); // This will trigger UI updates event.date.setFullYear(2025);</code></pre> <h2 id="named-state">Named State</h2> <p> Like signals, you can name state objects for global access. This is especially useful for shared application state: </p> <pre><code>// Create named state const appState = state({ user: 'Guest', theme: 'dark' }, 'app'); // Retrieve it anywhere const globalState = state.get('app'); // Get or create const settings = state.get('settings', { notifications: true });</code></pre> <h2 id="stored-state">Stored State</h2> <p> You can store named state objects in Storage objects (e.g. sessionStorage or localStorage) for persistence. It will be saved any time there is a change. Objects are automatically serialized to JSON and deserialized back to objects. </p> <pre><code>const user = state({name:'Guest', theme:'dark'}, {name:'user', storage:sessionStorage}); // Retrieve it elsewhere (even in another file) const sameUser = state.get('user'); // Get or create with default value // If 'user' exists, returns it. If not, creates it with default value. const score = state.get('user', {storage:sessionStorage, defaultValue:{name:'Guest', theme:'dark'}});</code></pre> <p>Note: Manually updating the object in storage will not trigger updates.</p> <h2 id="schema-validation">Schema Validation</h2> <p> State objects can be validated and transformed using the <code>schema</code> option. This ensures data integrity and can automatically coerce values to the expected types. </p> <pre><code>const user = state({ name: 'Alice', age: 25 }, { schema: 'auto' }); user.age = 26; // ✅ Works (same type) user.age = '30'; // ❌ Throws: Type mismatch user.status = 'active'; // ❌ Throws: Cannot add new property</code></pre> <h3>Built-in Schema Behaviors</h3> <table class="api-table"> <thead> <tr> <th>Behavior</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td><code>"auto"</code></td> <td>Infers a fixed schema from the initial value. Prevents adding new properties and enforces strict type checking.</td> </tr> <tr> <td><code>"dynamic"</code></td> <td>Like <code>auto</code>, but allows the state object to grow with new properties.</td> </tr> <tr> <td><code>"polymorphic"</code></td> <td>Allows growth and automatically <strong>coerces</strong> values to match the initial type (e.g., setting "100" to a number property saves it as the number 100).</td> </tr> </tbody> </table> <pre><code>// Polymorphic coerces types automatically const settings = state({ volume: 50, muted: false }, { schema: 'polymorphic' }); settings.volume = '75'; // ✅ Coerced to number 75 settings.muted = 'true'; // ✅ Coerced to boolean true settings.newProp = 'ok'; // ✅ Allowed (dynamic growth)</code></pre> <h3 id="json-schema-lite">JSON Schema Lite</h3> <p> When you load <code>lightview-x.js</code>, the schema engine is upgraded to a "JSON Schema Lite" validator. It supports standard Draft 7 keywords while remaining incredibly lightweight. </p> <h4>Supported Keywords</h4> <table class="api-table"> <thead> <tr> <th>Type</th> <th>Keywords</th> </tr> </thead> <tbody> <tr> <td><strong>String</strong></td> <td><code>minLength</code>, <code>maxLength</code>, <code>pattern</code>, <code>format: 'email'</code> </td> </tr> <tr> <td><strong>Number</strong></td> <td><code>minimum</code>, <code>maximum</code>, <code>multipleOf</code></td> </tr> <tr> <td><strong>Object</strong></td> <td><code>required</code>, <code>properties</code>, <code>additionalProperties: false</code></td> </tr> <tr> <td><strong>Array</strong></td> <td><code>items</code>, <code>minItems</code>, <code>maxItems</code>, <code>uniqueItems</code></td> </tr> <tr> <td><strong>General</strong></td> <td><code>type</code>, <code>enum</code>, <code>const</code></td> </tr> </tbody> </table> <h4>Named Schemas</h4> <p> You can register schemas globally and reference them by name. This encourages reuse and keeps your <code>state</code> initializations clean. </p> <pre><code>// 1. Register a schema Lightview.registerSchema('User', { type: "object", properties: { username: { type: "string", minLength: 3 }, email: { type: "string", format: "email" } }, required: ["username"] }); // 2. Use it by name const user = state({ username: "alice" }, { schema: "User" });</code></pre> <h3 id="transformations">Transformation Helpers</h3> <p> Unlike industry-standard validators (which only allow/disallow data), Lightview's engine supports <strong>declarative transformations</strong>. This allows you to specify how data should be "cleaned" before it hits the state. </p> <pre><code>const UserSchema = { type: "object", properties: { username: { type: "string", transform: "lower" // Using a registered helper name }, salary: { type: "number", transform: (v) => Math.round(v) // Using an inline function } } }; const user = state({ username: 'ALICE', salary: 50212.55 }, { schema: UserSchema }); // user.username is 'alice' (via "lower") // user.salary is 50213 (via inline function)</code></pre> <p> When using a <strong>string</strong> for the transform (like <code>"lower"</code>), Lightview looks for a matching function in the global registry. This requires that the helper has been registered via JPRX (requires <code>lightview-cdom.js</code>) or manually in <code>Lightview.helpers</code>. For more details on built-in helpers, see the <a href="/docs/cdom.html">cDOM documentation</a>. </p> <div class="code-block info" style="border-left: 4px solid #3b82f6; padding: 1rem; background: #eff6ff; margin-bottom: 2rem;"> <strong>🔍 Naming Tip:</strong> When using string names for transforms, use the <strong>bare helper name</strong> (e.g., <code>"lower"</code>, <code>"round"</code>). You do <strong>not</strong> need a leading <code>$</code>. </div> <h4>Loading Helpers</h4> <p> When using the <code>transform</code> keyword, Lightview searches for the named function in the following order: </p> <ol style="margin-bottom: 2rem;"> <li><strong>JPRX Helpers</strong>: Any helper registered via <code>registerHelper</code> (requires <code>lightview-cdom.js</code>). </li> <li><strong>Public API</strong>: Functions attached to <code>Lightview.helpers</code>.</li> <li><strong>Inline</strong>: You can also pass a function directly to the <code>transform</code> property. </li> </ol> <div class="code-block info" style="border-left: 4px solid #3b82f6; padding: 1rem; background: #eff6ff; margin-bottom: 2rem;"> <strong>💡 Note:</strong> To use JPRX helpers (like <code>math</code>, <code>string</code>, or <code>array</code> helpers) for state transformations, you must ensure <code>lightview-cdom.js</code> is loaded before your state is initialized. </div> <h3 id="lite-vs-full">Lite vs. Full Validators</h3> <p> Why use our "Lite" engine instead of a full validator like <strong>Ajv</strong>? </p> <table class="api-table"> <thead> <tr> <th>Feature</th> <th>Lightview Lite</th> <th>Full (Ajv, etc.)</th> </tr> </thead> <tbody> <tr> <td><strong>Size</strong></td> <td>~2KB (built-in)</td> <td>~40KB+ (external)</td> </tr> <tr> <td><strong>Transformation</strong></td> <td>✅ First-class support</td> <td>❌ Not supported (Validators only)</td> </tr> <tr> <td><strong>Performance</strong></td> <td>🚀 Optimized for UI cycles</td> <td>Heavy overhead for simple checks</td> </tr> <tr> <td><strong>Spec Compliance</strong></td> <td>Basic Draft 7 (No <code>$ref</code>)</td> <td>100% Full Specification</td> </tr> </tbody> </table> <h4 id="full-validator">Using a Full Validator</h4> <p> If you have complex enterprise schemas that require full specification compliance, you can swap out the Lightview engine for any industry-standard validator: </p> <div class="code-block warning" style="border-left: 4px solid #f59e0b; padding: 1rem; background: #fffbeb; margin-bottom: 2rem;"> <strong>⚠️ Warning:</strong> If you swap the validator, you will lose support for the declarative <code>transform</code> keyword in your schemas unless your external validator also supports it. </div> <pre><code>import Ajv from "ajv"; const ajv = new Ajv(); // Swap the validation hook Lightview.internals.hooks.validate = (value, schema) => { const valid = ajv.validate(schema, value); if (!valid) throw new Error(ajv.errorsText()); return true; };</code></pre> </main> </div>