Skip to content

agntperfect/spandrixJS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⚑ SpandrixEngine v2.0

A lightweight, reactive DOM templating engine inspired by the metaphysical principle of Spanda β€” the primordial pulse that brings structure to form.

Version License Size


πŸ“‹ Table of Contents


🧠 Philosophy

The name Spandrix comes from:

  • πŸ“Ÿ Spanda (Sanskrit): "Subtle pulse" β€” the first creative vibration of consciousness
  • 🧩 Matrix: A structure from which things manifest

SpandrixEngine represents the metaphysical bridge between awareness (data) and manifestation (DOM).


🌟 Features

Core Features

  • βœ… Reactive Data Binding – Automatic DOM updates when data changes
  • βœ… Component System – Full-featured components with props, methods, computed, lifecycle
  • βœ… Template Directives – data-if, data-show, data-repeat, data-model, data-fetch
  • βœ… Filter Pipeline – Chainable custom filters (| uppercase | truncate:50)
  • βœ… Global State – Centralized state management with watchers
  • βœ… Slot System – Content projection with named slots
  • βœ… Event Handling – Declarative event binding with arguments
  • βœ… Two-Way Binding – v-model support for forms and components
  • βœ… HTTP Client – Built-in request/response interceptors
  • βœ… Async Data Loading – data-fetch directive for AJAX
  • βœ… Performance Metrics – Built-in performance monitoring
  • βœ… Plugin System – Extensible architecture
  • βœ… Zero Dependencies – Pure vanilla JavaScript

Security

  • πŸ”’ HTML Sanitization by default
  • πŸ”’ Expression validation (prevents code injection)
  • πŸ”’ CSRF token support
  • πŸ”’ Configurable raw HTML output

πŸ“¦ Installation

CDN (Recommended for quick start)

<script src="https://cdn.jsdelivr.net/gh/agntperfect/spandrixJS/dist/spandrix.min.js"></script>

NPM

npm install spandrix-engine
import { SpandrixEngine } from 'spandrix-engine';

Download

Download spandrix.min.js from the releases page.


πŸš€ Quick Start

Basic Example

<!DOCTYPE html>
<html>
<head>
    <title>SpandrixEngine Demo</title>
</head>
<body>
    <div id="app">
        <h1>{{ title | uppercase }}</h1>
        <p>{{ message }}</p>
        <button data-on:click="greet">Say Hello</button>
    </div>

    <script src="spandrix.min.js"></script>
    <script>
        const engine = new SpandrixEngine('#app');
        
        engine.applyData({
            title: "Welcome to Spandrix",
            message: "A reactive templating engine",
            greet() {
                alert('Hello from Spandrix!');
            }
        });
    </script>
</body>
</html>

🎯 Core Concepts

1. Initialization

const engine = new SpandrixEngine('#app', {
    debug: false,                    // Enable debug logging
    strictExpressions: false,        // Throw errors on expression failures
    allowRawHTML: false,             // Allow {{{ }}} raw HTML interpolation
    missingValuePlaceholder: '',     // Placeholder for undefined values
    enablePerformanceMetrics: false, // Track performance metrics
    maxRecursionDepth: 50,           // Prevent infinite recursion
    componentIdPrefix: 'spx-c-',     // Component ID prefix
    csrfCookieName: 'XSRF-TOKEN',    // CSRF cookie name
    csrfHeaderName: 'X-XSRF-TOKEN'   // CSRF header name
});

2. Data Binding

SpandrixEngine uses reactive proxies to automatically update the DOM when data changes.

engine.applyData({
    count: 0,
    increment() {
        this.count++; // DOM updates automatically!
    }
});

3. Template Syntax

Escaped Output (HTML-safe):

<p>{{ user.name }}</p>

Raw Output (requires allowRawHTML: true):

<div>{{{ htmlContent }}}</div>

With Filters:

<p>{{ price | currency:'$' }}</p>
<p>{{ description | truncate:100:'...' }}</p>

πŸ“– API Reference

Engine Methods

applyData(data, template?)

Renders the root template with the provided data.

engine.applyData({
    title: "My App",
    users: [...]
});

renderFrom(url, options?)

Fetches JSON data from a URL and renders it.

engine.renderFrom('/api/data')
    .then(data => console.log('Rendered:', data))
    .catch(err => console.error('Error:', err));

setState(pathOrObject, value?)

Updates global state.

// Object syntax
engine.setState({ user: { name: 'Alice' }, count: 0 });

// Path syntax
engine.setState('user.name', 'Bob');

watchState(path, callback)

Watches for changes to global state.

const unwatch = engine.watchState('user.name', (newVal, oldVal) => {
    console.log(`Name changed from ${oldVal} to ${newVal}`);
});

// Later: stop watching
unwatch();

setGlobalData(data)

Sets global data accessible to all components.

engine.setGlobalData({
    apiUrl: 'https://api.example.com',
    theme: 'dark'
});

registerComponent(name, definition)

Registers a custom component.

engine.registerComponent('user-card', {
    template: `<div class="card">{{ name }}</div>`,
    props: ['name'],
    data() { return { count: 0 }; },
    methods: { ... },
    computed: { ... },
    created() { ... },
    mounted() { ... }
});

registerFilter(name, filterFn)

Registers a custom filter.

engine.registerFilter('reverse', (str) => {
    return String(str).split('').reverse().join('');
});

registerDirective(name, handler)

Registers a custom directive (advanced).

engine.registerDirective('focus', (el, value, context) => {
    if (value) el.focus();
});

use(plugin, options?)

Installs a plugin.

const myPlugin = {
    install(engine, options) {
        engine.registerFilter('myFilter', ...);
    }
};
engine.use(myPlugin, { option1: true });

addHook(hookName, callback)

Adds a lifecycle hook.

engine.addHook('afterComponentMount', (context, instance) => {
    console.log('Component mounted:', instance._componentId);
});

request(url, options?)

Makes an HTTP request with interceptors.

engine.request('/api/users', { method: 'POST', body: JSON.stringify(user) })
    .then(data => console.log(data));

addRequestInterceptor(fn)

Intercepts requests before they're sent.

engine.addRequestInterceptor((options, url) => {
    options.headers = options.headers || {};
    options.headers['Authorization'] = 'Bearer ' + token;
    return options;
});

addResponseInterceptor(successFn, errorFn)

Intercepts responses.

engine.addResponseInterceptor(
    (response) => {
        console.log('Response received:', response);
        return response;
    },
    (error) => {
        console.error('Request failed:', error);
        return error;
    }
);

enableDebug() / disableDebug()

Toggles debug logging.

engine.enableDebug();
engine.disableDebug();

config(options)

Updates configuration (before locking).

engine.config({ debug: true, allowRawHTML: true });

lockConfig()

Prevents further configuration changes.

engine.lockConfig();

getPerformanceMetrics()

Returns performance data.

const metrics = engine.getPerformanceMetrics();
console.log(metrics); // { renders: 10, updates: 5, avgRenderTime: 12.5 }

destroy()

Cleans up the engine and all components.

engine.destroy();

πŸ”§ Directives

data-if

Conditionally renders an element.

<div data-if="isVisible">This shows when isVisible is true</div>
<div data-if="!isHidden">This shows when isHidden is false</div>

data-show

Toggles CSS display property.

<div data-show="isActive">Toggles visibility without removing from DOM</div>

data-repeat

Iterates over arrays or objects.

<!-- Array iteration -->
<li data-repeat="item in items">{{ item.name }}</li>

<!-- With index -->
<li data-repeat="item, index in items">{{ index }}: {{ item.name }}</li>

<!-- Object iteration -->
<div data-repeat="value, key in user">{{ key }}: {{ value }}</div>

<!-- Object with index -->
<div data-repeat="value, key, index in items">
    {{ index }} - {{ key }}: {{ value }}
</div>

data-on:event

Attaches event listeners.

<!-- Simple handler -->
<button data-on:click="handleClick">Click Me</button>

<!-- With arguments -->
<button data-on:click="deleteItem(item.id, $event)">Delete</button>

<!-- Multiple events -->
<input data-on:focus="onFocus" data-on:blur="onBlur">

data-model

Two-way data binding for form inputs.

<!-- Text input -->
<input data-model="username" type="text">

<!-- Checkbox -->
<input data-model="agreed" type="checkbox">

<!-- Radio buttons -->
<input data-model="color" type="radio" value="red">
<input data-model="color" type="radio" value="blue">

<!-- Select dropdown -->
<select data-model="country">
    <option value="us">USA</option>
    <option value="uk">UK</option>
</select>

<!-- Textarea -->
<textarea data-model="message"></textarea>

<!-- Bind to $state -->
<input data-model="$state.searchQuery" type="text">

<!-- Bind to globalData -->
<input data-model="globalData.theme" type="text">

data-bind:attr or :attr

Dynamic attribute binding.

<!-- Bind attribute -->
<img :src="imageUrl" :alt="imageAlt">

<!-- Boolean attributes -->
<button :disabled="isLoading">Submit</button>

<!-- Class binding -->
<div :class="{ active: isActive, 'text-bold': isBold }">Text</div>
<div :class="['btn', 'btn-primary', { disabled: isDisabled }]">Button</div>
<div :class="dynamicClass">Dynamic</div>

<!-- Style binding -->
<div :style="{ color: textColor, fontSize: size + 'px' }">Styled</div>
<div :style="'color: red; font-size: 16px;'">Inline Style</div>

data-text

Sets element's text content (safer than innerHTML).

<p data-text="message"></p>
<!-- Filters work here too -->
<p data-text="description | truncate:50"></p>

data-html

Sets element's HTML content (requires allowRawHTML: true).

<div data-html="htmlContent"></div>

data-safe-html

Sanitizes and sets HTML content (always safe).

<div data-safe-html="userGeneratedContent"></div>

data-fetch

Fetches data from a URL and binds it.

<div data-fetch="/api/users" 
     data-fetch-as="users"
     data-fetch-method="GET"
     data-fetch-cache="true"
     data-fetch-loading-class="loading"
     data-fetch-error-class="error">
    
    <div data-if="users.$loading">Loading users...</div>
    <div data-if="users.$error">Error: {{ users.$error }}</div>
    
    <ul data-if="users.data">
        <li data-repeat="user in users.data">{{ user.name }}</li>
    </ul>
</div>

Fetch state object:

{
    $loading: false,     // True while fetching
    $error: null,        // Error message if failed
    data: null,          // Fetched data
    _internal: { ... }   // Internal state
}

🧩 Components

Basic Component

