Work With The TaggedJS Code

This guide is grounded in the real TaggedJS source. Each section points to current files and uses live excerpts so you can follow the actual patterns used in this repo.

Guide Index

๐Ÿ—‚๏ธ 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.

Back to top

๐Ÿšช 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>
        
Minimal TaggedJS app bootstrap

Back to top

๐Ÿงฉ 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)
        )
Render tag components inside a dynamic output

๐Ÿงต 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}`)
          )
        })
        
Source: 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}`)
          )
        })
        
Child callback triggers parent updates

โฑ๏ธ 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}
        }
        
Source: 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}`)
        })
        
Source: 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")
        ))
        
Source: src/destroys.tag.ts

Common uses include removing event listeners, stopping intervals, or disposing subscriptions tied to the component lifecycle.

Back to top

๐Ÿ–ผ๏ธ 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")
          )
        )
        
Import only the elements you use

๐Ÿท๏ธ 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")
        
Shorthand attributes with static and dynamic styles

โœจ 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')
        ])
        
Dynamic content cue vs event handler

๐Ÿ”‚ 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)
        )
        
Map each item and key the result

๐Ÿ–ฑ๏ธ 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}`)
        })
        
Source: src/basic.tag.ts

Back to top

๐Ÿ” 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.

Back to top

๐Ÿ“ก 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}`))
        ])
        
Source: src/subscriptions.tag.ts

Use 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}`))
        ))
        
Default emission with subscribeWith

๐ŸŽจ 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")
        ))
        
Source: 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), deleteAndUnsubscribe calls unsubscribe() 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"
        })
        
Source: src/providers.tag.ts

Back to top