Skip to content

houxitsystems/houxit.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 

Repository files navigation

Houxit.js

The Transparent Web Framework for Perfectionists

Built with intention. Designed for humans.


What is Houxit?

Houxit (pronounced HOW-zit) is a modern JavaScript framework for building reactive, component-driven web applications. It draws inspiration from the elegance of Vue.js while introducing a redesigned, more expressive API vocabulary — one where every name communicates what it actually does.

The name itself carries the philosophy: an exit from rigid conventions, toward a developer-first experience rooted in clarity, speed, and freedom.

Houxit is not trying to replace React, Vue, Angular, or Svelte by copying them. It's built on a different set of questions: What if the API said exactly what it meant? What if every piece had only one job, and that job was obvious?


Core Philosophy

Clarity over convention. Names in Houxit describe intent. initBuild initializes a build. token holds a reactive value you read with .data. stream gives you a live, reactive proxy of an object. model is your state. handlers are your methods. None of these require you to look up what they really do.

Granular reactivity. Houxit tracks dependencies at the property level. Only the precise parts of the DOM that depend on a changed value are updated — not the whole component tree.

Three authoring styles, one framework. You can write widgets as plain functions, as options objects, or as classes. You can use the declarative template syntax or drop to hyperscript render functions. You can write in a single .houxit file or compose from plain .js modules. Houxit meets you where you are.


Key Features at a Glance

  • Widget-driven architecture — The UI is a tree of self-contained, reusable widgets.
  • Granular reactivity — Updates target only the DOM nodes that actually changed.
  • Declarative templates — HTML-superset syntax with directives, blocks, and interpolation.
  • Two-way data binding — Keep inputs and state in sync effortlessly with $$model.
  • Three authoring APIs — Options API, Adapter API (composition), and Class API.
  • Built-in animation system$$animate and $$transite for declarative transitions.
  • Slots & scoped content — Pass content into widgets with default, named, and scoped slots.
  • TypeScript support — Full types out of the box.
  • CDN or build-tool usage — No CLI required for simple use cases.
  • .houxit Widget Unit Files — Co-locate script, template, and styles in one file.

A Quick Look

Here is a complete counter widget in a .houxit file:

<!-- Counter.houxit -->

<script build>
import { token } from 'houxit'

const count = token(0)

function increment() {
  count.data++
}
</script>

<template>
  <div>
    <p>You have clicked {{ count }} times.</p>
    <button @click="increment">Click me</button>
  </div>
</template>

<style $$scoped>
button {
  font-size: 1.2em;
  padding: 0.4em 1em;
}
</style>

A few things to notice right away:

  • token(0) creates a reactive value. You write count.data++ to mutate it.
  • In the template, {{ count }} — Houxit unwraps .data automatically.
  • @click is shorthand for $$on:click. Both work.
  • $$scoped on <style> means the CSS only applies to this widget.

Getting Started

CDN Usage

The fastest way to try Houxit — drop a script tag into any HTML file, no build step needed:

<!-- Development build -->
<script src="https://cdn.jsdelivr.net/npm/houxit/dist/houxit.global.js"></script>

<!-- Production build (minified) -->
<script src="https://cdn.jsdelivr.net/npm/houxit/dist/houxit.global.min.js"></script>

With the global build, Houxit is available as the Houxit global:

<body>
  <div id="app"></div>

  <script src="https://cdn.jsdelivr.net/npm/houxit/dist/houxit.global.js"></script>
  <script>
    const { initBuild } = Houxit

    initBuild({
      model() {
        this.message = 'Hello from Houxit!'
      },
      template: `<h1>{{ message }}</h1>`
    }).mount('#app')
  </script>
</body>

You can also use the ES module build with an import map:

<script type="importmap">
  {
    "imports": {
      "houxit": "https://cdn.jsdelivr.net/npm/houxit/dist/houxit.esm.js"
    }
  }
</script>

<script type="module">
  import { initBuild } from 'houxit'

  initBuild({ template: `<p>Hello!</p>` }).mount('#app')
</script>

Installation via npm

npm install houxit
import { initBuild } from 'houxit'

Scaffolding a Full Project

For projects that use .houxit Widget Unit Files and a full build pipeline:

npm create houxit@latest

Follow the prompts to name your project and select a preset: minimal, standard, or full. The scaffolded structure looks like this:

my-app/
├── src/
│   ├── widgets/
│   │   └── App.houxit
│   └── main.js
├── index.html
└── package.json

Start the dev server:

cd my-app
npm install
npm run dev

Your First App — initBuild

initBuild is the entry point for every Houxit application. It accepts a root widget definition and returns an application instance.

import { initBuild } from 'houxit'

const app = initBuild({
  model() {
    this.count = 0
  },
  handlers: {
    increment() {
      this.count++
    }
  },
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button $$on:click="increment">+</button>
    </div>
  `
})

app.mount('#app')

One special behavior: when Houxit mounts the root widget via initBuild and that widget has no template, render, or markdown option, it falls back to the innerHTML of the mount target as the template. This allows progressive enhancement of server-rendered HTML.

Resolution order for root widget output:

  1. render function (hyperscript)
  2. template option (string)
  3. markdown option (markdown string)
  4. innerHTML of the mount element (initBuild context only)

Widgets — The Core Unit

Everything in Houxit is a widget — a self-contained, reusable piece of UI that manages its own state, reacts to inputs, and produces rendered output.

Widgets communicate through clearly defined channels:

Channel Purpose
$params Declared inputs passed from the parent
$signals Declared events the widget emits
$events Native DOM events that fall through
$attrs Undeclared attributes passed from the parent

A Houxit application is a tree of widgets, rooted at the widget passed to initBuild.

Three Ways to Write a Widget

Houxit accepts widgets in three forms. All three are equally valid — use whichever fits your style or the complexity of the widget.

1. Plain Function

A function widget is treated directly as the build function. It runs once during initialization and must return a render function.

function Greeting({ params }) {
  return () => <h1>Hello, {params.name}!</h1>
}

This is the most minimal form — great for simple, stateless widgets.

2. Options Object

A structured object with named options. This is the most readable form for widgets with moderate complexity.

export default {
  model() {
    this.count = 0
  },
  handlers: {
    increment() {
      this.count++
    }
  },
  template: `<button $$on:click="increment">Clicked {{ count }} times</button>`
}

3. Class

Extend Houxit.Widget for an object-oriented authoring style.

class MyWidget extends Houxit.Widget {
  constructor() {
    super()
    this.model = {
      value: 'hello'
    }
  }

  template = `<p>{{ value }}</p>`
}

Widget Unit Files (.houxit)

A Widget Unit File (WUF) is the .houxit single-file format. It lets you co-locate a widget's script, template, and styles in one file — keeping related concerns together without coupling them.

<!-- src/widgets/Counter.houxit -->

<script build>
import { stream } from 'houxit'

const user = stream({ name: 'Chukwuemeka', score: 0 })

function addPoint() {
  user.score++
}
</script>

<template>
  <div class="counter">
    <p>{{ user.name }}: {{ user.score }} points</p>
    <button @click="addPoint">+1</button>
  </div>
</template>

<style>
.counter {
  font-family: sans-serif;
  padding: 1rem;
}
</style>

<script build> runs in the Adapter API context. Variables declared at the top level are automatically available in the template.

<script> (without build) uses the Options API — export a default widget definition object.

<style $$scoped> scopes CSS to this widget only. Without $$scoped, styles are global.


Options API

The Options API defines a widget as a plain object. Each key plays a specific, named role.

model

Declares the widget's reactive state. Define properties on this inside the function — Houxit reads them at instantiation, makes every property reactive, and merges them into the widget instance.

export default {
  model() {
    this.count = 0
    this.user = { name: 'Chukwuemeka', role: 'admin' }
    this.items = []
  }
}

In templates and handlers, state properties are accessed directly by name: count, user.name. In handlers, use this.count to mutate.

Why a function? Defining model as a function (rather than a plain object) ensures each widget instance gets its own independent copy of the state. If it were a plain object, all instances would share the same reference.

params

Declares the widget's accepted inputs. Params are passed by the parent and are read-only inside the widget. They're available in templates as $params.propName.

export default {
  params: {
    title: String,
    count: {
      type: Number,
      default: 0
    },
    onSave: Function
  }
}

handlers

Replaces what other frameworks call "methods". These are functions that operate on the widget's state and are callable from the template.

export default {
  handlers: {
    increment() {
      this.count++
    },
    async fetchData() {
      this.items = await api.getItems()
    }
  }
}

observers

Replaces watchers. Keys are reactive state paths (or functions returning a value); values are callbacks that fire when the observed value changes.

export default {
  observers: {
    count(newVal, oldVal) {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    },
    'user.name'(newName) {
      document.title = newName
    }
  }
}

You can also observe a computed expression:

observers: {
  // Watch a derived value
  [() => this.items.length](newLen) {
    console.log(`List now has ${newLen} items`)
  }
}

template

A string containing the widget's HTML output. Supports all Houxit template syntax.

export default {
  template: `
    <div>
      <h1>{{ title }}</h1>
      <p $$if="show">Visible</p>
    </div>
  `
}

render

An explicit render function using the h hyperscript helper. When render is present, template and markdown are ignored.

import { h } from 'houxit'

export default {
  render() {
    return h('div', null,
      h('h1', null, this.title),
      h('p', null, `Count: ${this.count}`)
    )
  }
}

Lifecycle Hooks

Lifecycle hooks run at precise moments in a widget's life. Define them as functions directly in the options object.

Hook When it runs
preBuild Before the widget is built/initialized
postBuild After the widget is built
init When the widget instance is first created
preMount Just before the widget is inserted into the DOM
postMount After the widget is mounted in the DOM
preUpdate Before a reactive update causes a re-render
postUpdate After a reactive update has re-rendered
export default {
  model() {
    this.count = 0
  },
  postMount() {
    console.log('Widget is in the DOM')
  },
  postUpdate() {
    console.log('DOM updated')
  }
}

Adapter API

The Adapter API is the composition-oriented authoring style — used in <script build> blocks in WUF files, or called inside the build function option of the Options API. It exposes reactive primitives, lifecycle hooks, and widget configuration as importable functions that can be freely composed and reused across modules.

Important: Adapter API functions must be called synchronously during widget initialization — at the top level of <script build> or directly inside build. They register themselves against the active widget instance, so calling them inside setTimeout, async continuations, or event handlers won't work.


token — Reactive Primitive

token wraps a single reactive value — a number, string, boolean, or any primitive. Reading .data tracks the dependency; writing .data triggers updates.

import { token } from 'houxit'

const count = token(0)

count.data        // read → 0
count.data = 5    // write → triggers updates
count.data++      // shorthand mutation

In templates inside <script build>, use the variable name directly — Houxit unwraps .data automatically:

<p>{{ count }}</p>        <!-- renders the value, not the token object -->
<p>{{ count * 2 }}</p>    <!-- expressions work too -->

token can also hold a reference to a DOM element or widget instance. Use $$bind:ref to connect it:

const inputEl = token(null)
<input $$bind:ref="inputEl" />

After mount, inputEl.data points to the actual DOM node.


stream — Reactive Object Proxy

stream creates a deeply reactive proxy of an object or array. Unlike token, there is no .data accessor — the stream is the object. You read and write properties directly on it, as if it were the original:

import { stream } from 'houxit'

const user = stream({ name: 'Chukwuemeka', age: 30 })

user.name           // read → 'Chukwuemeka'
user.name = 'Ike'   // write → triggers updates

const items = stream([])
items.push('first') // array mutations are tracked too

token vs stream — when to use which: Use token for a single primitive value (count, isOpen, title). Use stream for an object or array (user, settings, items). The key difference: token wraps its value inside .data; stream exposes the value directly as a proxy of the original object.

In templates, both are accessed by variable name — Houxit handles the rest.


createAgent — Cross-Widget Signal

createAgent creates a reactive getter/setter pair — ideal for sharing a piece of state across multiple widgets without prop-drilling.

import { createAgent } from 'houxit'

const [getCount, setCount] = createAgent(0)

getCount()                          // read → 0
setCount(5)                         // set directly
setCount(({ value }) => value + 1)  // update from previous value

Agents work well with Provider for injecting shared state into a widget subtree without threading props through every level.


computed — Derived Reactive Values

computed derives a reactive value from other reactive values. The function only re-runs when a dependency changes, and the result is cached between reads.

import { token, computed } from 'houxit'

const count = token(2)
const doubled = computed(() => count.data * 2)

doubled.data  // → 4 (updates automatically when count changes)

Think of computed as a smart, lazy-evaluated expression that stays in sync automatically. Use it whenever a value is derived from state rather than set directly.


observe — Watch a Reactive Source

observe watches a reactive source and fires a callback when it changes.

import { token, observe } from 'houxit'

const count = token(0)

observe(count, (newVal, oldVal) => {
  console.log(`changed: ${oldVal}${newVal}`)
})

// Watch a computed expression
observe(() => count.data * 2, (doubled) => {
  console.log('doubled:', doubled)
})

Options:

observe(source, callback, {
  initial: true,  // run immediately with the current value
  deep: true      // deeply observe nested objects/arrays
})

observe vs effectHook: Use observe when you want to react to a specific source changing. Use effectHook when you want an effect to auto-track whatever reactive state it reads.


effectHook — Auto-Tracked Side Effects

effectHook runs a callback immediately and re-runs it whenever any reactive state it reads changes. Houxit automatically tracks which reactive values were accessed during execution.

import { token, effectHook } from 'houxit'

const count = token(0)

effectHook(() => {
  document.title = `Count is ${count.data}`
  // Houxit tracks that this effect reads count.data
  // and re-runs it whenever count changes
})

Return a cleanup function to run before the next execution, or when the widget unmounts:

effectHook(() => {
  const timer = setInterval(() => count.data++, 1000)
  return () => clearInterval(timer)  // cleanup
})

Readonly & Shallow Variants

import { token, stream, readonly, shallow, readonlyStream, shallowStream } from 'houxit'

// Expose a token as read-only to the outside world
const count = token(0)
const readCount = readonly(count)      // reading works, writing throws

// Shallow stream: only reacts to top-level property changes
// (nested property mutations don't trigger updates)
const state = shallowStream({ nested: { value: 1 } })

// Readonly shallow
const locked = readonlyStream(state)

Use readonly to enforce one-directional data flow — expose state without allowing mutation from outside the widget.


Lifecycle Hooks in the Adapter API

All Options API lifecycle hooks have Adapter API equivalents as importable functions. Call them at the top level of <script build> or inside build.

import {
  preBuild, postBuild,
  init,
  preMount, postMount,
  preUpdate, postUpdate
} from 'houxit'

postMount(() => {
  console.log('mounted!')
})

postUpdate(() => {
  console.log('DOM updated')
})

Reactivity In Depth

Houxit's reactivity system is based on automatic dependency tracking. When a reactive value (token, stream, createAgent getter, or model property) is read during rendering or inside effectHook, Houxit records that dependency. When the value changes, Houxit knows exactly which parts of the DOM — or which effects — to re-run.

Fine-Grained Updates

const user = stream({ name: 'Chukwuemeka', age: 30 })

effectHook(() => {
  // This effect only re-runs when user.name changes.
  // Changing user.age does nothing here.
  console.log(user.name)
})

This is what "granular reactivity" means in practice. Houxit doesn't re-render the whole widget — it re-runs only the effects and DOM nodes that read the changed value.

Reactive Arrays

Mutations on a stream-wrapped array are tracked automatically:

const list = stream([1, 2, 3])

list.push(4)      // triggers update
list.splice(0, 1) // triggers update
list[0] = 99      // triggers update

Computed Values

import { token, computed } from 'houxit'

const price = token(100)
const tax = token(0.2)
const total = computed(() => price.data * (1 + tax.data))

total.data  // → 120
// Update either dependency → total recalculates automatically

The build Method — Bridging Options and Adapter APIs

The build option in the Options API is Houxit's bridge between the two authoring styles. When you define build, it runs in the Adapter API context — you can call stream, token, observe, effectHook, lifecycle hooks, and any other Adapter API function inside it.

build receives the widget's context object as its first argument and must return a render function.

import { stream, effectHook, postMount, h } from 'houxit'

export default {
  params: {
    initialCount: Number
  },

  build({ $params }) {
    const count = token($params.initialCount ?? 0)

    postMount(() => {
      console.log('Counter mounted with', count.data)
    })

    effectHook(() => {
      document.title = `Count: ${count.data}`
    })

    function increment() {
      count.data++
    }

    return () => h('div', null,
      h('p', null, `Count: ${count.data}`),
      h('button', { 'on:click': increment }, '+')
    )
  }
}

Note: this is undefined inside build. All state lives in the function closure. This is intentional — it enforces clean, explicit data flow with no hidden instance context.

A plain function widget is treated as a build function directly:

function Counter({ params }) {
  const count = token(params.initial ?? 0)

  return () => h('div', null,
    h('p', null, `${count.data}`),
    h('button', { 'on:click': () => count.data++ }, '+')
  )
}

Templates

Houxit templates are a valid HTML superset. They compile to efficient render code at build time (or at runtime for CDN usage). Any valid HTML is valid in a Houxit template.

Interpolation

Use double curly braces to interpolate reactive expressions into text:

<p>Hello, {{ name }}!</p>
<p>2 + 2 = {{ 2 + 2 }}</p>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please sign in' }}</p>

Interpolation is text-only by default — HTML in the value is escaped for security. To render raw HTML, use the $$html directive or {{@html}} block.


Directives

Directives are special attributes prefixed with $$. They give Houxit instructions on how to treat a DOM element reactively. Most directives also have a shorthand syntax.

$$if / $$else-if / $$else

Conditionally render an element. The element and its children are fully created or destroyed.

<p $$if="isLoggedIn">Welcome back!</p>
<p $$else-if="isPending">Loading...</p>
<p $$else-if="isFailed">Something went wrong.</p>
<p $$else>Please log in.</p>

$$if vs CSS display: none: $$if removes the element from the DOM entirely. Use it when the content truly shouldn't exist. If you want the element to remain in the DOM but be invisible (preserving state or avoiding re-mount costs), use a style binding instead.

$$for

Render a list. Use of syntax with a *key for efficient reconciliation:

<ul>
  <li $$for="item of items" *key="item.id">
    {{ item.name }}
  </li>
</ul>

With index:

<li $$for="(item, key, index) of items" *key="index">
  {{ index }}: {{ item.name }}
</li>

Always provide *key. The key tells Houxit which DOM node corresponds to which item when the list changes. Without it, Houxit can't reuse nodes efficiently and may produce incorrect behavior during reorders or deletions.

$$bind

Bind a DOM attribute or property to a reactive expression. The * shorthand prefix is the idiomatic form:

<!-- Full syntax -->
<img $$bind:src="user.avatarUrl" $$bind:alt="user.name" />

<!-- Shorthand -->
<img *src="user.avatarUrl" *alt="user.name" />

<!-- Boolean attributes -->
<button *disabled="isLoading">Submit</button>

<!-- Two-way binding for inputs -->
<input $$model="message" />

$$model is a special two-way binding directive for form elements. It keeps the element's value and the reactive state in sync automatically.

$$on

Attach an event listener. The @ shorthand is idiomatic:

<!-- Full syntax -->
<button $$on:click="increment">+</button>

<!-- Shorthand -->
<button @click="increment">+</button>

<!-- Inline expression -->
<button @click="count.data++">+</button>

<!-- Event modifiers -->
<form @submit|prevent="handleSubmit">...</form>

<!-- Multiple events -->
<div @click.hover|stop="select">...</div>

Event modifiers (after |) modify how the event is handled: prevent calls event.preventDefault(), stop calls event.stopPropagation(), and so on.

$$slot

Declares a slot outlet. The # shorthand is used on the parent side to target a named slot:

<!-- Inside a Card widget's template -->
<div class="card">
  <div class="card__header">
    <slot name="header" />
  </div>
  <div class="card__body">
    <slot />  <!-- default slot -->
  </div>
</div>

<!-- Using the Card widget -->
<Card>
  <h2 #header>My Title</h2>
  <p>Card body content.</p>
</Card>

Class & Style Bindings

Houxit extends standard class and style binding with a dot-notation shorthand.

Class Binding

<!-- Object syntax -->
<div $$bind:class="{ active: isActive, disabled: isDisabled }"></div>

<!-- Array syntax -->
<div $$bind:class="[baseClass, isActive ? 'active' : '']"></div>

<!-- Dot-notation: applies 'name' and 'header' as static classes -->
<div class.name.header></div>

<!-- Bound dot-notation: applies both classes when isHeader is truthy -->
<div *class.name.header="isHeader"></div>

Style Binding

<!-- Object syntax -->
<p $$bind:style="{ color: textColor, fontSize: size + 'px' }"></p>

<!-- Dot-notation: sets a CSS property to a literal value -->
<p style.color="red"></p>

<!-- Bound dot-notation: evaluated as a JS expression -->
<p *style.color="primaryColor"></p>

<!-- Multiple properties from one expression -->
<p *style.color.background="isDark ? 'white' : 'black'"></p>

Template Blocks

Blocks are multi-line template constructs for control flow and utility beyond what single-attribute directives can express. They use the {{@keyword}} / {{/keyword}} delimiter syntax.

Some blocks are void blocks — they close automatically after the opening tag and ignore children or closing tags.

@if

{{@if count > 10}}
  <p>Count is large!</p>
{{@else:if count > 5}}
  <p>Count is medium.</p>
{{@else}}
  <p>Count is small.</p>
{{/if}}

@else:if and @else are void blocks — they self-close. @else:if and @else are only valid inside an @if / /if pair.

@for

{{@for item of items}}
  <div class="item">
    <h3>{{ item.title }}</h3>
    <p>{{ item.body }}</p>
  </div>
{{/for}}

With index and key:

{{@for (item, i) of items}}
  <li *key="item.id">{{ i + 1 }}. {{ item.name }}</li>
{{/for}}

@const

Declare a local template variable scoped to the nearest enclosing block (or the template root). @const is a void block.

{{@const total = items.reduce((s, i) => s + i.price, 0)}}
<p>Total: {{ total }}</p>

{{@const greeting = `Hello, ${user.name}!`}}
<h1>{{ greeting }}</h1>

Use @const to name complex expressions once, avoiding repetition and making templates easier to read.

@html

Render a raw HTML string. @html is a void block. Only use with trusted content — this bypasses Houxit's automatic HTML escaping.

{{@html article.bodyHtml}}

@await

Render async content inline. Best used inside a Suspense widget.

<Suspense>
  <p #fallback>Loading...</p>

  {{@await fetchData()}}
</Suspense>

Suspense handles the pending state — while fetchData() hasn't resolved, the #fallback slot content is shown.

@class / @new

Advanced blocks for scenarios where class-based constructs are needed within the rendering logic.

{{@class Formatter=()}}
  {{@const fmt = @new Formatter(locale)}}
  <p>{{ fmt.format(value) }}</p>
{{/class}}

Animations & Transitions

Houxit has a built-in animation system with two directives:

  • $$transite — CSS transitions for elements entering or leaving the DOM (paired with $$if or $$for).
  • $$animate — Keyframe-style animations with full programmatic control.

$$transite

<p $$if="show" $$transite:fade="{ delay: 300 }">Hello</p>

Define the transition in your widget options:

export default {
  transitions: {
    fade(node, params) {
      return {
        keyframes: [
          { opacity: [0, 1], duration: 300 },   // enter
          { opacity: [1, 0], duration: 200 }    // leave
        ]
      }
    }
  }
}

$$animate

<div $$animate:spin>Loading...</div>
export default {
  animations: {
    spin(node, { to, from }, params) {
      return {
        keyframes: [
          { transform: 'rotate(0deg)' },
          { transform: 'rotate(360deg)' }
        ],
        duration: 1000,
        iterations: Infinity
      }
    }
  }
}

Dynamic Animation References

When declaring animations in Adapter mode, use bracket syntax in the template to evaluate the animation name as a JS expression:

<div $$animate:[spin]>Loading...</div>
<div $$transite:[whizzz]="{ duration: 400 }">Fading...</div>

To resolve a registered animation or transition function from an ancestor scope:

import { resolve } from 'houxit'

const fade = resolve.animation('fade')

function whizzz(node, params) {
  // Custom transition — does not require registration
}

Animations in Render Functions & JSX

In render functions, use the motion prop:

// JSX
<div motion={animate(whixx, {})} />

// Render function
h('input', { motion: transite(fade, {}) })

Or use the attach prop for full control:

<div attach={({ animate, transite }) => {
  transite(fly, {})
  animate(whoosh)
}} />

The Motion Widget

Motion is a declarative wrapper that manages enter/leave transitions for a group of elements — including text nodes, which are compiled to <span> elements.

<Motion name="slide" mode="out-in">
  <div $$if="view === 'home'" key="home">
    <Home />
  </div>
  <div $$if="view === 'about'" key="about">
    <About />
  </div>
</Motion>

Define the named transition in your widget options:

export default {
  transitions: {
    slide() {
      return {
        keyframes: [
          { transform: ['translateX(100%)', 'translateX(0)'], duration: 300 },
          { transform: ['translateX(0)', 'translateX(-100%)'], duration: 300 }
        ]
      }
    }
  }
}

Props, Events & Attrs

$params

The in-template and in-handlers accessor for declared params. Read-only.

<p>Hello, {{ $params.name }}!</p>

In the build function or Adapter API context, params arrive as part of the first argument:

build({ $params, slots, signals, events, attrs }) {
  // ...
}

In <script build>, use defineParams to declare and access typed params:

const $params = defineParams({ name: String, age: Number })

$signals and $events

$signals are declared events — explicitly defined as part of the widget's public API. Use them to communicate intentional output to the parent.

export default {
  signals: ['updateCount', 'save'],
  handlers: {
    save() {
      this.$signals.save(this.formData)
    }
  }
}

$events are undeclared native DOM events that fall through to the widget's root element without explicit declaration.

$signals vs $events: Use $signals for events your widget owns — things like submit, select, change that are part of the widget's contract. Use $events for native DOM events you're just passing through transparently.

$attrs

Contains all attributes passed to a widget that are not declared as params or signals — including class, style, and any event listeners not explicitly declared.

<!-- Parent -->
<MyInput class="large" placeholder="Type here..." />

<!-- MyInput template -->
<input $$bind="$attrs" />

By default, $attrs falls through to the widget's root element automatically. To disable:

{
  buildConfig: {
    forwardAttrs: false
  }
}
// Adapter API:
// use Houxit.defineConfig() instead

Then manually apply $attrs wherever appropriate with $$bind="$attrs".


Slots

Slots let parent widgets inject content into a child widget's template.

Default Slot

<!-- Button widget template -->
<button class="btn">
  <slot />
</button>

<!-- Usage -->
<Button>Click me</Button>

Named Slots

<!-- Modal widget template -->
<div class="modal">
  <header><slot name="header" /></header>
  <main><slot /></main>
  <footer><slot name="footer" /></footer>
</div>

<!-- Usage -->
<Modal>
  <h2 #header>Confirm</h2>
  <p>Are you sure?</p>
  <button #footer @click="confirm">Yes</button>
</Modal>

Scoped Slots

Scoped slots pass data up from the child widget to the parent's slot content. Use the context option method to expose data to the consumer — its return value becomes available in the slot:

export default {
  context() {
    return {
      item: { label: 'houxit' },
      index: 0
    }
  }
}
<!-- Consume the exposed data with $$provide -->
<List $$provide="{ item, index }">
  <strong>{{ index }}: {{ item.label }}</strong>
</List>

Slot content can also receive data from a specific named <slot> through the context attribute:

<!-- Widget template -->
<slot name="default" context="{ item }" />

<!-- Parent consumes it -->
<MyList>
  <span #default="{ item }">{{ item.name }}</span>
</MyList>

Built-in Widgets

Houxit ships a set of built-in widgets for common structural patterns.

Build

The dynamic widget component — renders any widget passed to its self prop. Equivalent to Vue's <component :is="...">.

<Build *self="currentWidget" *name="userName" />

self accepts a widget definition object, a function, a class, or a string name of a globally registered widget or HTML tag.

Suspense

Wraps async subtrees and displays a fallback slot while they resolve.

<Suspense>
  <Spinner #fallback />
  <AsyncDashboard />
</Suspense>

Works with {{@await}} blocks and widgets that have an async build function.

Portal

Renders children into a different DOM node — useful for modals, tooltips, and overlays that need to escape their parent's stacking context.

<Portal target="#modals">
  <div class="modal-overlay">
    <p>I'm rendered in #modals, not in-place.</p>
  </div>
</Portal>

Memo

Prevents re-renders of its children unless the specified key changes. Use for expensive or static subtrees.

<Memo *key="userId">
  <ExpensiveProfile *id="userId" />
</Memo>

Provider

Establishes a reactive value in the widget tree that descendant widgets can inject without prop-drilling. Works well with createAgent.

// Provide a value
import { inject } from 'houxit'
<Provider name="theme" *value="currentTheme">
  <App />
</Provider>
// Inject in any descendant widget
const theme = inject('theme')

Self

Renders the current widget recursively. Use for tree views, nested menus, or any fractal/recursive structure.

<!-- TreeNode widget -->
<div class="node">
  <span>{{ node.label }}</span>
  <div $$if="node.children.length" class="children">
    <Self $$for="child of node.children" *node="child" />
  </div>
</div>

Fragment

Renders multiple root elements without a wrapping DOM node — useful when a widget needs to return more than one sibling element.

<Fragment>
  <dt>Term</dt>
  <dd>Definition</dd>
</Fragment>

Render Functions & Hyperscript

When templates aren't flexible enough — or when you need to construct UI programmatically — use the h function to write render functions.

import { h } from 'houxit'

Signature:

h(type, props, ...children)
  • type — a string tag name ('div', 'p'), a widget definition, or a built-in widget.
  • props — an object of attributes, event handlers, and directives. Pass null if none.
  • children — strings, numbers, or more h() calls.
export default {
  render() {
    return h('ul', { class: 'list' },
      ...this.items.map(item =>
        h('li', { key: item.id }, item.name)
      )
    )
  }
}

Render functions have the same expressive power as templates — reactive state reads are tracked, events can be attached, and all built-in widgets can be used:

import { h, Suspense, Fragment } from 'houxit'

render() {
  return h(Fragment, null,
    h('h1', null, 'Title'),
    h(Suspense, {},
      [
        enSlot({ fallback: () => h('p', null, 'Loading...') }),
        h(AsyncWidget, { id: this.id })
      ]
    )
  )
}

The <script render> Tag

Inside a .houxit file, use a dedicated <script render> block instead of <template> or the render option. It receives the widget context and returns a render expression.

<!-- UserCard.houxit -->

<script build>
import { useParams, h } from 'houxit'

useParams({
  user: Object
})
</script>

<script render>
h('div', { class: 'card' },
  h('img', { src: $params.user.avatar }),
  h('h2', null, $params.user.name),
  h('p', null, $params.user.bio)
)
</script>

<script render> takes precedence over <template> if both are present. It's the recommended approach when your widget's output is highly dynamic or computed.


TypeScript Support

Houxit ships with full TypeScript types. Params, signals, model properties, and build function arguments are all typed.

import { defineWidget, token } from 'houxit'

interface User {
  name: string
  email: string
}

export default defineWidget({
  params: {
    user: Object as () => User
  },
  build({ params }: { params: { user: User } }) {
    const editedName = token(params.user.name)

    return () => h('input', {
      value: editedName.data,
      'on:input': (e: Event) => {
        editedName.data = (e.target as HTMLInputElement).value
      }
    })
  }
})

In <script build> blocks, use defineParams and defineSignals for typed declarations:

<script build lang="ts">
import { stream, defineParams, defineSignals } from 'houxit'

const $params = defineParams<{
  label: string
  disabled?: boolean
}>()

const emit = defineSignals<{
  click: [MouseEvent]
}>()

function handleClick(e: MouseEvent) {
  if (!$params.disabled) emit('click', e)
}
</script>

<template>
  <button *disabled="$params.disabled" @click="handleClick">
    {{ $params.label }}
  </button>
</template>

License

Houxit.js is released under the MIT License.

About

Houxit.js : The Blueprint for Modern Web Apps.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors