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 requestnonce(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,
resultstarts asnull(asok-state="{ result: null }"). Even though the feedback element hasasok-show="result"(which hides it via CSSdisplay: none), the directive runtime evaluatesasok-textexpressions during initial scope compilation.
- ❌ Avoid:
asok-text="result.message"In JavaScript, accessingnull.messagethrowsTypeError: Cannot read properties of null (reading 'message').- Recommended:
asok-text="result?.message"With optional chaining (?.),result?.messagesafely evaluates toundefinedwithout throwing. Once the API returns{ message: '...' }, the text updates reactively.- Valid Alternative:
asok-text="result && result.message"or initializingresultwith an empty object:asok-state="{ result: {} }".
What asok-fetch-post Handles Automatically:#
- Event Interception: Intercepts
submit(via Enter key or submit button) orclick. - Payload Extraction: Collects inputs via
FormDataas well as reactiveasok-modelproperties. - JSON & CSRF Headers: Sends a
POSTrequest withContent-Type: application/jsonand the auto-refreshed CSRF header (X-CSRF-Token). - Reactive State Updates: Automatically updates reactive state variables:
loading(truewhile fetching,falsewhen finished)response(decoded JSON payload, or custom variable withasok-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(trueif HTTP 2xx,falseotherwise)- Automatic Form Reset: Resets form inputs and
asok-modelstate values whenasok-resetis 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 setscolor-schemeanddata-themebefore the first browser paint. - Persistence: Theme choice (
dark,light,system) is saved underlocalStorage.getItem('asok-theme'). - JS API:
window.Asok.setTheme('dark'),window.Asok.toggleTheme(),window.Asok.getTheme().
RGPD / GDPR Cookie Consent#
Manage cookie compliance declaratively with automatic script blocking:
<!-- Cookie Consent Banner (auto-hidden once choice is saved) -->
<div asok-cookie-banner class="cookie-banner">
<p>We use cookies to improve your experience.</p>
<button asok-cookie-accept class="btn btn-primary">Accept</button>
<button asok-cookie-reject class="btn btn-default">Decline</button>
</div>
<!-- Footer reset link -->
<a href="#" asok-cookie-reset>Manage cookie preferences</a>
<!-- Analytics script blocked until user accepts cookies -->
<script type="text/plain" asok-cookie-script src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXX"></script>
- Reactive Store:
$store.cookieConsentreturns'accepted','rejected', or'pending'. - JS API:
window.Asok.acceptCookies(),window.Asok.rejectCookies(),window.Asok.resetCookieConsent().
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">×</button>
</div>
{% endfor %}
- Persistence: Remembers dismissal per key in
localStorageunderasok-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-copyis 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-copiedclass to the button for 2 seconds. - Automatically appends a
✓checkmark via.asok-copied::afterif nodata-copiedattribute is set. - If
data-copied(orasok-copied-text) is set, temporarily swaps button text to the custom label for 2 seconds before restoring the original text (data-labelortextContent). - Custom Event: Dispatches
asok:copiedondocumentwith{ 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:errorevents. - 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>
Was this page helpful?