Skip to content

Repository files navigation

Snapp Framework

A lightweight JSX/TSX framework for building fast, reactive web applications with direct DOM manipulation - no virtual DOM overhead.

License: MIT npm version


📋 Table of Contents


What is Snapp?

Snapp is a lightweight JavaScript framework that compiles JSX/TSX directly to native DOM operations. It's designed for developers who want:

  • ✅ JSX/TSX syntax you already know
  • ✅ Direct DOM control without abstraction layers
  • ✅ Reactive state that updates elements individually
  • ✅ Zero virtual DOM - just compiled JavaScript
  • ✅ TypeScript support out of the box
  • ✅ Automatic memory management with built-in cleanup

Why Snapp?

Feature Snapp Virtual DOM Frameworks
Learning Curve Native DOM skills New abstractions
Performance Direct DOM Reconciliation overhead
Debugging Browser DevTools Framework DevTools needed
Memory Efficient cleanup GC dependent

Core Concepts

1. JSX Compiles to DOM Operations

// What you write:
<button onClick={() => alert("Hi")}>Click me</button>;

// Gets compiled to:
snapp.create("button", { onClick: () => alert("Hi") }, "Click me");

2. Dynamic State with Arrow Functions

The key difference in Snapp is how you handle reactive values:

const count = snapp.dynamic(0);

// ❌ WRONG - accesses value once at render time
<p>{count.value}</p>

// ✅ CORRECT - wrapped in arrow function for reactivity
<p>{() => count.value}</p>

// When count updates, ONLY this text updates:
count.update(5);

Why arrow functions? Snapp tracks dynamic value access inside the function. When you call count.update(), it re-executes that function and updates just that specific text node, attribute, or style.

3. Regular Variables Stay Static

const staticText = "Hello";
const dynamicText = snapp.dynamic("World");

<div>
  {staticText} {/* Static - never changes */}
  {() => dynamicText.value} {/* Dynamic - updates when dynamicText changes */}
</div>;

Installation & Setup

Option 1: Create a New Project (Recommended)

The fastest way to get started:

npm create snapp-app my-app
cd my-app
npm install
npm run dev

This creates a ready-to-go project with this structure:

my-app/
├── views/                  # Your JSX/TSX source files
│   ├── index.jsx           # Main entry view
│   └── components/         # Reusable components
│       └── Links.jsx
├── public/                 # Build output (auto-generated by snapp-kit)
│   └── index.js            # Compiled JavaScript
├── style/
│   └── style.css           # Your CSS
├── assets/                 # Static files (images, fonts, etc.)
├── index.html              # Entry HTML file
└── package.json

Open http://localhost:9000 and start building!


Option 2: Manual Setup

1. Install packages:

npm install @snappjs/core
npm install -D snapp-kit

2. Create your project structure:

my-project/
├── views/
│   └── index.jsx
├── public/
├── index.html
└── package.json

3. Create index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Snapp App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
  <div id="app"></div>
  <script type="module" src="public/index.js"></script>
</body>
</html>

4. Create views/index.jsx:

import snapp from "@snappjs/core";

const App = () => {
  return <h1>Hello Snapp!</h1>;
};

const app = snapp.select("#app");
snapp.render(app, App());

5. Add scripts to package.json:

{
  "type": "module",
  "scripts": {
    "dev": "npx snapp build -W",
    "build": "npx snapp build -M"
  },
  "dependencies": {
    "@snappjs/core": "^3.0.0"
  },
  "devDependencies": {
    "snapp-kit": "^3.1.0"
  }
}

6. Run the dev server:

npm run dev

snapp-kit compiles views/index.jsx → public/index.js, starts a live server, and auto-refreshes on changes.



Quick Start

Example 1: Simple Counter

import snapp from "@snappjs/core";

const Counter = () => {
  const count = snapp.dynamic(0);

  return (
    <>
      <h2>Count: {() => count.value}</h2>
      <button onClick={() => count.update(count.value + 1)}>Increment</button>
    </>
  );
};

const app = snapp.select("#app");
snapp.render(app, Counter());

Example 2: Two-Way Binding

import snapp from "@snappjs/core";

