Create JUI controls — interactive form field components used in GWT/JUI applications. Use this skill when the user asks to: create a new control, build a custom form field, create an interactive input component, or asks about control patterns. Also trigger when the user mentions control, form field, value management, dirty detection, or asks how to create a reusable input that can be placed in a ControlForm.
66
79%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./jui-skills/jui-controls/SKILL.mdCreate custom controls that integrate with JUI's value management, validation, dirty detection and form mechanisms.
Controls extend Control<V, C> (which itself extends Component) and add:
setValue(V) / value() with dirty detectionControlForm, ControlContext, acceptorsUse a control when the UI element captures or presents a user-editable value. Use a component (see jui-components skill) for display-only UI.
public class MyControl extends Control<String, MyControl.Config> {
public static class Config extends Control.Config<String, Config> {
@Override
@SuppressWarnings("unchecked")
public MyControl build(LayoutData... data) {
return build(new MyControl(this), data);
}
}
private HTMLInputElement inputEl;
public MyControl(Config config) {
super(config);
}
@Override
protected String valueFromSource() {
if (inputEl == null)
return null;
return inputEl.value;
}
@Override
protected void valueToSource(String value) {
if (inputEl != null)
inputEl.value = (value != null) ? value : "";
}
@Override
protected INodeProvider buildNode(Element el, Config data) {
return Wrap.$(el).$(root -> {
root.style(styles().component());
Input.$(root, "text").by("input")
.on(e -> modified(), UIEventType.ONKEYUP, UIEventType.ONPASTE);
}).build(dom -> {
inputEl = (HTMLInputElement) manageFocusEl(dom.first("input"));
});
}
/************************************************************************
* CSS.
************************************************************************/
@Override
protected ILocalCSS styles() {
return LocalCSS.instance();
}
public static interface ILocalCSS extends IControlCSS {
// Declare custom styles here.
}
@CssResource(value = {
IComponentCSS.COMPONENT_CSS,
IControlCSS.CONTROL_CSS
}, stylesheet = """
.component {
/* Control container styles */
}
""")
public static abstract class LocalCSS implements ILocalCSS {
private static LocalCSS STYLES;
public static ILocalCSS instance() {
if (STYLES == null) {
STYLES = (LocalCSS) GWT.create(LocalCSS.class);
STYLES.ensureInjected();
}
return STYLES;
}
}
}valueFromSource()Extract the current value from DOM or internal state. Called by the framework after modified() is invoked.
@Override
protected String valueFromSource() {
if (inputEl == null)
return null;
return StringSupport.safe(inputEl.value);
}valueToSource(V value)Apply a value to the DOM / internal state. Called when setValue() is invoked externally.
@Override
protected void valueToSource(String value) {
if (inputEl != null)
inputEl.value = StringSupport.safe(value);
}buildNode(Element el, Config data)Render the control's DOM. Note that controls use the two-argument version buildNode(Element el, Config data) where data is the configuration. This differs from SimpleComponent which uses buildNode(Element el).
For DOM-building detail — and the rules for re-rendering part of a control at runtime (a Control is
a Component, so use its buildInto/rerender(), never the static Wrap.buildInto, for interactive
content) — see the jui-dombuilder skill.
@Override
protected INodeProvider buildNode(Element el, Config data) {
return Wrap.$(el).$(root -> {
root.style(styles().component());
// Build control DOM
}).build(dom -> {
// Extract elements
});
}Call modified() from event handlers when user interaction changes the control's value:
Input.$(root, "text").by("input")
.on(e -> modified(), UIEventType.ONKEYUP, UIEventType.ONPASTE);This triggers valueFromSource(), updates the dirty state, and fires IModifiedListener events.
setValue(V) ──> valueToSource(V) ──> DOM updated
│
user interacts
│
event handler
│
modified()
│
valueFromSource() ──> value() returns new value
│
dirty detection (compare to reset value)
│
IModifiedListener.onModified() firedprepareValueForAssignment(V value)Normalise values before storage (e.g., map null to empty list):
@Override
protected List<String> prepareValueForAssignment(List<String> value) {
if (value == null)
return new ArrayList<>();
return value;
}boolean empty(V value)Define when the value is considered empty. Default checks for null, empty String, empty Collection.
@Override
protected boolean empty(MyValue value) {
if (value == null)
return true;
return value.items().isEmpty();
}V clone(V value)Clone the value when the framework needs an independent copy (for reset value, dirty comparison). Default assumes immutability; override for mutable value types.
@Override
protected FormattedText clone(FormattedText value) {
if (value == null)
return null;
return value.clone();
}boolean equals(V v1, V v2)Custom equality for dirty detection. Default uses object equality.
@Override
protected boolean equals(FormattedText v1, FormattedText v2) {
if (v1 == v2)
return true;
if ((v1 == null) || (v2 == null))
return false;
return v1.computeHash() == v2.computeHash();
}Register focusable elements using manageFocusEl():
.build(dom -> {
inputEl = (HTMLInputElement) manageFocusEl(dom.first("input"));
});The first registered element becomes the default focus target. The IComponentCSS.focus() style is automatically toggled.
For comprehensive styling guidance -- localised CSS, CSS variables, style packs, and creating custom styles -- see the jui-styles skill. The key differences from components:
ILocalCSS extends IControlCSS (not IComponentCSS)IControlCSS extends IComponentCSS and adds: invalid(), read_only(), waiting()@CssResource must include both IComponentCSS.COMPONENT_CSS and IControlCSS.CONTROL_CSSControls follow the same variant model as components: a variant is a named, reusable bundle of
style/configuration (declared as a Style interface in Config) applied repeatably to give the
control a particular look in a particular context. The template below shows the pattern; see the
jui-styles skill (the Variants section) for the full treatment, and prefer reusing variants from
the project's dedicated Variants class where one exists.
public class XXXControl extends Control<T, XXXControl.Config> {
public static Config.Style DEFAULT_STYLE = Config.Style.STANDARD;
public static class Config extends Control.Config<T, Config> {
public interface Style {
public ILocalCSS styles();
public static Style create(ILocalCSS styles) {
return () -> styles;
}
public static final Style STANDARD = Style.create(StandardLocalCSS.instance());
}
private Style style = (DEFAULT_STYLE != null) ? DEFAULT_STYLE : Style.STANDARD;
public Config style(Style style) {
if (style != null)
this.style = style;
return this;
}
@Override
@SuppressWarnings("unchecked")
public XXXControl build(LayoutData... data) {
return build(new XXXControl(this), data);
}
}
public XXXControl(XXXControl.Config config) {
super(config);
}
@Override
protected T valueFromSource() {
// Extract value from DOM or internal state.
return null;
}
@Override
protected void valueToSource(T value) {
// Apply value to DOM or internal state.
}
@Override
protected INodeProvider buildNode(Element el, Config data) {
return Wrap.$(el).$(root -> {
root.style(styles().component());
// Build control DOM and event handlers.
}).build(dom -> {
// Extract element references.
});
}
/************************************************************************
* CSS.
************************************************************************/
@Override
protected ILocalCSS styles() {
return config().style.styles();
}
public static interface ILocalCSS extends IControlCSS {
// Custom style methods.
}
@CssResource(value = {
IComponentCSS.COMPONENT_CSS,
IControlCSS.CONTROL_CSS
}, stylesheet = """
.component {
/* ... */
}
""")
public static abstract class StandardLocalCSS implements ILocalCSS {
private static StandardLocalCSS STYLES;
public static ILocalCSS instance() {
if (STYLES == null) {
STYLES = (StandardLocalCSS) GWT.create(StandardLocalCSS.class);
STYLES.ensureInjected();
}
return STYLES;
}
}
}public class XXXControlCreator {
public static XXXControl.Config create() {
return new XXXControl.Config();
}
public static XXXControl build(Consumer<XXXControl.Config> cfg, LayoutData... data) {
XXXControl.Config config = new XXXControl.Config();
if (cfg != null)
cfg.accept(config);
return config.build(data);
}
public static XXXControl $(ContainerBuilder<?> el, Consumer<XXXControl.Config> cfg) {
return With.$(build(cfg), cpt -> el.render(cpt));
}
}Controls can compose other components internally. Use Cpt.$(parent, child) to insert child components into the control's DOM:
@Override
protected INodeProvider buildNode(Element el, Config data) {
editor = new Editor(editorConfig);
EditorToolbar toolbar = new EditorToolbar(tbConfig);
editor.bind(toolbar);
return Wrap.$(el).$(root -> {
root.style(styles().component());
Cpt.$(root, toolbar);
Cpt.$(root, editor);
}).build();
}Controls integrate with ControlForm via name and acceptor configuration:
// In a ControlForm constructor:
group(grp -> {
grp.control("label", "Label text", Controls.text(cfg -> {
cfg.name("fieldName");
cfg.acceptor("fieldName");
cfg.placeholder("Enter value");
cfg.validator(
NotEmptyValidator.validator("required"),
LengthValidator.validator(0, 100, "max {max} characters")
);
}), cell -> {
cell.grow(1).required();
cell.from(v -> v.getLabel()); // populate from source
cell.to((ctx, v, cmd) -> cmd.label(v)); // apply to command (dirty only)
});
});Listen for value changes via configuration:
new MyControl.Config()
.modifiedHandler((ctl, val, prior) -> {
Logger.info("Changed from " + prior + " to " + val);
})
.build();For delayed handling (e.g., search-as-you-type):
.modifiedHandler(DelayedModifiedHandler.create(300, (ctl, val, prior) -> {
performSearch(val);
}));When creating a new control:
V the control manages (String, List, custom type, etc.)Control<V, Config> — create Config extends Control.Config<V, Config> with build() methodvalueFromSource() — extract value from DOM statevalueToSource(V) — apply value to DOM statebuildNode(Element, Config) — render control DOM with event handlersmodified() — from event handlers when user changes the valuemanageFocusEl() in .build() callbackILocalCSS extends IControlCSS, include both IComponentCSS.COMPONENT_CSS and IControlCSS.CONTROL_CSSclone(), equals(), empty() — for non-trivial value typesXXXControlCreator with build() and $() methods3639345
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.