CtrlK
BlogDocsLog inGet started
Tessl Logo

jui-components

Create JUI components — reusable UI building blocks used in GWT/JUI applications. Use this skill when the user asks to: create a new component, build a custom UI element, create a panel or widget, or asks about component patterns. Also trigger when the user mentions component, SimpleComponent, StateComponent, buildNode, renderer, DomBuilder, or asks how to create a reusable UI element.

69

Quality

85%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

JUI Components

Create components that follow the standard JUI patterns and conventions.

Component Hierarchy

IComponent (interface)
  └── Component<C extends Config>    -- base class, heavy lifting
        ├── SimpleComponent           -- no config needed
        │     └── StateComponent<V>   -- re-renders on state change
        └── Control<V, C>            -- form fields with value management

Choose the base class based on the component's needs:

Base classWhen to use
SimpleComponentMost components. No formal configuration needed.
Component<Config>Component with formal builder-pattern configuration.
StateComponent<V>Component that re-renders automatically on state changes.
Control<V, C>Interactive form field with value, dirty detection, validation. For controls, use the jui-controls skill instead.

Rendering Approaches

Build method (preferred)

Override buildNode(Element) and return an INodeProvider built via DomBuilder:

public class MyComponent extends SimpleComponent {

    @Override
    protected INodeProvider buildNode(Element el) {
        return Wrap.$(el).$(root -> {
            // Build DOM structure using DomBuilder.
        }).build();
    }
}

With element extraction via the .use(n -> {}) callback:

@Override
protected INodeProvider buildNode(Element el) {
    return Wrap.$(el).$(root -> {
        Div.$(root).style(styles().header()).use(n -> headerEl = (Element) n);
        Div.$(root).style(styles().body()).use(n -> bodyEl = (Element) n);
    }).build();
}

Or with element extraction via the .build(dom -> {...}) callback:

@Override
protected INodeProvider buildNode(Element el) {
    return Wrap.$(el).$(root -> {
        Div.$(root).style(styles().header()).by("header");
        Div.$(root).style(styles().body()).by("body");
    }).build(dom -> {
        headerEl = dom.first("header");
        bodyEl = dom.first("body");
    });
}

Constructor rendering (simple components)

For simple or inline components, supply the renderer in the constructor:

public class MyComponent extends SimpleComponent {

    public MyComponent(String title) {
        renderer(root -> {
            H3.$(root).text(title);
        });
    }
}

With extraction:

public MyComponent(String title) {
    renderer(root -> {
        H3.$(root).text(title).use(n -> titleEl = (Element)n);
    });
}

Or

public MyComponent(String title) {
    renderer(root -> {
        H3.$(root).text(title).by("title");
    }, dom -> {
        titleEl = dom.first("title");
    });
}

DomBuilder Essentials