const FormExample = () => {
  const name = snapp.dynamic("");

  return (
    <div>
      <input
        type="text"
        value={() => name.value}
        onInput={(e) => name.update(e.target.value)}
        placeholder="Type your name..."
      />
      <p>Hello, {() => name.value || "stranger"}!</p>
    </div>
  );
};

const app = snapp.select("#app");
snapp.render(app, FormExample());

Example 3: Todo List

import snapp from "@snappjs/core";

const TodoApp = () => {
  const todos = snapp.dynamic([]);
  const input = snapp.dynamic("");

  const addTodo = () => {
    const newTodos = [...todos.value, input.value];
    todos.update(newTodos);
    input.update("");
  };

  return (
    <div>
      <h1>Todos</h1>
      <input
        value={() => input.value}
        onInput={(e) => input.update(e.target.value)}
        placeholder="Add a todo..."
      />
      <button onClick={addTodo}>Add</button>
      <ul>{() => todos.value.map((todo) => <li>{todo}</li>)}</ul>
    </div>
  );
};

const app = snapp.select("#app");
snapp.render(app, TodoApp());

Example 4: Components with Props

import snapp from "@snappjs/core";

const Card = ({ title, children }) => {
  return (
    <div style={{ border: "1px solid #ccc", padding: "16px", borderRadius: "8px" }}>
      <h3>{title}</h3>
      {children}
    </div>
  );
};

const App = () => {
  return (
    <div>
      <Card title="Welcome">
        <p>This is a card component!</p>
      </Card>
    </div>
  );
};

const app = snapp.select("#app");
snapp.render(app, App());

API Reference

snapp.create()

Creates a DOM element, component, or fragment.

create(
    element: string | Component | "<>",
    props?: SnappProps,
    ...children: SnappChild[]
): Element | DocumentFragment

Usage:

// HTML element
snapp.create("div", { id: "main" }, "Hello");

// Component
const MyComponent = (props) => <div>{props.children}</div>;
snapp.create(MyComponent, {}, "Hello");

// Fragment
snapp.create("<>", null, <div>A</div>, <div>B</div>);

// In JSX, you use the angle brackets directly:
<div id="main">Hello</div>
<MyComponent>Hello</MyComponent>
<>
    <div>A</div>
    <div>B</div>
</>

snapp.render()

Renders a component or element to the DOM.

render(
    target: Element,
    component: Element | DocumentFragment | string | number,
    type?: "replace" | "append" | "prepend" | "before" | "after" | null,
    callback?: (success: boolean) => void
): void

Parameters:

Parameter Type Default Description
target Element required The DOM element to render to
component Element | DocumentFragment | string | number required What to render
type string | null "replace" Where/how to render (pass null to replace children)
callback Function optional Called with true/false on success/failure

Render Types:

const content = <div>New content</div>;
const target = snapp.select("#app");

// Replace target's children (recommended default)
snapp.render(target, content, null);

// Replace the target element itself
snapp.render(target, content, "replace");

// Add as first child
snapp.render(target, content, "prepend");

// Add as last child
snapp.render(target, content, "append");

// Insert before target
snapp.render(target, content, "before");

// Insert after target
snapp.render(target, content, "after");

// With callback
snapp.render(target, content, null, (success) => {
  if (success) console.log("Rendered!");
});

Tip: Pass null as the third argument to replace the element's children (most common use case). Using "replace" replaces the element itself, which removes it from the DOM.


snapp.dynamic()

Creates a reactive state value.

dynamic<T = any>(initialValue?: T): DynamicValue<T>

Properties:

interface DynamicValue<T> {
  readonly value: T; // Get current value
  update: (newValue: T) => void; // Set new value
}

Usage:

// Create dynamic state
const count = snapp.dynamic(0);
const user = snapp.dynamic({ name: "John", age: 30 });
const isVisible = snapp.dynamic(true);

// Use in JSX with arrow function
<div>
  Count: {() => count.value}
  Name: {() => user.value.name}
  Visible: {() => (isVisible.value ? "Yes" : "No")}
</div>;

// Update state
count.update(5);
user.update({ name: "Jane", age: 25 });
isVisible.update(false);

// Update based on previous value
count.update(count.value + 1);

Dynamic State in Different Contexts:

// Text Content
const message = snapp.dynamic("Hello");
<p>{() => message.value}</p>;

// Attributes
const id = snapp.dynamic("item-1");
<div id={() => id.value}></div>;

