WildflowerJS Reactive JS, No BS*

A no-build reactive JavaScript framework, rooted in the web platform.
No build step. No dependencies. No lock-in.

<script src="wildflower.min.js"></script> ...and start building.

Back to Basics

The code you write is 100% web standard code. HTML stays HTML. JavaScript stays JavaScript. CSS stays CSS. No JSX, no templating language, no custom syntax to learn. If you know the web platform, you already know how to use this.

WildflowerJS extends the web platform. It doesn't replace it.

Your Development Simplified

Because you develop with 100% web standards, every tool in your existing chain already understands the code: IDE, browser DevTools, linter, formatter, screen reader, SEO crawler. Nothing to install, no custom file types, no sourcemaps. Save the file, refresh, and your change is live.

Just be a web developer.

Batteries Included: One Mental Model

Router, SSR, stores, computed properties, two-way binding, event modifiers, data pools, and TypeScript types, all built in, all speaking the same language. Learn data-bind once and you know binding everywhere: lists, pools, stores, forms. There's no five-library stack to keep in sync.

One script tag. Everything you need.

<div data-component="counter">
  <span data-bind="count"></span>
  <button data-action="increment">
    +1
  </button>
</div>

<script>
wildflower.component('counter', {
  state: { count: 0 },
  increment() { this.count++ }
})
</script>

How It Works

data-bind connects state to the DOM.

data-action connects events to methods.

this.count++ triggers a precise DOM update.

Mutate state. The DOM updates.

Two Reactivity Modes

data-list for automatic reactivity: mutate state, DOM updates. data-pool for explicit control: plain objects, zero proxy overhead, you say what changed.

Same template syntax. Different performance profile. From interactive forms to per-frame particle systems. You choose the right tradeoff for the job.

Try it. Right-click, inspect this demo. Every dot is a real DOM element.

See full demo →

* Build Step

Zero Toolchain

Modern frameworks ask you to install a compiler, a bundler, a package manager, hundreds of fragile transitive dependencies, and a framework-specific file format, before you write a single line of your application.

WildflowerJS was built starting from a single principle: no build step, no tooling. Ever.

WildflowerJS asks you to add a script tag.

There's no CLI scaffolding step, no config files, no .vue/.jsx/.svelte source format. You don't debug through sourcemaps or wait on a build pipeline. Your project has zero dependencies.

Performance isn't a tradeoff. Build steps optimize bundle delivery, not the runtime work that follows it. WildflowerJS writes directly to the DOM, with no virtual DOM or reconciliation pass between state change and update, so it doesn't need a build step to be fast.

The framework is full-featured without the toolchain: router, SSR, stores, computed properties, transitions, pools. You don't need a toolchain to use any of it.

my-app/
  index.html
  app.js
  style.css
  wildflower.min.js

That's the entire project. No package.json.
No node_modules. No config files. Ship it.

Zero Install. Zero Attack Surface.

Every dependency you install is trust extended to a maintainer you've never met, running scripts on your dev machine and in your CI. A typical React + Vite + UI‑lib setup pulls in 300+ transitive packages before you write a feature.

Each one is a potential intrusion vector. NPM worms, OAuth chains compromising deploy platforms, postinstall hijacking: the supply chain is now where production code gets compromised, not the deploy. And signing isn't a backstop: Mini Shai‑Hulud (May 2026) compromised 170+ packages whose malicious versions carried valid SLSA Build Level 3 provenance, because the attestation came from build infrastructure the worm had already taken over.

WildflowerJS users don't have this attack surface, by construction. There is no npm install, no postinstall script, no transitive package graph. The framework is one file you copy or pin by hash.

As of v1.1, the same holds for building the framework itself. WildflowerJS bundles with a vendored rollup and terser pipeline pulled as three SHA‑512‑pinned tarballs: no npm install, no transitive packages, no postinstall scripts in the build path. The entire toolchain is three files you verify by hash.

Zero dependencies is the absence of a problem the rest of the industry has not properly addressed.

A typical React/Vue project:

  npm install
  ├── hundreds of packages
  ├── from hundreds of maintainers
  ├── postinstall scripts run on install
  └── tens to hundreds of MB of transitive code

WildflowerJS:

  <script src="wildflower.min.js"></script>
  └── 1 file.
      No transitive dependencies.

Zero Lock-in

WildflowerJS works with the DOM, not instead of it. There's no virtual DOM intercepting your code and no compiler rewriting your markup. The render cycle is yours.

That means Leaflet, DataTables, Chart.js, D3, Three.js, any library that touches the DOM, just works. No wrapper packages or framework-specific escape hatches required. Drop in a script tag and use it.

Because your code is standard HTML and JavaScript, you're never locked in. Your skills transfer and your code is more portable. If you outgrow the framework, your knowledge doesn't expire.

This also means your "ecosystem" is all of the world of vanilla JS. Without compromises or hacks.

<!-- Use any library directly -->
<div data-component="map-view">
  <div id="map" style="height: 400px"></div>
</div>
wildflower.component('map-view', {
  state: { lat: 51.505, lng: -0.09 },
  init() {
    // Leaflet works as-is. No wrappers.
    this._map = L.map('map')
      .setView([this.lat, this.lng], 13);
    L.tileLayer('https://{s}.tile.osm.org'
      + '/{z}/{x}/{y}.png').addTo(this._map);
  }
})

Precise Reactivity

When you write this.count++, WildflowerJS updates the single DOM node bound to count. Nothing else is touched. There's no tree diffing or reconciliation pass to figure that out.

This isn't a tradeoff. You get fine-grained updates and a simple mental model. Change a property, the bound element updates. That's the entire reactivity model.

Other frameworks ask you to learn signals, accessors, memos, effects, and subscription lifecycles to achieve what WildflowerJS does with a property assignment.

wildflower.component('dashboard', {
  state: {
    users: 1420,
    status: 'healthy'
  },
  computed: {
    summary() {
      return this.users + ' users, ' + this.status;
    }
  },
  refresh() {
    this.users = 1421;
    // Only the elements bound to 'users'
    // and 'summary' update. Everything
    // else on the page is untouched.
  }
})

One Reactivity Model. Everywhere.

Components, Stores, and Plugins all share the same reactive foundation. State, computed properties, and methods work identically no matter where they live. Learn it once, it works the same way in a UI component, a global store, or a framework plugin.

Other frameworks make you learn a different system for each layer. React components use hooks, but stores need Redux or Zustand, which are completely different APIs. Vue components use reactive data, but Pinia stores have their own patterns. Every layer is a new mental model.

In WildflowerJS, there's one model. A store is a component without a template. A plugin is an entity that extends the framework itself, adding directives, lifecycle hooks, and services. The same this.count++ triggers the same reactivity everywhere.

This unlocks patterns other frameworks can't express. A store can run headless physics simulations with tick(), feeding data into a component that renders it through a pool, all using the same reactive primitives, no glue code required.

// Component: reactive UI
wildflower.component('cart', {
  state: { items: [] },
  computed: {
    total() { return this.items.length; }
  }
})

// Store: global shared state
wildflower.store('user', {
  state: { name: '', role: 'guest' },
  computed: {
    isAdmin() { return this.role === 'admin'; }
  }
})

// Plugin: extends the framework
wildflower.plugin({
  name: 'notifications',
  state: { items: [], unreadCount: 0 },
  computed: {
    hasUnread() { return this.unreadCount > 0; }
  },
  add(msg) { this.items.push(msg); this.unreadCount++; }
})
// Access globally: wildflower.$notifications.add(...)

// Same state. Same computed. Same methods.

Data Pools

Every framework wraps collection items in reactive proxies, whether the item needs it or not. WildflowerJS gives you a choice: data-list for push reactivity (automatic), data-pool for pull reactivity (explicit control, zero proxy overhead).

Pools render plain objects with the same template syntax as lists. Mutate the object, call markDirty(), and only that item updates. Full CRUD, selection, bulk operations, all faster than the push-reactive path.

And because pools use pull-based rendering, they scale to simulations, games, particle systems, and data visualizations at native frame rate. Use cases that would choke a virtual DOM. No other framework has anything like this.

<div data-component="user-table">
  <tbody data-pool="users" data-key="id">
    <template>
      <tr>
        <td data-bind="name"></td>
        <td data-bind="status"
            data-bind-class="status === 'active'
              ? 'badge success'
              : 'badge inactive'"></td>
      </tr>
    </template>
  </tbody>
</div>
wildflower.component('user-table', {
  pools: { users: {} },

  init() {
    // Populate: plain objects, no proxies
    data.forEach(u => this.pools.users.add(u));
  },

  // Optional: add tick() and the same pool
  // renders every frame. Same template, same
  // data, different rendering frequency.
  // That's the only difference between a
  // display table and a particle system.
})

Built for AI-Assisted Development

Because WildflowerJS is standard HTML and JavaScript, AI code assistants already know how to write it. There's no custom syntax to hallucinate or compiler quirks to work around. The code an AI generates runs exactly as written, with no build step between generation and execution.

We go further. WildflowerJS ships an AI-optimized reference page with patterns, anti-patterns, and examples designed for code generation context windows. Our llms.txt file follows the llms.txt convention for machine-readable documentation.

And for structured app generation, our Universal App Manifest lets you describe an entire application as a JSON schema (components, state, computed properties, methods, templates) and have an AI generate the working code from the manifest, mediated through framework-specific idiom files.

You: "Build me a todo app with
WildflowerJS"

AI reads llms.txt or ai-assistant.html
     ↓
Generates standard HTML + JS
     ↓
<div data-component="todo-app">
  <input data-model="newItem">
  <button data-action="addItem">
    Add
  </button>
  <ul data-list="items">
    <template>
      <li data-bind="text"></li>
    </template>
  </ul>
</div>
     ↓
Open in your browser. It works, and you can read and understand the code.

Event Modifiers & Patterns

Event modifiers control event behavior declaratively in HTML: debounce, throttle, key filtering, click-outside detection, and more. This page also covers custom DOM events and form event handling patterns.

Event Modifiers

WildflowerJS provides event modifiers to control event behavior declaratively in your HTML:

💡 Key Concept: Event modifiers let you handle common event patterns like preventDefault(), debouncing, and key filtering without writing boilerplate JavaScript code.

Basic Modifiers

Modifier Effect Example
data-event-prevent Calls event.preventDefault() <form data-action="submit:save" data-event-prevent>
data-event-stop Calls event.stopPropagation() <button data-action="handleClick" data-event-stop>
data-event-once Handler fires only once, then auto-removes <button data-action="initialize" data-event-once>
data-event-self Only fires if event.target is the element itself (not bubbled from children) <div data-action="closePanel" data-event-self>
data-event-capture Uses capture phase instead of bubble phase <div data-action="intercept" data-event-capture>
data-event-passive Marks listener as passive (improves scroll performance) <div data-action="scroll:track" data-event-passive>

Advanced Modifiers

Modifier Effect Example
data-event-outside Fires when clicking outside the element (great for dropdowns, modals) <div data-action="closeDropdown" data-event-outside>
data-event-if="condition" Only fires if condition evaluates to true <button data-action="save" data-event-if="isValid">
data-event-delay="ms" Delays handler execution by specified milliseconds <button data-action="save" data-event-delay="100">
Click Outside Detection: The data-event-outside modifier is especially useful for closing dropdowns, modals, and popovers when users click elsewhere. No JavaScript required, just add the attribute!

Timing Modifiers

Control how frequently event handlers are called:

Modifier Effect Example
data-event-debounce="ms" Delays handler until input stops for specified ms <input data-action="input:search" data-event-debounce="300">
data-event-throttle="ms" Limits handler to once per specified ms <div data-action="scroll:handleScroll" data-event-throttle="200">

Keyboard Modifiers

Filter keyboard events by specific keys:

Modifier Effect Example
data-event-key-enter Only triggers on Enter key <input data-action="keyup:submit" data-event-key-enter>
data-event-key-escape Only triggers on Escape key <input data-action="keyup:cancel" data-event-key-escape>
data-event-key-tab Only triggers on Tab key <input data-action="keydown:handleTab" data-event-key-tab>
data-event-key-space Only triggers on Space key <div data-action="keyup:toggle" data-event-key-space>
data-event-key-up Only triggers on Arrow Up <select data-action="keydown:prev" data-event-key-up>
data-event-key-down Only triggers on Arrow Down <select data-action="keydown:next" data-event-key-down>
data-event-key-left Only triggers on Arrow Left <div data-action="keydown:prev" data-event-key-left>
data-event-key-right Only triggers on Arrow Right <div data-action="keydown:next" data-event-key-right>

Modifier Key Combinations

Filter by modifier keys (Ctrl, Alt, Shift, Meta/Command):

Modifier Effect Example
data-event-key-ctrl Requires Ctrl key to be held <input data-action="keydown:selectAll" data-event-key-ctrl>
data-event-key-alt Requires Alt key to be held <input data-action="keydown:altAction" data-event-key-alt>
data-event-key-shift Requires Shift key to be held <input data-action="keydown:shiftAction" data-event-key-shift>
data-event-key-meta Requires Meta/Command key (Mac) <input data-action="keydown:cmdAction" data-event-key-meta>
data-event-key-ctrl+s Ctrl+S combination <div data-action="keydown:save" data-event-key-ctrl+s>
data-event-key-ctrl+z Ctrl+Z combination (undo) <div data-action="keydown:undo" data-event-key-ctrl+z>
data-event-key-meta+s Command+S on Mac <div data-action="keydown:save" data-event-key-meta+s>

Combining Modifiers

Multiple modifiers can be used together:

<!-- Prevent default and debounce search input -->
<input type="text"
       data-action="input:search"
       data-event-prevent
       data-event-debounce="300">

<!-- Submit form on Enter, prevent default -->
<input type="text"
       data-action="keyup:submitSearch"
       data-event-key-enter
       data-event-prevent>

<!-- Throttled scroll handler that stops propagation -->
<div data-action="scroll:handleScroll"
     data-event-throttle="100"
     data-event-stop>
</div>

<!-- Save with Ctrl+S or Cmd+S (cross-platform) -->
<div data-action="keydown:save" data-event-key-ctrl+s data-event-prevent></div>
<div data-action="keydown:save" data-event-key-meta+s data-event-prevent></div>

Event Modifiers Demo

Try the interactive example below to see how event modifiers work:

<div data-component="event-modifiers-demo">
    <div class="row">
        <!-- Debounce Demo -->
        <div class="col-md-6 mb-4">
            <h5>Debounced Search</h5>
            <p class="small text-muted">Uses <code>data-event-debounce="400"</code> to wait for typing to stop</p>
            <input type="text"
                   data-action="input:handleSearch"
                   data-event-debounce="400"
                   placeholder="Type to search..."
                   class="form-control mb-2">
            <div class="small">
                <strong>Search calls:</strong> <span data-bind="searchCount" class="badge bg-primary"></span>
                <span class="text-muted ms-2">(fires 400ms after you stop typing)</span>
            </div>
            <div class="small mt-1" data-show="lastSearch">
                <strong>Last search:</strong> "<span data-bind="lastSearch"></span>"
            </div>
        </div>

        <!-- Once Demo -->
        <div class="col-md-6 mb-4">
            <h5>One-Time Action</h5>
            <p class="small text-muted">Uses <code>data-event-once</code> to fire only once</p>
            <button data-action="handleOnce"
                    data-event-once
                    class="btn btn-warning mb-2">
                Click Me (Only Works Once!)
            </button>
            <div class="small">
                <strong>Clicks registered:</strong> <span data-bind="onceCount" class="badge bg-warning text-dark"></span>
                <span class="text-muted ms-2">(max 1, button auto-disables)</span>
            </div>
        </div>
    </div>

    <div class="row">
        <!-- Outside Click Demo -->
        <div class="col-md-6 mb-4">
            <h5>Click Outside Detection</h5>
            <p class="small text-muted">Uses <code>data-event-outside</code> to detect clicks outside</p>
            <div class="position-relative">
                <button data-action="toggleDropdown" class="btn btn-info">
                    Toggle Dropdown
                </button>
                <div data-show="dropdownOpen"
                     data-action="closeDropdown"
                     data-event-outside
                     class="position-absolute mt-1 p-3 border rounded bg-light shadow-sm"
                     style="z-index: 100; min-width: 200px;">
                    <strong>Dropdown Menu</strong>
                    <p class="small mb-2">Click outside this box to close it!</p>
                    <button class="btn btn-sm btn-secondary">Menu Item 1</button>
                    <button class="btn btn-sm btn-secondary ms-1">Menu Item 2</button>
                </div>
            </div>
            <div class="small mt-2">
                <strong>Outside clicks detected:</strong> <span data-bind="outsideClicks" class="badge bg-info"></span>
            </div>
        </div>

        <!-- Conditional Event Demo -->
        <div class="col-md-6 mb-4">
            <h5>Conditional Events</h5>
            <p class="small text-muted">Uses <code>data-event-if="isUnlocked"</code> to conditionally fire</p>
            <div class="form-check mb-2">
                <input type="checkbox" data-model="isUnlocked" class="form-check-input" id="unlock-check">
                <label class="form-check-label" for="unlock-check">
                    Unlock the button
                </label>
            </div>
            <button data-action="handleConditional"
                    data-event-if="isUnlocked"
                    class="btn mb-2"
                    data-bind-class="isUnlocked ? 'btn btn-success' : 'btn btn-secondary'">
                <span data-bind="isUnlocked ? 'Click Me! (Unlocked)' : 'Locked - Toggle Above'"></span>
            </button>
            <div class="small">
                <strong>Successful clicks:</strong> <span data-bind="conditionalClicks" class="badge bg-success"></span>
                <span class="text-muted ms-2">(only fires when unlocked)</span>
            </div>
        </div>
    </div>

    <div class="row">
        <!-- Delay Demo -->
        <div class="col-md-6 mb-4">
            <h5>Delayed Execution</h5>
            <p class="small text-muted">Uses <code>data-event-delay="1000"</code> to delay by 1 second</p>
            <button data-action="handleDelayed"
                    data-event-delay="1000"
                    class="btn btn-primary mb-2">
                Click for Delayed Action
            </button>
            <div class="small">
                <strong>Actions fired:</strong> <span data-bind="delayedCount" class="badge bg-primary"></span>
                <span class="text-muted ms-2">(click and wait 1 second)</span>
            </div>
        </div>

        <!-- Self Demo -->
        <div class="col-md-6 mb-4">
            <h5>Self-Only Events</h5>
            <p class="small text-muted">Uses <code>data-event-self</code> to ignore clicks bubbling from children</p>
            <div class="d-flex gap-2 mb-2">
                <!-- Without data-event-self -->
                <div data-action="handleWithoutSelf"
                     class="flex-grow-1 rounded btn-primary p-2 text-center"
                     style="cursor: pointer;">
                    <div class="small mb-1">Without self modifier</div>
                    <button data-action="handleChildClick" class="btn btn-sm btn-light">
                        Child Button
                    </button>
                </div>
                <!-- With data-event-self -->
                <div data-action="handleSelfClick"
                     data-event-self
                     class="flex-grow-1 rounded btn-info p-2 text-center"
                     style="cursor: pointer;">
                    <div class="small mb-1">With data-event-self</div>
                    <button data-action="handleChildClick2" class="btn btn-sm btn-light">
                        Child Button
                    </button>
                </div>
            </div>
            <div class="small">
                <strong>Left parent:</strong> <span data-bind="withoutSelfClicks" class="badge btn-primary"></span>
                <strong class="ms-2">Right parent:</strong> <span data-bind="selfClicks" class="badge btn-info"></span>
                <strong class="ms-2">Button clicks:</strong> <span data-bind="childClicks" class="badge bg-secondary"></span>
            </div>
            <div class="small text-muted">
                Click the background area (not the button) in both boxes - left parent fires, right parent doesn't (self modifier blocks bubbled clicks from children)
            </div>
        </div>
    </div>

    <!-- Reset Button -->
    <div class="text-center mt-3 pt-3 border-top">
        <button data-action="resetDemo" class="btn btn-secondary">
            Reset All Counters
        </button>
    </div>
</div>
wildflower.component('event-modifiers-demo', {
    state: {
        // Debounce demo
        searchCount: 0,
        lastSearch: '',

        // Once demo
        onceCount: 0,

        // Outside click demo
        dropdownOpen: false,
        outsideClicks: 0,

        // Conditional demo
        isUnlocked: false,
        conditionalClicks: 0,

        // Delay demo
        delayedCount: 0,

        // Self demo
        selfClicks: 0,
        withoutSelfClicks: 0,
        childClicks: 0
    },

    // Debounced search - only fires 400ms after typing stops
    handleSearch(event, element) {
        this.searchCount++
        this.lastSearch = event.target.value
        console.log('Search executed for:', event.target.value)
    },

    // One-time handler - only fires once ever
    handleOnce() {
        this.onceCount++
        console.log('One-time action fired!')
    },

    // Outside click detection
    toggleDropdown() {
        this.dropdownOpen = !this.dropdownOpen
    },

    closeDropdown() {
        if (this.dropdownOpen) {
            this.dropdownOpen = false
            this.outsideClicks++
            console.log('Dropdown closed via outside click')
        }
    },

    // Conditional event - only fires when isUnlocked is true
    handleConditional() {
        this.conditionalClicks++
        console.log('Conditional click registered!')
    },

    // Delayed execution - handler fires 1 second after click
    handleDelayed() {
        this.delayedCount++
        console.log('Delayed action executed after 1 second')
    },

    // Self-only demo - comparing with and without data-event-self
    handleWithoutSelf() {
        this.withoutSelfClicks++
        console.log('Parent WITHOUT self: fired (including from child clicks)')
    },

    handleSelfClick() {
        this.selfClicks++
        console.log('Parent WITH self: fired (only direct clicks)')
    },

    handleChildClick() {
        this.childClicks++
        console.log('Child button clicked (left box)')
    },

    handleChildClick2() {
        this.childClicks++
        console.log('Child button clicked (right box)')
    },

    // Reset all counters
    resetDemo() {
        this.searchCount = 0
        this.lastSearch = ''
        this.onceCount = 0
        this.dropdownOpen = false
        this.outsideClicks = 0
        this.isUnlocked = false
        this.conditionalClicks = 0
        this.delayedCount = 0
        this.selfClicks = 0
        this.withoutSelfClicks = 0
        this.childClicks = 0
    }
})
Live Preview

Debouncing Input Handlers

For two-way bindings, debounce the action that consumes the state rather than the state update itself. Pair data-action with data-event-debounce:

<!-- State updates immediately; onSearch runs 300ms after the last keystroke -->
<input type="text"
       data-model="searchQuery"
       data-action="input:onSearch"
       data-event-debounce="300"
       placeholder="Type to search...">

This is particularly useful for search inputs where you want to wait for the user to stop typing before triggering expensive operations.

Custom Events and Communication

Create custom events for component communication:

<div>
    <!-- Event Publisher Component -->
    <div data-component="event-publisher" class="mb-4">
        <p class="text-muted">Publish custom events that other components can listen to.</p>

        <div class="row">
            <div class="col-md-6">
                <h5>Predefined Events</h5>
                <button data-action="publishMessage" class="btn btn-primary me-2 mb-2">
                    Send Message Event
                </button>
                <button data-action="publishData" class="btn btn-secondary me-2 mb-2">
                    Send Data Event
                </button>
                <button data-action="publishAlert" class="btn btn-warning mb-2">
                    Send Alert Event
                </button>
            </div>

            <div class="col-md-6">
                <h5>Custom Events</h5>
                <div class="mb-2">
                    <input type="text"
                           data-model="customMessage"
                           placeholder="Enter custom message..."
                           class="form-control mb-2">
                </div>
                <div class="mb-2">
                    <select data-model="eventType" class="form-select mb-2">
                        <option value="info">Info Event</option>
                        <option value="warning">Warning Event</option>
                        <option value="success">Success Event</option>
                        <option value="error">Error Event</option>
                    </select>
                </div>
                <button data-action="publishCustom"
                        class="btn btn-info"
                        data-bind-class="customButtonClass">
                    Send Custom Event
                </button>
            </div>
        </div>

        <div class="mt-3">
            <small class="text-muted">
                Published events: <span data-bind="publishedCount" class="badge bg-primary"></span>
            </small>
        </div>
    </div>

    <!-- Event Subscriber Component -->
    <div data-component="event-subscriber" class="border-top pt-4">
        <p class="text-muted">Listening for custom events from the publisher above.</p>

        <div class="row">
            <div class="col-md-8">
                <div class="d-flex justify-content-between align-items-center mb-2">
                    <h5>Received Events (<span data-bind="eventCount"></span>):</h5>
                    <button data-action="clearEvents" class="btn btn-sm btn-danger">
                        Clear Events
                    </button>
                </div>
                <div class="border rounded p-2"
                     style="height: 200px; overflow-y: auto; background-color: var(--card-bg);">
                    <div data-list="receivedEvents">
                        <template>
                            <div class="d-flex justify-content-between align-items-start py-2 border-bottom">
                                <div class="flex-grow-1">
                                    <div>
                                        <span class="badge" data-bind-class="typeBadgeClass">
                                            <span data-bind="type"></span>
                                        </span>
                                        <strong data-bind="message" class="ms-2"></strong>
                                    </div>
                                    <small class="text-muted" data-show="data">
                                        Data: <code data-bind="dataPreview"></code>
                                    </small>
                                </div>
                                <small class="text-muted"><span data-bind="timestamp"></span></small>
                            </div>
                        </template>
                    </div>
                    <div data-show="isEmpty" class="text-center text-muted py-4">
                        No events received yet. Use the publisher above to send some events.
                    </div>
                </div>
            </div>

            <div class="col-md-4">
                <h5>Event Statistics</h5>
                <div class="p-3 border rounded" style="background-color: var(--card-bg);">
                    <p><strong>Total Events:</strong> <span data-bind="eventCount"></span></p>
                    <p><strong>Message Events:</strong> <span data-bind="messageCount"></span></p>
                    <p><strong>Data Events:</strong> <span data-bind="dataCount"></span></p>
                    <p><strong>Custom Events:</strong> <span data-bind="customCount"></span></p>
                    <p><strong>Last Event:</strong> <span data-bind="lastEventTime"></span></p>
                </div>
            </div>
        </div>
    </div>
</div>
// Event Publisher Component
wildflower.component('event-publisher', {
    state: {
        customMessage: '',
        eventType: 'info',
        publishedCount: 0
    },

    computed: {
        customButtonClass() {
            return this.customMessage.trim() ? 'btn btn-info' : 'btn btn-info disabled'
        }
    },

    publishMessage() {
        const event = new CustomEvent('app:message', {
            detail: {
                message: 'Hello from the publisher component!',
                timestamp: new Date().toLocaleTimeString(),
                source: 'publisher'
            }
        })
        document.dispatchEvent(event)
        this.publishedCount++
        console.log('Published message event:', event.detail)
    },

    publishData() {
        const sampleData = {
            id: Math.random().toString(36).substr(2, 9),
            value: Math.floor(Math.random() * 100),
            type: 'sample_data',
            items: ['item1', 'item2', 'item3'],
            metadata: {
                version: '1.0',
                created: new Date().toISOString()
            }
        }

        const event = new CustomEvent('app:data', {
            detail: {
                data: sampleData,
                timestamp: new Date().toLocaleTimeString(),
                source: 'publisher'
            }
        })
        document.dispatchEvent(event)
        this.publishedCount++
        console.log('Published data event:', event.detail)
    },

    publishAlert() {
        const alertTypes = ['System Update', 'New Feature Available', 'Maintenance Notice', 'Security Alert']
        const alertType = alertTypes[Math.floor(Math.random() * alertTypes.length)]

        const event = new CustomEvent('app:alert', {
            detail: {
                message: alertType,
                priority: 'high',
                timestamp: new Date().toLocaleTimeString(),
                source: 'publisher'
            }
        })
        document.dispatchEvent(event)
        this.publishedCount++
    },

    publishCustom() {
        const message = this.customMessage.trim()
        if (!message) return

        const event = new CustomEvent('app:custom', {
            detail: {
                message: message,
                type: this.eventType,
                timestamp: new Date().toLocaleTimeString(),
                source: 'publisher'
            }
        })
        document.dispatchEvent(event)
        this.publishedCount++
        this.customMessage = ''
        console.log('Published custom event:', event.detail)
    }
})

// Event Subscriber Component
wildflower.component('event-subscriber', {
    state: {
        receivedEvents: []
    },

    computed: {
        eventCount() {
            return this.receivedEvents.length
        },

        messageCount() {
            return this.receivedEvents.filter(e => e.type === 'Message').length
        },

        dataCount() {
            return this.receivedEvents.filter(e => e.type === 'Data').length
        },

        customCount() {
            return this.receivedEvents.filter(e => e.type === 'Custom' || e.type === 'Alert').length
        },

        isEmpty() {
            return this.receivedEvents.length === 0
        },

        lastEventTime() {
            return this.receivedEvents.length > 0
                ? this.receivedEvents[0].timestamp
                : 'None'
        },

        // Item-level computed properties for template
        typeBadgeClass() {
            const classes = {
                'Message': 'bg-primary',
                'Data': 'bg-success',
                'Alert': 'bg-warning',
                'Custom': 'bg-info'
            }
            return `badge ${classes[this.type] || 'bg-secondary'}`
        },

        dataPreview() {
            if (!this.data) return ''
            const str = JSON.stringify(this.data)
            return str.length > 50 ? str.substring(0, 50) + '...' : str
        }
    },

    init() {
        // Bind event handlers to maintain 'this' context
        this.handleMessage = this.handleMessage.bind(this)
        this.handleData = this.handleData.bind(this)
        this.handleAlert = this.handleAlert.bind(this)
        this.handleCustom = this.handleCustom.bind(this)

        // Listen for custom events
        document.addEventListener('app:message', this.handleMessage)
        document.addEventListener('app:data', this.handleData)
        document.addEventListener('app:alert', this.handleAlert)
        document.addEventListener('app:custom', this.handleCustom)

    },

    destroy() {
        // Clean up event listeners to prevent memory leaks
        document.removeEventListener('app:message', this.handleMessage)
        document.removeEventListener('app:data', this.handleData)
        document.removeEventListener('app:alert', this.handleAlert)
        document.removeEventListener('app:custom', this.handleCustom)

        console.log('Event subscriber destroyed and listeners removed')
    },

    handleMessage(event) {
        this.addEvent('Message', event.detail.message, null)
        console.log('Received message event:', event.detail)
    },

    handleData(event) {
        this.addEvent('Data', `Data received (${Object.keys(event.detail.data).length} properties)`, event.detail.data)
        console.log('Received data event:', event.detail)
    },

    handleAlert(event) {
        this.addEvent('Alert', event.detail.message, null)
        console.log('Received alert event:', event.detail)
    },

    handleCustom(event) {
        this.addEvent('Custom', `${event.detail.type}: ${event.detail.message}`, null)
        console.log('Received custom event:', event.detail)
    },

    addEvent(type, message, data = null) {
        this.receivedEvents.unshift({
            type: type,
            message: message,
            data: data,
            timestamp: new Date().toLocaleTimeString()
        })

        // Keep only last 20 events for performance
        if (this.receivedEvents.length > 20) {
            this.receivedEvents = this.receivedEvents.slice(0, 20)
        }
    },

    clearEvents() {
        this.receivedEvents = []
        console.log('Event log cleared')
    }
})
Live Preview

Form Event Handling

Handle form submission, validation, and input events:

