Skip to content

DigitalCoreHub/TinyPine

Repository files navigation

TinyPine.js

TinyPine.js

Minimal reactive micro-framework

Zero build · Zero config · Just write HTML

Version Size Tests License


📋 Table of Contents


⚡ Quick Start

CDN (Easiest)

<!DOCTYPE html>
<html>
    <head>
        <title>My App</title>
    </head>
    <body>
        <div t-data="{ message: 'Hello TinyPine!' }">
            <h1 t-text="message"></h1>
            <button t-click="message = 'Clicked!'">Click me</button>
        </div>

        <script src="https://unpkg.com/[email protected]/dist/tinypine.min.js"></script>
        <script>
            TinyPine.init();
        </script>
    </body>
</html>

NPM

npm install tinypine
import TinyPine from "tinypine";
TinyPine.init();

🚀 CLI Tool (v1.1.0)

Create TinyPine projects in seconds:

# Create a new project
npx tinypine-cli new myapp

# Start development server
cd myapp && npx tinypine-cli serve

# Add features (router, i18n, ui)
npx tinypine-cli add router
npx tinypine-cli add i18n
npx tinypine-cli add ui

# Build for production
npx tinypine-cli build

Features:

  • 🎨 Templates: Vanilla, Tailwind, SPA, SSR, UI Ready
  • 🔌 Modular add-ons: router, i18n, ui, devtools
  • ⚡ Vite dev/build integration

Learn more →


✨ What's New

🧭 Routes & Guards (v1.5.0)

Smart client-side routing with navigation guards, dynamic params, and programmatic control:

Smart Router with Dynamic Parameters:

<nav>
    <a t-link="'home'">Home</a>
    <a t-link="'about'">About</a>
    <a t-link="'user/42'">User Profile</a>
</nav>

<!-- Route Views -->
<div t-route="'home'">
    <h1>Welcome Home!</h1>
</div>

<div t-route="'about'">
    <h1>About Us</h1>
</div>

<!-- Dynamic Parameters -->
<div t-route="'user/:id'">
    <div t-data="{}">
        <h1 t-text="'User ID: ' + $route.params.id"></h1>
        <p t-text="'Path: ' + $route.path"></p>
    </div>
</div>

<!-- 404 Fallback -->
<div t-route="*">
    <h1>404 - Page Not Found</h1>
</div>

<script>
    TinyPine.router({
        default: "home",
    });
    TinyPine.init();
</script>

Navigation Guards:

TinyPine.router({
    // Global beforeEnter guard
    beforeEnter(to, from) {
        const isLoggedIn = TinyPine.store("auth").loggedIn;

        if (!isLoggedIn && to.path !== "login") {
            return "/login"; // Redirect to login
        }

        return true; // Allow navigation
    },

    // Global beforeLeave guard
    beforeLeave(to, from) {
        if (hasUnsavedChanges) {
            return confirm("Discard unsaved changes?");
        }
        return true;
    },

    // Route-specific guards
    routes: {
        admin: {
            beforeEnter: async (to, from) => {
                const isAdmin = await checkAdminRole();
                return isAdmin ? true : "/";
            },
        },
    },
});

Programmatic Navigation:

<div t-data="{ userId: 42 }">
    <!-- Using $router in templates -->
    <button t-click="$router.push('/user/' + userId)">View Profile</button>

    <button t-click="$router.back()">Go Back</button>

    <!-- Access route info -->
    <p t-text="'Current: ' + $router.current().path"></p>
</div>

t-link Directive with Active State:

<nav>
    <a t-link="'home'" class="nav-link">Home</a>
    <a t-link="'about'" class="nav-link">About</a>
    <a t-link="'contact'" class="nav-link">Contact</a>
</nav>

<style>
    .nav-link.active {
        color: blue;
        font-weight: bold;
    }
</style>

Route Lifecycle Events:

// Listen to route changes
TinyPine.router.on("route:change", (to, from) => {
    console.log("Navigated from", from.path, "to", to.path);

    // Track page views
    if (typeof gtag !== "undefined") {
        gtag("config", "GA_ID", { page_path: to.path });
    }
});

// Other events: route:enter, route:leave, route:error
TinyPine.router.on("route:enter", (to) => {
    document.title = `My App - ${to.path}`;
});

Router Features:

  • ✅ Dynamic route matching (user/:id, blog/:category/:post)
  • ✅ Nested routes support
  • ✅ Wildcard fallback (* for 404s)
  • ✅ Navigation guards (sync & async)
  • ✅ Query parameters (?tab=posts&page=1)
  • ✅ Programmatic navigation API
  • ✅ Active link detection
  • ✅ Route lifecycle events
  • ✅ Scroll behavior control

📖 Complete Router Guide →


�� Async Flow & Forms (v1.4.0)

Powerful async operations and comprehensive form validation:

Enhanced t-fetch with Debounce:

<div t-data="{ search: '', results: [] }">
    <!-- Debounce fetch requests -->
    <button
        t-fetch="'/api/search?q=' + search"
        debounce="500"
        method="POST"
        headers="{ 'Authorization': 'Bearer ' + token }"
    >
        Search
    </button>

    <!-- Lifecycle hooks -->
    <script>
        TinyPine.context({
            beforeFetch: async ({ url, method }) => {
                console.log("Starting request:", url);
                return true; // or false to cancel
            },
            afterFetch: ({ data }) => {
                console.log("Received:", data);
            },
        });
    </script>
</div>

Form Validation System:

<tp-form>
    <div t-data="{ email: '', password: '' }">
        <!-- Built-in validators -->
        <input t-model="email" t-validate="required|email" name="email" />

        <input
            t-model="password"
            t-validate="required|minLength:8"
            name="password"
            type="password"
        />

        <!-- Show validation errors -->
        <p t-show="$errors.email" t-text="$errors.email[0]?.message"></p>

        <!-- Disable submit if invalid -->
        <button type="submit" :disabled="$invalid">Submit</button>
    </div>
</tp-form>

Debounced Inputs:

<div t-data="{ searchQuery: '' }">
    <!-- Update state after 300ms of inactivity -->
    <input t-model="searchQuery" t-debounce="300" />
    <p t-text="'Searching for: ' + searchQuery"></p>
</div>

Built-in Validators:

  • required - Field must not be empty
  • email - Valid email format
  • min:N / max:N - Numeric range
  • minLength:N / maxLength:N - String length
  • pattern:regex - Custom regex pattern
  • url - Valid URL format
  • number - Numeric value
  • integer - Whole number

Form State Variables:

  • $errors - Validation error messages per field
  • $valid / $invalid - Overall form validity
  • $touched - Fields that have been focused
  • $dirty - Fields that have been modified
  • $pending - Async operations in progress

�🔒 Core Stability (v1.3.0)

Enhanced security and developer experience with powerful new features:

Safe Expression Evaluator:

<!-- Multiple statements with semicolons -->
<div t-data="{ count: 0 }">
    <button t-click="const doubled = count * 2; count = doubled">Double</button>
</div>

<!-- Ternary operators -->
<p t-text="count > 10 ? 'High' : 'Low'"></p>

<!-- Arrow functions -->
<button t-click="items.filter(x => x.active).length">Active Count</button>

XSS-Protected HTML Rendering:

<div t-data="{ content: '<p>Safe HTML</p>' }">
    <!-- Automatically sanitizes dangerous tags and scripts -->
    <div t-html="content"></div>
</div>

Shorthand Binding Syntax:

<!-- Use :attr instead of t-bind:attr -->
<img :src="imageUrl" :alt="imageAlt" />
<div :class="activeClass" :id="elementId"></div>

Enhanced Loop Context:

<ul>
    <li t-for="item in items" t-class:first="$first" t-class:last="$last">
        <span t-text="$index + 1"></span>: <span t-text="item"></span>
    </li>
</ul>

New Form Components:

  • tp-field - Form field wrapper with labels, validation, and error states
  • tp-input - Text input with icons, sizes, and validation states
  • tp-checkbox - Custom checkbox with labels
  • tp-file-upload - Drag & drop file upload with preview

📖 Form Components Guide →


🎨 TinyPine UI (v1.2.0)

Ready-to-use Tailwind CSS components:

<script src="https://unpkg.com/[email protected]/dist/tinypine.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/tinypine.ui.min.js"></script>
<link
    rel="stylesheet"
    href="https://unpkg.com/[email protected]/dist/tinypine.ui.css"
/>
<script src="https://cdn.tailwindcss.com"></script>

<div t-data="{ open: false }">
    <tp-button color="primary" size="md" icon="check">Save</tp-button>
    <tp-modal t-show="open" title="Confirm">
        <p>Are you sure?</p>
        <tp-button color="outline" t-click="open = false">Cancel</tp-button>
    </tp-modal>
    <tp-card title="User Info">
        <p>Content goes here</p>
    </tp-card>