engine.registerComponent('hello-world', {
    template: `
        <div class="hello">
            <h2>{{ greeting }}</h2>
            <p>{{ message }}</p>
        </div>
    `,
    data() {
        return {
            greeting: 'Hello',
            message: 'Welcome to Spandrix'
        };
    }
});
<hello-world></hello-world>

Component with Props

engine.registerComponent('user-card', {
    template: `
        <div class="card">
            <h3>{{ name }}</h3>
            <p>{{ bio | default:'No bio available' }}</p>
            <p>Age: {{ age }}</p>
        </div>
    `,
    props: {
        name: { type: String, required: true },
        age: { type: Number, default: 0 },
        bio: { default: '' }
    }
});
<user-card name="Alice" :age="30" :bio="userBio"></user-card>

Component with Methods

engine.registerComponent('counter', {
    template: `
        <div class="counter">
            <p>Count: {{ count }}</p>
            <button data-on:click="increment">+</button>
            <button data-on:click="decrement">-</button>
            <button data-on:click="reset">Reset</button>
        </div>
    `,
    data() {
        return { count: 0 };
    },
    methods: {
        increment() {
            this.count++;
        },
        decrement() {
            this.count--;
        },
        reset() {
            this.count = 0;
        }
    }
});

Component with Computed Properties

engine.registerComponent('price-calculator', {
    template: `
        <div>
            <input data-model="price" type="number" placeholder="Price">
            <input data-model="quantity" type="number" placeholder="Quantity">
            <p>Subtotal: {{ subtotal | currency }}</p>
            <p>Tax (10%): {{ tax | currency }}</p>
            <p>Total: {{ total | currency }}</p>
        </div>
    `,
    data() {
        return {
            price: 0,
            quantity: 0
        };
    },
    computed: {
        subtotal() {
            return this.price * this.quantity;
        },
        tax() {
            return this.subtotal * 0.1;
        },
        total() {
            return this.subtotal + this.tax;
        }
    }
});

Component with Watchers

engine.registerComponent('search-input', {
    template: `
        <div>
            <input data-model="query" placeholder="Search...">
            <p data-if="isSearching">Searching...</p>
        </div>
    `,
    data() {
        return {
            query: '',
            isSearching: false
        };
    },
    watch: {
        query(newVal, oldVal) {
            console.log(`Search query changed: ${oldVal} -> ${newVal}`);
            this.performSearch(newVal);
        }
    },
    methods: {
        performSearch(query) {
            this.isSearching = true;
            // Perform search...
            setTimeout(() => {
                this.isSearching = false;
            }, 1000);
        }
    }
});

Component Lifecycle

engine.registerComponent('lifecycle-demo', {
    template: `<div>{{ message }}</div>`,
    data() {
        return { message: 'Hello' };
    },
    created() {
        console.log('Component created');
        // Component instance created, data initialized
    },
    mounted() {
        console.log('Component mounted');
        // Component inserted into DOM
        // Can access this.$el
    },
    updated() {
        console.log('Component updated');
        // Called after re-render
    },
    beforeDestroy() {
        console.log('Component about to be destroyed');
        // Cleanup before component is removed
    },
    destroyed() {
        console.log('Component destroyed');
        // Component removed from DOM
    }
});

Component with Slots

engine.registerComponent('card-layout', {
    template: `
        <div class="card">
            <header class="card-header">
                <slot name="header"></slot>
            </header>
            <div class="card-body">
                <slot></slot>
            </div>
            <footer class="card-footer">
                <slot name="footer"></slot>
            </footer>
        </div>
    `
});
<card-layout>
    <template slot="header">
        <h2>Card Title</h2>
    </template>
    
    <p>This is the main content</p>
    
    <template slot="footer">
        <button>Close</button>
    </template>
</card-layout>

<!-- Vue-like syntax also supported -->
<card-layout>
    <template #header>
        <h2>Card Title</h2>
    </template>
    
    <p>Default slot content</p>
    
    <template #footer>
        <button>Close</button>
    </template>
</card-layout>

Component Events

engine.registerComponent('custom-button', {
    template: `
        <button data-on:click="handleClick">
            <slot></slot>
        </button>
    `,
    methods: {
        handleClick(event) {
            this.$emit('clicked', { message: 'Button was clicked!' });
        }
    }
});
<custom-button data-on:clicked="onButtonClicked">
    Click Me
</custom-button>

<script>
engine.applyData({
    onButtonClicked(data) {
        console.log(data.message);
    }
});
</script>

Two-Way Binding on Components (v-model)

engine.registerComponent('custom-input', {
    template: `
        <input :value="modelValue" 
               data-on:input="updateValue" 
               :placeholder="placeholder">
    `,
    props: {
        modelValue: { default: '' },
        placeholder: { default: '' }
    },
    model: {
        prop: 'modelValue',
        event: 'update:modelValue'
    },
    methods: {
        updateValue(event) {
            this.$emit('update:modelValue', event.target.value);
        }
    }
});
<custom-input data-model="username"></custom-input>
<!-- or -->
<custom-input :model-value="username" 
              data-on:update:modelValue="username = $event">
</custom-input>

.sync Modifier for Props

engine.registerComponent('dialog', {
    template: `
        <div data-if="visible">
            <button data-on:click="close">Close</button>
            <slot></slot>
        </div>
    `,
    props: ['visible'],
    methods: {
        close() {
            this.$emit('update:visible', false);
        }
    }
});
<dialog :visible.sync="isDialogOpen">
    Dialog content
</dialog>

Component API Methods

Inside a component, you have access to:

