**English** | [한국어](README.ko.md) | [日本語](README.ja.md)
# glendix
Hello! This is glendix and it's ever so brilliant! It's a Gleam library for Mendix Pluggable Widgets.
**You can write proper Mendix widgets using only Gleam — no JSX needed at all, how lovely is that!**
React is handled by [redraw](https://github.com/ghivert/redraw)/[redraw_dom](https://github.com/ghivert/redraw), TEA pattern by [lustre](https://github.com/lustre-labs/lustre), and Mendix types/widgets/marketplace are delegated to [mendraw](https://github.com/GG-O-BP/mendraw).
## What's New in v4.0
v4.0 delegates Mendix API types, widget bindings (.mpk), classic widgets, and marketplace functionality to **mendraw**. glendix now focuses purely on build tooling, external React component bindings, and the Lustre bridge.
### What's Changed Then
- **Mendix types moved to mendraw**: `import glendix/mendix` → `import mendraw/mendix`, all submodules (`editable_value`, `action`, `list_value`, etc.) now under `mendraw/mendix/*`
- **Interop moved to mendraw**: `import glendix/interop` → `import mendraw/interop`
- **Widget moved to mendraw**: `import glendix/widget` → `import mendraw/widget`, TOML config `[tools.glendix.widgets.*]` → `[tools.mendraw.widgets.*]`
- **Classic moved to mendraw**: `import glendix/classic` → `import mendraw/classic`
- **Marketplace moved to mendraw**: `gleam run -m glendix/marketplace` → `gleam run -m mendraw/marketplace`
- **glendix/binding stays**: external React component bindings remain in glendix
- **glendix/lustre stays**: Lustre TEA bridge remains in glendix
### Migration Cheatsheet (v3 → v4)
| Before (v3) | After (v4) |
|---|---|
| `import glendix/mendix.{type JsProps}` | `import mendraw/mendix.{type JsProps}` |
| `import glendix/mendix/editable_value` | `import mendraw/mendix/editable_value` |
| `import glendix/mendix/action` | `import mendraw/mendix/action` |
| `import glendix/interop` | `import mendraw/interop` |
| `import glendix/widget` | `import mendraw/widget` |
| `import glendix/classic` | `import mendraw/classic` |
| `gleam run -m glendix/marketplace` | `gleam run -m mendraw/marketplace` |
| `[tools.glendix.widgets.X]` | `[tools.mendraw.widgets.X]` |
## How to Put It In Your Project
Pop this into your `gleam.toml`:
```toml
# gleam.toml
[dependencies]
glendix = ">= 4.0.1 and < 5.0.0"
mendraw = ">= 1.1.9 and < 2.0.0"
```
### Peer Dependencies
Your widget project's `package.json` needs these as well:
```json
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"big.js": "^6.0.0"
}
}
```
> `big.js` is only needed if your widget uses Decimal attributes. Skip it if you don't!
## Let's Get Started!
Here's a dead simple widget — look how short it is!
```gleam
import mendraw/mendix.{type JsProps}
import redraw.{type Element}
import redraw/dom/attribute
import redraw/dom/html
pub fn widget(props: JsProps) -> Element {
let name = mendix.get_string_prop(props, "sampleText")
html.div([attribute.class("my-widget")], [
html.text("Hello " <> name),
])
}
```
`fn(JsProps) -> Element` — that's literally all a Mendix Pluggable Widget needs. Easy peasy!
### Using Lustre TEA Pattern
If you prefer The Elm Architecture, use the Lustre bridge — your `update` and `view` functions are 100% standard Lustre:
```gleam
import glendix/lustre as gl
import mendraw/mendix.{type JsProps}
import lustre/effect
import lustre/element/html
import lustre/event
import redraw.{type Element}
type Model { Model(count: Int) }
type Msg { Increment }
fn update(model, msg) {
case msg {
Increment -> #(Model(model.count + 1), effect.none())
}
}
fn view(model: Model) {
html.div([], [
html.button([event.on_click(Increment)], [
html.text("Count: " <> int.to_string(model.count)),
]),
])
}
pub fn widget(_props: JsProps) -> Element {
gl.use_tea(#(Model(0), effect.none()), update, view)
}
```
## All the Modules
### React & Rendering (via redraw)
| Module | What It Does |
|---|---|
| `redraw` | Components, hooks, fragments, context — the full React API in Gleam |
| `redraw/dom/html` | HTML tags — `div`, `span`, `input`, `text`, `none`, and loads more |
| `redraw/dom/attribute` | Attribute type + HTML attribute functions — `class`, `id`, `style`, and more |
| `redraw/dom/events` | Event handlers — `on_click`, `on_change`, `on_input`, with capture variants |
| `redraw/dom/svg` | SVG elements — `svg`, `path`, `circle`, filter primitives, and more |
| `redraw/dom` | DOM utilities — `create_portal`, `flush_sync`, resource hints |
### glendix Bridges
| Module | What It Does |
|---|---|
| `glendix/lustre` | Lustre TEA bridge — `use_tea`, `use_simple`, `render`, `embed` |
| `glendix/binding` | For using other people's React components — configure in `gleam.toml [tools.glendix.bindings]` |
| `glendix/define` | Interactive TUI editor for widget property definitions |
### mendraw (Mendix API & Widgets)
| Module | What It Does |
|---|---|
| `mendraw/mendix` | Core Mendix types (`ValueStatus`, `ObjectItem`, `JsProps`) + props accessors |
| `mendraw/interop` | Renders external JS React components (from `widget`/`binding`) as `redraw.Element` |
| `mendraw/widget` | For using `.mpk` widgets — auto-downloaded via `gleam.toml` — `component`, `prop`, `editable_prop`, `action_prop` |
| `mendraw/classic` | Classic (Dojo) widget wrapper — `classic.render(widget_id, properties)` |
| `mendraw/marketplace` | Search and download widgets from the Mendix Marketplace |
### JS Interop Bits
| Module | What It Does |
|---|---|
| `glendix/js/array` | Gleam List ↔ JS Array conversion |
| `glendix/js/object` | Create objects, read/write/delete properties, call methods, `new` instances |
| `glendix/js/json` | `stringify` and `parse` (parse returns a proper `Result`!) |
| `glendix/js/promise` | Promise chaining (`then_`, `map`, `catch_`), `all`, `race`, `resolve`, `reject` |
| `glendix/js/dom` | DOM helpers — `focus`, `blur`, `click`, `scroll_into_view`, `query_selector` |
| `glendix/js/timer` | `set_timeout`, `set_interval`, `clear_timeout`, `clear_interval` |
## Examples
### Attribute Lists
This is how you make a button with attributes — it's like a shopping list!
```gleam
import redraw/dom/attribute
import redraw/dom/events
import redraw/dom/html
html.button(
[
attribute.class("btn btn-primary"),
attribute.type_("submit"),
attribute.disabled(False),
events.on_click(fn(_event) { Nil }),
],
[html.text("Submit")],
)
```
### useState + useEffect
Here's a counter! Every time you press the button, the number goes up by one — magic!
```gleam
import gleam/int
import redraw
import redraw/dom/attribute
import redraw/dom/events
import redraw/dom/html
pub fn counter(_props) -> redraw.Element {
let #(count, set_count) = redraw.use_state(0)
redraw.use_effect(fn() { Nil }, Nil)
html.div([], [
html.button(
[events.on_click(fn(_) { set_count(count + 1) })],
[html.text("Count: " <> int.to_string(count))],
),
])
}
```
### Reading and Writing Mendix Values
Here's how you get values out of Mendix and do things with them:
```gleam
import gleam/option.{None, Some}
import mendraw/mendix.{type JsProps}
import mendraw/mendix/editable_value as ev
import redraw.{type Element}
import redraw/dom/html
pub fn render_input(props: JsProps) -> Element {
case mendix.get_prop(props, "myAttribute") {
Some(attr) -> {
let display = ev.display_value(attr)
let editable = ev.is_editable(attr)
// ...
}
None -> html.none()
}
}
```
### Using Other People's React Components (Bindings)
You can use React libraries from npm without writing any `.mjs` files yourself — isn't that ace!
**1. Add bindings to `gleam.toml`:**
```toml
[tools.glendix.bindings]
recharts = ["PieChart", "Pie", "Cell", "Tooltip", "Legend"]
```
**2. Install the package:**
```bash
npm install recharts
```
**3. Run `gleam run -m glendix/install`**
**4. Write a nice Gleam wrapper:**
```gleam
// src/chart/recharts.gleam
import glendix/binding
import mendraw/interop
import redraw.{type Element}
import redraw/dom/attribute.{type Attribute}
fn m() { binding.module("recharts") }
pub fn pie_chart(attrs: List(Attribute), children: List(Element)) -> Element {
interop.component_el(binding.resolve(m(), "PieChart"), attrs, children)
}
pub fn pie(attrs: List(Attribute), children: List(Element)) -> Element {
interop.component_el(binding.resolve(m(), "Pie"), attrs, children)
}
```
**5. Use it in your widget:**
```gleam
import chart/recharts
import redraw/dom/attribute
pub fn my_chart(data) -> redraw.Element {
recharts.pie_chart(
[attribute.attribute("width", 400), attribute.attribute("height", 300)],
[
recharts.pie(
[attribute.attribute("data", data), attribute.attribute("dataKey", "value")],
[],
),
],
)
}
```
### Using .mpk Widgets
You can use Marketplace widgets as React components — auto-downloaded via `gleam.toml`.
Register your widget in `gleam.toml` and run `gleam run -m glendix/install`:
```toml
[tools.mendraw.widgets.Charts]
version = "3.0.0"
# s3_id = "com/..." ← if you have this, no auth needed!
```
It downloads to `build/widgets/` cache and generates everything automatically.
**Have a look at the auto-generated `src/widgets/*.gleam` files:**
```gleam
// src/widgets/switch.gleam (made automatically!)
import mendraw/mendix.{type JsProps}
import mendraw/interop
import mendraw/widget
import redraw.{type Element}
import redraw/dom/attribute
pub fn render(props: JsProps) -> Element {
let boolean_attribute = mendix.get_prop_required(props, "booleanAttribute")
let action = mendix.get_prop_required(props, "action")
let comp = widget.component("Switch")
interop.component_el(
comp,
[
attribute.attribute("booleanAttribute", boolean_attribute),
attribute.attribute("action", action),
],
[],
)
}
```
**4. Use it in your widget:**
You can pass Mendix props through directly, or create values from scratch using the widget prop helpers:
```gleam
// Creating values from scratch (e.g. in Lustre TEA views)
import mendraw/widget
widget.prop("caption", "Hello") // DynamicValue
widget.editable_prop("text", value, display, set_value) // EditableValue
widget.action_prop("onClick", fn() { do_something() }) // ActionValue
```
```gleam
import widgets/switch
switch.render(props)
```
### Downloading Widgets from the Marketplace
You can search for widgets on the Mendix Marketplace and download them right from the terminal — it's dead handy!
**1. Put your Mendix PAT in `.env`:**
```
MENDIX_PAT=your_personal_access_token
```
> You can get a PAT from [Mendix Developer Settings](https://user-settings.mendix.com/link/developersettings) — click **New Token** under **Personal Access Tokens**. You'll need the `mx:marketplace-content:read` permission.
**2. Run this:**
```bash
gleam run -m mendraw/marketplace
```
**3. Use the lovely interactive menu:**
```
── Page 1/5+ ──
[0] Star Rating (54611) v3.2.2 — Mendix
[1] Switch (50324) v4.0.1 — Mendix
...
Number: download | Search term: filter by name | n: next | p: previous | r: reset | q: quit
> 0 ← type a number to download it
> star ← type a word to search
> 0,1,3 ← use commas to pick several at once
```
Downloaded widgets are cached in `build/widgets/` and automatically added to your `gleam.toml` — no need to commit `.mpk` files to source control!
## Build Scripts
| Command | What It Does |
|---------|-------------|
| `gleam run -m glendix/install` | Installs deps + downloads TOML widgets + makes bindings + generates widget files |
| `gleam run -m mendraw/marketplace` | Searches and downloads widgets from the Marketplace |
| `gleam run -m glendix/define` | Interactive TUI editor for widget property definitions |
| `gleam run -m glendix/build` | Makes a production build (.mpk file) |
| `gleam run -m glendix/dev` | Starts a dev server (with HMR) |
| `gleam run -m glendix/start` | Connects to a Mendix test project |
| `gleam run -m glendix/lint` | Checks your code with ESLint |
| `gleam run -m glendix/lint_fix` | Fixes ESLint problems automatically |
| `gleam run -m glendix/release` | Makes a release build |
## Why We Made It This Way
- **Delegate, don't duplicate.** React bindings belong to redraw. TEA belongs to lustre. Mendix types and widgets belong to mendraw. glendix only handles build tooling, external React component bindings, and the Lustre bridge.
- **Opaque types keep everything safe.** JS values like `JsProps` and `EditableValue` are wrapped up in Gleam types so you can't accidentally do something wrong — the compiler catches it!
- **`undefined` turns into `Option` automatically.** When JS gives us `undefined` or `null`, Gleam gets `None`. When there's a real value, it becomes `Some(value)`. No faffing about!
- **Two rendering paths.** Use redraw for direct React, or use the Lustre bridge for TEA — both output `redraw.Element`, so they compose freely.
## Thank You
glendix v4.0 is built on top of the brilliant [redraw](https://github.com/ghivert/redraw), [lustre](https://github.com/lustre-labs/lustre), and [mendraw](https://github.com/GG-O-BP/mendraw) ecosystems. Cheers to all projects!
## Licence
[Blue Oak Model Licence 1.0.0](LICENSE)