// Styles
const color = snapp.dynamic("blue");
<p style={{ color: () => color.value }}>Colored text</p>;

// Event Handlers (regular functions)
const handleClick = () => alert("clicked");
<button onClick={handleClick}>Click</button>;

// Conditional Rendering
const showHeader = snapp.dynamic(true);
<>{() => (showHeader.value ? <header>Title</header> : null)}</>;

snapp.on()

Listens for DOM ready events.

on(event: string, callback: () => void): void

Usage:

// Wait for DOM to be ready before accessing elements
snapp.on("DOM", () => {
  const element = snapp.select("#myElement");
  console.log("Element is in DOM:", element);
});

// snapp.on("DOM") is called after snapp.render() completes
const App = () => {
  return <h2 id="myElement">Title</h2>;
};

snapp.render(snapp.select("#app"), App(), null, () => {
  snapp.on("DOM", () => {
    console.log("Now we can access #myElement");
  });
});

snapp.select() / snapp.selectAll()

Query DOM elements using CSS selectors.

select(selector: string | string[]): Element | Element[] | null
selectAll(selector: string | string[]): NodeListOf<Element> | NodeListOf<Element>[] | null

Usage:

// Single selector
const element = snapp.select("#myId");
const element = snapp.select(".myClass");

// Multiple selectors (returns array)
const elements = snapp.select(["#id1", "#id2"]);

// Select all matching
const items = snapp.selectAll(".item");
const items = snapp.selectAll(".item, .product");

// Multiple selectors for selectAll
const results = snapp.selectAll([".class1", ".class2"]);
// Returns array of NodeLists

// Returns null if not found
const missing = snapp.select("#doesNotExist"); // null

snapp.applystyle() / snapp.removestyle()

Manage element styles programmatically.

applystyle(
    element: Element | Element[],
    styles: Record<string, string | number>
): void

removestyle(
    element: Element | Element[],
    styles?: Record<string, string | number> | boolean
): void

Usage:

const box = snapp.select("#box");

// Apply styles
snapp.applystyle(box, {
  backgroundColor: "blue",
  padding: "20px",
  "border-radius": "8px", // CSS property names with hyphens work
});

// Remove specific styles
snapp.removestyle(box, {
  backgroundColor: "blue",
  padding: "20px",
});

// Remove all styles
snapp.removestyle(box, true);

// Multiple elements
const boxes = snapp.selectAll(".box");
snapp.applystyle(boxes, { color: "red" });
snapp.removestyle(boxes, { color: "red" });

snapp.remove()

Remove elements from the DOM.

remove(items: Element | Element[]): void

Usage:

const element = snapp.select("#myElement");
snapp.remove(element);

// Remove multiple
const items = snapp.selectAll(".item");
snapp.remove(items);

snapp.running()

Simple health check to verify Snapp is loaded correctly.

running(): string  // Returns "Snapp is running!"

Usage:

console.log(snapp.running()); // "Snapp is running!"

Type Definitions

SnappChild

type SnappChild =
  | string
  | number
  | Element
  | DocumentFragment
  | SnappComponent
  | SnappChild[]
  | null
  | undefined
  | boolean;

Represents anything that can be rendered as a child element.

SnappProps

type SnappProps = Record<string, any>;

Props object for components. Can contain any key-value pairs.

SnappComponent

type SnappComponent<P extends SnappProps = SnappProps> = (
  props: P & { children?: SnappChild[] }
) => Element | DocumentFragment;

A component function that takes props and returns a DOM element or fragment.

Example:

interface ButtonProps {
  label: string;
  onClick?: (e: Event) => void;
}

const MyButton: SnappComponent<ButtonProps> = (props) => {
  return <button onClick={props.onClick}>{props.label}</button>;
};

DynamicValue

interface DynamicValue<T = any> {
  readonly value: T;
  update: (newValue: T) => void;
}

Reactive state container that notifies subscribers when updated.

RenderType

type RenderType = "before" | "prepend" | "replace" | "append" | "after";

Determines where elements are rendered relative to the target.

EventHandler

type EventHandler = (event: Event) => void;

Function called when an event fires.

HTMLAttributes

Comprehensive attribute interface supporting:

  • Global attributes: id, class, style, title, etc.
  • Data attributes: data-*
  • ARIA attributes: aria-*
  • All event handlers: onClick, onSubmit, onChange, etc.