{
    // Properties
    this.$el,           // Host element
    this.$props,        // Component props
    this.$slots,        // Slot content
    this.$refs,         // Element references (future feature)
    this.$engine,       // Engine instance
    
    // Methods
    this.$emit(event, data),    // Emit custom event
    this.$update(),             // Force re-render
    this.$watch(path, callback),// Watch data changes
    this.$destroy(),            // Destroy component
    
    // Data & State
    this.dataProperty,          // Component data
    this.computedProperty,      // Computed properties
    this.method(),              // Component methods
    
    // Global access
    this.$state,                // Global state
    this.globalData             // Global data
}

🎨 Filters

Built-in Filters

uppercase

{{ text | uppercase }}

lowercase

{{ text | lowercase }}

capitalize

{{ text | capitalize }}
<!-- "hello world" -> "Hello world" -->

truncate

{{ text | truncate:50 }}
{{ text | truncate:50:'...' }}

currency

{{ price | currency }}
{{ price | currency:'€' }}
{{ price | currency:'$':2 }}

date

{{ timestamp | date }}
{{ timestamp | date:'short' }}
{{ timestamp | date:'long' }}
{{ timestamp | date:'time' }}

json

{{ object | json }}
{{ object | json:4 }}

default

{{ value | default:'N/A' }}

Custom Filters

// Simple filter
engine.registerFilter('reverse', (str) => {
    return String(str).split('').reverse().join('');
});

// Filter with arguments
engine.registerFilter('repeat', (str, times) => {
    return String(str).repeat(times);
});

// Complex filter
engine.registerFilter('highlight', (text, searchTerm) => {
    if (!searchTerm) return text;
    const regex = new RegExp(`(${searchTerm})`, 'gi');
    return String(text).replace(regex, '<mark>$1</mark>');
});

Chaining Filters

{{ description | truncate:100 | uppercase }}
{{ price | currency:'$' | default:'Free' }}
{{ user.bio | truncate:50:'...' | capitalize }}

πŸ—„οΈ State Management

Global State

// Initialize state
engine.setState({
    user: null,
    isAuthenticated: false,
    cart: [],
    theme: 'light'
});

// Update state
engine.setState('user', { name: 'Alice', id: 1 });
engine.setState('cart', [...engine.$state.cart, newItem]);

// Watch state changes
const unwatch = engine.watchState('user', (newUser, oldUser) => {
    console.log('User changed:', newUser);
});

// Access in templates
// {{ $state.user.name }}
// <div data-if="$state.isAuthenticated">...</div>

Global Data

// Set global data (available to all components)
engine.setGlobalData({
    apiUrl: 'https://api.example.com',
    version: '2.0.0',
    features: ['reactive', 'components', 'filters']
});

// Access in templates
// {{ globalData.version }}
// {{ globalData.apiUrl }}

// Access in components
this.globalData.apiUrl

Component-Level State

engine.registerComponent('todo-list', {
    template: `...`,
    data() {
        return {
            todos: [],
            newTodo: ''
        };
    },
    methods: {
        addTodo() {
            this.todos.push({
                id: Date.now(),
                text: this.newTodo,
                done: false
            });
            this.newTodo = '';
        }
    }
});

🌐 HTTP & Data Fetching

Basic Request

// GET request
engine.request('/api/users')
    .then(users => console.log(users))
    .catch(err => console.error(err));

// POST request
engine.request('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice' })
})
.then(response => console.log(response));

Load JSON

engine.loadJSON('/api/data')
    .then(data => console.log(data));

Render from URL

engine.renderFrom('/api/page-data')
    .then(data => console.log('Page rendered with:', data));

Request Interceptors

// Add auth token to all requests
engine.addRequestInterceptor((options, url) => {
    options.headers = options.headers || {};
    options.headers['Authorization'] = `Bearer ${getToken()}`;
    return options;
});

// Log all requests
engine.addRequestInterceptor((options, url) => {
    console.log('Request:', options.method, url);
    return options;
});

Response Interceptors

// Handle errors globally
engine.addResponseInterceptor(
    // Success handler
    (response, url, options) => {
        console.log('Success:', url);
        return response;
    },
    // Error handler
    (error, url, options) => {
        if (error.message.includes('401')) {
            redirectToLogin();
        }
        return error;
    }
);

Data Fetch Directive

<div data-fetch="/api/posts" 
     data-fetch-as="posts"
     data-fetch-method="GET"
     data-fetch-cache="true">
    
    <!-- Loading state -->
    <div data-if="posts.$loading">
        <x-loading message="Loading posts..."></x-loading>
    </div>
    
    <!-- Error state -->
    <div data-if="posts.$error">
        <x-alert type="error" :title="posts.$error">
            Failed to load posts.
        </x-alert>
    </div>
    
    <!-- Success state -->
    <div data-if="posts.data">
        <article data-repeat="post in posts.data">
            <h2>{{ post.title }}</h2>
            <p>{{ post.excerpt | truncate:200 }}</p>
        </article>
    </div>
</div>

CSRF Protection

SpandrixEngine automatically includes CSRF tokens in POST/PUT/PATCH/DELETE requests:

// Token is read from cookie and added to header
const engine = new SpandrixEngine('#app', {
    csrfCookieName: 'XSRF-TOKEN',
    csrfHeaderName: 'X-XSRF-TOKEN'
});

πŸ”„ Lifecycle Hooks

