Skip to content

Add helper for using signal effects with components #21629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Legioth opened this issue Jun 4, 2025 · 1 comment · Fixed by #21674
Closed

Add helper for using signal effects with components #21629

Legioth opened this issue Jun 4, 2025 · 1 comment · Fixed by #21674
Assignees
Labels

Comments

@Legioth
Copy link
Member

Legioth commented Jun 4, 2025

Describe your motivation

When configuring a component instance using a signal, you want the signal effect to be deactivated when the target component is detached to avoid leaking memory. For convenience, the effect should be enabled again if the component is attached again. There's also an opportunity for a limited set of helper methods for common cases, e.g. when a single signal value is bound directly to a single component setter or for formatting a string based on one or several signals.

Describe the solution you'd like

Base functionality for creating a generic effect that is active while a target component is attached.

ComponentEffect.effect(myComponent, () -> {
  Notification.show("Component is attached and signal value is " + someSignal.value());
});

Bind a single signal to a setter on the component

ComponentEffect.bind(mySpan, stringSignal, Span::setText);
ComponentEffect.bind(mySpan, stringSignal.map(value -> !value.isEmpty(), Span::setVisible);

Bind a formatted string based on 1..n signals.

ComponentEffect.format(mySpan, Span::setText, "The price of %s is %.2f", nameSignal, priceSignal);

All methods return a Registation that can be used to close the underlying effect and remove any attach/detach listeners.

Describe alternatives you've considered

We might want to eventually integrate this kind of functionality directly in the base Component class but that makes sense only after the feature flag is removed.

The ordering of arguments is inconsistent between the bind and the format overloads. This is on purpose: format takes a list of signals as varargs which forces that to be last whereas bind uses the regular "from, to" order of parameters. This ordering for bind does also keep the door open for adding something like bind(component, signal, converter, setter) in the future. In all cases, the component instance is an "implicit this" and does therefore always come first.

@Legioth
Copy link
Member Author

Legioth commented Jun 4, 2025

For reference, this is how I implemented this class in a prototype:

public class ComponentEffect {
    private final Runnable task;

    private boolean closed = false;
    private Runnable effectShutdown = null;

    private ComponentEffect(Component owner, Runnable task) {
        this.task = task;
        owner.addAttachListener(attach -> {
            enableEffect();

            owner.addDetachListener(detach -> {
                disableEffect();
                detach.unregisterListener();
            });
        });

        if (owner.isAttached()) {
            enableEffect();
        }
    }

    private void enableEffect() {
        if (closed) {
            return;
        }

        assert effectShutdown == null;
        effectShutdown = Signal.effect(task);
    }

    private void disableEffect() {
        if (effectShutdown != null) {
            effectShutdown.run();
            effectShutdown = null;
        }
    }

    public void close() {
        disableEffect();
        closed = true;
    }

    public static Registration effect(Component owner, Runnable task) {
        ComponentEffect effect = new ComponentEffect(owner, task);

        return effect::close;
    }

    public static <C extends Component, T> Registration bind(C owner,
            Signal<T> signal, BiConsumer<C, T> setter) {
        return effect(owner, () -> {
            setter.accept(owner, signal.value());
        });
    }

    public static <C extends Component> Registration format(C owner,
            BiConsumer<C, String> setter, String format, Signal<?>... signals) {
        return effect(owner, () -> {
            Object[] values = Stream.of(signals).map(Signal::value).toArray();
            setter.accept(owner, String.format(format, values));
        });
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Development

Successfully merging a pull request may close this issue.

2 participants