SimplyFlow is an experimental browser library for building small reactive web applications with ordinary HTML and ordinary JavaScript data.
The intended beginner-facing API is:
import { app } from '@muze-labs/simplyflow'
const counter = app({
container: document.getElementById('counter'),
data: {
count: 0
},
commands: {
add1() {
this.data.count++
}
}
})<div id="counter">
<button data-simply-command="add1">+</button>
<span data-simply-field="count"></span>
</div>For editable values, use data-simply-edit:
<input data-simply-edit="name">
<span data-simply-field="name"></span>The page updates automatically whenever app.data changes. Use data-simply-edit on form fields when the user should be able to edit a value directly. Text inputs, textareas and selects edit string values; checkboxes edit booleans or toggle values in arrays; radio buttons edit the selected value.
Buttons inside list templates can pass the current item or one of its fields to a command:
<ul data-simply-list="todos">
<template>
<li>
<button data-simply-command="removeTodo" data-simply-value=":value.id">Remove</button>
<span data-simply-field="text"></span>
</li>
</template>
</ul>data-simply-list is intentionally one-or-many friendly: if todos is an array, it renders every item; if it is a single value, it renders one item; if it is empty, it renders nothing. This keeps linked-data style values usable when a property may contain either one value or many values.
Custom top-level options become app properties, so you can add services without extra ceremony:
const contacts = simply.app({
data: { contacts: [] },
api: metro.jsonApi('/api/'),
actions: {
async loadContacts() {
this.data.contacts = await this.api.get('contacts.json')
}
}
})If an unknown option looks like a typo of a built-in app option, SimplyFlow logs a warning, but still adds the option to the app.
Reusable element behavior can be attached with data-simply-behavior:
<div data-simply-behavior="tabs"></div>const page = simply.app({
data: {},
behaviors: {
tabs(element) {
// Set up the tabs element.
}
}
})HTML fragments can be included inside an app container without a build step:
<link rel="simply-include" href="header.html">The include observer is scoped to the app and stops when app.destroy() is called.
Keyboard shortcuts can be added with the shortcuts option:
const notes = simply.app({
data: {},
shortcuts: {
'Control+s'() {
this.actions.save()
}
}
})npm install @muze-labs/simplyflowor using Git:
git clone https://github.com/muze-labs/simplyflow.git<script src="https://cdn.jsdelivr.net/npm/@muze-labs/simplyflow/dist/simply.flow.js"></script>Then use the beginner-facing simply.app() API:
const counter = simply.app({
data: { count: 0 },
commands: {
add1() {
this.data.count++
}
}
})The tutorials focus on simply.app(), but the browser global also exposes the lower-level APIs directly for projects that use script tags and do not want a build step:
const data = simply.signal({ title: 'Hello' })
simply.bind({ root: data })
const table = simply.model({ data: [] })
table.addEffect(simply.model.sort({ property: 'title' }))The browser bundle also intentionally provides global html and css template tags. They return strings, but many code editors recognize these tag names and provide syntax highlighting inside template literals:
const page = simply.app({
templates: {
card: html`<article data-simply-field=":value.title"></article>`
},
styles: {
card: css`.selected { font-weight: bold; }`
}
})For no-build module examples that use an import map, prefer mapping the complete browser bundle instead of every split workspace package:
<script type="importmap">
{
"imports": {
"@muze-labs/simplyflow": "https://cdn.jsdelivr.net/npm/@muze-labs/simplyflow/dist/simply.flow.js",
"@muze-nl/metro": "https://cdn.jsdelivr.net/npm/@muze-nl/metro@0.6.19/src/everything.mjs"
}
}
</script>
<script type="module">
import '@muze-labs/simplyflow'
import '@muze-nl/metro'
const hnpwa = simply.app({
data: { items: [] }
})
</script>This is the recommended beginner/example path: import the complete SimplyFlow bundle, plus any external library the example directly uses. Advanced users can still use the split packages or subpath imports below when they want smaller, tree-shakeable bundles with a build step.
Module imports are still available when you prefer explicit imports:
import { signal, effect, batch } from '@muze-labs/simplyflow/state'
import { bind } from '@muze-labs/simplyflow/bind'
import { model, paging, sort, filter, columns } from '@muze-labs/simplyflow/model'
const data = signal({ title: 'Hello' })
bind({ root: data })SimplyFlow is maintained as a small monorepo. The main @muze-labs/simplyflow package is a convenience package for beginners and script-tag users. The implementation is split into smaller ESM packages that can also be imported directly:
import { signal, effect } from '@muze-labs/simplyflow-state'
import { bind } from '@muze-labs/simplyflow-bind'
import { model } from '@muze-labs/simplyflow-model'
import { app } from '@muze-labs/simplyflow-app'For projects that prefer one dependency, the main package keeps stable subpath exports:
import { signal, effect } from '@muze-labs/simplyflow/state'
import { bind } from '@muze-labs/simplyflow/bind'The state, bind, model, and app packages are pure ESM and marked as side-effect-free so bundlers can tree-shake unused exports. The main @muze-labs/simplyflow entry point still intentionally initializes the browser global API and registers <simply-render>, so use subpath or direct package imports when minimum bundle size matters.
Or check the examples for more information.
This repository is a private npm workspace root. The published main package lives in packages/simplyflow; the smaller layer packages live in packages/state, packages/bind, packages/model, and packages/app.
npm install
npm test
npm run buildWhen publishing the split packages, publish the dependency packages first (state, then bind/model, then app, then simplyflow) so the main package can resolve its package dependencies.
MIT © Muze.nl
Contributions are welcome, but make sure that all code is MIT licensed. If you want to send a merge request, please make sure that there is a ticket that shows the bug/feature and reference it. If you find any problem, please do file a ticket, but you should not expect a timely resolution. This project is still very experimental, don't use it in production unless you are ready to fix problems yourself.