Skip to content

Latest commit

 

History

History
226 lines (169 loc) · 6.04 KB

File metadata and controls

226 lines (169 loc) · 6.04 KB

State Management

state — Local State

Creates a reactive context scoped to the element and its children.

<div state="{ count: 0, name: 'World' }">
  <h1>Hello, <span bind="name"></span>!</h1>
  <p>Count: <span bind="count"></span></p>
  <button on:click="count++">+1</button>
  <button on:click="count = 0">Reset</button>
</div>

store — Global Store

A global reactive store accessible from anywhere. Ideal for auth state, theme, shared data.

<!-- Define store (once, typically at the top of the page) -->
<div store="app" value="{
  user: null,
  theme: 'dark',
  lang: 'en',
  notifications: []
}"></div>

<!-- Access store from anywhere -->
<nav>
  <span bind="$store.app.user.name"></span>
  <button on:click="$store.app.theme = $store.app.theme === 'dark' ? 'light' : 'dark'">
    Toggle Theme
  </button>
</nav>

<!-- In a deeply nested component -->
<footer>
  <span bind="$store.app.notifications.length + ' notifications'"></span>
</footer>

Pre-initializing Stores via config()

You can also create stores programmatically with NoJS.config(). This is useful for hydrating state from localStorage, setting auth tokens, or defining multiple stores before the DOM is processed:

<script>
  NoJS.config({
    stores: {
      auth:  { user: null, token: localStorage.getItem('token') },
      cart:  { items: [], total: 0 },
      theme: { mode: 'dark', accent: 'blue' }
    }
  });
</script>

<!-- These work immediately — no <div store> needed -->
<span bind="$store.auth.token"></span>
<span bind="$store.cart.items.length + ' items'"></span>

Stores created via config() won't be overwritten by a later <div store> with the same name.


into — Write Fetch Results to a Store

The into attribute on any HTTP directive writes the response directly into a named global store.

<!-- Define an empty store -->
<div store="currentUser" value="{}"></div>

<!-- Fetch and write into the store -->
<div get="/me" as="user" into="currentUser">
  <p>Fetched: <span bind="user.name"></span></p>
</div>

<!-- Read from the store anywhere else on the page -->
<nav>
  <span bind="$store.currentUser.name"></span>
  <span bind="$store.currentUser.email"></span>
</nav>

The store doesn't need to be pre-defined — into will create it if it doesn't exist:

<!-- No store directive needed — into creates it automatically -->
<button call="/api/auth/refresh"
        method="post"
        into="session">
  Refresh Session
</button>

<!-- These update reactively when the call completes -->
<span bind="$store.session.token"></span>
<span bind="$store.session.expiresAt"></span>

computed — Derived State

Values that are automatically recalculated when dependencies change:

<div state="{ price: 100, quantity: 2, taxRate: 0.1 }">

  <div computed="subtotal" expr="price * quantity"></div>
  <div computed="tax" expr="subtotal * taxRate"></div>
  <div computed="total" expr="subtotal + tax"></div>

  <p>Subtotal: $<span bind="subtotal"></span></p>
  <p>Tax: $<span bind="tax"></span></p>
  <p>Total: $<span bind="total"></span></p>

  <input type="number" model="quantity" />

</div>

watch — Side Effects on State Change

Execute an action whenever a value changes:

<div state="{ search: '' }"
     watch="search"
     on:change="console.log('Search changed:', search)">

  <input model="search" />

</div>

State Persistence

Persist state across page reloads:

<!-- Persists to localStorage -->
<div state="{ theme: 'dark', sidebar: true }"
     persist="localStorage"
     persist-key="app-settings">
  ...
</div>

<!-- Persists to sessionStorage -->
<div state="{ cartItems: [] }"
     persist="sessionStorage"
     persist-key="cart">
  ...
</div>

persist-fields — Selective Persistence

Use persist-fields to control exactly which fields are saved and restored. Fields not listed are never written to storage, which is useful for keeping sensitive values (tokens, passwords) out of localStorage/sessionStorage.

<!-- Only `theme` and `sidebar` are persisted — `token` never touches storage -->
<div state="{ theme: 'dark', sidebar: true, token: '' }"
     persist="localStorage"
     persist-key="app-settings"
     persist-fields="theme, sidebar">
  ...
</div>

persist-fields accepts a comma-separated list of field names. Whitespace around each name is trimmed, so "theme, sidebar" and "theme,sidebar" are equivalent.

Attribute Description
persist Storage backend: "localStorage" or "sessionStorage"
persist-key Unique storage key. Required when persist is set
persist-fields Comma-separated list of fields to persist. Omit to persist all fields

NoJS.notify() — Flush Store Updates from JavaScript

When external JavaScript (interceptors, helper functions, <script> blocks) mutates a store via NoJS.store, the DOM bindings don't update automatically because the mutation bypasses the framework's expression engine. Call NoJS.notify() after mutating the store to flush all pending DOM updates.

<script>
  function addToCart(item) {
    NoJS.store.cart.items.push(item);
    NoJS.store.cart.total += item.price;
    NoJS.notify(); // ← triggers DOM update
  }
</script>

<div store="cart" value="{ items: [], total: 0 }"></div>
<span bind="$store.cart.items.length + ' items'"></span>

Interceptor example

<script>
  NoJS.interceptor('response', (response) => {
    if (response.status === 401) {
      NoJS.store.auth.user = null;
      NoJS.store.auth.token = null;
      NoJS.notify(); // ← flush before redirect
      NoJS.router.push('/login');
      throw new Error('Session expired');
    }
    return response;
  });
</script>

When do I need notify()? Only when you mutate NoJS.store from plain JavaScript — outside of HTML expressions like on:click or bind. If you write on:click="$store.cart.count++" directly in HTML, the framework handles notification automatically.


Next: Events →