A modern reimagining of Backbone.js — same proven MVC patterns, no jQuery, no Underscore, ships as a native ES module.
- Zero hard dependencies (includes a minimal built-in utility layer)
- Native ES module — works in browsers and any bundler (Vite, Rollup, webpack 5+)
- Full TypeScript types included
- Familiar Backbone API — drop-in for most use cases
Docs
Install
Or grab the file directly:
- ostov.js — development build
- ostov.min.js — production build
Usage
npm / bundler
import { Model, Collection, View, Router, Events } from 'ostovjs';
Or import the full namespace:
import Ostov from 'ostovjs';
Browser — ES module
<script type="module"> import { Model, Collection } from './ostov.js'; </script>
Browser — classic script tag
<script src="ostov.js"></script> <!-- Ostov is now available as a global variable -->
✨ Why Ostov?
Backbone had a great core idea:
- explicit state (models)
- event-driven updates
- separation of concerns
Ostov keeps that — but removes legacy baggage:
- ❌ no jQuery
- ❌ no Underscore
- ✅ ES modules
- ✅ ES classes
- ✅ TypeScript generics
🚀 TypeScript + Classes
Model
import { Model } from 'ostovjs'; interface TodoAttrs { title: string; completed: boolean; } export class Todo extends Model<TodoAttrs> { defaults() { return { title: '', completed: false, }; } toggle() { this.set('completed', !this.get('completed')); } }
Collection
import { Collection } from 'ostovjs'; import { Todo } from './Todo'; export class TodoList extends Collection<Todo> { model = Todo; completed() { return this.where({ completed: true }); } }
View (class-based)
import { View } from 'ostovjs'; import { Todo } from './Todo'; export class TodoView extends View<Todo> { events() { return { 'click [data-action="toggle"]': 'onToggle', }; } initialize() { this.listenTo(this.model, 'change', this.render); } render() { this.el.innerHTML = ` <button data-action="toggle">Toggle</button> <span>${this.model.get('title')}</span> `; return this; } onToggle() { this.model.toggle(); } }
⚡ Declarative bindings (fine-grained reactivity)
Instead of re-rendering the whole view on every change, a view can declare a
bindings hash that keeps individual DOM nodes in sync with its model —
signals-style reactivity built on the change events the model already fires:
import { Model, View } from 'ostovjs'; class CounterView extends View { el = '#app'; model = new Model({ count: 0, busy: false }); bindings = { '#counter:innerHTML': 'count', // model attribute → DOM property '#btn:disabled': 'busy', // boolean property ':data-count': 'count', // empty selector → the view's root el '#status:textContent': (m) => m.get('count') > 10 ? 'high' : 'low', // computed }; events = { 'click #btn': 'increment' }; increment() { this.model.set('count', this.model.get('count') + 1); // DOM updates itself } }
Syntax rules
- Each key is
"selector:target". The split happens on the last colon, so selectors with pseudo-classes work:'li:first-child:textContent'. - An empty selector (
':textContent') targets the view's root element. - Every element matching the selector is updated, not just the first one.
targetis written as a DOM property when the element has one (innerHTML,textContent,value,checked,disabled, ...) and as an attribute otherwise (class,data-*,aria-*). For attributes,truesets an empty attribute andfalse/nullremoves it — standard boolean-attribute semantics.null/undefinedproperty values render as'', never as the string"undefined". (styleis not supported as a target — bindclassinstead.)- The value is a model attribute name, or a function
(model) => valuefor computed bindings (functions repaint on any model change). bindingsitself may be a function returning the hash, likeevents.
Current model values are painted immediately when the view binds; after that,
string bindings repaint only when their attribute changes. {silent: true}
sets don't repaint, matching model event semantics.
Lifecycle
applyBindings(bindings?)/removeBindings()mirrordelegateEvents/undelegateEvents:setElementre-applies bindings to the new element,remove()cleans them up, and assigningview.modellater (re)binds automatically.- Bound elements are re-queried on every repaint, so if your
render()rebuildsinnerHTML, just callthis.applyBindings()at the end ofrender()and the bindings attach to the fresh nodes.
🧩 Using Handlebars (templating)
Ostov does not force a templating system — you can plug in anything.
Example with Handlebars:
import Handlebars from 'handlebars'; import { View } from 'ostovjs'; const template = Handlebars.compile(` <div> <h3>{{title}}</h3> <button data-action="toggle"> {{#if completed}}Undo{{else}}Complete{{/if}} </button> </div> `); export class TodoView extends View { initialize() { this.listenTo(this.model, 'change', this.render); } render() { this.el.innerHTML = template(this.model.toJSON()); return this; } }
👉 You can use any templating engine:
- Handlebars
- Mustache
- JSX (custom)
- plain strings
🔁 Backbone-style .extend(...)
Ostov still supports classic Backbone patterns:
import { Model, View } from 'ostovjs'; const Todo = Model.extend({ defaults: { title: '', completed: false, }, toggle() { this.set('completed', !this.get('completed')); }, }); const TodoView = View.extend({ events: { 'click button': 'toggle', }, initialize() { this.listenTo(this.model, 'change', this.render); }, render() { this.el.innerHTML = this.model.get('title'); return this; }, toggle() { this.model.toggle(); }, });
👉 This is useful if:
- you're migrating from Backbone
- you prefer prototype-style inheritance
🆚 Modern vs Legacy usage
| Style | Use |
|---|---|
| Classes + TS | ✅ recommended |
.extend(...) |
✅ supported |
| Templates | any (no lock-in) |
🧠 Core Idea
Ostov gives you primitives:
- Model
- Collection
- View
- Router
- Events
No magic. Just structure.
🎯 When to use
- small / medium apps
- dashboards
- internal tools
- apps where React feels like overkill
🚫 When not to use
- heavy ecosystem requirements
- large teams needing strict conventions
⭐ Support
If you like Backbone-style architecture with modern TypeScript —
drop a star ⭐
🧨 Philosophy
Less framework. More control.
Built with Ostov
TLDR extension for google chrome — Get a TLDR summary of any page using OpenAI. Ask follow-up questions in a dedicated tab.