<div data-component="form-handler">
    <p class="text-muted">Real-time validation with comprehensive event handling.</p>

    <form data-action="handleSubmit" class="needs-validation" novalidate>
        <div class="row">
            <div class="col-md-6">
                <h5>Personal Information</h5>

                <div class="mb-3">
                    <label class="form-label">Full Name: <span class="text-danger">*</span></label>
                    <input type="text"
                           data-model="form.name"
                           data-action="blur:validateName input:handleNameInput"
                           class="form-control"
                           placeholder="Enter your full name"
                           required>
                    <div class="text-danger small mt-1" data-bind="errors.name"></div>
                    <small class="form-text text-muted">
                        Length: <span data-bind="nameLength"></span> characters
                    </small>
                </div>

                <div class="mb-3">
                    <label class="form-label">Email Address: <span class="text-danger">*</span></label>
                    <input type="email"
                           data-model="form.email"
                           data-action="input:validateEmail focus:handleEmailFocus"
                           class="form-control"
                           placeholder="Enter your email"
                           required>
                    <small class="form-text" data-bind-class="emailHintClass">
                        <span data-bind="emailHint"></span>
                    </small>
                </div>

                <div class="mb-3">
                    <label class="form-label">Age: <span class="text-danger">*</span></label>
                    <input type="text" inputmode="numeric"
                           data-model="form.age"
                           data-action="change:validateAge input:handleAgeInput"
                           class="form-control"
                           placeholder="Enter your age"
                           required>
                    <div class="text-danger small mt-1" data-bind="errors.age"></div>
                    <small class="form-text text-muted">
                        Age range: 18-100 years
                    </small>
                </div>

                <div class="mb-3">
                    <label class="form-label">Country:</label>
                    <select data-model="form.country"
                            data-action="change:handleCountryChange"
                            class="form-select">
                        <option value="">Select your country</option>
                        <option value="us">United States</option>
                        <option value="ca">Canada</option>
                        <option value="uk">United Kingdom</option>
                        <option value="au">Australia</option>
                        <option value="de">Germany</option>
                        <option value="fr">France</option>
                        <option value="other">Other</option>
                    </select>
                </div>

                <div class="mb-3">
                    <div class="form-check">
                        <input type="checkbox"
                               data-model="form.newsletter"
                               data-action="change:handleNewsletterChange"
                               class="form-check-input"
                               id="newsletter">
                        <label class="form-check-label" for="newsletter">
                            Subscribe to newsletter
                        </label>
                    </div>
                </div>
            </div>

            <div class="col-md-6">
                <h5>Form Status & Actions</h5>

                <div class="p-3 border rounded mb-3" style="background-color: var(--card-bg);">
                    <h6>Validation Status</h6>
                    <div class="mb-2">
                        <span class="badge" data-bind-class="nameValidationClass">
                            Name: <span data-bind="nameValidationText"></span>
                        </span>
                    </div>
                    <div class="mb-2">
                        <span class="badge" data-bind-class="emailValidationClass">
                            Email: <span data-bind="emailValidationText"></span>
                        </span>
                    </div>
                    <div class="mb-2">
                        <span class="badge" data-bind-class="ageValidationClass">
                            Age: <span data-bind="ageValidationText"></span>
                        </span>
                    </div>
                    <hr>
                    <p><strong>Form Valid:</strong>
                        <span data-bind="isFormValid" class="badge" data-bind-class="formValidClass"></span>
                    </p>
                </div>

                <div class="mb-3">
                    <button type="submit"
                            data-bind-class="submitButtonClass"
                            data-bind-attr="{ disabled: isSubmitDisabled }">
                        <span data-bind="submitButtonText"></span>
                    </button>
                    <button type="button"
                            data-action="resetForm"
                            class="btn btn-secondary ms-2">
                        Reset Form
                    </button>
                    <button type="button"
                            data-action="fillSampleData"
                            class="btn btn-info ms-2">
                        Fill Sample Data
                    </button>
                </div>

                <div class="mb-3">
                    <h6>Event Log (<span data-bind="eventCount"></span> events):</h6>
                    <div class="border rounded p-2"
                         style="height: 150px; overflow-y: auto; font-family: monospace; font-size: 0.8em; background-color: var(--card-bg);">
                        <div data-list="eventLog">
                            <template>
                                <div class="d-flex justify-content-between py-1 border-bottom">
                                    <span><span data-bind="timestamp"></span> - <span data-bind="event"></span></span>
                                    <small class="badge bg-secondary"><span data-bind="field"></span></small>
                                </div>
                            </template>
                        </div>
                        <div data-show="noEvents" class="text-center text-muted py-3">
                            No events logged yet. Start interacting with the form.
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <div class="mt-4" data-show="submissionResult">
        <div class="alert alert-success">
            <h6>✅ Form Submitted Successfully!</h6>
            <p>The form data has been processed. Here's what was submitted:</p>
            <pre class="mb-0" style="font-size: 0.85em;"><code data-bind="submissionResult"></code></pre>
        </div>
    </div>
