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

Component arguments are TaggedJS inputs. They are values the parent passes down to a child component, similar to Angular @Input() properties.

A tag component is a stable instance after it is mounted. Its main function sets up local state and returns the view once; later parent changes update the dynamic output blocks instead of recreating the whole child.

That is why child tags that receive changing arguments should be rendered inside _=>. The dynamic wrapper gives TaggedJS a place to send the latest parent values into the already-mounted child.

export const parent = tag(() => {
          let counter = 0

          return div(
            button.onClick(() => counter++)("Increment"),
            _=> boltTag(counter)
          )
        })

        const boltTag = tag((parentCounter: number) => {
          boltTag.updates(args => [parentCounter] = args)

          return div(_=> `parent counter: ${parentCounter}`)
        })
        
Source: src/basic.tag.ts

Use .updates(...) when the child only needs the later values. It receives the current argument array in call order and copies those values into the local variables used by the child view.

const boltTag = tag((parentCounter: number) => {
          boltTag.updates(args => [parentCounter] = args)

          return div(
            div(_=> `parent counter: ${parentCounter}`)
          )
        })
        
Source: src/basic.tag.ts

๐Ÿ”Œ Inputs and Updates

The important distinction is lifecycle timing, not two competing ways to declare inputs. The function parameters define the component inputs. .updates(...) runs on later parent argument changes. .inputs(...) runs during initial input setup and again on later argument changes.

Reach for .inputs(...) when incoming arguments need normalization every time they enter the component lifecycle. In this repo that usually means turning a function input into an output callback with output(...), because the callback must be bound on the first render and whenever the parent passes a new function.

This keeps the Angular-shaped model clear: inputs are values flowing down from the parent, updates are the moment those values are synchronized into an existing component, and dynamic _=> output blocks are what re-render from those synchronized values.

import { tag, output, footer, span, button } from "taggedjs"

        const Footer = tag((
          todosCount: number,
          removeCompleted: () => any,
          route: string,
          activeTodoCount: number,
        ) => {
          Footer.inputs(args => {
            [todosCount, removeCompleted, route, activeTodoCount] = args
            removeCompleted = output(removeCompleted)
          })

          return footer(
            span(_=> activeTodoCount),
            _=> (todosCount - activeTodoCount) > 0 &&
              button.onClick(() => removeCompleted())("Clear completed")
          )
        })
        
Source: src/todo/components/footer.ts

๐Ÿช Functions for Output

Function arguments are the TaggedJS output pattern. The parent passes a function down, the child calls it when something happens, and the parent updates its own state.

That mirrors Angular @Output() event flow: inputs move data down into the child, outputs notify the parent that something happened.

Calling output binds the callback to the parent render context. When the child invokes that wrapped callback, TaggedJS can re-run the parent's affected _=> output blocks after the parent state changes.

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

When a function input may change, wrap it again inside .updates(...) or use .inputs(...) for the initial-and-update form. The repository repeats this pattern in innerCounters, funInPropsChild, propsDebug, and Footer.

import { tag, output, button } from "taggedjs"

        const child = tag((onSave: () => void) => {
          onSave = output(onSave)

          child.updates(args => {
            [onSave] = args
            onSave = output(onSave)
          })

          return button.onClick(onSave)("Save")
        })
        
Re-wrap output callbacks after assigning new inputs

โฑ๏ธ 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

๐Ÿงพ outerHTML for HTML Strings

Call .outerHTML on the result of a tag component to render it as a plain HTML string. This is useful for server-side rendering, static HTML generation, tests, and snapshots.

outerHTML is a getter, not a function. Use MyTag().outerHTML, not MyTag.outerHTML or MyTag().outerHTML().

import { tag, div, span } from "taggedjs"

        const Greeting = tag((name: string) =>
          div("hello world", () => name)
        )

        const html = Greeting(" love").outerHTML

        console.log(html)
        // <div>hello world love</div>

        const Nested = tag(() =>
          div.class`wrapper`(
            span("hello world")
          )
        )

        Nested().outerHTML
        // <div class="wrapper"><span>hello world</span></div>
        
Serialize a tag output directly

