Guide Index
- ๐๏ธ Project Layout
- ๐ช Entry Point
- ๐งฉ Component Pattern
- ๐ง Component Display
- ๐งต Component Arguments
- ๐ช Functions for Output
- โฑ๏ธ Async Callback Wrapper
- โณ tag.promise
- ๐งน onDestroy Cleanup
- ๐ผ๏ธ Display
- ๐ฆ Element Imports
- ๐ท๏ธ attributes``
- โจ Dynamic Content _=>
- ๐ Map Loops
- ๐ฑ๏ธ Event Handlers
- ๐ Reactive Updates
- โ๏ธ React vs TaggedJS
- ๐ก Subscriptions & Observables
- ๐งพ Output Subscriptions
- ๐จ Subscriptions in Attributes
- โป๏ธ Subscription Lifecycle
- ๐งน Manual Unsubscribe
๐๏ธ Project Layout
The gh-pages branch is a full app. Source code lives in src/, the runtime entry is in src/index.ts, and the main view assembly lives in src/app.tag.ts plus the menu in src/menu.tag.ts.
Use the documentation/ folder for doc pages, styles, and future guides. Keep documentation separate from app runtime code so it remains focused and easy to host.
๐ช Entry Point
To start a TaggedJS app, place a custom element in your HTML and mount the component with tagElement. The mount call connects your component to that element and triggers the first render.
This is the minimal setup: an HTML document, a root element, and a module script that defines a tag component and mounts it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>TaggedJS App</title>
</head>
<body>
<app id="app-root"></app>
<script type="module">
import { tag, tagElement, div, h1 } from "taggedjs"
const App = tag(() =>
div(
h1("Hello TaggedJS")
)
)
tagElement(App, document.getElementById("app-root"))
</script>
</body>
</html>
๐งฉ Component Pattern
Components are created by calling tag and returning mock element functions like div, button, and p. Below are some simple examples. You may see syntax used in ways you have not seen before BUT all code is native vanilla JavaScript that is supported everywhere.
Basic Counter Component
import { tag, p, button } from 'taggedjs'
export const basicCounter = tag(() => (counter = 0) => [
p('Counter: ', _=> counter}),
button.onClick(() => counter++)('Increment Counter')
])
โ๏ธ ABOVE Explanation: The function "basicCounter" becomes a tag component when wrapped in a tag() call. The tag requires no inputs/props/arguments. The new tag/component is designed to create a local variable counter that is set to 0 and increments when a button is clicked.
Basic show/hide Component
import { tag, div, button } from 'taggedjs'
export const basicShowHide = tag(() => (showDiv = true) =>
div(
button.onClick(() => showDiv = !showDiv)(
_=> `Toggle Div (${showDiv ? 'Hide' : 'Show'})`
),
_=> showDiv && div('Now you see me')
)
)
โ๏ธ ABOVE Explanation: The function "basicShowHide" becomes a tag component when wrapped in a tag() call. The tag requires no inputs/props/arguments. The new tag/component is designed to create a local variable "showDiv" that is toggled true/false when a button is clicked.
The tag(() => (counter = 0) => div(_=> counter)) form is shorthand for declaring local variables and returning markup. It is the same as tag(() => { let counter = 0; return div(_=> counter) }), just more compact.
Returning an array lets you emit multiple root elements without a wrapper. () => [div('hello'), div('world')] is the no-wrapper alternative to () => div('hello world') when you want separate siblings.
๐ง Component Display
When you pass arguments to a tag component, render it inside a _=> block so TaggedJS treats argument changes as updates.
This keeps the tag mounted and lets .updates(...) receive new arguments without recreating the component.
Using _=> also helps it stand out as dynamic display, while something like div.onClick(() => {}) reads more like an event handler. It's optional, but recommended for clearer intent.
return div(
_=> boltTag(counter)
)๐งต Component Arguments
Tag components receive arguments like normal functions, but you must opt in to argument updates when those values change. Call .updates(...) inside the tag to re-assign the latest arguments in the same order they were passed.
This keeps local variables in sync with the parent without re-running the entire tag function.
const boltTag = tag((parentCounter: number) => {
boltTag.updates(args => [parentCounter] = args)
return div(
div(_=> `parent counter: ${parentCounter}`)
)
})
src/basic.tag.ts๐ช Functions for Output
TaggedJS treats function arguments as outputs: when the child calls the function, the parent can update state and re-render the dependent _=> segments.
This mirrors Angular-style outputs and keeps data flowing up without recreating the child component.
Calling output with the callback binds the caller to the currently running tag, so when the child triggers it, TaggedJS knows which parent output to re-evaluate.
import { output, tag, button } from "taggedjs"
export const child = tag((onSave: () => void) => {
onSave = output(onSave)
return button.onClick(() => onSave())("Save")
})
export const parent = tag(() => {
let saved = 0
return div(
_=> child(() => saved++),
div(_=> `saved: ${saved}`)
)
})
โฑ๏ธ Async Callback Wrapper
TaggedJS exports callback to wrap async handlers (events, timers, promises) so the current tag can re-evaluate dependent _=> outputs when the async work completes.
Call callback inline and assign it to a new variable so it can be registered and cleaned up with the async API.
import { callback } from "taggedjs"
const getHash = () => window.location.hash.substring(1) || '/'
const HashRouter = () => {
const memory = {route: getHash()}
const onHashChange = callback(() => memory.route = getHash())
window.addEventListener('hashchange', onHashChange)
return {memory, onHashChange}
}
todo/src/HashRouter.function.tsโณ tag.promise
Set tag.promise to a Promise to tell TaggedJS that a new render cycle should run when that async work completes.
Do not use await tag.promise or await the promise inline; the promise is only a signal for re-render, not a value to block on.
const promiseTag = tag(() => {
let x = 0
tag.promise = new Promise((resolve) => {
setTimeout(() => {
++x
resolve(x)
}, 250)
})
return div.id`tag-promise-test`(_=> `count ${x}`)
})
src/async.tag.ts๐งน onDestroy Cleanup
Use onDestroy to run cleanup logic when a tag component is removed. For host elements, host.onDestroy lets you attach cleanup at the element level.
import { tag, div, button, onDestroy, host, signal } from "taggedjs"
const contentTag = tag(() => {
onDestroy(() => {
// closing logic here
})
return div("this tag will be destroyed")
})
const destroys = tag(() => (showContent: boolean) =>
div(
"Content:", _=> showContent && contentTag(),
button
.onClick(() => { showContent = !showContent } )
(_=> showContent ? "destroy" : "restore")
))
src/destroys.tag.tsCommon uses include removing event listeners, stopping intervals, or disposing subscriptions tied to the component lifecycle.
๐ผ๏ธ Display
These sections cover the rendering building blocks: importing elements, applying attributes``, mapping lists, and wiring event handlers.
๐ฆ Element Imports
TaggedJS exposes HTML elements as functions you can import directly. This keeps your render output explicit, avoids string-based templates, and makes composition feel like regular JavaScript.
Benefits include clear dependency lists, easy refactors, and better editor autocomplete because each element is a real import instead of a string tag.
import { div, span, button } from "taggedjs"
export const example = tag(() =>
div(
span("Hello"),
button.onClick(() => alert("Hi"))("Click")
)
)
๐ท๏ธ attributes``
TaggedJS supports a shorthand attribute syntax using tagged template calls. You can set attributes with concise chains like div.id`identifier`.style`border:1px solid black;` to keep the markup compact.
For dynamic values, switch to a function call. The .style(_=> border) form keeps the same chain, but marks the style as reactive so it updates when the value changes.
div
.id`identifier`
.style`border:1px solid black;`
("attributes shorthand")
const border = "border:2px solid blue;"
div
.id`identifier`
.style(_=> border)
("dynamic style")
โจ Dynamic Content _=>
The _=> prefix is a visual cue that the content is dynamic and will re-evaluate when values change. It is meant to stand out from () =>, which you will usually see in event handlers like onClick.
const counter = tag(() => [
p(_=> count ),
button.onClick(() => count++), 'increment')
])
๐ Map Loops
TaggedJS uses normal JavaScript array mapping for list rendering. Put the array.map inside a _=> block so the list is reactive.
Return a tag for each item and add .key(...) when the list can be reordered or removed so TaggedJS can keep elements stable.
const items = ['a', 'b', 'c']
_=> items.map((item, index) =>
div(
'item:', _=> item,
' index:', _=> index,
button.onClick(() => items.splice(index, 1))('remove')
).key(item)
)
๐ฑ๏ธ Event Handlers
Event handlers use method chaining like button.onClick(...). The code in src/basic.tag.ts shows the standard pattern.
export const clicker = tag(() => {
let counter = 0
return button.onClick(() => counter++)(_=> `Increment Counter: ${counter}`)
})
src/basic.tag.ts๐ Reactive Updates
Reactive updates are driven by closures and tracked by the TaggedJS runtime. When a function uses values in an arrow callback (for example, _=>), the runtime re-evaluates that part of the view when the values change.
In the example above, the p tags and the final conditional _=> showDiv && boltTag(counter) are reactive segments.
โ๏ธ React vs TaggedJS
React typically re-runs the component function to produce the next render output, then reconciles the result. TaggedJS keeps the main tag function stable and re-evaluates only the dynamic segments marked with _=>, which helps focus updates on the specific parts that changed.
๐ก Subscriptions & Observables
TaggedJS treats observable streams as first-class render inputs. Use subscribe (and subscribeWith) inside output blocks to turn emissions into DOM updates.
A "LikeObservable" is any object with subscribe(callback) that returns a subscription object (or function) with an unsubscribe() method. TaggedJS subscribes during render and automatically cleans up when that output is removed.
๐งพ Output Subscriptions
Use subscribe(observable, map?) to display the latest value. If you pass a callback, TaggedJS uses it to map emissions to output.
To combine multiple observables, use subscribe.all([a$, b$], ([a, b]) => ...) (which uses a combined subject under the hood) or pipe([a$, b$], values => ...) if you already have a list of observables.
import { tag, ValueSubject, subscribe, span, button } from "taggedjs"
const count$ = new ValueSubject(0)
export const counter = tag(() => [
button.onClick(() => count$.next(count$.value + 1))("Increment"),
span(_=> subscribe(count$)),
span(_=> subscribe(count$, value => `count: ${value}`))
])
src/subscriptions.tag.tsUse subscribeWith when you want an initial default value before the first emission. It emits the default (or current .value if available) and then switches to live updates.
import { tag, ValueSubject, subscribeWith, span } from "taggedjs"
const status$ = new ValueSubject("idle")
export const status = tag(() => (
span(_=> subscribeWith(status$, "idle", value => `status: ${value}`))
))
๐จ Subscriptions in Attributes
Subscriptions also work in attributes. The runtime wires the attribute once and updates the value on each emission.
import { tag, ValueSubject, subscribeWith, div } from "taggedjs"
const color$ = new ValueSubject("tomato")
export const swatch = tag(() => (
div({
style: subscribeWith(color$, "tomato", color => ({ backgroundColor: color }))
}, "color swatch")
))
src/subscribeAttributes.tag.tsโป๏ธ Subscription Lifecycle
When a subscribe output is rendered, TaggedJS creates an internal subscription context that stores the latest values and the list of active subscriptions.
- Subscribes to each observable on first render and stores the returned subscriptions in
contextItem.subContext.subscriptions. - When the output is removed (conditional turns false, array diff removes it, or component is destroyed),
deleteAndUnsubscribecallsunsubscribe()on each stored subscription and clears the sub-context. - If a value changes from a subscription to something else, TaggedJS destroys the old subscription and then updates the DOM with the new value.
For debugging, Subject.globalSubCount$ tracks active subscriptions and is incremented on subscribe and decremented on unsubscribe.
๐งน Manual Unsubscribe
If you subscribe manually via Subject.subscribe (outside of subscribe(...)), you are responsible for calling subscription.unsubscribe(). onDestroy is the usual place to do that.
import { tag, Subject, onDestroy } from "taggedjs"
const updates$ = new Subject(0)
export const listener = tag(() => {
const subscription = updates$.subscribe(value => {
// side effects here
})
onDestroy(() => subscription.unsubscribe())
return "listening"
})
src/providers.tag.ts