npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

modular-openscriptjs

v1.0.31

Published

OpenScriptJs Framework - A lightweight, reactive JavaScript framework for building modern web applications

Readme

Modular OpenScript Framework

npm version License: MIT

A modern, lightweight, reactive JavaScript framework for building scalable web applications with zero runtime dependencies. OpenScript combines IoC, reactive state management, and component-based architecture into a powerful yet simple package.

✨ Why OpenScript?

  • ⚡️ Reactive State - Automatic UI updates with proxy-based state
  • 🧩 Component-Based - Modular, reusable components with lifecycle hooks
  • 🔄 Client-Side Routing - Fluent API with parameters and nested routes
  • 📡 Event-Driven - Broker/Mediator pattern for decoupled architecture
  • 🎯 IoC Container - Centralized dependency management
  • 🎨 Framework Agnostic - Works with Tailwind, Bootstrap, or vanilla CSS
  • 🛠️ Vite Plugin - Production-ready build tools
  • 📦 Zero Dependencies - No runtime dependencies

🚀 Quick Start

Installation

npm install modular-openscriptjs

Scaffold a New Project

The fastest way to start:

npx create-ojs-app my-app
cd my-app
npm run dev

Choose from templates:

  • basic - Clean starter with vanilla CSS
  • tailwind - TailwindCSS with responsive design
  • bootstrap - Bootstrap 5 integration

Your First Component

import { Component, app, state, ojs } from "modular-openscriptjs";

const h = app("h");

class Counter extends Component {
  constructor() {
    super();
    this.count = state(0);
  }

  increment() {
    this.count.value++;
  }

  render() {
    return h.div(
      h.h2("Count: ", this.count.value),
      h.button({ onclick: this.increment.bind(this) }, "Increment")
    );
  }
}

ojs(Counter);

📖 Core Concepts

1. Components

Class Components with lifecycle hooks:

import { Component, app } from "modular-openscriptjs";

const h = app("h");

class UserCard extends Component {
  async mount() {
    // Called when component mounts
    console.log("Component mounted");
  }

  unmount() {
    // Called when component unmounts
    console.log("Component unmounted");
  }

  render(...args) {
    return h.div(
      { class: "card" },
      h.h3("User Profile"),
      h.p("Content here"),
      ...args
    );
  }
}

Functional Components for simple UI:

const Button = (text, onClick) => {
  return h.button({ onclick: onClick }, text);
};

const Card = (title, content) => {
  return h.div({ class: "card" }, h.h2(title), h.div(content));
};

2. Reactive State

State automatically triggers re-renders:

import { state } from "modular-openscriptjs";

// Create reactive state
const count = state(0);

// Read value
console.log(count.value); // 0

// Update value (triggers re-render)
count.value++;

// Listen to changes
count.listener((s) => {
  console.log("New:", s.value);
  console.log("Previous:", s.previousValue);
});

State in Components:

class TodoList extends Component {
  constructor() {
    super();
    this.todos = state([]);
  }

  addTodo(text) {
    this.todos.value = [
      ...this.todos.value,
      { id: Date.now(), text, done: false },
    ];
  }

  toggleTodo(id) {
    this.todos.value = this.todos.value.map((t) =>
      t.id === id ? { ...t, done: !t.done } : t
    );
  }

  render() {
    return h.div(
      h.input({
        placeholder: "Add todo...",
        onkeypress: (e) => {
          if (e.key === "Enter") {
            this.addTodo(e.target.value);
            e.target.value = "";
          }
        },
      }),
      h.ul(
        ...this.todos.value.map((todo) =>
          h.li(
            { class: todo.done ? "done" : "" },
            h.input({
              type: "checkbox",
              checked: todo.done,
              onchange: () => this.toggleTodo(todo.id),
            }),
            h.span(todo.text)
          )
        )
      )
    );
  }
}

3. Routing

Simple yet powerful client-side routing:

import { app } from "modular-openscriptjs";

const router = app("router");
const h = app("h");

// Basic routes
router.on(
  "/",
  () => {
    h.HomePage({ parent: document.body, resetParent: true });
  },
  "home"
);

router.on(
  "/about",
  () => {
    h.AboutPage({ parent: document.body, resetParent: true });
  },
  "about"
);

// Routes with parameters
router.on(
  "/users/{id}",
  () => {
    const userId = router.params.id;
    h.UserProfile(userId, { parent: document.body, resetParent: true });
  },
  "users.view"
);

// Grouped routes
router.prefix("admin").group(() => {
  router.on(
    "/dashboard",
    () => {
      h.AdminDashboard({ parent: document.body, resetParent: true });
    },
    "admin.dashboard"
  );

  router.on(
    "/users",
    () => {
      h.AdminUsers({ parent: document.body, resetParent: true });
    },
    "admin.users"
  );
});

router.listen();

Navigation:

// Navigate to named route
router.to("home");

// Navigate with parameters
router.push("/users/123");

// Go back
router.back();

4. Global State with Contexts