</div>

<script>
    TinyPine.init();
</script>

Available Components:

  • tp-button - Button with color, size, type, icon props
  • tp-modal - Modal with title prop, auto backdrop/close
  • tp-card - Card with title prop, Tailwind styling

Theme Support:

TinyPine.theme = "dark"; // or 'light'

📖 Complete UI Usage Guide → | 📖 Türkçe Kılavuz →


🔄 Component Lifecycle (v1.1.2+)

Mount Lifecycle

Run code after a component is rendered:

<div
    t-data="{
  count: 0,
  mounted(el, ctx) {
    el.classList.add('mounted');
    console.log('Component mounted!');
  }
}"
>
    <span t-text="'Count: ' + count"></span>
    <button t-click="count++">+1</button>
</div>

Global mount listener:

TinyPine.onMount((el, ctx) => {
    console.log("🌱 Mounted:", el);
});

Unmount Lifecycle (v1.1.3)

Run cleanup when a component is removed:

<div
    t-data="{
  count: 0,
  beforeUnmount(el, ctx) {
    console.log('About to remove...');
  },
  unmounted(el, ctx) {
    console.log('Removed!');
  }
}"
>
    <span t-text="'Count: ' + count"></span>
</div>

Global unmount listener:

TinyPine.onUnmount((el, ctx) => {
    console.log("🧹 Cleaned up:", el);
});

Emits component:mounted and component:unmounted events via the global event bus.


🌱 Sprout.js Readiness (v1.1.1)

TinyPine v1.1.1 introduces features for educational and sandbox environments:

Lite Mode:

TinyPine.start("#app", { mode: "lite" });
// Disables: devtools, store, router, i18n

Safe Mode:

TinyPine.start("#app", { safe: true });
// Wraps all directive executions in try/catch

Silent Debug:

TinyPine.debugOptions.silent = true;
// Suppresses [TinyPine] console logs

Global Event Bus:

TinyPine.on("directive:click", (el, ctx) => {
    console.log("Click detected");
});

🎯 Core Features

  • 100 Passing Tests - Comprehensive test coverage
  • 📘 TypeScript Support - Full type definitions included
  • 🛠️ DevTools Integration - Live debugging & inspection
  • 🌍 i18n Ready - Built-in internationalization
  • 🔄 Global Store - Shared state management
  • 📡 Async Support - t-fetch, t-await directives
  • 🌐 Router System - Hash-based navigation
  • 🎨 Transitions - Smooth animations built-in
  • 🔌 Plugin API - Extensible architecture
  • 📊 Performance - Optimized for production

📚 Directives

Core Directives

Directive Description Example
t-data Create reactive scope <div t-data="{ count: 0 }">
t-text Update text content <span t-text="message"></span>
t-html Update HTML content (XSS-safe, v1.3.0) <div t-html="content"></div>
t-show Toggle visibility (with transitions) <p t-show="isVisible">Hello</p>
t-click Click handlers <button t-click="count++">+</button>
t-model Two-way binding <input t-model="name">

List Rendering & Events

Directive Description Example
t-for List rendering <li t-for="item in items">
.prevent/.stop Event modifiers <button t-click="save.prevent">
t-init Lifecycle hook <div t-init="console.log('Mounted!')">

Advanced Bindings

Directive Description Example
t-bind Dynamic attributes <img t-bind:src="url">
t-class Conditional classes <div t-class:active="isActive">
t-ref DOM references <input t-ref="input">
t-transition CSS transitions <div t-transition="fade">

🎯 Examples

Counter App

<div t-data="{ count: 0 }">
    <button t-click="count--">-</button>
    <span t-text="count"></span>
    <button t-click="count++">+</button>
</div>

Todo List

<div
    t-data="{
  todos: [],
  newTodo: '',
  methods: {
    add() {
      if(this.newTodo) this.todos.push(this.newTodo);
      this.newTodo = '';
    },
    remove(i) {
      this.todos.splice(i, 1);
    }
  }
}"
>
    <input t-model="newTodo" placeholder="Add todo" />
    <button t-click="methods.add()">Add</button>

    <ul>
        <li t-for="(todo, i) in todos">
            <span t-text="todo"></span>
            <button t-click="methods.remove(i)">Remove</button>
        </li>
    </ul>
