Runtime reference · v1.2.6

StrideJS API & Runtime Reference

Reactive state, directives, forms, collections, shared state, events, HTML includes, transitions, routing, network interception, lifecycle behavior, and cleanup—documented as one browser runtime rather than a stack of plugins.

Quick start

Stride is an ES module. There is no required installer or build step.

<style>
  [s-cloak] { display: none !important; }
</style>

<div id="counter" s-cloak>
  <button s-click="decrement">-</button>
  <strong s-text="count"></strong>
  <button s-click="increment">+</button>
</div>

<script type="module">
  import Stride from 'https://cdn.stridejs.com/v1/stride.min.js';

  Stride.create('#counter', {
    state: { count: 0 },
    methods: {
      increment() { this.state.count++; },
      decrement() { this.state.count--; }
    }
  });
</script>
Recommended: use s-cloak on a component whose initial HTML should remain hidden until Stride performs its first render.

Mental model

Stride.create() owns one DOM root. State and methods belong to that root, while an optional global store and event bus can connect otherwise independent components.

LayerPurpose
HTMLNormal markup plus small s-* directives.
Component stateLocal reactive data for one Stride root.
MethodsPlain JavaScript functions invoked by directives or lifecycle hooks.
Global storeOptional shared reactive state and shared methods.
Event busOptional message passing without direct component references.

Stride uses direct DOM operations rather than a virtual DOM. Reactive writes are batched into a microtask so several synchronous state changes normally produce one render before the browser paints.

Stride.create()

await Stride.create(selector, config)
Creates one reactive component rooted at the first matching element. Returns the component context.
const app = await Stride.create('#app', {
  state: { message: 'Hello' },
  methods: {
    clear() { this.state.message = ''; }
  },
  mounted() {},
  updated() {},
  unmounted() {}
});

console.log(app.state.message);

Inside component methods, this exposes:

  • this.state — local reactive state.
  • this.store — the global store, when configured.
  • this.storeMethods — global store methods.
  • this.emit(), this.on(), this.off() — event bus helpers.
  • this.fetch() — Stride's interceptable fetch wrapper.
  • this.methods — the component's bound method collection.

Calling create() again on an already-active root returns its existing context instead of attaching duplicate listeners.

Reactive state

Plain objects and arrays inside state are exposed through cached native Proxy objects. Nested mutations such as push(), property assignment, and deletion schedule a render.

Stride.create('#app', {
  state: {
    user: { name: 'Ada' },
    items: []
  },
  methods: {
    update() {
      this.state.user.name = 'Grace';
      this.state.items.push({ name: 'Compiler' });
      // Both writes are batched into the same render turn.
    }
  }
});
Deep-reactivity boundary: Stride deeply observes plain objects and arrays. Native/built-in objects and class instances—including Date, Map, Set, RegExp, and user-defined class instances—are preserved by reference so their native behavior remains intact.

Mutating one of those preserved objects internally does not itself pass through Stride's reactive proxy and therefore does not automatically schedule a render. Assign a replacement value to the containing reactive property when the UI needs to react to that change:

// Native mutation remains native, but does not itself schedule a Stride render.
this.state.cache.set('name', 'Stride');

// Replacing the reactive property does schedule a render.
this.state.cache = new Map(this.state.cache).set('name', 'Stride');
Rendering timing: state itself changes immediately. DOM synchronization is queued in a microtask, so code that must run after Stride has painted should use updated() rather than reading the DOM immediately after an assignment.

Bindings resolve local state first. Most directives can also explicitly reference shared state with the store. prefix.

Directive reference

s-text="path"
Writes a state value using textContent. Null or undefined becomes an empty string.
<span s-text="user.name"></span>
<span s-text="store.status"></span>
s-attr:name="path"
Binds one HTML attribute.

true adds an empty boolean attribute; false, null, or undefined removes it; other values are converted to strings.

<a s-attr:href="profileUrl">Profile</a>
<button s-attr:disabled="isSaving">Save</button>
s-class:class-name="path"
Toggles one CSS class from truthiness without replacing other classes.
<div class="panel" s-class:is-active="active"></div>
s-click="method"
Calls a component method or, if no local method exists, a store method.
<button s-click="save">Save</button>
<button s-click="load('/account.html')">Load</button>

The current inline expression format supports either no argument or one literal argument. Without an inline argument, the native click event is passed as the first method argument.

s-click.prevent="method"
Same as s-click, but calls preventDefault() first.
<a href="/fallback" s-click.prevent="openPanel">Open panel</a>
s-change="method"
Calls a method on the native change event.
<select s-change="changeEnvironment">...</select>
s-keyup:key="method"
Calls a method when KeyboardEvent.key matches the modifier, case-insensitively.
<input s-keyup:enter="submitSearch">
<input s-keyup:escape="closeSearch">
s-if="path"
Structural conditional.

