Asok Directives

Keywords: client side reactivity, reactive directives, asok click, asok model, asok show, asok text, frontend directives

Asok includes native reactive directives for building interactive UIs without custom JavaScript. These directives are automatically processed by a lightweight, built-in runtime (~5KB).

Production Requirements & Zero-Eval Security

Asok achieves Zero-Eval Security in production for all reactive directives:

  • No 'unsafe-eval' Required: Unlike other lightweight reactive frameworks, Asok does not require the 'unsafe-eval' directive in your Content Security Policy (CSP).

  • Server-Side Precompilation: All JavaScript expressions inside asok-* attributes are precompiled on the server into safe JavaScript functions.

  • Cryptographically Nonced Injection: Precompiled functions are registered in the browser using standard <script> tags protected by a cryptographically strong request nonce (both during initial render and dynamic WebSocket updates).

  • Enterprise-Grade Protection: Your production applications can enforce an exceptionally strict CSP that blocks 'unsafe-eval' completely, keeping you fully protected against Cross-Site Scripting (XSS).

If you are using third-party libraries that require eval(), you can still manually force it by adding CSP_UNSAFE_EVAL=true to your .env file. Otherwise, no configuration is required!

Directives & Islands Architecture (Selective Hydration)

With the introduction of the Islands Architecture, asok-* directives are integrated with selective client-side hydration:

  • Inside Islands: Directives within components using client:load, client:visible, or client:idle are hydrated dynamically according to their respective triggers.

  • Outside Islands (Static): Directives outside of interactive islands are served statically as plain HTML, avoiding any overhead on the client side.

  • Zero JS Overhead: If a page contains no interactive components or active directives, Asok skips injecting the directives runtime (asok_directives.min.js) completely.

State Management

asok-state — Component state

Define reactive local state for a component:

<div asok-state="{ count: 0, name: 'Alice' }">
  <p asok-text="'Count: ' + count"></p>
  <p asok-text="'Name: ' + name"></p>
  <button asok-on:click="count++">Increment</button>
</div>

State is scoped to the component and its children. Changes trigger automatic re-renders.

$store — Global state

Access shared state across all components:

<!-- Component 1 -->
<div asok-state="{}">
  <button asok-on:click="$store.theme = 'dark'">Dark Mode</button>
</div>

<!-- Component 2 (updates automatically) -->
<div asok-state="{}" asok-class="$store.theme === 'dark' ? 'bg-black text-white' : ''">
  <span asok-text="'Current theme: ' + $store.theme"></span>
</div>

The store uses dependency tracking — only components that use a property are updated when it changes (10-20x faster than updating everything).

Access from JavaScript:

window.Asok.store.theme = 'dark';
window.Asok.store.user = { name: 'Alice', role: 'admin' };

Declarative Data & Form Submissions

asok-fetch-post & asok-fetch-post-async — Zero-Code Form Submissions

Automate JSON form submissions to API endpoints without writing custom JavaScript. asok-fetch-post automatically extracts form inputs and reactive state, attaches CSRF tokens, sends a JSON POST request, and populates reactive state variables with the response.

Full End-to-End Contact Form Example

1. Frontend Template (src/pages/index.html or SPA Route):
<template asok-route="/contact">
  <section asok-state="{ name: '', email: '', message: '', result: null }">
    <h2 class="text-2xl font-bold mb-4">Contact Us</h2>

    <form asok-fetch-post="/api/contact" asok-fetch-as="result" asok-reset class="space-y-4">
      <div>
        <label for="name" class="block text-sm font-medium">Your Name</label>
        <input id="name" type="text" name="name" asok-model="name" placeholder="Your Name" required class="input" />
      </div>

      <div>
        <label for="email" class="block text-sm font-medium">Your Email</label>
        <input id="email" type="email" name="email" asok-model="email" placeholder="Your Email" required class="input" />
      </div>

      <div>
        <label for="message" class="block text-sm font-medium">Your Message</label>
        <textarea id="message" name="message" asok-model="message" placeholder="Your Message" required class="textarea"></textarea>
      </div>

      <!-- Submit button with reactive loading state -->
      <button type="submit" asok-bind:disabled="loading" class="btn btn-primary">
        <span asok-show="!loading">Send Message</span>
        <span asok-show="loading">Sending...</span>
      </button>

      <!-- Feedback messages -->
      <p asok-show="result" style="color: green;" asok-text="result?.message"></p>
      <p asok-show="error" style="color: red;" asok-text="error"></p>
    </form>
  </section>
