SmartPermissions (Preview)
A reactive UI control system — the SmartGuard engine. Add an if="" attribute
to any element and it shows, hides, disables, or removes itself based on live
smartState values. No JavaScript in your templates. Works on any HTML element
or the semantic <smart-permission> wrapper.
The engine auto-scans the DOM on boot and watches for new elements via
MutationObserver — so dynamically inserted content is handled automatically.
Expressions are re-evaluated reactively whenever any referenced state key changes.
<script src="smart-permission.js"></script>
How It Works
Loading smart-permission.js bootstraps three things simultaneously:
// 1. smartState — a reactive key/value store exposed on window smartState.set("user.role", "admin"); smartState.get("user.role"); // → "admin" // 2. SmartGuard engine — scans all [if] elements and subscribes // to the state keys referenced in each expression // SmartGuard is exposed as window.SmartGuard // 3. MutationObserver — auto-scans newly added nodes so AJAX-injected // content gets bound automatically — no manual re-scan needed // In your HTML — no JS needed here at all: <div if="user.role === 'admin'">Admin Panel</div> // → Visible when user.role === "admin", hidden otherwise. // → Re-evaluates instantly when smartState.set("user.role", ...) is called.
The if="" Attribute
Add if="" to any HTML element — a <div>,
<button>, <section>, or
<smart-permission>. The value is a JavaScript expression evaluated
against the current smartState store. When the expression is truthy the
element shows; when falsy the active mode is applied.
<!-- On any element — role check --> <div if="user.role === 'admin'">Admin Panel</div> <!-- Negation --> <button if="!isLoggedIn">Login</button> <!-- Boolean flag --> <div if="isLoggedIn">Welcome back!</div> <!-- Semantic wrapper <smart-permission> — use with mode="replace" for fallback --> <smart-permission if="user.role === 'admin'" mode="replace"> <button>Delete Account</button> <fallback><span>⛔ No Access</span></fallback> </smart-permission>
smartState — The Reactive Store
smartState is a small reactive key/value store exposed on
window.smartState. Set any key from your page script — usually initialized
from a Django context variable — and every [if] element that references that
key re-evaluates immediately. Dot-notation is supported for nested objects.
// ── Set values ────────────────────────────────────────────── smartState.set("user", { role: "admin", balance: 1500 }); smartState.set("isLoggedIn", true); smartState.set("features", { analytics: true, newDashboard: false }); smartState.set("permissions", { deleteUser: true, editUser: false }); // Dot-path notation for nested keys smartState.set("user.role", "editor"); // updates user.role, notifies watchers // ── Get values ────────────────────────────────────────────── smartState.get("user.role"); // → "editor" smartState.get("user.balance"); // → 1500 // ── Subscribe to changes ───────────────────────────────────── smartState.subscribe("user.role", () => { console.log("Role changed to:", smartState.get("user.role")); }); // Subscribe to all changes (*) smartState.subscribe("*", () => updateUI()); // ── Read the whole store (for debugging / JSON.stringify) ──── smartState.getStore(); // → { user: {...}, isLoggedIn: true, ... }
Expressions
Expressions are JavaScript evaluated against the smartState store using
with(state). Top-level identifiers are extracted to determine which state
keys to subscribe to — the element re-evaluates automatically when any of them change.
You can use any JS comparison, logical operator, or ternary.
<!-- Simple boolean --> <div if="isLoggedIn">...</div> <!-- Equality --> <div if="user.role === 'admin'">...</div> <!-- AND — both conditions must be true --> <div if="isLoggedIn && user.balance > 1000">Premium Feature</div> <!-- OR — either condition --> <div if="user.role === 'admin' || features.newDashboard">New UI</div> <!-- Negation --> <button if="!isLoggedIn">Login</button> <!-- Multiple conditions --> <div if="permissions.deleteUser && isLoggedIn">...</div> <!-- Array includes --> <div if="['admin','editor'].includes(user.role)">...</div> <!-- Nested path --> <div if="features.analytics"><smart-chart ...></smart-chart></div>
Modes Overview
Add mode="" alongside if="" to control what happens when the
expression is falsy.
display:none. Element stays in the DOM. Default.<fallback> content instead. Best with <smart-permission>.mode="remove"
The element is physically removed from the DOM when the expression is falsy. A comment node placeholder is left so it can be re-inserted when the expression becomes truthy again. Inspect the DOM with DevTools — the element will literally not be there. This is the most secure mode for sensitive actions because the markup isn't even downloadable when hidden.
<!-- Button is removed from DOM when permissions.deleteUser is false --> <div if="permissions.deleteUser" mode="remove" > <button class="btn btn-danger">Delete User</button> </div> <!-- Inspect DOM when false → you'll see only a comment: <!-- [SmartGuard removed]: permissions.deleteUser -->
mode="disable"
All input, button, select, textarea,
and a elements inside the container are disabled and made non-interactive.
The container is dimmed to 50% opacity. Previous disabled states are remembered and
restored when the expression becomes truthy.
<!-- Entire form section is disabled until user has edit permission --> <div if="permissions.editUser" mode="disable" > <smart-input type="text" name="username" label="Username"></smart-input> <button type="submit">Save Changes</button> </div> // When false: // → all inputs get disabled=true, tabindex=-1, pointer-events:none // → container gets pointer-events:none, opacity:0.5, user-select:none // When true again: // → all previous disabled states are restored (disabled inputs stay disabled)
mode="replace" + <fallback>
When the expression is false, the main content is hidden and a
<fallback> element (or [slot="fallback"] or
.sg-fallback) is shown in its place. When true, the main content is visible
and the fallback is hidden. Best used with the <smart-permission>
semantic wrapper.
<!-- show dangerous action or a no-access message --> <smart-permission if="user.role === 'admin'" mode="replace" > <button class="btn btn-danger">⚠️ Delete Account</button> <fallback> <span style="color:var(--text-3);font-size:.8rem;">⛔ Admin only</span> </fallback> </smart-permission> <!-- Compound expression + replace --> <smart-permission if="permissions.deleteUser && isLoggedIn" mode="replace" > <button class="btn btn-danger">🗑 Bulk Delete</button> <fallback><span>Permission denied</span></fallback> </smart-permission> <!-- Alternate slot name for fallback --> <smart-permission if="isLoggedIn" mode="replace"> <div class="dashboard">...</div> <div slot="fallback">Please log in</div> </smart-permission>
Lazy Rendering
Add the lazy boolean attribute. The element is never mounted to the DOM
until the expression first becomes truthy. This is useful for heavy components like charts,
rich editors, or expensive lists that shouldn't be initialized when they're not visible.
A comment placeholder is inserted in its place.
<!-- Heavy chart is NEVER mounted until features.analytics becomes true --> <div if="features.analytics" lazy > <smart-chart api="/api/revenue/" x-field="date" y-field="amount" title="Revenue" ></smart-chart> </div> // Before features.analytics = true: DOM has only a comment node. // After features.analytics = true: element is inserted and initialized. // If features.analytics goes false again: element is hidden/removed (per mode). // Does NOT re-mount on next true — it's already in the DOM at that point.
Enter / Leave Animations
Add enter= and/or leave= with an animation name. Enter plays
when the element becomes visible; leave plays before it hides/removes. If SmartEffects
is loaded, it delegates to that engine — otherwise built-in CSS keyframe animations are used.
<!-- Slides in when isLoggedIn is true, fades out when false --> <div if="isLoggedIn" enter="slide" leave="fade" > <div class="welcome-banner">👋 Welcome back!</div> </div> <!-- Scale pop-in for feature cards --> <div if="features.analytics" enter="scale" leave="slide" > Analytics Widget </div> // Built-in animation durations: // enter: 300-350ms leave: 250-300ms // leave callback fires after animation completes before hide/remove
Role Presets Pattern
A common pattern for role-based UIs — define a presets object in JS and
apply it to reset all state keys at once. This keeps all permission logic in one place
and makes it trivial to switch between user types during development.
const PRESETS = { admin: { role:"admin", loggedIn:true, analytics:true, deleteUser:true, editUser:true }, editor: { role:"editor", loggedIn:true, analytics:true, deleteUser:false, editUser:true }, viewer: { role:"viewer", loggedIn:true, analytics:false, deleteUser:false, editUser:false }, guest: { role:"guest", loggedIn:false, analytics:false, deleteUser:false, editUser:false }, }; function applyPreset(name) { const p = PRESETS[name]; if (!p) return; smartState.set("user.role", p.role); smartState.set("isLoggedIn", p.loggedIn); smartState.set("features.analytics", p.analytics); smartState.set("permissions.deleteUser", p.deleteUser); smartState.set("permissions.editUser", p.editUser); }
Django Integration
Initialize smartState from Django's template context using
. This is the recommended pattern — the server sets
the initial state, and the client reacts to it immediately. Never put sensitive permission
checks only in JS; always back them with server-side views and DRF permissions too.
def dashboard(request): user = request.user return render(request, 'dashboard.html', { 'user_state': json.dumps({ 'role': user.groups.values_list('name', flat=True).first() or 'viewer', 'balance': user.profile.balance, }), 'is_logged_in': json.dumps(user.is_authenticated), 'features': json.dumps({ 'analytics': user.has_perm('myapp.view_analytics'), 'newDashboard': FeatureFlag.is_enabled('new_dashboard', user), }), 'permissions': json.dumps({ 'deleteUser': user.has_perm('auth.delete_user'), 'editUser': user.has_perm('auth.change_user'), }), }) <script> smartState.set("user", ); smartState.set("isLoggedIn", ); smartState.set("features", ); smartState.set("permissions", ); </script> <div if="user.role === 'admin'"> <smart-chart api="/api/admin-metrics/" ...></smart-chart> </div> <div if="features.analytics" lazy> <smart-chart api="/api/analytics/" ...></smart-chart> </div>
Interactive Demo
Modify state below and watch elements react instantly. This page uses the actual SmartPermission engine — inspect the DOM in DevTools to see elements being removed.
Attributes
| Attribute | Type | Description | Default |
|---|---|---|---|
| if | expression | JavaScript expression evaluated against smartState. Element shows when truthy, active mode applies when falsy. Required. | — |
| mode | string | hide — display:none. remove — physically removes from DOM. disable — disables all interactive children. replace — shows <fallback> content. | hide |
| lazy | boolean | Element is not mounted to the DOM until the expression is truthy for the first time. Prevents initialization of heavy components. | false |
| enter | string | Animation when element becomes visible. Values: fade · slide · scale · slide-up. | — |
| leave | string | Animation when element is about to hide/remove. Same values as enter. Animation completes before the hide/remove action. | — |
JS API
| API | Description |
|---|---|
| smartState.set(key, value) | Set a state value. Dot-notation supported for nested paths. Triggers re-evaluation of all elements that reference this key. |
| smartState.get(key) | Read a value from the store. Supports dot-notation. |
| smartState.subscribe(key, fn) | Subscribe to changes on a key. Returns an unsubscribe function. Use "*" to subscribe to all changes. |
| smartState.getStore() | Returns the entire store object. Useful for debugging or serializing state. |
| SmartGuard.refresh() | Manually re-evaluate all bound elements. Useful after bulk state changes that don't go through smartState.set(). |
| SmartGuard.scan(root) | Scan a subtree for new [if] elements and bind them. Called automatically by MutationObserver — only needed for edge cases. |
| SmartGuard.unbind(el) | Remove bindings for a specific element and clean up its subscriptions. |
| SmartGuard.destroy() | Tear down the entire engine — disconnect observer, clear all bindings and subscriptions. |