</div>

Login Form

<div
    t-data="{
  email: '',
  password: '',
  loggedIn: false,
  methods: {
    login() {
      this.loggedIn = true;
    }
  }
}"
>
    <div t-show="!loggedIn">
        <input t-model="email" placeholder="Email" />
        <input t-model="password" type="password" placeholder="Password" />
        <button t-click="methods.login()">Login</button>
    </div>

    <div t-show="loggedIn">
        <p t-text="'Welcome, ' + email"></p>
    </div>
</div>

🔧 Advanced Features

Global Store

Create shared state across components:

// Create stores
TinyPine.store("auth", { user: "Guest", loggedIn: false });
TinyPine.store("ui", { theme: "light" });

// Use in any component
<div t-data="{}">
    <span t-text="$store.auth.user"></span>
    <button t-click="$store.auth.loggedIn = true">Login</button>
</div>;

Custom Plugins

TinyPine.use({
    name: "Toast",
    init(TinyPine) {
        TinyPine.directive("toast", (el, message) => {
            alert(message);
        });
    },
});

Async Data Fetching

Enhanced t-fetch (v1.3.0)

Fetch data from APIs with race condition control, loading/error states, and lifecycle hooks:

<!-- Basic fetch with loading/error states -->
<div t-data="{ posts: [], $loading: false, $error: null }">
    <div t-fetch="'/api/posts'">
        <!-- Loading indicator -->
        <p t-show="$loading">⏳ Loading posts...</p>

        <!-- Error message -->
        <p t-show="$error" t-text="'Error: ' + $error"></p>

        <!-- Posts list -->
        <ul t-show="!$loading && !$error">
            <li t-for="post in posts">
                <h3 t-text="post.title"></h3>
            </li>
        </ul>
    </div>
</div>

<!-- Advanced: Lifecycle hooks -->
<div t-data="{ users: [] }">
    <div
        t-fetch="'/api/users'"
        @t:onFetchStart="console.log('Fetching...')"
        @t:onFetchEnd="console.log('Done!', $event.detail.data)"
        @t:onFetchError="console.error('Failed:', $event.detail.error)"
    >
        <ul>
            <li t-for="user in users" t-text="user.name"></li>
        </ul>
    </div>
</div>

<!-- Dynamic URL with reactive state -->
<div t-data="{ userId: 1, user: null }">
    <input t-model="userId" type="number" />
    <div t-fetch="'/api/users/' + userId">
        <p t-show="$loading">Loading...</p>
        <p t-text="user?.name"></p>
    </div>
</div>

Features:

  • Race Condition Control - Automatically cancels outdated requests
  • AbortController - Properly cancels requests when URL changes
  • State Variables - $loading, $error, $response automatically managed
  • Lifecycle Events - t:onFetchStart, t:onFetchEnd, t:onFetchError
  • Request Tracking - Prevents duplicate requests for same URL

t-await for Promises

Handle promises with loading and error states:

<div t-data="{ user: {} }">
    <div t-await="fetch('/api/user').then(r=>r.json())">
        <div t-loading="'Loading user...'">⏳ Loading...</div>
        <div t-error="'Failed to load user.'">❌ Error!</div>
        <p t-text="user?.name || ''"></p>
    </div>
</div>

Hash Router

Build SPAs with hash-based routing:

<nav>
    <a href="#/home">Home</a>
    <a href="#/about">About</a>
</nav>

<div t-route="'home'">🏠 Home Page</div>
<div t-route="'about'">ℹ️ About Page</div>

<script>
    TinyPine.router({
        default: "home",
        onChange(route) {
            console.log("Route changed →", route);
        },
    });
</script>

Internationalization (i18n)

Add multi-language support:

<script>
    // Setup translations
    TinyPine.i18n(
        {
            en: { greeting: "Hello World!", welcome: "Welcome!" },
            tr: { greeting: "Merhaba Dünya!", welcome: "Hoş geldiniz!" },
        },
        { default: "en", cache: true }
    );
</script>

<div t-data>
    <h1 t-text.lang="'greeting'"></h1>
    <button t-click="$lang = 'tr'">🇹🇷 Türkçe</button>
    <button t-click="$lang = 'en'">🇺🇸 English</button>
</div>

Dynamic Locale Loading:

TinyPine.loadLocale("tr", "/lang/tr.json");
TinyPine.loadLocale("en", "/lang/en.json");

Event Modifiers