</template>
2. Backend Handler (src/pages/api/contact/page.py):
from asok import Request, jsonify


def post(request: Request):
    # Retrieve the JSON payload submitted by asok-fetch-post
    data = request.get_json()
    name = data.get('name', '').strip()
    email = data.get('email', '').strip()
    message = data.get('message', '').strip()

    if not name or not email or not message:
        return jsonify({'message': 'All required fields must be filled.'}, status=400)

    # Process message (e.g. save to DB, send email notification...)
    # send_notification_email(name, email, message)

    return jsonify({
        'status': 'success',
        'message': f'Thank you {name}, your message has been sent successfully!'
    })

💡 Important: Avoiding TypeError: Cannot read properties of null (reading 'message')

At component initialization, result starts as null (asok-state="{ result: null }"). Even though the feedback element has asok-show="result" (which hides it via CSS display: none), the directive runtime evaluates asok-text expressions during initial scope compilation.

  • ❌ Avoid: asok-text="result.message" In JavaScript, accessing null.message throws TypeError: Cannot read properties of null (reading 'message').

  • Recommended: asok-text="result?.message" With optional chaining (?.), result?.message safely evaluates to undefined without throwing. Once the API returns { message: '...' }, the text updates reactively.

  • Valid Alternative: asok-text="result && result.message" or initializing result with an empty object: asok-state="{ result: {} }".

What asok-fetch-post Handles Automatically:

  • Event Interception: Intercepts submit (via Enter key or submit button) or click.

  • Payload Extraction: Collects inputs via FormData as well as reactive asok-model properties.

  • JSON & CSRF Headers: Sends a POST request with Content-Type: application/json and the auto-refreshed CSRF header (X-CSRF-Token).

  • Reactive State Updates: Automatically updates reactive state variables:

    • loading (true while fetching, false when finished)

    • response (decoded JSON payload, or custom variable with asok-fetch-as="data")

    • error (error message string if API returns 4xx/5xx or network error)

    • status & statusCode (HTTP status code, e.g. 200, 201, 422, 500)

    • ok (true if HTTP 2xx, false otherwise)

  • Automatic Form Reset: Resets form inputs and asok-model state values when asok-reset is present and the request succeeds.

asok-fetch-as — Custom Response Variable Name

By default, JSON responses are stored in response. Customize the target variable name using asok-fetch-as:

<form asok-fetch-post="/api/login" asok-fetch-as="auth">
  <input asok-model="email" type="email" name="email">
  <input asok-model="password" type="password" name="password">
  <button type="submit">Sign In</button>
</form>

<div asok-show="auth?.token">
  <p>Signed in as <span asok-text="auth?.user?.name"></span></p>
</div>

JS Optional Chaining (?.) & Nullish Coalescing (??) Support

