Skip to content

Discussing FX inspired composable dsl pattern #6

Description

@ivan-velikanov

Hi @hjohn — reviving this closed thread ( #1 ) only to show you where your feedback led, not to reopen anything. I went ahead and built the composable DSL we'd debated — luvjfx (https://github.com/luvml/luvjfx) — and I ended up adopting most of your objections as hard constraints rather than arguing them. The two libraries are fully independent and coexist happily; I mostly wanted to show you the parts that came straight from your comments, and how I lean on your reactive layer rather than reinventing it.

Things you were right about, now baked in:

  • Discoverability is non-negotiable. I dropped the free-static t()/value() idea entirely. Every property is a fluent method on a typed wrapper, so it's pure IDE autocomplete — you learn ~50 factory names (which mirror the JavaFX class names: VBoxvbox) and the IDE surfaces the rest. Your "t/onAction out of nowhere" smell is gone, because the target type is always visible at the call site.
  • No forced .build(). Factories return a thin wrapper holding the real node in a public .node field; .attr(n -> …) hands you the typed node for anything uncovered. Nothing is ever blocked.
  • "Compose shared behaviour via default methods on interfaces" — that's literally the spine: a self-typed FxNodeOps<S,N> base, one trait-interface per property, bundled per control, all default methods. And I don't hand-write them (your "it grows every release" worry): the whole surface is generated from JavaFX by reflection — 367 property-traits across 51 controls, re-runnable per release. What you write stays plain Java.

The one idea I'd offer back. Your sticking point was: "if I had a way to mark Node as one of the accepted vararg parameters, I'd be all for a type-safe solution." Typing the vararg as a functional interfaceConsumer<? super N> — turns out to be that key. A bare configurator lambda and typed children then share one argument list, all checked against the node:

import static luvjfx.Fx.*;

var form = vbox().spacing(8).padding(16).nodes(
    label("Name"),
    textField().promptText("Your name"),
    button("OK").defaultButton(true).onAction(e -> submit())
);

// config-lambda + children in one varargs; raw/foreign nodes wrap via fx(...):
vbox(v -> v.spacing(8), label("Name"), fx(existingJavaFxNode));

It reads like SwiftUI/Flutter — but stays plain Java. The same little form across three declarative UIs:

// SwiftUI
VStack(spacing: 8) {
    Text("Name")
    TextField("Your name", text: $name)
    Button("OK") { submit() }
}
// Flutter (Dart)
Column(children: [
  const Text('Name'),
  TextField(onChanged: (v) => name = v),
  ElevatedButton(onPressed: submit, child: const Text('OK')),
]);
// luvjfx (Java)
vbox().spacing(8).nodes(
    label("Name"),
    textField().promptText("Your name"),
    button("OK").onAction(e -> submit())
);

Same nested tree, same terseness (actually more terse than flutter) — but no new language, no compiler plugin, no build(), and .node is a real TextField you can do anything with. That was the whole motivation: JavaFX can read as cleanly as the declarative-UI crowd without anyone leaving Java.

Your reactive layer, used as-is. I deliberately didn't rebuild Observe/Domain/Model — that part of FX is genuinely well-designed, so luvjfx just binds to it. luvjfx builds the tree, fx-values owns value+validity, and they meet only at ObservableValue/Property:

var roundTrip  = BooleanModel.of(false);
var departure  = ObjectModel.of(today, Domain.bounded(today, today.plusYears(1)));
var returnDate = ObjectModel.<LocalDate>of(today.plusDays(7), Domain.any());

// the return date's *domain* depends on departure — your Observe + Domain:
Observe.values(roundTrip, departure)
    .compute((rt, dep) -> Boolean.TRUE.equals(rt) && dep != null
        ? Domain.bounded(dep, dep.plusYears(1)) : Domain.inapplicable())
    .subscribe(returnDate::setDomain);

datePicker().disable(roundTrip.map(rt -> !rt))          // generated ObservableValue binding overload
    .attr(dp -> Models.edits(dp.valueProperty(), returnDate));   // edits = a 3-line control→model bridge
button("Book").disable(Observe.booleans(departure.valid(), returnDate.valid()).allTrue().map(v -> !v));

(fx-core/fx-values aren't on Central yet, so I clone + mvn install them locally — a release would make this an ordinary dependency.)

The model↔control bridge is still a hair verbose (.attr(dp -> Models.edits(...))); I think it can collapse into a fluent .model(returnDate) overload so the wiring disappears entirely — but that's a refinement for another day.

That's all — shared in the spirit of the original exchange. Both libraries stand on their own; if any of this is useful to FX, take it freely, and I'm indebted to your reactive design either way. Thanks for the original pushback — it shaped the whole thing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions