Popstart.js

A radically different approach to frontend development. Write complex application logic directly in HTML attributes with automatic error handling, promise chaining, and state management.

STATUS

Very much in development. API of the supporting utilities may change (however the core Popstart/PopEvent API is stable and will not change). Documentation mostly generated by LLM and only lightly reviewed.

With that said, this is an awesome library that will change the way you think and code. It makes even complex UIs quite manageable and is better than reactive frameworks in nearly every respect (readability, security, performance) and far more powerful than AlpineJS or HTMX.

Have a look through the docs and see if you agree. There’s no Git repo yet, so if you want to discuss, please email the project name (without .js) at this domain.

At a Glance

HTML-Driven Programming

Popstart converts your functions into insta-promises, so you can chain them together and they will execute, in order, and stop if there’s an error.

<!-- Chain multiple functions on a single click -->
<button 
	click="auth.login, ui.showDashboard, metrics.track"
	hover="ui.showTooltip"
	blur="ui.hideTooltip"
	login-endpoint="/api/login"
	track-event="login_success">
	Login
</button>

Dynamic Error Handling

You can even specify error handlers for each function, or just use the global default error handler.

<form 
	submit="api.submitForm"
	submitForm-error="ui.showToast"
	showToast-message="Failed to submit!">
</form>

Promise Chaining Through HTML

Promise chaining allows sequential execution with data passing.

<div 
	click="fetch.getData, transform.process, storage.save"
	getData-url="/api/data"
	process-format="json"
	save-writedatapath="lastSave">

Attribute-Based Function Arguments

Check out the auto-mapping of function parameters.

<input 
	change="validation.check"
	check-minLength="3"
	check-pattern="[A-Za-z]+"
	check-message="Letters only!">

The validation.check() function might look like this:

validation.check = function(data, minLength, pattern, message) {
	// because `data` wasn't passed in from the HTML call,
	// it will be set to `undefined`
    // (you could also use scrape on the form as shown below.)

    // do something with minLength, pattern, and message ...
}

Installation and Getting Started

Download one of the following and add it to your project:

    <script src="popstart-full.min.js"></script>

Advanced Patterns

Multi-Step Form Validation with Error States

In this example, the form will be validated, then the user will be registered (remotely via an API call), and finally, the user will be redirected to the dashboard. If any step fails, the error handler will be called.

For example, if the validation step fails, your custom ui.showError() function might display “Invalid input” and processing would stop there, meaning api.register() and ui.redirect() would not be called.

If that succeeded but the api.register step fails, the ui.showToast function might display “Registration failed”.

<form 
	submit="validate.user, api.register, ui.redirect"
	validate-required="email,password"
	validate-email="email@format"
	validate-error="ui.showError"
	register-endpoint="/api/users"
	register-error="ui.showToast"
	showError-class="error-state"
	showToast-duration="5000"
	redirect-url="/dashboard">
	<input name="email" type="email">
	<input name="password" type="password">
</form>

Cascading UI Updates

<div class="dashboard">
	<button 
		click="data.fetch-1, ui.updateChart-1, ui.updateTable-1"
		fetch-url="/api/stats"
		fetch-writedatapath="currentStats"
		updateChart-target="#mainChart"
		updateTable-target="#dataTable"
		updateChart-error="ui.showError-1"
		showError-message="Chart update failed"
		showError-class="chart-error">
		Refresh
	</button>
</div>

Dynamic Data Loading with Transforms

<select 
	change="api.getOptions, transform.format-1, ui.populate"
	getOptions-url="/api/choices"
	format-type="dropdown"
	format-writedatapath="formattedChoices"
	populate-target="#dependent-select"
	populate-template="option-template"
	getOptions-error="ui.disable">
</select>

State/Flow Management

<div 
	load="ws.connect, ui.ready"
	connect-url="wss://api"
	connect-error="ui.offline"
	ws-message="handlers.process"
	process-writedatapath="lastMessage"
	process-error="ui.showError">
</div>

Complex Authentication Flow

Function calls can be comma-separated, or longer ‘promise chains’ can be broken across multiple lines for readability.

<button 
	click="
		auth.check
		auth.refresh
		auth.login
	"
	check-token="localStorage.token"
	check-error="auth.logout"
	refresh-writedatapath="newToken"
	login-redirect="/dashboard"
	login-error="ui.loginError"
	loginError-message="Session expired">
	Login
</button>

Conditional UI Updates

Functions that aren’t already defined in the global scope can be defined in any appropriate namespace that you’ve defined, such as api or ui.

<div 
	mouseover="check.permission-1, ui.showTooltip-1"
	mouseout="ui.hideTooltip-1"
	click="check.permission-2, actions.perform-1"
	permission-role="admin"
	permission-error="ui.disable"
	showTooltip-text="Admin only"
	perform-action="delete"
	perform-target="record-123">
</div>

Data Synchronization

Functions that aren’t already returning a promise will be executed synchronously, as you’d expect. (Behind the scenes, those functions are wrapped in a promise, so all functions and promises are formed into promise chains.)

<div 
	poll="sync.check, sync.download, sync.process"
	check-interval="5000"
	check-writedatapath="syncStatus"
	download-condition="needsSync"
	process-writedatapath="lastSync"
	process-error="ui.syncError">
</div>

Progressive Enhancement

<form 
	submit="validate.form-1, api.submit-1, ui.success-1"
	validate-fields="all"
	validate-rules="required,email,length"
	validate-error="ui.fieldError"
	submit-method="POST"
	submit-transform="json"
	success-message="Saved!"
	fieldError-class="invalid">
</form>

How it works

Popstart.js is a lightweight library that enables HTML-driven programming by mapping DOM events to function calls. It allows you to define complex application logic directly in HTML attributes, without writing JavaScript code.

Here’s how it works:

  1. Define functions in HTML attributes: Use custom attributes like click, submit, change, etc., to specify which functions to call on DOM events.

  2. Pass arguments via attributes: Use custom attributes like fetch-url, submit-method, change-pattern, etc., to pass arguments to functions. Arguments named event and element are automatically populated with the triggering event and DOM element, and this from the calling UI DOM elemtn is passed in if you use a normal (function()) instead of a lambda (()=>) function (which don’t support this). Attribute names are automatically mapped to function parameters, so change-pattern=5 change-message=“Invalid input” will be passed as function(5,"Invalid input"). You can optionally prefix all attributes with data- or x- to avoid conflicts with other libraries.

  3. Chain multiple functions: Separate function calls with commas to execute them. They will be executed in sequence, with proper Promise handling, and any non-promise-returning functions (regular functions) will be executed synchronously as you’d expect. If a function returns a Promise, the next function will be called only after the Promise is resolved. If there is an error, the error handler for that function will be called (functionName-error="errorHandlerName"), or a generic error handler will be called, and the chain will stop.

Why Popstart?

Core Concepts

Function Chaining

Chain multiple functions with commas:

<button click="auth.login, ui.update, metrics.track">Login</button>

Functions execute in sequence, with proper Promise handling.

Argument Passing

Arguments are automatically mapped from attributes:

<button 
	click="api.fetch"
	fetch-url="/api/data"
	fetch-method="POST"
	fetch-body="json">
	Load Data
</button>

Special argument names:

Error Handling

Each function can have its own error handler:

<form 
	submit="api.submit"
	submit-error="ui.showError"
	showError-message="Submit failed!">
</form>

Data Persistence

Save function results for later use:

<div 
	click="api.getData"
	getData-writedatapath="lastResponse">
</div>

Access stored data with:

debug(__.data.lastResponse)

Event Prevention

Stop event propagation or prevent default:

<a 
	click="handler.process"
	click-stop="true"
	click-prevent="true"
	href="#">
	Click Me
</a>

Comparison to Alpine.js and HTMX

Popstart is designed to have the power of Vue.js or React, but its syntax is probably closer to Alpine.js or HTMX. It also draws inspiration from jQuery, Angular, and lodash.

Here’s a quick comparison:

Core Features

Feature Popstart HTMX Alpine.js
Function Calling click="fn1,fn2,fn3" hx-get="/api" @click="doThing()"
Learning Curve Low - just HTML attrs† Medium - new syntax Medium-High - custom directives
Promise Chaining Native ✓ No (server side generation) Manually (using x-init)
Error Handling Per-function automatic Global only Try/catch blocks
Server Dependency None Server must return HTML None
State Management Built-in data store None Reactive system
Event Control Automatic Manual Manual
Key Differentiator Chain any function Server generates HTML Reactive data binding
Minified Size (after gzip) 6KB (tiny) to 14KB (full) 16KB 16KB

† You can optionally prefix your attributes with data- or x- to avoid conflicts with other libraries.

Security features are critical, so let’s compare those. See also the FAQ and security section below. We prioritize security issues, so if you see any, please open an issue or send an email to security@.

Security Features

Security Feature Popstart HTMX Alpine.js
Full CSP Compliance Yes Yes 2024 and later
eval() or Embeds JS directly No No Embedded JS but sandboxed
Dynamic functions (insecure) No No No
Arbitrary JS execution No No Yes (sandboxed)

Security

Popstart’s attribute-based approach improves maintainability and reduces the surface area of code:

Additionally, the writedatapath pattern is:

CSP

Popstart doesn’t allow arbitrary dynamic functions to be created and called, or arbitrary JS to be executed as in old-style HTML embeds (onclick="alert('xss')"), which are rightly blocked by CSP. All functions have to already exist and also be identified in the HTML. In order to attack, you’d have to already have another vulnerability to chain off of, as well as the ability to inject arbitrary js func AND modify or inject html. This is a much higher bar than the traditional onclick or eval attacks.

No Dynamic Functions

Popstart has a limited attack surface because functions must be pre-defined and arbitrary JS can’t be executed.

If an attacker can inject arbitrary JS or HTML into your code, they could just inject a script tag with malicious code, and the application would be completely compromised, with or without Popstart or any other library.

Dynamic functions are specifically disallowed by CSP. (If you’re not using CSP, you should be!)

XSS

XSS remains a concern for all libraries of this nature, from jQuery to React. The only way to prevent XSS is to sanitize user input and properly escape output. Popstart provides a built-in sanitizer for attributes and it will automatically use DOMPurify if it’s available, but you should always sanitize user input and escape output.

Other Security Features

In the meantime, you get:

HTML Comparison

Popstart
<!-- Popstart -->
<button 
	click="getData, updateUI"
	getData-url="/api"
	getData-error="showError">
</button>
HTMX
<!-- HTMX -->
<button 
	hx-get="/api"
	hx-target="#result"
	hx-swap="innerHTML">
</button>
Alpine.js
<!-- Alpine -->
<button 
	x-data="{ getData() {...} }"
	@click="getData().then(updateUI)">
</button>

AJAX

Even though you can just natively use fetch or axios, you can also use the more powerful AJAX library that’s built in to full editions of Popstart. have native support to pass data between promises.

PopStart’s AJAX functions enable complex API interactions directly from HTML attributes, with automatic JSON parsing and data storage.

Basic Examples

Simple GET with auto-storage

<!-- Simple GET with auto-storage -->
<button
    click="__.get"
    get-url="/api/users"
    get-writedatapath="users">
    Load Users
</button>

Chained API calls

<!-- Chained API calls -->
<div
    load="__.get, __.post"
    get-url="/api/token"
    get-writedatapath="auth"
    post-url="/api/validate"
    post-readdatapath="auth">
</div>

GET with query parameters

The get-querydatapath attribute allows you to specify a path in the data store where the query parameters are stored, and they will be automatically encoded and appended to the URL.

<!-- GET with query parameters -->
<button
    click="__.get"
    get-url="/api/search"
    get-querydatapath="searchParams"
    get-writedatapath="results">
    Search
</button>

POST with stored form data

Form fields scraped from the form are automatically stored in __.data.form by default in JSON format, and then passed to the server, all automatically.

Errors are automatically handled or you can manually specify a custom error handler.

<!-- POST with stored form data -->
<form
    submit="__.scrape,__.post, ui.showResponse"
    post-url="/api/submit"
    post-readdatapath="form"
    post-writedatapath="response"
    submit-prevent="true"
	showResponse-target="#response"
	showResponse-message="Submitted!">
</form>

Direct HTML Injection

If you have thoroughly sanitized your HTML on the server-side, you can also use direct HTML injection, similar to HTMX.

SECURITY WARNING: Direct HTML injection is a security risk and should be avoided if possible. Always sanitize user input and use proper security headers.

<!-- Direct HTML injection (Warning, XSS risks) -->
<div
    click="__.getto"
    getto-url="/api/template"
    getto-selector="#target">
    Load Template
</div>

AJAX API Reference

As noted above, the AJAX functions are optional and only available in the full or custom builds.

AJAX Core Methods

Method Description Parameters
__.get GET request url, opt, writedatapath, notjson, querydatapath
__.post POST request url, data, opt, readdatapath, writedatapath, querydatapath
__.put PUT request url, data, opt, readdatapath, querydatapath
__.delete DELETE request url, opt, querydatapath
__.patch PATCH request url, data, opt, readdatapath, querydatapath
__.getto GET + innerHTML url, opt, writedatapath, notjson, querydatapath, selector

AJAX Options Object