When false, the element is physically detached from the live DOM and a comment anchor preserves its insertion point. Stride intentionally retains the detached node internally so the same node can be reinserted later; this is DOM detachment, not a claim that the node itself is garbage-collected.

Nested Stride components inside that subtree are also intentionally preserved so they can resume if the condition becomes true again. If application logic knows a nested component will never return, explicitly call Stride.destroy() for that nested component before permanently abandoning the conditional subtree.

<aside s-if="showNotice">Saved.</aside>
s-show="path"
Fast visibility toggle that keeps the element mounted.

Stride switches the element's inline display value to none !important while hidden and restores its prior inline display value when shown again. It does not override an unrelated external stylesheet rule that independently keeps the element hidden.

<div s-show="menuOpen">...</div>
s-cloak
Prevents raw template content from flashing before initialization.
[s-cloak] { display: none !important; }

<div id="app" s-cloak>...</div>
s-include="fragment.html"
Loads an HTML fragment before the component is initialized. Nested includes are resolved up to 20 levels.
<section s-include="/components/account-card.html"></section>
s-transition="name"
Adds name-enter and name-leave classes around s-if or s-show state changes.
<div s-if="open" s-transition="fade" class="fade">...</div>
s-route / s-view
Marks same-origin SPA navigation links and the fragment viewport. See the router section for configuration and dynamic parameters.

Loops

s-loop="arrayPath"
Repeats the element carrying s-loop once per array item.
<ul>
  <li s-loop="items">
    <span s-text="index"></span>
    <input s-model="item.name">
    <button s-click="selectItem">
      <span s-text="item.name"></span>
    </button>
  </li>
</ul>

Inside the repeated element:

  • item is the current array value.
  • item.name resolves a property of the current item.
  • index is the zero-based item index.
  • item and index are loop aliases only inside a rendered s-loop row; outside a loop, state properties literally named item or index work normally.
  • s-text, s-attr:, s-class:, s-show, events, s-model="item.*", and bare s-model="item" can be used inside the loop.

Stride reuses an existing rendered loop node when the item at that index is the same object/value, and updates its bindings in place. Insertions/removals/order changes update only affected repeated nodes rather than blindly deleting every row on every unrelated state write.

Primitive arrays can also be edited directly with a bare item model:

<div s-loop="tags">
  <input s-model="item">
</div>

Stride.create('#app', {
  state: { tags: ['fast', 'small', 'native'] }
});
Engineering Note: To maintain maximum DOM rendering speeds, avoid nesting structural directives (like a second s-loop or s-if) inside an active loop template row. If your dataset requires complex nested trees, cleanly isolate that layer into a separate Stride component container, or utilize high-speed inline utilities like s-show or s-class inside the repeated row.

Models & forms

s-model="path"
Two-way binding between a form control and local, store, or loop-item state.
ControlState value
Text, textarea, selectString
CheckboxBoolean
RadioSelected radio's string value
Multiple selectArray of selected string values
<input s-model="name">
<input type="checkbox" s-model="accepted">
<input s-model="store.search">
<input s-model="item.name">
<input s-model="item"> <!-- primitive array item inside s-loop -->
s-submit="method"
Prevents page reload, validates native HTML constraints, builds a FormData-derived payload, then calls the method.
<form s-submit="register" novalidate>
  <input name="username" required minlength="5">
  <input name="email" type="email" required>
  <button type="submit">Register</button>
</form>

Stride.create('#account', {
  methods: {
    register(payload, event) {
      console.log(payload.username, payload.email);
    }
  }
});

If validation fails, Stride adds s-form-invalid to the form and s-input-error plus aria-invalid="true" to invalid inputs, selects, and textareas. Provide CSS for the visual state you want.