<button t-click="save.prevent">Prevent default</button>
<button t-click="methods.init.once">Run once</button>
<button t-click="methods.close.outside">Close on outside click</button>

📦 Installation

CDN

<script src="https://unpkg.com/[email protected]/dist/tinypine.min.js"></script>
<script>
    TinyPine.init();
</script>

NPM

npm install tinypine

TypeScript Support:

import TinyPine from "tinypine";
// Full IntelliSense and type checking

Testing

npm test

Test Coverage:

  • ✅ 100 passing tests (8 test files)
  • ✅ Core directives (t-text, t-show, t-click, t-model, t-for)
  • ✅ Form components (tp-input, tp-checkbox, tp-file-upload)
  • ✅ Global stores and reactivity
  • ✅ Keyed list diffing (v1.3.1)
  • ✅ Router and validation (v1.3.1)
  • ✅ Context management and lifecycle hooks
  • ✅ Edge cases and performance

📖 Testing Guide →


⚙️ API Reference

Core Methods

  • TinyPine.init(root?) - Initialize TinyPine
  • TinyPine.start(selector, opts?) - Start with options (lite/safe mode)
  • TinyPine.store(name, data) - Create global store
  • TinyPine.watch(path, callback) - Watch changes
  • TinyPine.use(plugin) - Register plugin
  • TinyPine.directive(name, handler) - Custom directive
  • TinyPine.component(name, config) - Register custom component
  • TinyPine.transition(name, config) - Register transition

Lifecycle & Events

  • TinyPine.onMount(callback) - Global mount listener
  • TinyPine.onUnmount(callback) - Global unmount listener
  • TinyPine.on(event, callback) - Event listener
  • TinyPine.off(event, callback) - Remove event listener
  • TinyPine.emit(event, ...args) - Emit event

Context Variables

  • $parent - Parent scope data
  • $root - Root scope data
  • $refs - DOM references
  • $el - Current element
  • $store - Global stores
  • $lang - Current i18n language

🎨 Styling

TinyPine works with any CSS framework:

<link
    href="https://cdn.jsdelivr.net/npm/tailwindcss@2/dist/tailwind.min.css"
    rel="stylesheet"
/>
<div t-data="{ count: 0 }" class="p-8">
    <button t-click="count++" class="px-4 py-2 bg-blue-500 text-white rounded">
        Count: <span t-text="count"></span>
    </button>
</div>

🐛 Debugging

Enable debug mode:

TinyPine.debug = true;
// Console will show: [TinyPine] count changed → 1

// Silent mode (suppress logs)
TinyPine.debugOptions.silent = true;

DevTools

Built-in developer tools panel:

<script>
    TinyPine.debug = true;
    TinyPine.devtools({ position: "bottom-right", theme: "dark" });
</script>

Features:

  • 📊 Live Store Inspector
  • 🎯 Context Viewer
  • ⏱️ Reactivity Timeline
  • 📈 Performance Monitor

🌍 Browser Support

✅ Chrome 49+ ✅ Firefox 18+ ✅ Safari 10+ ✅ Edge 49+

Works everywhere JavaScript Proxies are supported.


📈 Roadmap

  • v1.0.0 - Stable Release with TypeScript & Tests
  • v1.1.0 - CLI Tool & Ecosystem Expansion
  • v1.1.1 - Sprout.js Readiness (Lite/Safe Modes)
  • v1.1.2 - Component Mount Lifecycle
  • v1.1.3 - Component Unmount Lifecycle
  • v1.2.0 - TinyPine UI Components
  • v1.3.0 - Core Stability & Form Components
  • v1.3.1 - Keyed Diffing & Advanced Features (100% Test Coverage!)
  • v1.4.0 - Async Flow & Form Validation
  • v1.5.0 - Smart Router with Guards & Navigation
  • 🔜 v1.6.0 - Performance Optimizations & Virtual DOM (Planned)
  • 🔜 v1.7.0 - DevTools 2.0 & Router Inspector (Planned)
  • 🔜 v2.0.0 - Ecosystem & TypeScript-first Rewrite (Planned)

📚 Documentation

Comprehensive guides and best practices:


🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


📄 License

MIT License - Use freely in your projects!


Made with ❤️ by TinyPine Team

GitHub · Issues · Documentation

About

Minimal, comfortable & intuitive reactive micro-framework — “HTML reactivity, zero build, zero stress.”

Topics

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •