if=""
<smart-permission>

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.

Reactive 4 Modes Compound Expressions Lazy Rendering Enter/Leave Animations Zero JS in Templates
<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.

hide
Sets display:none. Element stays in the DOM. Default.
remove
Physically removes the element from the DOM. Re-inserts when true.
disable
Disables all inputs, buttons, links inside. Dims to 50% opacity.
replace
Shows <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.

fade slide scale slide-up
<!-- 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.

Live State Controls
👑 Admin Panel
✏️ Editor Tools
🔒 Login Required
🗑 Delete Action
⚠️ Danger Zone No Access
Live smartState store

Attributes

AttributeTypeDescriptionDefault
ifexpressionJavaScript expression evaluated against smartState. Element shows when truthy, active mode applies when falsy. Required.
modestringhidedisplay:none. remove — physically removes from DOM. disable — disables all interactive children. replace — shows <fallback> content.hide
lazybooleanElement is not mounted to the DOM until the expression is truthy for the first time. Prevents initialization of heavy components.false
enterstringAnimation when element becomes visible. Values: fade · slide · scale · slide-up.
leavestringAnimation when element is about to hide/remove. Same values as enter. Animation completes before the hide/remove action.

JS API

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