Share state across your entire application:

import { context, putContext, app } from "modular-openscriptjs";

const h = app("h");

// Register contexts
putContext(["app", "user"], "AppContext");

// Initialize state
const ac = context("app");
ac.states({
  theme: "light",
  language: "en",
});

const uc = context("user");
uc.states({
  name: "Guest",
  isLoggedIn: false,
});

// Use in any component
class Header extends Component {
  toggleTheme() {
    ac.theme.value = ac.theme.value === "light" ? "dark" : "light";
  }

  render() {
    return h.header(
      h.h1(`Welcome, ${uc.name.value}`),
      h.button(
        { onclick: this.toggleTheme.bind(this) },
        `Theme: ${ac.theme.value}`
      )
    );
  }
}

5. Event System

Decouple business logic with events:

import { Mediator, app, payload, Utils } from "modular-openscriptjs";

const broker = app("broker");

// Register events
broker.registerEvents({
  user: {
    login: true,
    logout: true,
  },
  notification: {
    show: true,
  },
});

// Create mediator for business logic
class UserMediator extends Mediator {
  $$user = {
    login: (ed, event) => {
      const data = Utils.parsePayload(ed);
      console.log("User logged in:", data.message);

      // Update UI
      broker.send(
        "notification:show",
        payload({
          message: "Login successful!",
        })
      );
    },

    logout: (ed, event) => {
      console.log("User logged out");

      broker.send(
        "notification:show",
        payload({
          message: "Goodbye!",
        })
      );
    },
  };
}

new UserMediator();

// Emit events from anywhere
class LoginButton extends Component {
  handleLogin() {
    broker.send(
      "user:login",
      payload({
        username: "john_doe",
        id: 123,
      })
    );
  }

  render() {
    return h.button({ onclick: this.handleLogin.bind(this) }, "Login");
  }
}

6. IoC Container

Access services through the container:

import { app } from "modular-openscriptjs";

// Get services
const h = app("h");
const router = app("router");
const broker = app("broker");
const contextProvider = app("contextProvider");

// Register custom values
app().value("apiUrl", "https://api.example.com");
app().value("config", { debug: true });

// Access custom values
const apiUrl = app("apiUrl");
const config = app("config");

🏗️ Project Structure

Typical project layout:

my-app/
├── src/
│   ├── components/
│   │   ├── Header.js
│   │   ├── Footer.js
│   │   └── TodoList.js
│   ├── contexts.js      # Global state
│   ├── routes.js        # Route definitions
│   ├── events.js        # Event registry
│   ├── mediators/       # Business logic
│   │   └── UserMediator.js
│   ├── main.js          # Entry point
│   └── style.css
├── index.html
├── vite.config.js
└── package.json

🎨 Framework Integration

TailwindCSS

import { app } from "modular-openscriptjs";

const h = app("h");

class Card extends Component {
  render() {
    return h.div(
      { class: "bg-white rounded-lg shadow-lg p-6" },
      h.h2({ class: "text-2xl font-bold mb-4" }, "Card Title"),
      h.p({ class: "text-gray-600" }, "Card content here")
    );
  }
}

Bootstrap

class Alert extends Component {
  render(message, type = "info") {
    return h.div({ class: `alert alert-${type}` }, message);
  }
}

🔧 Build Configuration

Vite Setup

// vite.config.js
import { defineConfig } from "vite";
import { openScriptComponentPlugin } from "modular-openscriptjs/plugin";

export default defineConfig({
  plugins: [openScriptComponentPlugin()],
  build: {
    target: "es2015",
    minify: "terser",
  },
});

This plugin ensures component names survive minification.

Build Commands

# Development server
npm run dev

# Production build
npm run build

# Preview production build
npm run preview

💡 Advanced Features

Fragments

Return multiple elements without a wrapper:

class List extends Component {
  render() {
    return h.$(
      // Fragment
      h.h1("Title"),
      h.p("Paragraph 1"),
      h.p("Paragraph 2")
    );
  }
}

State Listeners

React to state changes:

const count = state(0);

count.listener((s) => {
  console.log(`Count changed from ${s.previousValue} to ${s.value}`);

  if (s.value > 10) {
    console.warn("Count is getting high!");
  }
});

Multi-Event Listeners

Listen to multiple events:

class NotificationMediator extends Mediator {
  $$user = {
    // Triggers on BOTH login AND logout
    login_logout: (ed, event) => {
      console.log(`User event: ${event}`);
      this.showNotification(`User ${event.split(":")[1]}`);
    },
  };
}

Computed Properties

Use getters for derived state:

class TodoList extends Component {
  constructor() {
    super();
    this.todos = state([]);
  }

  get completedCount() {
    return this.todos.value.filter((t) => t.done).length;
  }

  get activeCount() {
    return this.todos.value.length - this.completedCount;
  }

  render() {
    return h.div(
      h.p(`${this.activeCount} active, ${this.completedCount} completed`)
      // ... rest of render
    );
  }
}

📚 Complete Example