</div>
wildflower.component('form-handler', {
    state: {
        form: {
            name: '',
            email: '',
            age: '',
            country: '',
            newsletter: false
        },
        errors: {
            name: '',
            email: '',
            age: ''
        },
        submissionResult: '',
        eventLog: [],
        isSubmitting: false
    },

    computed: {
        // Form validation status
        isFormValid() {
            return this.form.name.trim() &&
                   this.form.email.trim() &&
                   this.form.age &&
                   !this.errors.name &&
                   !this.errors.email &&
                   !this.errors.age
        },

        // Field-specific computed properties
        nameLength() {
            return this.form.name.length
        },

        nameValidationClass() {
            if (!this.form.name) return 'bg-secondary'
            return this.errors.name ? 'bg-danger' : 'bg-success'
        },

        nameValidationText() {
            if (!this.form.name) return 'Empty'
            return this.errors.name ? 'Invalid' : 'Valid'
        },

        emailValidationClass() {
            if (!this.form.email) return 'bg-secondary'
            return this.errors.email ? 'bg-danger' : 'bg-success'
        },

        emailValidationText() {
            if (!this.form.email) return 'Empty'
            return this.errors.email ? 'Invalid' : 'Valid'
        },

        emailHint() {
            if (!this.form.email) return 'Enter a valid email address'
            if (this.errors.email) return this.errors.email
            return 'Email format looks good'
        },

        emailHintClass() {
            if (!this.form.email) return 'text-muted'
            return this.errors.email ? 'text-danger' : 'text-success'
        },

        ageValidationClass() {
            if (!this.form.age) return 'bg-secondary'
            return this.errors.age ? 'bg-danger' : 'bg-success'
        },

        ageValidationText() {
            if (!this.form.age) return 'Empty'
            return this.errors.age ? 'Invalid' : 'Valid'
        },

        formValidClass() {
            return this.isFormValid ? 'bg-success' : 'bg-danger'
        },

        // Submit button logic
        submitButtonClass() {
            const baseClass = 'btn'
            if (this.isSubmitting) return `${baseClass} btn-secondary`
            return this.isFormValid ? `${baseClass} btn-primary` : `${baseClass} btn-primary disabled`
        },

        submitButtonText() {
            if (this.isSubmitting) return 'Submitting...'
            return 'Submit Form'
        },

        isSubmitDisabled() {
            return !this.isFormValid || this.isSubmitting
        },

        // Event log
        eventCount() {
            return this.eventLog.length
        },

        noEvents() {
            return this.eventLog.length === 0
        }
    },

    // Form submission
    async handleSubmit(event, element) {
        event.preventDefault()

        this.logEvent('Form submission attempted', 'form')

        // Validate all fields
        this.validateName()
        this.validateEmail()
        this.validateAge()

        if (this.isFormValid) {
            this.isSubmitting = true
            this.logEvent('Form validation passed, submitting...', 'form')

            try {
                // Simulate API call
                await new Promise(resolve => setTimeout(resolve, 1500))

                // Success - show result
                this.submissionResult = JSON.stringify({
                    ...this.form,
                    submittedAt: new Date().toISOString(),
                    validationStatus: 'passed'
                }, null, 2)

                this.logEvent('Form submitted successfully', 'form')

                // Auto-reset after showing result
                setTimeout(() => {
                    this.resetForm()
                }, 5000)

            } catch (error) {
                this.logEvent('Form submission failed', 'form')
                console.error('Form submission error:', error)
            } finally {
                this.isSubmitting = false
            }
        } else {
            this.logEvent('Form validation failed', 'form')
        }
    },

    // Field validation methods
    validateName(event, element) {
        const name = this.form.name.trim()

        if (!name) {
            this.errors.name = 'Name is required'
        } else if (name.length < 2) {
            this.errors.name = 'Name must be at least 2 characters'
        } else if (name.length > 50) {
            this.errors.name = 'Name must be less than 50 characters'
        } else {
            this.errors.name = ''
        }

        if (event) this.logEvent(`Name validated: ${this.errors.name || 'valid'}`, 'name')
    },

    validateEmail(event, element) {
        const email = this.form.email.trim()
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

        if (!email) {
            this.errors.email = 'Email is required'
        } else if (!emailRegex.test(email)) {
            this.errors.email = 'Please enter a valid email address'
        } else {
            this.errors.email = ''
        }

        if (event) this.logEvent(`Email validated: ${this.errors.email || 'valid'}`, 'email')
    },

    validateAge(event, element) {
        const age = parseInt(this.form.age)

        if (!this.form.age) {
            this.errors.age = 'Age is required'
        } else if (isNaN(age)) {
            this.errors.age = 'Please enter a valid number'
        } else if (age < 18) {
            this.errors.age = 'Must be at least 18 years old'
        } else if (age > 100) {
            this.errors.age = 'Must be 100 years or younger'
        } else {
            this.errors.age = ''
        }

        if (event) this.logEvent(`Age validated: ${this.errors.age || 'valid'}`, 'age')
    },

    // Input event handlers
    handleNameInput(event, element) {
        this.logEvent(`Name input: "${event.target.value}"`, 'name')
    },

    handleEmailFocus(event, element) {
        this.logEvent('Email field focused', 'email')
    },

    handleAgeInput(event, element) {
        this.logEvent(`Age input: ${event.target.value}`, 'age')
    },

    handleCountryChange(event, element) {
        this.logEvent(`Country selected: ${event.target.value || 'none'}`, 'country')
    },

    handleNewsletterChange(event, element) {
        this.logEvent(`Newsletter: ${event.target.checked ? 'subscribed' : 'unsubscribed'}`, 'newsletter')
    },

    // Utility methods
    resetForm() {
        this.form = {
            name: '',
            email: '',
            age: '',
            country: '',
            newsletter: false
        }
        this.errors = {
            name: '',
            email: '',
            age: ''
        }
        this.submissionResult = ''
        this.eventLog = []
        this.logEvent('Form reset', 'form')
    },

    fillSampleData() {
        this.form = {
            name: 'John Doe',
            email: 'john.doe@example.com',
            age: '28',
            country: 'us',
            newsletter: true
        }

        // Validate the sample data
        this.validateName()
        this.validateEmail()
        this.validateAge()

        this.logEvent('Sample data filled', 'form')
    },

    logEvent(description, field) {
        this.eventLog.unshift({
            timestamp: new Date().toLocaleTimeString(),
            event: description,
            field: field
        })

        // Keep only last 20 events
        if (this.eventLog.length > 20) {
            this.eventLog = this.eventLog.slice(0, 20)
        }
    }
})
Live Preview