{
    json: true,           // Send/receive JSON
    parseJSON: true,      // Parse response as JSON
    headers: {},          // Custom headers
    withCredentials: false, // CORS credentials
    auth: {               // Basic auth
        username: "user",
        password: "pass"
    },
    query: {},            // Query parameters
    followRedirects: false, // Follow 3xx redirects
    onProgress: (evt) => {} // Progress handler
}

AJAX Advanced Usage

As noted above, the AJAX functions are optional and only available in the full or custom builds. As with all Popstart functions, you can chain multiple AJAX or non-promise-returning calls together, and you can pass data between many using the writedatapath attribute or the __.data object.

For all functions in __, you can always drop the __. namespace prefix if you prefer (as shown below), which can make your HTML more readable, but beware of naming conflicts, such as a method named post in the global scope; In the event of a conflict, the __. prefix will always take precedence.

Authentication + Custom Headers

<button
    click="post"
    post-url="/api/secure"
    post-opt='{"auth":{"username":"admin","password":"secret"},"headers":{"X-Custom":"value"}}'>
    Secure Post
</button>

Progress Tracking

<div
    click="get"
    get-url="/api/large-file"
    get-opt='{"onProgress":"ui.updateProgress"}'
    get-notjson="true">
    Download
</div>

Multiple Data Paths

<form
    submit="post, ui.showSuccess"
    post-url="/api/submit"
    post-readdatapath="formData"
    post-writedatapath="response"
    post-querydatapath="queryParams"
    showSuccess-message="Saved!">
</form>

Error Handling

<button
    click="get, ui.update"
    get-url="/api/data"
    get-writedatapath="apiData"
    get-error="ui.showError"
    showError-message="API Error">
    Load
</button>

Multiple calls

The optional utility functions include a function called incrHTML that increments the innerHTML of an element from a start value to an end value. (It’s really designed for a select dropdown for things like years and months, but it’s just an incrementer and could be used for other things.)

Notice how it can be used twice in the same startup tag, and the selectors are different. Just append -1, -2, etc to the function and its attributes to differentiate them. Because - is not a valid character in a function name or variable name in Javascript, it’s safe to use as a separator and works naturally in this context.

<div startup=__.incrHTML-1,__.incrHTML-2
	incrHTML-selector-1=.yearsource
	incrHTML-replaceValue-1=1900
	incrHTML-start-1=2016
	incrHTML-end-1=1900
	incrHTML-step-1=-1
	incrHTML-selector-2=.daysource
	incrHTML-replaceValue-2=1
	incrHTML-start-2=1
	incrHTML-end-2=31
	incrHTML-step-2=1>
	<select class="daysource form-select form-control-alt rnd wauto iblock" name=day placeholder=Day aria-label="Please select day of birth">
		<option>1</option>
	</select>
	<select class="yearsource form-select form-control-alt rnd wauto iblock" name=year placeholder=Year aria-label="Please select year of birth">
		<option>1900</option>
	</select>
</div>

Function Usage

Any function can be called from HTML attributes - no registration needed. Functions are automatically Promise-wrapped for proper sequencing.

Unlike traditional JS frameworks, Popstart doesn’t require you to define functions in a specific way or register them with the library. You can call any function that’s in scope, whether it’s a global function, a function in a namespace, or a function defined in the same or different file (as long as it’s loaded before the DOM event is triggered).

SECURITY NOTE: Popstart doesn’t allow arbitrary dynamic functions to be created and called, or arbitrary JS to be executed as in old-style HTML embeds (onclick="alert('xss')"). All functions have to already exist and also be identified in the calling html. In order to attack, you’d have to already have another vulnerability to chain off of, as well as the ability to inject arbitrary js func AND modify or inject html. This is a much higher bar than the traditional onclick or eval attacks. Under CSP, you can’t use inline JS, so you’re already protected from that. If someone could inject arbitrary JS or modify your html, you’ve already been fully compromised and it’s game over, and no library can protect you.

Regular Functions

// 'this' is automatically set to the triggering DOM element
const updateElement = function(text, color) {
	this.innerHTML = text
	this.style.color = color
}

Calling that function from HTML:

<div 
	click="updateElement"
	updateElement-text="Hello!"
	updateElement-color="red">
	Click me
</div>

Arrow Functions

Arrow Functions don’t support this, but can otherwise be used in the same way as regular functions.

As with regular functions, if they return a Promise, the next function in the chain will be called only after the Promise is resolved. If they do not return a promise, then the function will be wrapped in a promise automatically so they can be called in the correct order.