The getter renders the component with its stored arguments, then serializes the returned TaggedJS element using the same behavior as elementVarToHtmlString.

import { elementVarToHtmlString } from "taggedjs"

        const html = elementVarToHtmlString(Greeting(" love"))
        
Equivalent manual serializer form

Event handlers are intentionally omitted from the returned HTML string, matching direct elementVarToHtmlString output.

๐Ÿ”Ž Static HTML / SEO Output

The same string output is what people usually mean by server-side rendering, static rendering, pre-rendering, or SEO output. TaggedJS can create the HTML before the browser runs the app, so crawlers and users receive real document content instead of an empty mount element.

This documentation page already uses that pattern. scripts/renderDocsHtml.ts mounts GuideApp in a Happy DOM document, reads mount.innerHTML, and writes that string between the SSR markers in documentation/index.html.

import { Window as HappyWindow } from "happy-dom"
        import fs from "fs"
        import { tagElement } from "taggedjs"
        import { GuideApp } from "../src/documentations/guide.js"

        const window = new HappyWindow()
        globalThis.window = window as any
        globalThis.document = window.document as any

        const mount = document.createElement("app")
        mount.id = "docs-app"
        document.body.appendChild(mount)

        tagElement(GuideApp, mount)

        const html = mount.innerHTML
        fs.writeFileSync("documentation/index.html", html)
        
Generate static HTML from a TaggedJS component

The saved page now contains the rendered header, table of contents, and guide sections as normal HTML inside #docs-app. That is the SEO-friendly output: the content exists in the file before client JavaScript downloads.

<app id="docs-app">
          <!-- taggedjs:ssr-start -->
          <header>...</header>
          <main>...</main>
          <!-- taggedjs:ssr-end -->
        </app>

        <script type="module">
          import { runDocs } from "../assets/dist/bundle.js"
          runDocs()
        </script>
        
Static content in the mount element, followed by the client app

On load, the browser parses that string into real DOM immediately. Then the module script calls runDocs(). This repo currently clears #docs-app and mounts GuideApp again so the page becomes the normal interactive TaggedJS app.

export function runDocs() {
          const mount = document.getElementById("docs-app")
          if(!mount) return

          mount.innerHTML = ""
          tagElement(GuideApp, mount)
        }
        
Client-side remount after the static HTML is visible

That flow is close to what frameworks like Next.js do for React in goal: send useful HTML first, then attach the client runtime. The current TaggedJS docs implementation is a static pre-render plus client remount; it does not preserve and hydrate the existing DOM nodes.

๐Ÿงน 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

๐Ÿ”‚ Arrays - Map Looping

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. Alternatively, if the first rendered element has an id like div.id`x-${x.id}`, that id can act as the array value identifier.

Map each item and key the result
import { div, tag } from "taggedjs"

        const arrayTag = tag(() => {
          const items = ['a', 'b', 'c']
          
          return div(
            items.map((item, index) =>
              div(`hello world item ${index}`).key(item)
            )
          )
        })
        

Use a stable id as the loop key
const users = [
          { id: 101, name: 'Ada' },
          { id: 102, name: 'Grace' },
          { id: 103, name: 'Linus' }
        ]

        _=> users.map(user =>
          div.id(_=> `user-${user.id}`)(
            span('name: ', _=> user.name)
          )
        )
        

๐Ÿ–ฑ๏ธ 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

Start by using subscribe(observable) to display the latest value from an observable.

๐Ÿงฉ Map Values to Output

If you pass a callback, TaggedJS uses it to map emissions to output. This is useful for formatting or wrapping values.

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"),
          /* output just the emitted value */
          span('count:', subscribe(count$)),

          /* output the emitted value plus a label */
          span(subscribe(count$, value => `count: ${value}`))
        ])
        
Source: src/subscriptions.tag.ts

๐Ÿšซ No _=> Wrapper

Do not wrap subscribe(...) with _=> โ€” subscribe(...) already returns a tagged value that the runtime recognizes and manages.

๐Ÿ”— Combine Observables

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.

Default value with subscribeWith

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") // anything with a subscribe method will do

        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