For the full DomBuilder reference — and especially the rules for updating the DOM at runtime (when Wrap.buildInto is safe versus when you must use the component's own buildInto or rerender(), and state-driven StateComponent views) — see the jui-dombuilder skill. A summary follows.

Element creation

Each HTML element has a corresponding class with static $() methods:

Div.$(parent)           // <div>
Span.$(parent)          // <span>
H1.$(parent)            // <h1>  (also H2, H3, H4, H5, H6)
P.$(parent)             // <p>
A.$(parent)             // <a>
Button.$(parent)        // <button>
Input.$(parent, "text") // <input type="text">
Label.$(parent)         // <label>
Em.$(parent)            // <em>

Common builder methods

Div.$(parent)
    .style("cssClass1", "cssClass2")  // CSS classes
    .id("uniqueId")                    // HTML id attribute
    .attr("data-key", "value")         // arbitrary attribute
    .text("content")                   // text content
    .by("refName")                     // reference for extraction
    .css("margin-top: 1em;")           // inline style
    .css(CSS.WIDTH, Length.pct(100))    // typed inline style
    .testId("test-ref")                // test ID for automation
    .$(inner -> {                      // child builder lambda
        // Build children
    });

Event handling

// Click handler (most common)
Button.$(parent).text("Click me")
    .onclick(e -> handleClick());

// General event handler with event types
Div.$(parent)
    .on(e -> handleEvent(e), UIEventType.ONCLICK, UIEventType.ONMOUSEDOWN);

// Handler with access to the element node
Button.$(parent).text("Action")
    .on((e, n) -> handleWithNode(e, (Element) n), UIEventType.ONMOUSEDOWN);

// Prevent default / stop propagation
Button.$(parent).text("No focus steal")
    .on(e -> {
        e.stopEvent();
        doSomething();
    }, UIEventType.ONMOUSEDOWN);

Inserting child components

Components implementing IDomInsertable (all SimpleComponent subclasses) can be inserted:

// Using Cpt helper
Cpt.$(parent, myChildComponent);

// Using insert
parent.insert(myChildComponent);

// Using creator helper
ButtonCreator.$(parent, cfg -> {
    cfg.label("Click me");
    cfg.handler(cb -> { /* ... */ cb.complete(); });
});

Text nodes

Text.$(parent, "Some text content");
Text.nbsp();  // non-breaking space: \u00A0
Text.bull();  // bullet: \u2022

Conditional rendering

Use plain if statements (not .iff()) for conditional DOM:

Wrap.$(el).$(root -> {
    root.style(styles().component());
    if (showHeader) {
        Div.$(root).style(styles().header()).$(header -> {
            H3.$(header).text(title);
        });
    }
    Div.$(root).style(styles().body()).$(body -> {
        // Always rendered
    });
}).build();

Runtime DOM updates

Update the DOM without full re-render by manipulating extracted elements:

// Direct text update
DomSupport.innerText(titleEl, newTitle);

// Rebuild a STATIC section (no events, no child components).
Wrap.buildInto(bodyEl, el -> {
    P.$(el).text(newContent);
});

// Rebuild an INTERACTIVE section. Use the component's own buildInto (NOT Wrap.buildInto)
// so event handlers are registered and child components are adopted — otherwise the
// onclick below renders but never fires. See the jui-dombuilder skill for the full trap.
buildInto(bodyEl, el -> {
    A.$(el).text("Click").onclick(e -> handleClick());
});

Full re-render

When state changes substantially, re-render the entire component:

public void updateData(Data newData) {
    this.data = newData;
    rerender();
}

State-driven Components (StateComponent<V>)

StateComponent<V> is a SimpleComponent whose rendering is a pure function of an external state variable. Instead of calling rerender() yourself, you store the state in a StateVariable (IStateVariable<V>); the component listens to it and re-renders automatically whenever the state changes. Because the re-render goes through the normal renderer path, events and child components stay wired (the same guarantee as rerender()).

Choose StateComponent<V> over plain rerender() when a single piece of (possibly shared) state determines what is on screen.

public class CounterView extends StateComponent<ValueStateVariable<Integer>> {

    public CounterView() {
        super(new ValueStateVariable<Integer>(0));   // pass the state to the super constructor
        renderer(root -> {
            P.$(root).text("Counter: " + state().value());
            Button.$(root).text("+").onclick(e -> state().assign(state().value() + 1));
        });
    }
}

Access the state with state(). The component re-renders when the state emits a change.

One state, many views

The state lives outside the component, so several components can wrap the same state instance — mutating it re-renders all of them. This is the idiom for "multiple parts of the screen depend on one piece of data".

Mutation methods on the state (model–view)

Subclass StateVariable<V> and expose domain mutators that call modify(...). The state behaves like a model and the StateComponent like its view — mutating the model anywhere re-renders every view bound to it.

public static class Errors extends StateVariable<Errors> {
    private List<String> items = new ArrayList<>();
    public List<String> items()     { return items; }
    public void clear()             { modify(v -> v.items.clear()); }
    public void add(String message) { modify(v -> v.items.add(message)); }
}

public static class ErrorList extends StateComponent<Errors> {
    public ErrorList(Errors state) {
        super(state);
        renderer(root -> {
            if (state().items().isEmpty())
                return;
            Ul.$(root).$(list -> state().items().forEach(i -> Li.$(list).text(i)));
        });
    }
}

Calling errors.add("Bad input") re-renders every ErrorList bound to that errors. The component can also mutate via modify(Consumer<V>) (a convenience that delegates to the state).

Loading / error lifecycle

LifecycleStateVariable<V> adds loading and error states the renderer can interrogate, so a remote fetch drives a spinner → content transition through the same mechanism:

public static class MenuItems extends LifecycleStateVariable<MenuItems> {
    private List<String> items = new ArrayList<>();
    public List<String> items() { return items; }
    public void load() {
        loading();                         // renders the loading branch
        remoteLoad(result -> modify(v -> { // later: populate and re-render
            v.items.clear();
            v.items.addAll(result);
        }));
    }
}

// In the renderer: if (state().isLoading()) { /* spinner */ } else { /* list */ }

Navigation-awareness

If the StateComponent implements INavigationAware / INavigationAwareChild, state changes are blocked from re-rendering while the component is off-screen and replayed when navigated to — so a state-driven view inside a tab or card stays correct without re-asserting it in onNavigateTo.

Inline state component

StateComponentCreator.$(parent, state, (s, el) -> { … }) builds a state component inline, without a subclass.

Configuration Pattern

For components with formal configuration, extend Component<Config>:

public class MyComponent extends Component<MyComponent.Config> {

    public static class Config extends Component.Config {

        private String title;
        private boolean compact;

        public Config title(String title) {
            this.title = title;
            return this;
        }

        public Config compact(boolean compact) {
            this.compact = compact;
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public MyComponent build(LayoutData... data) {
            return (MyComponent) super.build(new MyComponent(this), data);
        }
    }

    public MyComponent(Config config) {
        super(config);
    }

    @Override
    protected INodeProvider buildNode(Element el) {
        return Wrap.$(el).$(root -> {
            root.style(styles().component());
            if (config().title != null)
                H3.$(root).text(config().title);
        }).build();
    }
}

Usage:

MyComponent cpt = new MyComponent.Config()
    .title("Hello")
    .compact(true)
    .build();

Record-based configuration (simple cases)

For components with a small fixed set of parameters:

public class MyComponent extends SimpleComponent {

    public static record Config(String title, String icon) {
        public static Config of(String title) {
            return new Config(title, null);
        }
    }

    public MyComponent(Config config) {
        renderer(root -> {
            H4.$(root).text(config.title());
        });
    }
}

Usage: Cpt.$(parent, new MyComponent(MyComponent.Config.of("Title")));

CSS / Styling

For comprehensive styling guidance -- localised CSS, CSS variables, style packs, and creating custom styles -- see the jui-styles skill. The key points for components:

  • ILocalCSS extends IComponentCSS
  • @CssResource must include IComponentCSS.COMPONENT_CSS
  • The .component class is applied automatically to the root element
  • Scope child styles under .component (e.g. .component .header)
  • Reference styles via styles().methodName() (names are obfuscated)
  • For variants, declare a Style interface in Config (the style-pack pattern). A variant is a named, reusable bundle of style/configuration applied repeatably to give the component a particular look in a particular context. See the jui-styles skill (the Variants section) for the full pattern, and prefer reusing variants from the project's dedicated Variants class where one exists.

Child Components

Injection during rendering

The simplest approach — insert child components into the DOM tree:

@Override
protected INodeProvider buildNode(Element el) {
    return Wrap.$(el).$(root -> {
        Div.$(root).style(styles().toolbar()).$(toolbar -> {
            Cpt.$(toolbar, new MyButton("Save", () -> save()));
        });
        Div.$(root).style(styles().content()).$(content -> {
            Cpt.$(content, childPanel);
        });
    }).build();
}

Attachment points (single component slot)

For a single component slot that can be assigned before or after rendering:

@Override
protected INodeProvider buildNode(Element el) {
    return Wrap.$(el).$(root -> {
        Div.$(root).apply(n -> registerAttachmentPoint("content", (Element) n));
    }).build();
}

public void setContent(IComponent cpt) {
    findAttachmentPoint("content").setComponent(cpt);
}

Region points (multiple components with layout)

For container-like behaviour with layout:

@Override
protected INodeProvider buildNode(Element el) {
    return Wrap.$(el).$(root -> {
        Div.$(root).apply(region("CONTENT", MinimalLayout.config().build()));
    }).build();
}

public void add(IComponent cpt) {
    findRegionPoint("CONTENT").add(cpt);
}

Lifecycle Hooks

MethodWhen called
onAfterRender()After initial render completes
onBeforeRender()Before render starts
onDispose()When component is disposed
onResize()When component is resized
@Override
protected void onAfterRender() {
    super.onAfterRender();
    // Post-render setup (e.g., add extra CSS classes to root)
    getRootEl().classList.add(extraStyles().scope());
}

@Override
protected void onDispose() {
    super.onDispose();
    // Clean up external listeners, timers, etc.
}

Focus and Blur

Register focusable elements during rendering:

.build(dom -> {
    Element focusEl = manageFocusEl(dom.first("input"));
});

The first registered focus element becomes the default. When the component gains/loses focus, IComponentCSS.focus() is automatically toggled.

Creator Classes

Convention for helper classes that simplify component creation:

public class MyComponentCreator {

    public static MyComponent.Config config() {
        return new MyComponent.Config();
    }

    public static MyComponent build(Consumer<MyComponent.Config> cfg, LayoutData... data) {
        return ComponentCreatorSupport.build(new MyComponent.Config(), cfg, null, data);
    }

    public static MyComponent $(ContainerBuilder<?> el, Consumer<MyComponent.Config> cfg) {
        return ComponentCreatorSupport.$(el, new MyComponent.Config(), cfg, null);
    }
}

Usage:

// Via creator
MyComponent cpt = MyComponentCreator.build(cfg -> {
    cfg.title("Hello");
});

// Into DOM builder
MyComponentCreator.$(parent, cfg -> {
    cfg.title("Hello");
});

Debugging

Set flags on Component (typically in the application entry point):

FlagEffect
Component.DEBUG_RENDER = trueLog every render/re-render to console
Component.DEBUG_OUTLINE = trueBorder around each component
Component.DEBUG_NAME = trueComponent name in component DOM attribute

Modal Dialogs

A component is frequently opened in a dialog (create/edit forms, confirmations, custom panels) via a static open(...) method backed by a shared IDialogOpener, implementing IProcessable (apply), IEditable (seed on open) and IResetable (clean baseline). That is a topic in its own right — use the jui-modals skill for the dialog-enabling pattern, create/update form pairs, ModalDialogCreator / ModalDialog / NotificationDialog, actions, and lifecycle. Build the component here; wrap and drive it there.

Behavioural Interfaces

InterfacePurpose
IEditable<V>Component can be loaded with data via edit(V)
IResetableComponent can be reset to initial state
IDirtableComponent reports dirty state
IProcessable<R>Component can process and return a result
IOpenAwareComponent is notified when opened (e.g. in a dialog)
ICloseAwareComponent is notified when closed
IActivateAwareComponent is notified when activated (e.g. tab selected)

Workflow

When creating a new component:

  1. Choose base classSimpleComponent for most cases, Component<Config> if formal configuration is needed, StateComponent if state-driven re-rendering is desired.
  2. Choose rendering approachbuildNode(Element) override (preferred) or renderer(...) in constructor (simple cases).
  3. Build DOM — Use DomBuilder classes (Div, Span, Button, etc.) with Wrap.$(el).$(...).build().
  4. Add styles — Create ILocalCSS / LocalCSS inner classes with @CssResource. Include IComponentCSS.COMPONENT_CSS.
  5. Extract elements — Use .by("ref") during build and dom.first("ref") in the .build() callback.
  6. Handle events — Use .onclick(), .on() etc. on element builders.
  7. Add child components — Use Cpt.$(parent, child) or parent.insert(child).
  8. Add lifecycle hooks — Override onAfterRender(), onDispose() etc. as needed.
  9. (Optional) Create creator classMyComponentCreator with build() and $() methods.
  10. (Optional) Add dialog support — Static open() method using ModalDialogCreator.
Repository
juiproject/jui-stack
Last updated
First committed

Is this your skill?

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.