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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions