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
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>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>