Controllers & Data đź§
By default, the UI components in non-activity.stream are "dumb". They take a collection property and render it. That's it. No network requests, no persistence, no magic.
We call this the "Dumb UI / Smart Controller" pattern.
To make the UI interactive—to allow users to like, reply, boost, or drag-and-drop reorder items—you need to inject a Controller.
What is a Controller?
A Controller is simply a JavaScript object that implements the NonActivityStreamController interface. It acts as the bridge between the UI and your data source (whether that's a local database, a remote server, or a decentralized network).
When you pass a controller to a <non-activity-stream>, the component automatically discovers its capabilities and "lights up" with interactive features.
The IndexedDBController
The easiest way to get started with persistence is to use the built-in IndexedDBController. This controller stores your stream data locally in the browser's IndexedDB, providing an offline-first experience.
import { IndexedDBController } from '@nonactivity.stream/elements/controllers';
// Create a new controller instance
const controller = new IndexedDBController('my-local-database');
// Initialize the database
await controller.init();
// Pass the controller to the stream component
const feed = document.getElementById('my-feed');
feed.controller = controller;
With the IndexedDBController attached, any changes made in the UI (like reordering items or liking a post) will be saved locally and persist across page reloads.
Reactive Updates
Controllers don't just handle user input; they also push updates back to the UI.
The NonActivityStreamController interface includes an updates() method that returns an AsyncIterable. The <non-activity-stream> component listens to this stream of updates and automatically re-renders when new data arrives.
This means your UI stays perfectly in sync with your underlying storage, whether the data is coming from a local database or a real-time WebSocket connection.
Building Your Own Controller
The real power of the Controller pattern is that you can build your own.
Want to connect your stream to a real ActivityPub backend like Mastodon? Write a MastodonController.
Want to store your data in a local SQLite database using our shrubdb package? Write a SQLiteController.
As long as your object implements the required methods (like, unlike, reply, move, updates, etc.), the UI components will happily use it.
This is how we achieve the "Bring Your Own Backend" philosophy. The UI doesn't care where the data comes from, as long as it speaks ActivityStreams 2.0.