glendix is the JavaScript-target build and rendering bridge for Mendix
Pluggable Widgets written in Gleam.
Its package boundary is explicit:
- glendix owns Pluggable Widgets Tools orchestration, widget definition editing, external npm React bindings, and the Lustre-to-React bridge;
- mendraw owns Mendix client values and bindings generated from installed
.mpkassets; - mxpak owns Marketplace search, package download, cache, lockfiles, and workspace deduplication;
- glendam owns generic browser automation.
Glendix does not implement Marketplace access or browser automation.
[dependencies]
glendix = ">= 6.0.0 and < 7.0.0"Add mendraw only when the project uses Mendix client values or installed MPK
components, and add/use mxpak only when package acquisition is required.
A widget project's package.json normally includes the Mendix Pluggable Widgets
Tools and their React peer dependencies.
import mendraw/mendix
import redraw
import redraw/dom/attribute
import redraw/dom/html
pub fn widget(props: mendix.JsProps) -> redraw.Element {
let title = mendix.get_string_prop(props, "title")
html.section([attribute.class("widget")], [html.text(title)])
}This example composes Glendix with Mendraw in the application. Glendix itself remains independently usable for external React bindings and Lustre rendering.
import glendix/lustre as glendix_lustre
import gleam/int
import lustre/effect
import lustre/element
import lustre/element/html
import lustre/event
import redraw
type Model { Model(count: Int) }
type Message { Increment }
fn update(model: Model, message: Message) -> #(Model, effect.Effect(Message)) {
case message {
Increment -> #(Model(model.count + 1), effect.none())
}
}
fn view(model: Model) -> element.Element(Message) {
html.button([event.on_click(Increment)], [
html.text("Count: " <> int.to_string(model.count)),
])
}
pub fn component() -> redraw.Element {
glendix_lustre.use_tea(#(Model(0), effect.none()), update, view)
}Use keyed_host when a typed props-derived revision must restart the entire
Lustre application:
pub fn component(props: Props) -> redraw.Element {
glendix_lustre.keyed_host(
key: props_revision(props),
props: props,
render: fn(current_props) {
glendix_lustre.use_tea(
init(current_props),
update,
view,
)
},
)
}Derive props_revision in pure Gleam from the application's typed state. An
unchanged key preserves the application while the callback receives fresh
props; a changed key remounts it. Directly keying an evaluated use_tea result
does not create the React component boundary that owns its hooks, and
lustre/element/keyed only affects children inside the running Lustre tree.
Configure exports in gleam.toml:
[tools.glendix.bindings]
recharts = ["PieChart", "Pie"]Install the npm package, then run gleam run -m glendix/install. Glendix owns
both component lookup and element construction; Mendraw is not required:
import gleam/result
import glendix/binding
import redraw
import redraw/dom/attribute
pub fn pie_chart(
attributes attributes: List(attribute.Attribute),
children children: List(redraw.Element),
) -> Result(redraw.Element, binding.BindingError) {
use module <- result.try(binding.module("recharts"))
use component <- result.try(binding.resolve(module, "PieChart"))
Ok(binding.element(component, attributes, children))
}binding.element_ creates an element with children only, and
binding.void_element creates one without children.
For a module that must finish asynchronous setup before use, configure an extended binding:
[tools.glendix.bindings."@ironcalc/workbook"]
exports = ["Workbook"]
initializer = "init"
retry = "on-failure"The initializer must be a zero-argument Promise-returning export. retry is
"never" by default (cache failure until binding.reset_initialization) or
"on-failure" (allow a later attempt after settlement). Create state with
binding.initialization, call binding.initialize, and store the returned
ModuleInitialization. Concurrent calls share its Promise. Record completion
with binding.settle_initialization, then use binding.initialized_module for
both rendering and non-React consumers. Lustre can use
binding.initialization_effect; React Suspense can use
binding.use_initialization with that same Promise.
Glendix keeps only a generated static-import/direct-call FFI for names selected
from TOML after package compilation. All Promise and retry orchestration uses
gleam/javascript/promise; the generated code has no dynamic import or Promise
cache, preserving deterministic Rollup and WebAssembly asset discovery.
Use glendix/js/environment to read prefers-color-scheme without exposing
window.matchMedia. color_scheme returns Light, Dark, or System;
resolved_color_scheme returns ResolvedLight, ResolvedDark, or
ResolutionUnavailable. Dynamic preference-change subscriptions are
intentionally out of scope, so re-query during an application-driven
re-render.
Build one plain JavaScript configuration object from ordered typed entries and pass it through a Redraw attribute to an external component:
import glendix/binding
import glendix/js/environment
import glendix/js/object
import redraw
import redraw/dom/attribute
pub fn themed_component(
component component: binding.JsComponent,
) -> redraw.Element {
let theme = case environment.resolved_color_scheme() {
environment.ResolvedDark -> "dark"
environment.ResolvedLight -> "light"
environment.ResolutionUnavailable -> "light"
}
let configuration =
object.from_entries([#("theme", object.string(theme))])
binding.element(
component,
[attribute.attribute("config", object.from_object(configuration))],
[],
)
}object.from_entries preserves ordinary string-key order, keeps the last value
for duplicate keys, and safely stores special keys as data.
Use glendix/js/file for Gossamer-backed declarative download resources and
Plinth-backed modern file selection. It returns typed capability, cancellation,
metadata-validation, handle-open, and byte-read errors. Glendix deliberately
does not add a hidden-input fallback or application-specific parsing and
filename policy.
See the browser file capability contract for the API, validation order, fallback policy, and ecosystem/residual-FFI matrix.
Package acquisition is a separate step owned by mxpak:
[tools.mxpak]
mode = "extract"
[tools.mxpak.widgets.Charts]
version = "3.0.0"mxp install
gleam run -m mendraw/install
gleam run -m glendix/install
gleam run -m glendix/buildmxp installwrites package assets tobuild/widgets/.mendraw/installgenerates typed MPK bindings.glendix/installinstalls JavaScript dependencies and generates Glendix npm bindings.glendix/buildcreates the production.mpk.
Projects that do not use Marketplace widgets omit the first two steps.
| Command | Responsibility |
|---|---|
gleam run -m glendix/install |
Install JS dependencies and generate Glendix npm bindings |
gleam run -m glendix/define |
Edit widget property definitions |
gleam run -m glendix/dev |
Run the development build/server |
gleam run -m glendix/build |
Build a production .mpk |
gleam run -m glendix/start |
Connect to the configured Mendix test project |
gleam run -m glendix/lint |
Run lint checks |
gleam run -m glendix/lint_fix |
Apply lint fixes |
gleam run -m glendix/release |
Run the release build |
gleam deps download
gleam format --check
gleam check
gleam build --warnings-as-errors
gleam docs build
gleam test --runtime bun