Global Hooks

engine.addHook('beforeComponentCreate', (context, instance) => {
    console.log('Creating component:', instance._componentId);
});

engine.addHook('afterComponentMount', (context, instance) => {
    console.log('Component mounted:', instance._componentId);
});

engine.addHook('beforeRootRender', (engine, template, data) => {
    console.log('About to render root');
});

engine.addHook('afterRootRender', (engine, template, data) => {
    console.log('Root render complete');
});

Available Hooks

  • beforeComponentCreate
  • afterComponentCreate
  • beforeComponentMount
  • afterComponentMount
  • beforeComponentUpdate
  • afterComponentUpdate
  • beforeComponentDestroy
  • afterComponentDestroy
  • beforeRootRender
  • afterRootRender

Component Lifecycle

engine.registerComponent('example', {
    template: `<div>{{ message }}</div>`,
    data() {
        return { message: 'Hello' };
    },
    created() {
        // Data initialized, before DOM creation
        console.log('created');
    },
    mounted() {
        // Component in DOM, can access this.$el
        console.log('mounted');
    },
    updated() {
        // After re-render (data changed)
        console.log('updated');
    },
    beforeDestroy() {
        // Before component removal
        console.log('beforeDestroy');
    },
    destroyed() {
        // Component removed from DOM
        console.log('destroyed');
    }
});

πŸš€ Advanced Usage

Plugin System

const MyPlugin = {
    install(engine, options) {
        // Add custom filter
        engine.registerFilter('myFilter', (val) => {
            return val.toUpperCase();
        });
        
        // Add custom component
        engine.registerComponent('my-component', {
            template: '<div>Plugin Component</div>'
        });
        
        // Add global method
        engine.myMethod = function() {
            console.log('Custom method');
        };
        
        // Hook into lifecycle
        engine.addHook('afterComponentMount', (ctx, inst) => {
            console.log('Plugin: component mounted');
        });
    }
};

engine.use(MyPlugin, { option: 'value' });

Performance Monitoring

const engine = new SpandrixEngine('#app', {
    enablePerformanceMetrics: true
});

// Get metrics
const metrics = engine.getPerformanceMetrics();
console.log(metrics);
// { renders: 10, updates: 5, avgRenderTime: 12.5 }

// Reset metrics
engine.resetPerformanceMetrics();

Custom Directives

// Register custom directive
engine.registerDirective('focus', (element, value, dataContext, componentInstance) => {
    if (value) {
        element.focus();
    }
});
<input data-focus="shouldFocus" type="text">

Vue Syntax Compatibility

SpandrixEngine automatically converts Vue.js syntax:

<!-- Vue syntax (converted automatically) -->
<div v-if="condition">Content</div>
<div v-show="visible">Content</div>
<div v-for="item in items">{{ item }}</div>
<button v-on:click="handler">Click</button>
<button @click="handler">Click</button>
<input v-bind:value="val">
<input :value="val">
<input v-model="data">

<!-- Becomes Spandrix syntax -->
<div data-if="condition">Content</div>
<div data-show="visible">Content</div>
<div data-repeat="item in items">{{ item }}</div>
<button data-on:click="handler">Click</button>
<button data-on:click="handler">Click</button>
<input data-bind:value="val">
<input data-bind:value="val">
<input data-model="data">

Dynamic Templates

engine.registerComponent('dynamic-template', {
    template() {
        // Template can be a function
        if (this.mode === 'list') {
            return `<ul><li data-repeat="item in items">{{ item }}</li></ul>`;
        } else {
            return `<div data-repeat="item in items">{{ item }}</div>`;
        }
    },
    props: ['mode', 'items']
});

Nested Components

engine.registerComponent('parent-component', {
    template: `
        <div class="parent">
            <h2>Parent</h2>
            <child-component :message="childMessage"></child-component>
        </div>
    `,
    data() {
        return {
            childMessage: 'Hello from parent'
        };
    }
});

engine.registerComponent('child-component', {
    template: `
        <div class="child">
            <p>{{ message }}</p>
        </div>
    `,
    props: ['message']
});

Accessing Parent Component

engine.registerComponent('nested', {
    template: `<div>{{ parentData }}</div>`,
    computed: {
        parentData() {
            // Access parent component instance (use with caution)
            return this._parentComponentInstance?.someData || 'No parent';
        }
    }
});

Manual Component Updates

engine.registerComponent('manual-update', {
    template: `<div>{{ time }}</div>`,
    data() {
        return { time: Date.now() };
    },
    mounted() {
        // Update every second
        this.timer = setInterval(() => {
            this.time = Date.now();
            // this.$update(); // Not needed, data change triggers update
        }, 1000);
    },
    beforeDestroy() {
        clearInterval(this.timer);
    }
});

Watchers in Components

engine.registerComponent('search-with-debounce', {
    template: `
        <div>
            <input data-model="query" placeholder="Search...">
            <p>{{ results.length }} results</p>
        </div>
    `,
    data() {
        return {
            query: '',
            results: []
        };
    },
    mounted() {
        // Watch with $watch
        this.$watch('query', (newVal, oldVal) => {
            this.debouncedSearch(newVal);
        });
    },
    methods: {
        debouncedSearch: debounce(function(query) {
            // Perform search
            fetch(`/api/search?q=${query}`)
                .then(r => r.json())
                .then(data => {
                    this.results = data;
                });
        }, 300)
    }
});

Error Handling