window.updateElement = (text, color) => {
		document.getElementById('div').innerHTML = text
			document.getElementById('div').style.color = color
}

Promise-Returning Functions

Here’s a toy example that would throw away the JSON that was fetched but, if a successful HTTP status code was returned, the updateElement function would be called and update the element with the text “Done!”:

Here’s the function that returns a Promise:

fetchJSON = (url, method = 'GET') => fetch(url, { method }).then(r => r.json());

Here’s an updateElement function (it will be wrapped in a Promise automatically):

updateElement = function(text){this.innerHTML = text}

Here’s the HTML to call those functions:

<button 
	click="fetchJSON, updateElement"
	fetchJSON-url="/api/data"
	updateElement-text="Done!">
	Load
</button>

If there was an error in the fetchJSON function, the global default error handler (error()) would be called.

Namespaced Functions

Functions can live anywhere in the global scope or namespaces - Popstart will find them automatically.

window.api = {
	getData: (endpoint) => fetch(endpoint),
	process: function(data) {
		this.textContent = data  // 'this' works in regular functions
	}
}

If you create a namespace in the global window object named api, you can call functions from that namespace by prefixing the function name with the namespace followed by a period, like api.getData in the example above:

<div 
	load="api.getData, api.process"
	getData-endpoint="/api/status">
</div>

Attribute Prefixes

Configure additional attribute prefixes:

__.config.AttrPrefixes.push('data-')

Event Argument Names

Add custom argument names that receive the event:

__.config.eventNameArgs.push('evt')

Error Argument Names

Add custom argument names that receive errors:

__.config.errorNameArgs.push('err')

State Management

Access last operation data:

__.state.lastOp.last  // Last function result
__.state.lastOp.ev	// Last event
__.state.lastOp.this  // Last element

FAQ

Q. How does this compare to traditional JS frameworks?

A. Popstart is a different approach to frontend development:

Compared to React or Vue.js, Popstart is a lightweight, HTML-driven library that works directly in the DOM, which is much faster and more flexible and allows you to write complex application logic directly in HTML attributes.

Popstart also lets you modify the DOM outside of Popstart’s control. It’s not all or nothing! Popstart plays nicely with other libraries and frameworks. You can use Popstart with jQuery, HTMX, Alpine.js, or any other library. (In the full build or in your custom build, Popstart includes an optional DOM mutation observer to detect changes to the DOM and re-apply Popstart to new elements.)

Please see Comparison Table above for more details compared to Alpine.js and HTMX.

Core API Reference

Event Attributes

Attribute Description Example
click Mouse click click=“auth.login, ui.update”
submit Form submission submit=“validate.form, api.send”
change Value change change=“data.update”
load Element loaded load=“init.setup”
startup Document loaded startup=“app.init”

Control Attributes

Suffix Purpose Example
-error Error handler login-error=“ui.showError”
-prevent Prevent default click-prevent=“true”
-stop Stop propagation click-stop=“true”
-writedatapath Store result fetch-writedatapath=“userData”

Global Object (__)

The double-underscore __ object contains all the shared data, configuration settings, state information, and utility functions.

__.data           // Shared data store
__.state.lastOp   // Last operation details
__.config         // Configuration settings

Configuration Options

__.config = {
    AttrPrefixes: ['data-', 'x-'],    // Optional attribute prefixes
    eventNameArgs: ['event', 'e'],     // Event argument names
    errorNameArgs: ['error', 'err'],   // Error argument names
    debug: false                       // Enable debug logging
}

Logging Functions

Function Purpose
debug() Development details
info() General information
warn() Non-fatal warnings
error() Errors
danger() Critical errors

Utility Functions

Function Purpose Example
__.incrHTML Generate incremental HTML incrHTML-start=“1” incrHTML-end=“10”
__.getEl Element selector getEl(‘.myClass’)
__.clone Deep clone objects clone(myObject)

Build Process

The build process (if desired) is a simple shell script. If you want to exclude a file from the build, just add that filename to the file exclude.txt before running ./build.sh.

The build.sh simply walks through the files in the src directory, concatenates them, and minifies them with minify (sudo apt-get install minify)

Browser Support

License

MIT Licensed, Copyright © by Popchat Inc.

Contributing

PRs welcome! Popstart is feature complete and the core library is not likely to receive any additional features, but bug fixes and performance improvements are always welcome. Please add features to the higher-numbered files in src/ as appropriate, or create new files as needed.

The examples in this README were partly written by AI, so please open an issue if you see any errors.

Copyright © 2021 Popchat Inc. | Popstart | GitHub Popstart.js