Core Architecture

How Snapp Works

  1. JSX Compilation → esbuild (via snapp-kit) compiles JSX to snapp.create() calls
  2. Element Creation → snapp.create() builds native DOM elements
  3. Dynamic Tracking → When you use () => dynamicValue.value, Snapp tracks dependencies
  4. Subscriptions → Each dynamic value tracks which elements depend on it
  5. Updates → When you call update(), only affected text nodes/attributes/styles change
  6. Cleanup → MutationObserver automatically cleans up when elements are removed

Dependency Tracking Algorithm

When you write:

const count = snapp.dynamic(0);
<p>{() => count.value}</p>;

Snapp does the following:

  1. Start tracking: Set track_dynamic = new Set()
  2. Execute function: Call () => count.value, which accesses the value getter
  3. Track access: Inside value getter, add ID to track_dynamic
  4. Create subscription: Store the function and which dynamic values it depends on
  5. On update: When count.update(5) is called, re-execute the function and update the text node

This is why arrow functions are essential - they defer execution so Snapp can track what's accessed.

Event Delegation

Rather than adding listeners to every element, Snapp uses event delegation — a single listener per event type on the document, which is more efficient:

// Single listener per event type
document.addEventListener("click", eventTemplate);

// Template checks if target has snapp-e-click attribute
const elWithAttr = target.closest("[snapp-e-click]");
if (elWithAttr) {
  const elementDataId = elWithAttr.getAttribute("snapp-data");
  elementEvent["click"][elementDataId](event);
}

Memory Management

MutationObserver watches for removed elements and automatically cleans up event listeners and dynamic subscriptions — no manual cleanup needed.


🚀 Coming Soon

These features are planned for upcoming v3 releases:

snapp.state() + <Render> — Reactive Lists

Auto-updating DOM lists from arrays/objects, similar to .map() in React but with fine-grained DOM updates:

const items = snapp.state([
  { id: 1, name: "Apple" },
  { id: 2, name: "Banana" },
]);

<Render for={items} key="id">
  {(item) => <li>{item.name}</li>}
</Render>

// Add, remove, update — DOM updates automatically
items.push({ id: 3, name: "Cherry" });
items.remove(1);

snapp.ref() — DOM References

Get a reference to a DOM element without querying:

const myDiv = snapp.ref();
<div ref={myDiv}>Hello</div>

// Access later
myDiv.current.style.color = "red";

snapp.computed() — Derived Values

Automatically derived reactive values:

const count = snapp.dynamic(5);
const doubled = snapp.computed(() => count.value * 2);

<p>Doubled: {() => doubled.value}</p> // Updates when count changes

snapp.effect() — Side Effects

Run side effects when dynamic values change:

const query = snapp.dynamic("");
snapp.effect([query], () => {
  document.title = `Search: ${query.value}`;
});

<Show> — Conditional Rendering

Cleaner conditional rendering:

<Show when={() => isLoggedIn.value}>
  <Dashboard />
</Show>

@snappjs/router — SPA Routing

Client-side routing as a separate package:

import { Router, Route, Link } from "@snappjs/router";

const router = Router({
  routes: [
    { path: "/", page: () => import("./pages/home.js") },
    { path: "/about", page: () => import("./pages/about.js") },
    { path: "/user/:id", page: () => import("./pages/user.js") },
  ],
});

router.mount(snapp.select("#app"));

Other Planned Features

  • Improved TypeScript JSX types — proper attribute types instead of any
  • Error boundaries — catch rendering errors gracefully

Contributing

We welcome contributions! Here's how to get involved:

Development Setup

# Clone the repo
git clone https://github.com/SnappJS/snapp.git
cd snapp

# Install dependencies
npm install

# Start development watch mode
npm run dev

# Build for production
npm run build

Code Style

  • Use TypeScript for type safety
  • Follow existing code patterns in src/core.ts
  • Add JSDoc comments for public APIs
  • Test with JSX examples

Pull Request Process

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with clear commit messages
  4. Ensure code compiles: npm run build
  5. Submit PR with description

License

MIT - See LICENSE file for details


Support

About

Snapp gives you manual DOM power with modern component convenience. Know JavaScript and HTML? You already know Snapp.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Contributors

Languages