Here's a full-featured app:

import {
  Component,
  app,
  state,
  context,
  putContext,
  Mediator,
  payload,
  ojs,
} from "modular-openscriptjs";

const h = app("h");
const broker = app("broker");

// Setup context
putContext("todos", "TodoContext");
const tc = context("todos");
tc.states({ todos: [], filter: "all" });

// Register events
broker.registerEvents({
  todo: {
    added: true,
    removed: true,
    toggled: true,
  },
});

// Business logic mediator
class TodoMediator extends Mediator {
  $$todo = {
    added: (ed) => {
      console.log("Todo added:", ed);
    },

    removed: (ed) => {
      console.log("Todo removed:", ed);
    },

    toggled: (ed) => {
      console.log("Todo toggled:", ed);
    },
  };
}

new TodoMediator();

// Main component
class TodoApp extends Component {
  constructor() {
    super();
    this.input = state("");
  }

  addTodo() {
    if (this.input.value.trim()) {
      const todo = {
        id: Date.now(),
        text: this.input.value,
        done: false,
      };

      tc.todos.value = [...tc.todos.value, todo];
      broker.send("todo:added", payload(todo));
      this.input.value = "";
    }
  }

  toggleTodo(id) {
    tc.todos.value = tc.todos.value.map((t) =>
      t.id === id ? { ...t, done: !t.done } : t
    );
    broker.send("todo:toggled", payload({ id }));
  }

  removeTodo(id) {
    tc.todos.value = tc.todos.value.filter((t) => t.id !== id);
    broker.send("todo:removed", payload({ id }));
  }

  get filteredTodos() {
    switch (tc.filter.value) {
      case "active":
        return tc.todos.value.filter((t) => !t.done);
      case "done":
        return tc.todos.value.filter((t) => t.done);
      default:
        return tc.todos.value;
    }
  }

  render() {
    return h.div(
      { class: "todo-app" },

      // Header
      h.header(
        h.h1("My Todos"),
        h.div(
          { class: "input-group" },
          h.input({
            value: this.input.value,
            placeholder: "What needs to be done?",
            oninput: (e) => (this.input.value = e.target.value),
            onkeypress: (e) => e.key === "Enter" && this.addTodo(),
          }),
          h.button({ onclick: () => this.addTodo() }, "Add")
        )
      ),

      // Filters
      h.div(
        { class: "filters" },
        ...["all", "active", "done"].map((f) =>
          h.button(
            {
              class: tc.filter.value === f ? "active" : "",
              onclick: () => (tc.filter.value = f),
            },
            f.toUpperCase()
          )
        )
      ),

      // Todo list
      h.ul(
        ...this.filteredTodos.map((todo) =>
          h.li(
            h.input({
              type: "checkbox",
              checked: todo.done,
              onchange: () => this.toggleTodo(todo.id),
            }),
            h.span({ class: todo.done ? "done" : "" }, todo.text),
            h.button({ onclick: () => this.removeTodo(todo.id) }, "×")
          )
        )
      ),

      // Stats
      h.footer(h.span(`${this.filteredTodos.length} items`))
    );
  }
}

ojs(TodoApp);

📖 API Reference

Core Exports

| Export | Type | Description | | ------------ | -------- | --------------------- | | Component | Class | Base component class | | app | Function | Access IoC container | | state | Function | Create reactive state | | ojs | Function | Bootstrap application | | context | Function | Access context | | putContext | Function | Register context | | Mediator | Class | Base mediator class | | payload | Function | Create event payload | | Utils | Object | Utility functions |

Component Lifecycle

| Method | Description | | --------------- | ----------------------------------- | | constructor() | Initialize component | | mount() | Component mounted (async supported) | | render() | Generate component UI | | unmount() | Component unmounted |

State API

| Property/Method | Description | | ---------------- | ---------------------------------- | | .value | Get/set state value | | .listener(fn) | Add state change listener | | .previousValue | Previous state value (in listener) |


🎯 Best Practices

✅ Do's

  • Use contexts for global state
  • Keep components small and focused
  • Leverage lifecycle hooks appropriately
  • Use mediators for business logic
  • Use computed properties for derived state

❌ Don'ts

  • Don't mutate state directly
  • Don't mix business logic with UI
  • Don't create functions in render
  • Don't emit events in tight loops

🐛 Troubleshooting

Component not re-rendering?

  • Ensure state is updated via .value =
  • Verify state is used in render()

Events not firing?

  • Check events are registered
  • Verify event names match exactly

Router not working?

  • Call router.listen() after defining routes
  • Check route paths are correct

📦 Package Info

  • Size: ~95KB (ES), ~42KB (UMD)
  • Dependencies: Zero runtime dependencies
  • Browser Support: Modern browsers (ES6+)
  • License: MIT

📚 Learn More


🤝 Contributing

We welcome contributions! See our Contributing Guide.


📄 License

MIT © Levi Kamara Zwannah


Built with ❤️ using OpenScript

⭐ Star on GitHub | 📦 View on npm