Asok directives fully support JavaScript optional chaining (?., ?.[, ?.()) and nullish coalescing (??) inside expressions with complete server-side security AST validation:

<p asok-text="response?.user?.profile?.name || 'Guest'"></p>
<p asok-text="auth?.details?.[0] ?? 'No details'"></p>

Auto-Populated Reactive State Variables

Inside components handling async actions or fetch directives, Asok manages the following reactive state variables automatically:

Variable

Type

Description

loading

Boolean

true while the request is in flight, false otherwise.

response

Object|null

Parsed JSON object returned by the server on success (2xx).

error

String|null

Error message string if the server returned 4xx/5xx or a network error.

status / statusCode

Number

Exact HTTP response status code (e.g. 200, 201, 400, 422, 500).

ok

Boolean

true if HTTP status is 2xx, false otherwise.

Display & Visibility

asok-show / asok-hide

Toggle element visibility with display: none:

<div asok-state="{ visible: true }">
  <div asok-show="visible">I'm visible</div>
  <div asok-hide="visible">I'm hidden</div>
  <button asok-on:click="visible = !visible">Toggle</button>
</div>

asok-text

Set text content reactively:

<div asok-state="{ count: 0 }">
  <p asok-text="'Count: ' + count"></p>
  <button asok-on:click="count++">+</button>
</div>

Class & Attribute Binding

asok-class — Dynamic classes

Three syntaxes for maximum flexibility:

<div asok-state="{ isOpen: false, status: 'success' }">
  <!-- 1. Toggle a single class -->
  <div asok-class:active="isOpen">Toggle</div>

  <!-- 2. Conditional expression -->
  <div asok-class="isOpen ? 'text-blue-500 font-bold' : 'text-red-500'">
    Conditional classes
  </div>

  <!-- 3. Object (multiple toggles) -->
  <div asok-class="{ 'active': isOpen, 'disabled': !enabled, 'success': status === 'success' }">
    Multiple classes
  </div>
</div>

Perfect for Tailwind CSS with long class lists:

<div asok-class="isOpen ? 'bg-white px-4 py-2 border border-gray-300 rounded-md shadow-sm' : 'bg-gray-100'">
  ...
</div>

asok-bind:attr

Bind any HTML attribute:

<div asok-state="{ url: '/page', disabled: false }">
  <a asok-bind:href="url">Link</a>
  <button asok-bind:disabled="disabled">Button</button>
  <input asok-bind:placeholder="'Enter ' + fieldName">
</div>

Forms & Input

asok-model

Two-way data binding for form inputs:

<div asok-state="{ name: '', email: '' }">
  <input asok-model="name" placeholder="Name">
  <input type="email" asok-model="email" placeholder="Email">

  <p asok-text="'Hello ' + name + '! Your email is ' + email"></p>
</div>

Works with:

  • Text inputs (<input type="text">)

  • Checkboxes (<input type="checkbox">)

  • Radio buttons (<input type="radio">)

  • Select dropdowns (<select>)

  • Textareas (<textarea>)

Event Handling

asok-on:event

Listen to any DOM event:

<div asok-state="{ count: 0 }">
  <button asok-on:click="count++" asok-text="'Clicked ' + count + ' times'"></button>
  <input asok-on:input="count = $event.target.value.length">
  <div asok-on:mouseenter="hovered = true">Hover me</div>
</div>

Event modifiers:

<!-- Prevent default -->
<form asok-on:submit.prevent="handleSubmit()">...</form>

<!-- Stop propagation -->
<button asok-on:click.stop="doSomething()">Click</button>

<!-- Debounce (300ms default) -->
<input asok-on:input.debounce-500="search()">

<!-- Key filters -->
<input asok-on:keydown.enter="submit()">
<input asok-on:keydown.escape="close()">

<!-- Outside clicks -->
<div asok-on:click.outside="open = false">...</div>

Conditional Rendering

asok-if / asok-elif / asok-else

Conditional rendering (elements are removed from DOM):

<div asok-state="{ role: 'admin', count: 5 }">
  <template asok-if="role === 'admin'">
    <p>Admin panel</p>
  </template>
  <template asok-elif="role === 'user'">
    <p>User dashboard</p>
  </template>
  <template asok-else>
    <p>Guest view</p>
  </template>
</div>

Loops

asok-for

Iterate over arrays:


### Keyed Reconciliation (`asok-key`)

To optimize DOM updates and preserve element state when list items are reordered, filtered, or mutated, use `asok-key` to specify a unique item identifier:

```html
<div asok-state="{ users: [{ id: 101, name: 'Alice' }, { id: 102, name: 'Bob' }] }">
  <ul>
    <template asok-for="u in users" asok-key="u.id">
      <li><span asok-text="u.name"></span></li>
    </template>
  </ul>
</div>

Data Fetching

asok-fetch — Declarative HTTP requests

Fetch JSON data automatically:

<!-- Auto-fetch on mount -->
<div asok-state="{ users: null, loading: false, error: null }"
     asok-fetch="/api/users"
     asok-fetch-as="users">

  <div asok-show="loading">Loading...</div>
  <div asok-show="error" asok-text="'Error: ' + error"></div>

  <div asok-show="users">
    <p><span asok-text="users.length"></span> users loaded</p>
  </div>
</div>

<!-- Fetch on click -->
<button asok-fetch="/api/products"
        asok-fetch-as="products"
        asok-fetch-on="click">
  Load Products
</button>

Attributes:

  • asok-fetch="/url" — URL to fetch (GET request)

  • asok-fetch-as="varname" — Variable name (default: “data”)

  • asok-fetch-on="event" — Trigger event (default: “load”)

Automatically sets loading and error in the component state.

asok-fetch-async — Custom async expressions

For more control, use async JavaScript expressions:

<div asok-state="{ data: null, loading: false, error: null }">
  <!-- Single fetch -->
  <button asok-fetch-async="data = await fetch('/api/users').then(r => r.json())">
    Load
  </button>

  <!-- Parallel fetches -->
  <button asok-fetch-async="[users, products] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/products').then(r => r.json())
  ])">
    Load All
  </button>
</div>

Attributes & Behavior:

  • Trigger: Defaults to click (ideal for buttons). You can set asok-fetch-on="load" to run it automatically when the component mounts/loads.

  • Assignment: Unlike asok-fetch, it does not use asok-fetch-as. You must assign the returned data directly inside the expression (e.g. my_var = await ...).

Choosing between asok-fetch and asok-fetch-async

Use Case

asok-fetch

asok-fetch-async

URL Type

Static strings only (e.g., "/api/users")

Dynamic expressions (e.g., "/api/users/" + userId)

Default Trigger

load (executes immediately on mount)

click (executes on click, customize with asok-fetch-on)

Usage Style

Declarative (zero custom JavaScript)

Code expression (using native await fetch(...))

HTTP Methods

GET requests only

Any method (GET, POST, PUT, DELETE, headers, etc.)

Capabilities

❌ Basic property binding only

Can chain .then() or parallelize with Promise.all()

Advanced

asok-ref

Get a reference to an element:

<div asok-state="{}">
  <input asok-ref="emailInput">
  <button asok-on:click="$refs.emailInput.focus()">Focus Email</button>
</div>

asok-init

Run code when component initializes:

<div
  asok-state="{ time: null }"
  asok-init="
    time = new Date().toLocaleTimeString();

    setInterval(() => {
      time = new Date().toLocaleTimeString();
    }, 1000);
  "
>
  <p>Heure actuelle : <span asok-text="time"></span></p>
</div>

asok-teleport

Render content in a different location:

<template asok-teleport="#modal-container">
  <div class="modal">Modal content</div>
</template>

<!-- Elsewhere in the page -->
<div id="modal-container"></div>

asok-cloak

Hide element until directives are initialized (prevents flash of unstyled content):

<style>
  [asok-cloak] { display: none; }
</style>

<div asok-state="{ loaded: false, message: 'Hello' }" asok-cloak>
  <span asok-text="message"></span>
</div>

Built-in Theme Management

Asok provides a zero-JS declarative theme toggle system with native anti-FOUC (Flash of Unstyled Content) protection:

<!-- Toggle between dark and light mode -->
<button asok-theme-toggle>Toggle Theme</button>

<!-- Specific mode triggers -->
<button asok-theme-toggle="dark">Dark Mode</button>
<button asok-theme-toggle="light">Light Mode</button>
<button asok-theme-toggle="system">Follow OS Theme</button>

<!-- Reactive Store Binding -->
<div asok-bind:class="{ 'bg-dark text-white': $store.theme === 'dark' }">
  Current theme: <span asok-text="$store.theme"></span>
</div>
  • Anti-FOUC: Injects a 3-line synchronous script in <head> that sets color-scheme and data-theme before the first browser paint.

  • Persistence: Theme choice (dark, light, system) is saved under localStorage.getItem('asok-theme').

  • JS API: window.Asok.setTheme('dark'), window.Asok.toggleTheme(), window.Asok.getTheme().

Dismissible Elements & Announcements

Per-element dismissals for announcements, alerts, and banners:

{% for a in announcements %}
<div asok-dismiss="announcement-{{ a.id }}" class="alert">
  <span>{{ a.message }}</span>
  <button asok-dismiss-trigger class="close-btn">&times;</button>
</div>
{% endfor %}
  • Persistence: Remembers dismissal per key in localStorage under asok-dismiss:key.

  • JS API: window.Asok.dismiss('key'), window.Asok.isDismissed('key').

Zero-JS UI Directives

asok-copy — Copy to Clipboard

Copy static text, input field contents, target elements (#id), or code blocks (<pre>) with rich visual feedback:

<!-- 1. Copy explicit text -->
<button asok-copy="DISCOUNT20">Copy Code</button>

<!-- 2. Copy from target input or element selector -->
<button asok-copy="#input-field">Copy Input</button>

<!-- 3. Text label swap on copy (swaps to "Copied!" for 2 seconds) -->
<button asok-copy="SECRET" data-label="Copy" data-copied="Copied!">Copy</button>

<!-- 4. Inside code blocks (auto-finds <pre> tag in parent .code-block) -->
<div class="code-block">
  <button asok-copy data-label="Copy" data-copied="Copied!">Copy</button>
  <pre><code>print("Hello World")</code></pre>
</div>
  • Automatic Target Resolution: If asok-copy is empty on a button inside a .code-block, Asok automatically locates and copies the content of the contained <pre> or <code> tag.

  • Visual Feedback:

    • Adds .asok-copied class to the button for 2 seconds.

    • Automatically appends a ✓ checkmark via .asok-copied::after if no data-copied attribute is set.

    • If data-copied (or asok-copied-text) is set, temporarily swaps button text to the custom label for 2 seconds before restoring the original text (data-label or textContent).

  • Custom Event: Dispatches asok:copied on document with { text, element } details.

asok-modal — Accessible Modals

Declarative modal management with automatic backdrop click and Escape key support:

<button asok-modal-open="my-modal">Open Modal</button>

<dialog asok-modal="my-modal" class="modal">
  <h2>Modal Title</h2>
  <p>Modal content...</p>  <button asok-modal-close>Close</button>
</dialog>

asok-tabs — Tabbed Interfaces

Zero-code tabs with ARIA attribute management:

<div asok-tabs>
  <button asok-tab="tab1">Tab 1</button>
  <button asok-tab="tab2">Tab 2</button>

  <div asok-tab-panel="tab1">Content for Tab 1</div>
  <div asok-tab-panel="tab2" style="display: none">Content for Tab 2</div>
</div>

asok-scroll-top & asok-scroll-to — Smooth Scrolling

Smart back-to-top button (automatically hidden if content fits on screen or scroll position is near top):

<button asok-scroll-top="300">Back to top</button>
<button asok-scroll-to="#pricing">View Pricing</button>

asok-char-count — Character Counter

Live remaining character counter for inputs and textareas:

<textarea id="bio" maxlength="200"></textarea>
<span asok-char-count="#bio"></span>

asok-progress — Top Navigation Progress Bar

Declarative top page-loading progress bar:

<div asok-progress class="fixed top-0 left-0 right-0 z-70 h-0.5 opacity-0 pointer-events-none transition-opacity duration-200">
  <div class="h-full w-0 bg-accent shadow-[0_0_8px_var(--color-accent)] transition-[width] duration-700 ease-out"></div>
</div>
  • Automatic Navigation Tracking: Listens to internal link clicks, form submissions, SPA page transitions, and asok:before/asok:success/asok:error events.

  • Smooth Fill: Automatically animates fill width to 90% during request, and completes to 100% before fading out.

  • JS API: window.Asok.startProgress(), window.Asok.finishProgress().

HTML Attribute Aliases & Data Attributes

All Asok UI directives support standard data-asok-* and data-* prefixes for strict HTML5 validation compliance:

Feature / Utility

Primary Attribute

HTML5 data-asok-* Alias

HTML5 data-* Alias

Theme Toggle

asok-theme-toggle

data-asok-theme-toggle

data-theme-toggle

Cookie Banner

asok-cookie-banner

data-asok-cookie-banner

data-cookie-banner

Cookie Accept

asok-cookie-accept

data-asok-cookie-accept

data-cookie-accept

Cookie Reject

asok-cookie-reject

data-asok-cookie-reject

data-cookie-reject

Cookie Reset

asok-cookie-reset

data-asok-cookie-reset

data-cookie-reset

Cookie Script

asok-cookie-script

data-asok-cookie-script

data-cookie-script

Dismissible UI

asok-dismiss

data-asok-dismiss

data-dismiss

Dismiss Trigger

asok-dismiss-trigger

data-asok-dismiss-trigger

data-dismiss-trigger

Copy Clipboard

asok-copy

data-asok-copy

data-copy

Modal Dialog

asok-modal

data-asok-modal

data-modal

Modal Open

asok-modal-open

data-asok-modal-open

data-modal-open

Modal Close

asok-modal-close

data-asok-modal-close

data-modal-close

Scroll Top

asok-scroll-top

data-asok-scroll-top

data-scroll-top

Scroll To

asok-scroll-to

data-asok-scroll-to

data-scroll-to

Char Counter

asok-char-count

data-asok-char-count

data-char-count

Tab Group

asok-tabs

data-asok-tabs

data-tabs

Tab Header

asok-tab

data-asok-tab

data-tab

Tab Panel

asok-tab-panel

data-asok-tab-panel

data-tab-panel

Page Progress

asok-progress

data-asok-progress

data-progress

Keyed List

asok-key

asok-key-ref

data-key

Special Variables

Inside directive expressions, you have access to:

Variable

Description

$store

Global store (shared across components)

$el

Current element

$event

Event object (in event handlers)

$refs

Object of referenced elements

$nextTick(fn)

Run function after next DOM update

Example: Complete Todo App

<div class="todo-app" asok-state="{ todos: [], newTodo: '', filter: 'all' }">
  <h1>📝 Asok Todo List</h1>

  <!-- Add todo form -->
  <form asok-on:submit.prevent="todos.push({text: newTodo, done: false}); newTodo = ''">
    <input type="text" asok-model="newTodo" placeholder="What needs to be done?" required>
    <button type="submit">Add</button>
  </form>

  <!-- Filter buttons -->
  <div style="margin: 20px 0;">
    <button asok-on:click="filter = 'all'" asok-class:active="filter === 'all'">All</button>
    <button asok-on:click="filter = 'active'" asok-class:active="filter === 'active'">Active</button>
    <button asok-on:click="filter = 'done'" asok-class:active="filter === 'done'">Done</button>
  </div>

  <!-- Todo list -->
  <ul>
    <template asok-for="todo in todos.filter(t => filter === 'all' || (filter === 'active' && !t.done) || (filter === 'done' && t.done))">
      <li asok-class="{ 'line-through': todo.done }">
        <input type="checkbox" asok-model="todo.done">
        <span asok-text="todo.text"></span>
        <button class="delete-btn" asok-on:click="todos.splice(index, 1)">×</button>
      </li>
    </template>
  </ul>

  <!-- Stats -->
  <div class="stats">
    <p><span asok-text="todos.filter(t => !t.done).length"></span> items left</p>
    <p><span asok-text="todos.length"></span> total items</p>
  </div>
</div>