Dynamic Child Content and Updating Props

If you are coming from React, Vue, or Svelte, the main trap is assuming that a child tag's function re-runs whenever the parent has new values. A TaggedJS tag sets up a stable component instance. After that, dynamic function children, subscriptions, and input handlers are the places where changing values are read again.

Static strings, static arrays, and objects computed during a tag render are snapshots. If the parent later changes state, those snapshots do not magically recompute inside an already-mounted child.

Rules of Thumb

  • subscribe(observable) creates reactive output for one observable.
  • subscribe.all([a$, b$], callback) combines multiple observable values in one render callback and avoids nested subscription confusion.
  • Function children like () => value or (_: unknown) => value are not just event callbacks. In output and attributes, they mark values that must be re-evaluated on updates.
  • .inputs(...) is the TaggedJS equivalent of handling updated component props or arguments. Use it when local derived variables depend on changing inputs.
  • Arrays returned from .map(...) should use .key(stableValue) when items can be added, removed, or reordered.

Broken: Derived Child Data Freezes

This version combines two stores correctly with subscribe.all, but then passes a precomputed parsed object into a child that renders a static string. The child view is set up once, so later card changes can leave the details panel showing old JSON.

import { tag, subscribe, section, div, h2, pre } from "taggedjs"

        const Dashboard = tag(() =>
          section(
            subscribe.all([hardware$, rotation$], ([hardware, rotation]) => {
              const payload = { hardware, rotation }
              const card = visibleCard(payload, rotation.currentCardId)
              const parsed = parsedDataForCard(card)

              return div(
                h2(card.title),
                DetailsPanel(card, parsed)
              )
            })
          )
        )

        const DetailsPanel = tag((card: Card, parsed: unknown) =>
          pre(JSON.stringify(parsed, null, 2))
        )
        
The child receives snapshots and renders a static pre block

Fixed: Update Inputs and Render Dynamic Values

Keep the parent subscription focused on combining stores and selecting the current card. Put child-specific derived data inside the child, then refresh it from .inputs(...) when the parent passes a new card.

The pre(() => JSON.stringify(parsed, null, 2)) child is dynamic. TaggedJS can call it again after parsed is reassigned in the input handler.

import { tag, subscribe, section, div, h2, pre } from "taggedjs"

        const Dashboard = tag(() =>
          section(
            subscribe.all([hardware$, rotation$], ([hardware, rotation]) => {
              const payload = { hardware, rotation }
              const card = visibleCard(payload, rotation.currentCardId)

              return div(
                h2(() => card.title),
                _ => DetailsPanel(card)
              )
            })
          )
        )

        const DetailsPanel = tag((card: Card) => {
          let parsed = parsedDataForCard(card)

          DetailsPanel.inputs(([nextCard]) => {
            card = nextCard
            parsed = parsedDataForCard(card)
          })

          return pre(() => JSON.stringify(parsed, null, 2))
        })
        
The child owns derived local data and re-reads it dynamically

Function Children Are Dynamic Rendering

Use function children anywhere a value must be read again: text nodes, conditional children, mapped children, and attributes. Event handlers also use functions, but the meaning is different: onClick(() => save()) handles an event, while span(() => label) renders a dynamic value.

const StatusBadge = tag((status: Status) => {
          StatusBadge.inputs(([nextStatus]) => status = nextStatus)

          return span
            .class(() => `status status-${status.kind}`)
            (() => status.label)
        })
        
Dynamic text and dynamic attributes

Stable Keys for Mapped Tags

When rendering a mapped array of tags, give each rendered item a stable primitive key from the data, such as an id, slug, or persistent name. Do not use fresh inline objects or arrays as keys because their identity changes every render and prevents stable matching.

const CardList = tag((cards: Card[]) => {
          CardList.inputs(([nextCards]) => cards = nextCards)

          return ul(
            () => cards.map(card =>
              CardRow(card).key(card.id)
            )
          )
        })

        // Avoid keys that are recreated each render:
        // CardRow(card).key({ id: card.id })
        // CardRow(card).key([card.id])
        
Use stable keys from item data

Back to top