# loomx
**loomx** is a strongly‑typed, server‑driven UI framework for Rust.
It renders HTML fragments and pages using typed components, designed for **HTMX‑first** applications with minimal JavaScript.
At its core, loomx lets you:
- Define components as **typed Rust structs**
- Render them using **Askama templates**
- Register components at compile time
- Render fragments dynamically by name
- Integrate cleanly with **Axum**
- Avoid JavaScript-heavy frontend frameworks
---
## Project Goals
- Strong typing end‑to‑end (props → templates → rendering)
- Server‑driven HTML with HTMX
- Explicit, debuggable architecture
- Framework‑agnostic core
- Thin transport integrations (Axum today, others possible later)
Non‑goals:
- Client‑side virtual DOM
- JS component systems
- Implicit magic routing
---
## Workspace Layout
```text
loomx/
├─ crates/
│ ├─ loomx-core # Registry, rendering, props parsing
│ ├─ loomx-macros # register_component! macro
│ ├─ loomx-axum # Axum integration (routes, handlers)
│ └─ loomx-cli # CLI (experimental)
├─ components/
│ └─ loomx-components-basic
│ └─ Example component pack
├─ examples/
│ └─ basic # Example Axum app
```
---
## Core Concepts
### Components
A component is:
- A Rust struct (typed props)
- An Askama template
- A stable string name
```rust
#[derive(Template, Deserialize)]
#[template(path = "components/hello.html")]
pub struct Hello {
pub name: String,
}
impl Component for Hello {
const NAME: &'static str = "hello";
}
register_component!(Hello);
```
### Component Registration
Components are registered at **compile time** using `inventory`.
- No global mutable registries
- No runtime plugin loading
- Deterministic startup behavior
Duplicate component names are detected at startup.
---
## Rendering
```rust
use loomx::render;
let html = render("hello", json!({ "name": "Ada" }))?;
```
Rendering returns HTML or a typed error.
---
## Props Parsing (Core)
`loomx-core` provides a transport‑agnostic parser:
- `application/json`
- `application/x-www-form-urlencoded`
```rust
parse_props(content_type, body_bytes)
```
This logic is reused by all integrations.
---
## Axum Integration
`loomx-axum` provides:
### Generic Fragment Routes
- `GET /fragments/:name` → query params
- `POST /fragments/:name` → form or JSON body
```rust
Router::new().merge(loomx_axum::fragment_router())
```
### Typed Fragment Routes (Ergonomic)
```rust
Router::new().merge(
loomx_axum::typed_fragment_routes! {
"/fragments/hello" => ("hello", HelloProps),
}
)
```
This generates:
- `GET /fragments/hello`
- `POST /fragments/hello` (form)
- `POST /fragments/hello/json` (JSON)
All fully typed.
---
## HTMX Support
loomx is HTMX‑first:
- Fragment responses are HTML
- HTMX headers are detected
- Designed for `hx-get`, `hx-post`, `hx-swap`
No JS framework required.
---
## Logging
Structured logging via `tracing`:
- Component render attempts
- HTMX detection
- Request metadata
- Startup validation
---
## Safety & Correctness
- Strong typing everywhere
- Duplicate component detection
- Explicit linking of component crates
- No hidden runtime state
---
## Status
loomx is **early but functional**.
Current focus:
- API stabilization
- Documentation
- CLI & tooling
- More integrations (Actix / Poem)
---
## License
MIT
---
## Update: Typed Rendering & Gallery
loomx now provides an ergonomic typed rendering helper:
- `render_with<T: Serialize>(name, &T)` — preferred for application code
This avoids manual construction of `serde_json::Value` while preserving the dynamic
component registry model.
The example application includes an **interactive component gallery** at `/gallery`
showing:
- inline rendering
- HTMX-powered preview slots
- typed fragment routes under `/typed/...`
See `docs/gallery.md` for details.