SpikiJS Framework
SpikiJS is an ultra-lightweight (~5KB), high-performance JavaScript framework designed for building reactive user interfaces directly in HTML without complex build tooling or transpilation steps.
SpikiJS enforces strict separation of concerns to comply with strict Content Security Policy (CSP). No eval() or unsafe inline code execution is used.
- No Inline JS Code: HTML attributes cannot contain JS code, expressions, or operators (e.g.
s-click="count++"is invalid). Allowed Exception: Boolean directives (s-if,:hidden,:disabled,:checked) accept state property names directly or with logical NOT!(e.g.:hidden="!show"). - No Attribute Parameters: Function parameters are not supported inside HTML attributes (e.g.
s-click="save(item)"is invalid). Bind only the method name:s-click="save". Accessthis.item(orthis.child,this.indexin loops) directly inside the method! - No Dot Modifiers: Attribute names do NOT support dots or modifiers (e.g.
s-submit.prevent="submitForm"is invalid). Write clean directive names:s-submit="submitForm"and calle.preventDefault()inside your method if needed. - State Access via
this: Access and mutate variables directly inside JS methods viathis.variableName.
Installation
1. Script Tag / CDN
Include the UMD script directly. Spiki will automatically bind to window.spiki.
<script src="https://unpkg.com/spikijs"></script>
2. ES Module / NPM
Install via NPM for modern bundlers (Vite, Webpack, Rollup).
npm install spikijs
import spiki from 'spikijs';
Quick Start
Define components with s-data="name", register state logic using spiki.data(), and start the engine.
<div s-data="counterApp">
<button s-click="decrement">-1</button>
<span s-text="count"></span>
<button s-click="increment">+1</button>
</div>
spiki.data('counterApp', () => ({
count: 0,
increment() { this.count++; },
decrement() { this.count--; }
}));
spiki.start();
Directives Reference Summary
| Directive | Description | Example |
|---|---|---|
| s-data | Defines component instance scope | <div s-data="myApp"> |
| s-text | Sets safe text content | <span s-text="username"> |
| s-html | Sets inner HTML content | <div s-html="htmlString"> |
| :[attr] | Binds dynamic HTML attribute | <img :src="avatarUrl"> |
| :class | Toggles dynamic classes (HTML & SVG safe) | <div :class="classMap"> |
| s-click | Attaches click event listener to element | <button s-click="save"> |
| s-[event] | Attaches native event listener (s-input, s-keyup, etc.) |
<input s-input="onInput"> |
| s-model | Two-way data binding for form inputs | <input s-model="query"> |
| s-if | Conditionally mounts/removes DOM (supports ! negation) |
<p s-if="isOnline"> / <p s-if="!isOnline"> |
| :hidden | Toggles visibility via native attribute (s-show replacement, supports !) |
<div :hidden="!show"> |
| s-for | Loops array inside <template> | <template s-for="item in items"> |
| spiki.compose() | Merges modular state slices/factories into a single reactive scope | spiki.compose(modA, modB) |
s-data Component Scope
s-data="componentName" marks an element as the root of a Spiki component. State returned by the component factory function becomes reactive.
spiki.data('myApp', () => ({
title: 'Hello Spiki',
user: { name: 'Alice' }
}));
s-text & s-html
Use s-text for safe text insertion and s-html for rendering raw HTML. Functions passed to s-text receive the element instance el as the first parameter.
spiki.data('textDemo', () => ({
message: 'Reactive Text Content',
richHtml: '<span class="text-emerald-500 font-bold">Safe HTML Content</span>',
upperMessage(el) {
return this.message.toUpperCase();
}
}));
:[attr] & :class
Prefix any standard HTML or SVG attribute with : for dynamic binding. :class accepts string class names or objects with boolean values, fully safe for both HTML and SVG elements.
// HTML & SVG binding
<svg :class="svgClass" viewBox="0 0 24 24">...</svg>
spiki.data('attrDemo', () => ({
isActive: false,
toggleActive() { this.isActive = !this.isActive; },
get activeClass() {
return this.isActive ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-700';
},
get svgClass() {
return this.isActive ? 'stroke-indigo-500 text-indigo-500' : 'stroke-slate-400 text-slate-400';
}
}));
s-click & s-[event] Event Handling
Attach standard DOM event listeners using s-click, s-input, s-submit, s-keyup, etc. Event handlers receive the native Event object e without inline arguments.
spiki.data('eventDemo', () => ({
lastLog: 'None',
handleClick(e) {
this.lastLog = 'Clicked button at x: ' + e.clientX;
},
handleKeyup(e) {
if (e.key === 'Enter') this.lastLog = 'Enter pressed!';
else this.lastLog = 'Key pressed: ' + e.key;
}
}));
s-model & s-value
s-model creates two-way bindings for inputs, textareas, checkboxes, and radios. s-value provides one-way value binding.
s-if Conditional Mounting
s-if="condition" conditionally mounts or completely unmounts DOM nodes from the tree based on boolean evaluation. Supports negation using ! prefix.
<!-- Mounted when show is true -->
<div s-if="show">Mounted when true</div>
<!-- Mounted when show is false (negation) -->
<div s-if="!show">Mounted when false</div>
s-for & s-key
Render lists by placing s-for="item in array" on a <template> element. Always supply a unique key with s-key="id" for optimal DOM reuse. Inside loop event handlers, Spiki automatically provides this.item.
- #
<template s-for="(fruit, idx) in fruits" s-key="id">
<li>
<span s-text="fruit.name"></span>
<!-- Notice: No arguments passed to removeItem! -->
<button s-click="removeItem">Remove</button>
</li>
</template>
spiki.data('listDemo', () => ({
fruits: [ { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' } ],
removeItem() {
// 'this.fruit' and 'this.idx' are automatically injected into 'this'!
this.fruits.splice(this.idx, 1);
}
}));
this.child, this.index)
Inside a loop like <template s-for="(child, index) in children">, event handlers like s-click="removeItem" do NOT require arguments. Spiki automatically injects this.child (current item) and this.index (current index) directly into this when the handler runs!
s-init & s-destroy
s-init="onMount" executes immediately when the component mounts. s-destroy="onUnmount" runs when the component is unmounted from DOM tree to prevent memory leaks. Both functions receive the DOM element el as parameter.
<div s-data="userApp" s-init="onMount" s-destroy="onUnmount">
<span s-text="status"></span>
</div>
spiki.data('userApp', () => ({
status: 'Booting...',
onMount(el) {
console.log('Component mounted on element:', el);
this.status = 'Ready';
},
onUnmount(el) {
console.log('Cleaning up resources...');
}
}));
s-effect Reactive Side Effects
s-effect="handler" registers an automatic reactive watcher function. Spiki automatically tracks all reactive state properties accessed inside the handler function and re-runs it whenever those properties mutate!
<div s-data="noteApp" s-effect="autoSave">
<textarea s-model="note"></textarea>
</div>
spiki.data('noteApp', () => ({
note: '',
autoSave() {
// Read reactive state (this.note) and save to external storage
localStorage.setItem('user_note', this.note);
}
}));
s-effect
Do NOT mutate component state properties (e.g. this.count++) inside an s-effect function! Modifying state read inside an effect re-triggers the watcher endlessly, causing an infinite loop. Use s-effect strictly for side effects like localStorage, DOM calls, or external network requests.
Instance API & DOM References
Inside any component method, this grants direct access to state properties as well as built-in Spiki instance helpers.
this.$refs
Dictionary of DOM elements marked with s-ref="key". Grants direct access to raw HTML elements without querySelector.
this.$root
Reference to the root element containing s-data. Useful for class toggles, container dimensions, or custom events.
this.$store
Shared global reactive store. Any property accessed via this.$store automatically triggers component updates when changed.
spiki.raw(proxy)
Unwraps a Spiki Proxy object back into a plain JavaScript object. Essential for JSON.stringify, logging, or API calls.
spiki.compose(...modules)
Merges multiple modular state slices or factories into a single unified reactive scope while preserving native getters and setters.
1
DOM References with s-ref
Use s-ref="name" to register direct DOM element references inside this.$refs. Ideal for focusing inputs, reading canvas contexts, controlling video elements, or integrating 3rd-party libraries.
<input s-ref="textInput" type="text">
<button s-click="focusInput">Focus</button>
spiki.data('refDemo', () => ({
focusInput() {
// Access raw HTMLInputElement directly
this.$refs.textInput.focus();
},
selectText() {
this.$refs.textInput.select();
}
}));
2
Root Element Reference with this.$root
this.$root points to the component's root container element (the HTML tag bearing s-data).
spiki.data('rootDemo', () => ({
highlightRoot() {
// Manipulate root container element directly
this.$root.classList.toggle('ring-2');
this.$root.classList.toggle('ring-indigo-500');
}
}));
3
Global Store spiki.store()
Register a shared global store using spiki.store('name', stateObject). Any component accessing this.$store.name automatically subscribes to store state updates.
init() Store Execution
If your store object defines an init() method, Spiki automatically executes store.init.call(store) immediately upon store registration! This is ideal for loading persisted state from localStorage or initializing listeners.
spiki.store('settings', {
theme: 'dark',
initialized: false,
init() {
// Automatically runs when store is registered!
this.theme = localStorage.getItem('theme') || 'dark';
this.initialized = true;
}
});
spiki.data('myComponent', () => ({
get currentTheme() {
return this.$store.settings.theme; // Reactive state access!
}
}));
4
Unwrapping Reactive Proxies with spiki.raw()
Spiki wraps state objects in ES6 Proxies for reactive tracking. Use spiki.raw(proxy) to unwrap a proxy object back into a plain, non-reactive JavaScript object or array.
spiki.data('userForm', () => ({
user: { name: 'Alice', email: 'alice@example.com' },
saveUser() {
// Extract raw un-proxied JS object
const rawPayload = spiki.raw(this.user);
console.log("Raw Object:", rawPayload);
fetch('/api/user', {
method: 'POST',
body: JSON.stringify(rawPayload)
});
}
}));
5
Modular State Composition with spiki.compose()
Split large component logic into reusable modules or factories. spiki.compose() merges them into a single reactive component while preserving native getters and setters. All methods share access to the same reactive proxy via this.
const createCounterSlice = () => ({
count: 0,
increment() { this.count++; }
});
const createThemeSlice = () => ({
darkMode: false,
get themeMode() { return this.darkMode ? 'Dark' : 'Light'; },
toggleTheme() { this.darkMode = !this.darkMode; }
});
spiki.data('composeDemo', () =>
spiki.compose(
createCounterSlice,
createThemeSlice,
{
init() {
console.log('Composed component initialized!');
}
}
)
);