const engine = new SpandrixEngine('#app', {
    strictExpressions: true // Throw errors instead of silent fails
});

// Global error handling via hooks
engine.addHook('afterComponentCreate', (context, instance) => {
    try {
        // Your code
    } catch (error) {
        console.error('Component creation error:', error);
    }
});

Memory Management

// Cleanup when done
engine.destroy();

// Component-level cleanup
engine.registerComponent('cleanup-demo', {
    template: `<div>Component</div>`,
    mounted() {
        this.interval = setInterval(() => {
            console.log('Tick');
        }, 1000);
    },
    beforeDestroy() {
        // Clean up to prevent memory leaks
        clearInterval(this.interval);
        this.interval = null;
    }
});

πŸ’‘ Examples

Complete Todo App

<!DOCTYPE html>
<html>
<head>
    <title>Todo App - SpandrixEngine</title>
    <style>
        .todo-app { max-width: 600px; margin: 50px auto; font-family: sans-serif; }
        .todo-item { padding: 10px; border-bottom: 1px solid #ddd; }
        .todo-item.done { text-decoration: line-through; opacity: 0.6; }
        .controls { margin: 20px 0; }
        .controls button { margin-right: 10px; }
    </style>
</head>
<body>
    <div id="app">
        <div class="todo-app">
            <h1>{{ title }}</h1>
            
            <div class="controls">
                <input data-model="newTodo" 
                       data-on:keyup.enter="addTodo"
                       placeholder="What needs to be done?">
                <button data-on:click="addTodo">Add</button>
            </div>
            
            <div class="filters">
                <button data-on:click="filter = 'all'" 
                        :class="{ active: filter === 'all' }">All</button>
                <button data-on:click="filter = 'active'"
                        :class="{ active: filter === 'active' }">Active</button>
                <button data-on:click="filter = 'done'"
                        :class="{ active: filter === 'done' }">Done</button>
            </div>
            
            <div class="todo-list">
                <div class="todo-item" 
                     data-repeat="todo in filteredTodos"
                     :class="{ done: todo.done }">
                    <input type="checkbox" 
                           data-model="todo.done"
                           data-on:change="saveTodos">
                    <span>{{ todo.text }}</span>
                    <button data-on:click="removeTodo(todo.id)">Delete</button>
                </div>
            </div>
            
            <p>{{ stats }}</p>
        </div>
    </div>

    <script src="spandrix.min.js"></script>
    <script>
        const engine = new SpandrixEngine('#app', { debug: true });
        
        engine.applyData({
            title: 'My Todo List',
            newTodo: '',
            filter: 'all',
            todos: JSON.parse(localStorage.getItem('todos') || '[]'),
            
            computed: {
                filteredTodos() {
                    if (this.filter === 'active') {
                        return this.todos.filter(t => !t.done);
                    } else if (this.filter === 'done') {
                        return this.todos.filter(t => t.done);
                    }
                    return this.todos;
                },
                stats() {
                    const total = this.todos.length;
                    const done = this.todos.filter(t => t.done).length;
                    return `${done} / ${total} completed`;
                }
            },
            
            addTodo() {
                if (!this.newTodo.trim()) return;
                this.todos.push({
                    id: Date.now(),
                    text: this.newTodo,
                    done: false
                });
                this.newTodo = '';
                this.saveTodos();
            },
            
            removeTodo(id) {
                this.todos = this.todos.filter(t => t.id !== id);
                this.saveTodos();
            },
            
            saveTodos() {
                localStorage.setItem('todos', JSON.stringify(this.todos));
            }
        });
    </script>
</body>
</html>

User Dashboard

<div id="app">
    <div class="dashboard">
        <header>
            <h1>Dashboard</h1>
            <div data-if="$state.user">
                Welcome, {{ $state.user.name }}!
                <button data-on:click="logout">Logout</button>
            </div>
        </header>
        
        <!-- Fetch users from API -->
        <div data-fetch="/api/users" 
             data-fetch-as="users"
             data-fetch-loading-class="loading">
            
            <x-loading data-if="users.$loading" 
                       message="Loading users..."></x-loading>
            
            <x-alert data-if="users.$error" 
                     type="error" 
                     :title="users.$error"></x-alert>
            
            <div data-if="users.data" class="user-grid">
                <user-card data-repeat="user in users.data"
                          :name="user.name"
                          :email="user.email"
                          :avatar="user.avatar"
                          data-on:view="viewUser(user)">
                </user-card>
            </div>
        </div>
    </div>
</div>

<script>
const engine = new SpandrixEngine('#app');

// Set initial state
engine.setState({
    user: { name: 'Alice', id: 1 },
    selectedUser: null
});

// Register user card component
engine.registerComponent('user-card', {
    template: `
        <div class="card">
            <img :src="avatar" :alt="name">
            <h3>{{ name }}</h3>
            <p>{{ email }}</p>
            <button data-on:click="$emit('view')">View Profile</button>
        </div>
    `,
    props: ['name', 'email', 'avatar']
});

engine.applyData({
    viewUser(user) {
        engine.setState('selectedUser', user);
        console.log('Viewing:', user);
    },
    logout() {
        engine.setState('user', null);
        // Redirect to login
    }
});
</script>

Form Validation

<div id="app">
    <form-validator>
        <h2>Sign Up</h2>
        
        <div class="field">
            <label>Username</label>
            <input data-model="username" data-on:blur="validateUsername">
            <span class="error" data-if="errors.username">
                {{ errors.username }}
            </span>
        </div>
        
        <div class="field">
            <label>Email</label>
            <input data-model="email" type="email" data-on:blur="validateEmail">
            <span class="error" data-if="errors.email">
                {{ errors.email }}
            </span>
        </div>
        
        <div class="field">
            <label>Password</label>
            <input data-model="password" type="password" data-on:blur="validatePassword">
            <span class="error" data-if="errors.password">
                {{ errors.password }}
            </span>
        </div>
        
        <button data-on:click="submit" :disabled="!isValid">
            Submit
        </button>
    </form-validator>
</div>

<script>
const engine = new SpandrixEngine('#app');

engine.registerComponent('form-validator', {
    template: `<div class="form"><slot></slot></div>`,
    data() {
        return {
            username: '',
            email: '',
            password: '',
            errors: {}
        };
    },
    computed: {
        isValid() {
            return this.username && this.email && this.password &&
                   Object.keys(this.errors).length === 0;
        }
    },
    methods: {
        validateUsername() {
            if (!this.username) {
                this.errors.username = 'Username is required';
            } else if (this.username.length < 3) {
                this.errors.username = 'Username must be at least 3 characters';
            } else {
                delete this.errors.username;
            }
        },
        validateEmail() {
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            if (!this.email) {
                this.errors.email = 'Email is required';
            } else if (!emailRegex.test(this.email)) {
                this.errors.email = 'Invalid email format';
            } else {
                delete this.errors.email;
            }
        },
        validatePassword() {
            if (!this.password) {
                this.errors.password = 'Password is required';
            } else if (this.password.length < 8) {
                this.errors.password = 'Password must be at least 8 characters';
            } else {
                delete this.errors.password;
            }
        },
        submit() {
            this.validateUsername();
            this.validateEmail();
            this.validatePassword();
            
            if (this.isValid) {
                console.log('Form submitted:', {
                    username: this.username,
                    email: this.email,
                    password: this.password
                });
            }
        }
    }
});
</script>

Real-time Search

<div id="app">
    <search-box></search-box>
</div>

<script>
const engine = new SpandrixEngine('#app');

engine.registerComponent('search-box', {
    template: `
        <div class="search">
            <input data-model="query" 
                   placeholder="Search products..."
                   data-on:input="onSearch">
            
            <x-loading data-if="isSearching" 
                       message="Searching..."></x-loading>
            
            <div class="results" data-if="results.length && !isSearching">
                <div class="result" data-repeat="result in results">
                    <h4>{{ result.name }}</h4>
                    <p>{{ result.description | truncate:100 }}</p>
                    <span class="price">{{ result.price | currency }}</span>
                </div>
            </div>
            
            <p data-if="!results.length && !isSearching && query">
                No results found for "{{ query }}"
            </p>
        </div>
    `,
    data() {
        return {
            query: '',
            results: [],
            isSearching: false,
            debounceTimer: null
        };
    },
    methods: {
        onSearch() {
            clearTimeout(this.debounceTimer);
            
            if (!this.query.trim()) {
                this.results = [];
                return;
            }
            
            this.debounceTimer = setTimeout(() => {
                this.performSearch();
            }, 300);
        },
        async performSearch() {
            this.isSearching = true;
            
            try {
                const response = await fetch(`/api/search?q=${this.query}`);
                this.results = await response.json();
            } catch (error) {
                console.error('Search error:', error);
                this.results = [];
            } finally {
                this.isSearching = false;
            }
        }
    },
    beforeDestroy() {
        clearTimeout(this.debounceTimer);
    }
});
</script>

Modal Dialog System

<div id="app">
    <button data-on:click="openModal">Open Modal</button>
    
    <x-modal data-model="isModalOpen" 
             title="Confirmation"
             :close-on-click-overlay="true">
        
        <p>Are you sure you want to proceed?</p>
        
        <template slot="footer">
            <button data-on:click="confirm">Confirm</button>
            <button data-on:click="isModalOpen = false">Cancel</button>
        </template>
    </x-modal>
</div>

<script>
const engine = new SpandrixEngine('#app');

engine.applyData({
    isModalOpen: false,
    openModal() {
        this.isModalOpen = true;
    },
    confirm() {
        console.log('Confirmed!');
        this.isModalOpen = false;
    }
});
</script>

🌍 Browser Support

Browser Version
Chrome β‰₯ 60
Firefox β‰₯ 60
Safari β‰₯ 12
Edge β‰₯ 79
Opera β‰₯ 47

Required Features:

  • ES6 Proxy
  • ES6 Classes
  • Promises
  • Fetch API (or polyfill)

⚑ Performance

Optimization Tips

  1. Use data-show instead of data-if when toggling frequently

    <!-- Better for frequent toggles -->
    <div data-show="isVisible">Content</div>
    
    <!-- Better for conditional rendering -->
    <div data-if="isVisible">Content</div>
  2. Avoid deep nesting in data-repeat

    <!-- Less optimal -->
    <div data-repeat="cat in categories">
        <div data-repeat="item in cat.items">
            <div data-repeat="variant in item.variants">
                ...
            </div>
        </div>
    </div>
    
    <!-- Better: flatten data structure -->
    <div data-repeat="item in flattenedItems">...</div>
  3. Use computed properties for expensive operations

    computed: {
        expensiveValue() {
            // Only recalculated when dependencies change
            return this.items.filter(...).map(...).reduce(...);
        }
    }
  4. Debounce user input handlers

    methods: {
        onInput: debounce(function(event) {
            this.search(event.target.value);
        }, 300)
    }
  5. Enable performance metrics in development

    const engine = new SpandrixEngine('#app', {
        enablePerformanceMetrics: true,
        debug: true
    });
  6. Clean up in lifecycle hooks

    mounted() {
        this.timer = setInterval(() => {...}, 1000);
    },
    beforeDestroy() {
        clearInterval(this.timer); // Prevent memory leaks
    }

Benchmarks

Operation Time (avg)
Initial render (1000 items) ~15ms
Update single item ~2ms
Re-render component ~5ms
Filter 1000 items ~3ms
Component mount ~1ms

Tested on Chrome 120, Intel i7, 16GB RAM


πŸ”’ Security

XSS Protection

By default, SpandrixEngine sanitizes all output:

<!-- Safe: automatically escaped -->
<p>{{ userInput }}</p>

<!-- Unsafe: requires allowRawHTML: true -->
<div>{{{ htmlContent }}}</div>

<!-- Always safe: sanitized even if raw -->
<div data-safe-html="userContent"></div>

Expression Validation

const engine = new SpandrixEngine('#app', {
    warnOnUnsafeEval: true // Default: true
});

// These are blocked:
// {{ eval('malicious code') }}
// {{ Function('return malicious')() }}
// {{ setTimeout('malicious', 0) }}

CSRF Protection

// Automatically adds CSRF token to mutations
const engine = new SpandrixEngine('#app', {
    csrfCookieName: 'XSRF-TOKEN',
    csrfHeaderName: 'X-XSRF-TOKEN'
});

Content Security Policy

SpandrixEngine is CSP-friendly when strictExpressions: false (default):

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self'">

πŸ› Troubleshooting

Common Issues

1. "Expression not updating"

// ❌ Wrong: Modifying non-reactive object
const data = { count: 0 };
engine.applyData(data);
data.count++; // Won't trigger update

// βœ… Correct: Use reactive proxy
engine.applyData({ count: 0 });
engine._currentRootData.count++; // Triggers update

2. "Component not re-rendering"

// ❌ Wrong: Adding property after initialization
data() {
    return { items: [] };
},
methods: {
    addProp() {
        this.newProp = 'value'; // Not reactive!
    }
}

// βœ… Correct: Define all properties upfront
data() {
    return {
        items: [],
        newProp: null // Will be reactive
    };
}

3. "Filter not found"

// Make sure filter is registered before use
engine.registerFilter('myFilter', (val) => val);
engine.applyData({...}); // Now can use {{ value | myFilter }}

4. "Memory leak"

// ❌ Wrong: Not cleaning up
mounted() {
    this.timer = setInterval(() => {...}, 1000);
}

// βœ… Correct: Clean up
beforeDestroy() {
    clearInterval(this.timer);
}

5. "data-fetch not working"

<!-- Make sure you're checking the right properties -->
<div data-fetch="/api/data" data-fetch-as="myData">
    <!-- ❌ Wrong -->
    <div data-if="myData">{{ myData }}</div>
    
    <!-- βœ… Correct -->
    <div data-if="myData.data">{{ myData.data }}</div>
</div>

Debug Mode

const engine = new SpandrixEngine('#app', { debug: true });
// Logs all reactive changes, renders, and lifecycle events

πŸ“š Migration from Other Frameworks

From Vue.js

SpandrixEngine is inspired by Vue and shares similar syntax:

// Vue
new Vue({
    el: '#app',
    data: { count: 0 },
    methods: { increment() { this.count++; } }
});

// Spandrix
const engine = new SpandrixEngine('#app');
engine.applyData({
    count: 0,
    increment() { this.count++; }
});

Differences:

  • No virtual DOM (direct DOM manipulation)
  • Smaller bundle size
  • Simpler API
  • No single-file components (.vue)

From React

// React
function Counter() {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// Spandrix
engine.registerComponent('counter', {
    template: '<button data-on:click="increment">{{ count }}</button>',
    data() { return { count: 0 }; },
    methods: { increment() { this.count++; } }
});

From jQuery

// jQuery
$('#button').click(function() {
    var count = parseInt($('#count').text()) + 1;
    $('#count').text(count);
});

// Spandrix
engine.applyData({
    count: 0,
    increment() { this.count++; }
});
// Template: <button data-on:click="increment">{{ count }}</button>

🀝 Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

Development Setup

git clone https://github.com/agntperfect/spandrixJS.git
cd spandrixJS
npm install
npm run dev

Running Tests

npm test

Building

npm run build

πŸ“„ License

MIT License - see LICENSE file for details.


πŸ—ΊοΈ Roadmap

v2.1 (Planned)

  • TypeScript definitions
  • Dev tools browser extension
  • SSR (Server-Side Rendering)
  • Virtual scrolling component
  • Animation system

v2.2 (Future)

  • Time-travel debugging
  • Component lazy loading
  • i18n plugin
  • Form validation plugin
  • State persistence plugin

πŸ“Š Stats

  • Size: ~45KB minified (~12KB gzipped)
  • Dependencies: 0
  • Bundle: UMD, ESM, CommonJS
  • License: MIT
  • First Release: 2025
  • Current Version: 2.0.0

Built with passion. Driven by philosophy. Rendered with purpose.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published