Use @Input(enhancer=...) to add presets or conversions to an input's generated builder step.
The enhancer is an interface with one result type parameter and one abstract method matching the
input's generated setter name and builder-facing value type. Its public default methods return the result
parameter directly and delegate to that setter.
package react4j.examples.input_enhancers;
public interface ColorEnhancer<R> {
R color(String value);
default R red() {
return color("red");
}
}
Select it on a method input or constructor parameter:
package react4j.examples.input_enhancers;
import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;
@View(type = View.Type.STATEFUL)
abstract class Swatch {
@Input(enhancer = ColorEnhancer.class)
abstract String color();
@Render
ReactNode render() {
return ReactNode.of(color());
}
}
Helpers on the first step also have static shortcuts:
package react4j.examples.input_enhancers;
import react4j.ReactNode;
public final class Presets {
private Presets() {}
public static ReactNode redSwatch() {
return SwatchBuilder.red();
}
}
The result parameter describes what setting the input returns:
| Setter behavior | Helper result |
|---|---|
| More required inputs remain | The next builder step |
| Several optional inputs share the step | The same builder step |
| Setting this input finishes construction | ReactNode |
A terminal helper returns the finished node; it needs no additional build() call. Helpers preserve
the existing ordering, required calls, defaults and canonical setters. An optional input may itself
be terminal, so use the generated return type to determine whether construction has finished.
Enhancers may inherit their setter and default methods, including from compiled libraries. Helpers
can declare their own method type parameters, varargs and checked exceptions; static shortcuts retain
those contracts. Keep the interface accessible from the generated builder's package and follow the
project's JSpecify configuration. Its setter must accept only values that the
input can accept; a generated nullable setter may implement an enhancer setter that accepts non-null
values. The abstract setter has no method type parameters or declared exceptions. The interface's result
parameter has no Java bound beyond Object.
Each generated builder uses one implementation for all its steps. The processor rejects incompatible
helper signatures, erased overloads and generic parent interfaces across those steps. Use distinct
names such as compactLayout() and untitled() when two inputs would otherwise introduce different
preset() methods. Compatible shared inherited defaults and ordinary overloads are allowed.
The interface has only its one abstract input setter; additional abstract capabilities are rejected. Private and static interface utilities remain implementation details. Tree-context inputs have no builder step and cannot select an enhancer.
Composing ready builders
A step that exposes build() also implements Buildable. Both Buildable and ReactNode extend
Renderable, which composition APIs accept. Supply a ready builder directly to children, DOM helpers,
context providers, roots or portals; the receiving API calls build() immediately. Each occurrence
builds separately, and exceptions propagate. There is no implicit caching or freezing.
For example, supplying the required message leaves this builder ready, with an optional class name:
package react4j.examples.input_enhancers;
import org.jspecify.annotations.Nullable;
import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;
import react4j.dom.DOM;
import react4j.dom.proptypes.html.HtmlProps;
@View(type = View.Type.STATEFUL)
abstract class Notice {
@Input
abstract String message();
@Input
abstract @Nullable String className();
@Render
ReactNode render() {
return DOM.p(new HtmlProps().className(className()), message());
}
}
Declare an @Render method's return type as Renderable when it may return a ready builder:
package react4j.examples.input_enhancers;
import react4j.Renderable;
import react4j.annotations.Render;
import react4j.annotations.View;
@View(type = View.Type.STATEFUL)
abstract class NoticeHost {
@Render
Renderable render() {
return NoticeBuilder.message("Saved");
}
}
Existing ReactNode return types and explicit build() calls remain valid. A Java variable, field or
helper declared as ReactNode still requires a materialized node; use Renderable when it should
also hold a ready builder. Steps with required calls remaining cannot be supplied as renderables.
Context consumer callbacks and generated render bridges build their returned value on each invocation.
Children inputs
An input whose effective name is children automatically selects react4j.ChildrenEnhancer.
It supplies children(Stream<? extends Renderable>), which consumes the stream once in encounter
order and delegates to the array setter. No enhancer annotation is needed:
package react4j.examples.input_enhancers;
import org.jspecify.annotations.Nullable;
import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;
@View(type = View.Type.STATEFUL)
abstract class ChildrenPanel {
@Input
abstract @Nullable String title();
@Input
abstract ReactNode @Nullable [] children();
@Render
ReactNode render() {
final var title = title();
final var children = children();
return ReactNode.of(
null == title ? null : ReactNode.of(title), null == children ? null : ReactNode.of(children));
}
}
Both child and children always finish construction and return ReactNode, including when
other optional inputs share their step. Set optional properties first, then supply the children.
Optional children may still be omitted through build(). First-step stream helpers also have
static shortcuts:
package react4j.examples.input_enhancers;
import java.util.stream.Stream;
import react4j.ReactNode;
public final class ChildrenPresets {
private ChildrenPresets() {}
public static ReactNode titledPanel() {
return ChildrenPanelBuilder.title("Items")
.children(Stream.of(NoticeBuilder.message("Saved"), NoticeBuilder.message("Synchronized")));
}
public static ReactNode untitledPanel() {
return ChildrenPanelBuilder.children(Stream.of(NoticeBuilder.message("Saved")));
}
public static ReactNode emptyMenu() {
return MenuPanelBuilder.empty();
}
}
An explicit enhancer replaces the automatic selection. Extend ChildrenEnhancer to keep its
stream helper while adding presets:
package react4j.examples.input_enhancers;
import react4j.ChildrenEnhancer;
public interface MenuChildren<R> extends ChildrenEnhancer<R> {
default R empty() {
return children();
}
}
package react4j.examples.input_enhancers;
import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;
@View(type = View.Type.STATEFUL)
abstract class MenuPanel {
@Input(enhancer = MenuChildren.class)
abstract ReactNode[] children();
@Render
ReactNode render() {
return ReactNode.of(children());
}
}
A standalone custom enhancer supplies only its own helpers, so it can also provide its own stream
overload. Omitting enhancer or specifying void.class selects the standard default for children
and no enhancer for other inputs. Tree-context inputs do not receive the default.
Generated setters for ReactNode and ReactNode[] inputs now accept Renderable and Renderable[],
with the same nullness. The view's input declarations can keep their node types: builders are
materialized before storage. Update custom node-input enhancer setters to those widened types.
Overrides of the standard children stream helper must use Stream<? extends Renderable> as well.
Ordinary data inputs and untyped property maps are not traversed for builders.
When migrating existing chains, move setters after child or children before that call and
remove the trailing build():
// Before
BreadcrumbBuilder.label("Overview").child(menu).leaf(false).build();
// After
BreadcrumbBuilder.label("Overview").leaf(false).child(menu);