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>
<script type="module">
import spiki from 'spikijs';
// 1. Register Component State & Methods
spiki.data('counterApp', () => ({
count: 0,
increment() { this.count++; },
decrement() { this.count--; }
}));
// 2. Start Engine
document.addEventListener('DOMContentLoaded', () => {
spiki.start();
});
</script>
Directives Reference Summary
| Directive | Description | Example |
|---|---|---|
| s-data | Defines component instance scope (accepts 1 or more modules/factories) | <div s-data="myApp"> |
| s-text | Sets safe text content | <span s-text="username"> |
| s-html | Sets inner HTML content (XSS caution) | <div s-html="richText"> |
| s-if | Conditionally mounts/unmounts DOM nodes (supports ! negation) |
<div s-if="!isOnline">Offline</div> |
| :hidden | Toggles native hide-show (supports ! negation) |
<div hidden :hidden="!isVisible"> |
| s-for | Loops array items inside <template> with auto-injected context | <template s-for="(item, idx) in list" s-key="id"> |
| s-key | Unique key tracking for list items | <template ... s-key="id"> |
| s-model | Two-way data binding for form inputs | <input s-model="query"> |
| s-value | One-way value binding for input elements | <input s-value="formattedText" readonly> |
| s-effect | Automatic reactive watcher (runs side effects on state mutation) | <div s-effect="saveToStorage"> |
| s-ref | Registers DOM element reference in this.$refs |
<input s-ref="textInput"> |
| s-init | Executes hook on component mount | <div s-init="onMount"> |
| s-destroy | Executes hook on component unmount | <div s-destroy="onUnmount"> |
| s-ignore | Skips DOM compilation for static blocks | <div s-ignore> |
| :[attr] | Binds dynamic attribute (HTML & SVG safe) | <img :src="userAvatar"> |
| :class | Toggles dynamic classes (HTML & SVG safe) | <div :class="activeClass"> |
| s-click | Attaches click event listener to element | <button s-click="saveItem"> |
| s-[event] | Attaches native event listener (s-input, s-keyup, etc.) |
<input s-input="handleInput"> |
| this.$root | Accesses component's root container DOM element | this.$root.classList.add('active') |
| this.$refs | Object containing all s-ref DOM elements |
this.$refs.textInput.focus() |
| this.$store | Accesses registered global stores reactively | this.$store.settings.theme |
| spiki.store() | Registers global store (executes init() automatically) |
spiki.store('settings', { ... }) |
| spiki.data() | Registers component state with one or more modules/factories | spiki.data('app', createAuth, createCart) |
| spiki.raw() | Unwraps Spiki Proxy to a plain JavaScript object | const obj = spiki.raw(this.user) |
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.
e.stopPropagation()
When an inner element with s-click sits inside a container that also has s-click, calling e.stopPropagation() in the inner method cleanly prevents the outer container's listener from executing.
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.
spiki.data('formDemo', () => ({
name: 'Alice',
isSubscribed: true,
get displayName() { return this.name || 'Anonymous'; },
get formattedName() { return this.name ? `Developer: ${this.name}` : ''; }
}));
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.data(name, ...fns)
Registers a component and merges multiple modular factory functions into a single unified reactive scope while preserving getters and computed properties.
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.data()
For large applications, organize code into smaller modular factory functions and pass them directly into spiki.data('name', ...fns). Spiki automatically merges them into a single reactive component while preserving getters and computed properties. All methods share access to the unified state 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('modularDemo',
createCounterSlice,
createThemeSlice,
() => ({
init() {
console.log('Composed component initialized!');
}
})
);