.s-input-error { border-color: #ef4444; }

If several successful controls have the same name, the payload value becomes an array rather than silently discarding earlier values.

Transitions

s-transition="fade" is intentionally CSS-driven. Stride only coordinates lifecycle timing.

.fade {
  opacity: 1;
  transform: translateY(0);
  transition: opacity .25s ease, transform .25s ease;
}
.fade-enter {
  opacity: 0;
  transform: translateY(-8px);
}
.fade-leave {
  opacity: 0;
  transform: translateY(8px);
}

<div s-if="open" s-transition="fade" class="fade">...</div>

Stride calculates the browser's transition duration/delay and waits before detaching or hiding a leaving element. If state flips back while a leave is pending, the leave is cancelled instead of removing a now-visible element.

Global store

Stride.store(config)
Creates one shared reactive state object plus optional shared methods.
Stride.store({
  state: {
    user: null,
    notifications: 0
  },
  methods: {
    clearNotifications() {
      this.state.notifications = 0;
    }
  }
});

Read shared values from HTML with the explicit store. namespace:

<span s-text="store.notifications"></span>
<button s-click="clearNotifications">Clear</button>

Inside a local method, use this.store and this.storeMethods:

methods: {
  openInbox() {
    console.log(this.store.notifications);
    this.storeMethods.clearNotifications();
  }
}

Store writes schedule refreshes for registered Stride components. A component temporarily detached by s-if intentionally remains registered so it can stay current and resume if the same subtree is reinserted. Stride.destroy() removes a component from that update list; Stride also performs teardown when its own router/include/loop operations permanently discard owned component content.

Manual DOM removal: if application code permanently removes a Stride component root with native DOM operations such as element.remove() or by replacing an ancestor's innerHTML, call Stride.destroy() first. Otherwise the detached component can remain registered and strongly reachable—including its state and subscriptions—and can continue receiving store-driven renders. If nothing else releases it, that is a persistent memory leak.

Stride.store() is a singleton initializer. A later call returns the existing store; if that later call supplies new state or methods, Stride logs a warning because those values are intentionally ignored.

Event bus

Stride.on(event, callback)
Registers a listener and returns an unsubscribe function.
Stride.off(event, callback)
Removes one previously registered listener.
Stride.emit(event, payload)
Synchronously sends a payload to the current listeners.
const stop = Stride.on('toast', payload => {
  console.log(payload.message);
});

Stride.emit('toast', { message: 'Saved' });
stop();

The same helpers are available as this.on, this.off, and this.emit inside component methods and lifecycle hooks. Subscriptions created with component-scoped this.on() are automatically unsubscribed when that component is destroyed. A top-level Stride.on() subscription is global and remains the caller's responsibility to unsubscribe.

HTML includes

s-include fetches reusable HTML before Stride scans the component for directives. Multiple includes at the same level are fetched concurrently, and nested includes are then resolved recursively.

<div id="account">
  <header s-include="/parts/header.html"></header>
  <section s-include="/parts/account.html"></section>
</div>
System Architecture: Included markup is injected natively using innerHTML. Because client-side JavaScript operates strictly within the browser's sandbox environment, s-include cannot access or expose any server file that isn't already publicly readable by the end-user's web browser directly.

Best Practices: Ensure your fragment paths don't expose administrative file locations to public browser inspection tools. As an industry baseline, front-end layers are visual highways; your backend server environment (PHP, Go, Python, C++, etc.) must always serve as the final judge and absolute filter for data validation and access control.

Stride.fetch() & interceptors

Stride.fetch(url, config) wraps the browser's native fetch() and optionally runs one global request interceptor and one global response interceptor. Both may be synchronous or asynchronous.

Stride.interceptors.request = async config => {
  const token = await getToken();
  return {
    ...config,
    headers: { ...config.headers, Authorization: `Bearer ${token}` }
  };
};

Stride.interceptors.response = async response => {
  if (response.status === 401) showLogin();
  return response;
};

Stride.create('#profile', {
  methods: {
    async load() {
      const response = await this.fetch('/api/profile');
      const data = await response.json();
      this.state.name = data.name;
    }
  }
});

s-include and the Stride router also use this same fetch pipeline, so shared authentication/error logic can apply consistently.

Router

The router is a small same-origin fragment router. It updates browser history, supports back/forward navigation, dynamic path parameters, a 404 fragment, cancellation of stale in-flight navigations, and an afterEach hook.

<nav>
  <a href="/home" s-route>Home</a>
  <a href="/profile/ada" s-route>Ada</a>
</nav>

<main s-view>Initial shell content</main>

<script type="module">
import Stride from 'https://cdn.stridejs.com/v1/stride.min.js';

Stride.router({
  routes: {
    '/home': '/views/home.html',
    '/profile/:username': '/views/profile.html',
    '/404': '/views/404.html'
  },
  // Optional: set true when a direct /profile/ada request should
  // resolve immediately instead of preserving the server-rendered shell.
  resolveInitial: false,
  afterEach({ path, params, view, initial }) {
    console.log(path, params);
  }
});
</script>

Route links

s-route can be an empty marker and use the anchor's href, which is recommended for progressive fallback:

<a href="/about" s-route>About</a>

If s-route contains a non-empty value, that value is preferred as the SPA destination. Keep it aligned with href unless you intentionally want different fallback behavior.

Dynamic parameters

A route such as /profile/:username exposes decoded params through Stride.routeParams. If a global store exists, it also receives store.routeParams.

Fetched route fragments can use escaped text placeholders:

<h1>Profile: {{username}}</h1>

Route parameter replacements are HTML-escaped before insertion.

Initial navigation

By default, Stride.router() leaves the existing s-view contents untouched when the router is initialized. This is useful when the server-rendered page or application shell already contains the correct initial content.

Set resolveInitial: true when a direct request to a configured client route such as /profile/ada should fetch and render its route fragment immediately on boot. History back/forward and later s-route clicks resolve normally in either mode.

Route completion event

Stride.on('route:loaded', ({ path, params, view }) => {
  // Initialize route-specific behavior here if needed.
});
Server requirement: history-mode SPA URLs such as /profile/ada require your web server to return the application shell for application routes on a full browser refresh.

Lifecycle hooks

mounted()
Runs after the first render and after the component has been registered, before s-cloak is removed.
updated()
Runs after a queued reactive render. Multiple synchronous writes are normally reported as one update.
unmounted()
Runs when an s-if subtree is detached, and when Stride.destroy() explicitly tears down the component.
Stride.create('#chart', {
  state: { points: [] },
  mounted() { drawChart(this.state.points); },
  updated() { drawChart(this.state.points); },
  unmounted() { console.log('Stride-managed node removed'); }
});
Important: because unmounted() is also used by s-if, a component containing several conditional removals may call it more than once during its lifetime. Keep the hook safe to run repeatedly.
Nested components inside s-if: detaching an s-if subtree is treated as temporary. Nested Stride component roots inside that retained subtree stay registered rather than being destroyed automatically. If your application knows such a nested component will never be shown again, explicitly destroy that nested component before the subtree is permanently abandoned.

Cleanup

Stride.destroy(selectorOrElement)
Removes Stride's delegated listeners, component-scoped event subscriptions, global-store registration, structural runtime state, and then runs unmounted(). Structural authoring markup is restored so the same root can be initialized again later.
Stride.destroy('#temporary-widget');

Use Stride.destroy() before application code permanently removes a component root with native DOM APIs. Skipping destruction is not only a missed lifecycle callback: Stride can retain a strong reference to the detached component, so its state/subscriptions remain reachable and store writes can continue scheduling work against an unreachable DOM subtree.

Manual removal can leak: call Stride.destroy(root) before root.remove(), replacing an ancestor with innerHTML, or another application-controlled permanent removal. Stride can automatically clean up permanent removals performed by its own router/include/loop machinery, but it cannot infer the intent behind arbitrary DOM code outside the library.

Component-scoped listeners registered with this.on() are removed automatically by Stride.destroy(). Top-level listeners created with Stride.on() are global; retain their unsubscribe function and call it when that global listener is no longer needed.

Semantics & current limits

  • Modern browser library: Stride does not provide an IE-compatible fallback runtime.
  • Nested component roots: independent sibling roots are the cleanest composition model. Initialized child roots are respected as component boundaries, so later event/render propagation does not cross into them.
  • Nested structural ownership: an ancestor scans its descendants for structural s-loop and s-if directives during initialization. If a nested child's own structural template is still ordinary, unclaimed markup at that time, the ancestor can claim and physically extract it before the child is initialized. Do not rely on a later child initialization to reclaim structural markup an ancestor has already processed. Prefer sibling component roots or keep structural ownership within one component.
  • Structural directives belong below the root: place s-loop and s-if on descendants of the element passed to Stride.create(), not on the component root itself.
  • Nested structural loop directives: do not nest s-loop or structural s-if inside an s-loop template in v1.2.6. For complex nested trees, isolate the nested layer into another appropriately owned component or use non-structural bindings such as s-show or s-class.
  • Retained s-if subtrees: s-if is intentionally reversible. Nested components inside a detached conditional subtree remain alive so the same subtree can return. If application logic makes that removal permanent, explicitly destroy nested components that will never return.
  • Manual DOM removal requires cleanup: before permanently removing a component root outside Stride's own router/include/loop operations, call Stride.destroy(). Otherwise the detached component may remain registered and strongly reachable, which can become a persistent memory leak.
  • Deep reactivity boundary: plain objects and arrays are deeply reactive. Native objects and class instances are preserved by reference; mutating their internal state does not itself schedule a render, while replacing the containing reactive property does.
  • One global store: Stride.store() creates a singleton shared store for the module instance.
  • One literal click argument: directive expressions are intentionally not a JavaScript evaluator.
  • HTML fragments are trusted markup: s-include and route fragments use innerHTML.
  • No general moustache templating: use s-text for reactive values. {{param}} substitution is specifically a router-fragment convenience for route parameters.
  • No automatic type coercion for ordinary text controls: text/select values remain strings; checkbox and multiple-select have the special model behavior documented above.

Browser requirements

Stride targets modern evergreen browsers and relies on standard platform features including ES modules, Proxy, fetch, FormData, URL, queueMicrotask, requestAnimationFrame, and the History API.

If you need to support a specific browser/version matrix, test that matrix as part of your release process rather than assuming support from the library name alone.