Open Legioth opened 3 years ago
I like the idea, especially combined with use cases like e.g. https://cookbook.vaadin.com/ui-eventbus - I'm wondering about the naming of whileAttached
.. this whole concept sounds like a "lifecircle".. could this naming be used in this regard? Or is this name too broad?
I'm also not sure about the naming. I'm treating whileAttached
more as a discussion starter and less as a definite proposal.
I'm not sure about "lifecircle" either - it seems like a quite accurate description but it's on the other hand quite fluffy and seems to have some controversial associations outside the tech domain.
I'm not too fond about the naming whileAttached
either as even if it is semantically correct, I would not have not search for it since I have not seen it elsewhere. Maybe something that would be closer to the existing addAttachListener
could work.
@Tag("my-web-component")
public class MyWebcomponent extends Component {
public MyComponent() {
addUiAttachCallback(ui -> initializeClientSide());
}
}
I would like it to be somehow explicit that the code is related to being attached to a new UI instance, so it could be used to fix all the issues related to @PreserveOnRefresh
e.g. when the component instance stays the same but the initialization needs to happen again. But to follow the existing practice, it kind of should be an method you can override instead of a callback you add, but then again, probably the onAttach
+onDetach
methods suffer a bit from not being so easy to discover, as I see people use the listeners even inside the components (not intended this way). Or do everything inside the constructor.
Also, I don't like the way the detach callback is returned. Instead I would just add another API for it, or instead chain it explicitly like
public MessageTray extends Div {
public MessageTray() {
addUiAttachCallback(ui -> doStuff(ui, this))
.addUiDetachCallback(ui -> stopDoingStuff(ui, this)); // or withUiDetachCallback
}
}
but that is just personal preference, to me the API where you have to return something is not as it hides what the code is meant for. But the chaining is not really that much different from just explicitly calling Component::addUiDetachCallback
.
If the method name seems too long, it could maybe be onUiAttach(ui -> doStuff(ui, this));
but then it is again slightly different naming convention to what we have for similar things.
One big benefit to having another methods to override when used inside the component is the discoverability - I always see onAttach
and onDetach
for the specific logic related to attaching/detaching the component.
chaining is not really that much different from just explicitly calling
Component::addUiDetachCallback
.
There's a massive difference: state. If the thing you're initializing returns a registration object or if you pass in a listener instance that you also need to use for removing, then you have to do explicit bookkeeping (i.e. an instance field) to make that same value available also in the detach handler. If just want to listen to an attach event and optionally also a separate detach event, then you can just as well use current APIs. The primary reason for the suggested API is that it lets you directly carry over state between a specific attach event and the corresponding detach event.
One big benefit to having another methods to override when used inside the component is the discoverability
The drawback is that overridden methods are only available inside the component. There are also some cases when you want to hook on to the life cycle of another component (e.g. much of CollaborationEngine). If we offer a public
method, then it's available for all use cases. If we only offer a protected
method to override, then we are limiting the benefits to only a subset of the cases. There might be a case for offering both, but I'm not sure that would be worthwhile for a helper like this.
I see people use the listeners even inside the components (not intended this way).
I use it because it's less code to type for effectively the same end result (the difference being slightly more allocated memory from the callback instance and the entry in a listener list). The listener API can often be a semantically descriptive oneliner, whereas the overridden method carries multiple lines of pure boilerplate and whitespace. I'm not sure if I would even include onAttach
if I would design Flow today - I see it mostly as a relic from old versions of Vaadin that didn't have addAttachListener
.
An alternative way of reaching the same benefits would be to reuse the existing entry points (onAttach
and addAttachListener
), but supplement them to also support this particular use case.
Carrying over state from the attach event could be streamlined if AttachEvent
would have a method for registering a corresponding one-time detach listener. The detach callback could be in the form of a Registration
since the detach event isn't necessary when the same info is already in the attach event which remains in the closure.
Firing an initial event if the component is already attached would only be necessary for addAttachListener
but not for onAttach
. The most smooth way might be to return a custom Registration
subtype that would allow chaining in the extra instruction, e.g. addAttachListener(listener).fireNowIfAttached()
, but this would break binary compatibility. A safer alternative would be to introduce an overload that takes an enum signal that it should be fired immediately. A similar "eager" listener mode would also be useful with e.g. value change listeners.
Taken together, my initial example could then be expressed in this way:
someComponent.addAttachListener(event -> {
SomeType someInstance = registerSomething(someComponent, ui);
event.onNextDetach(() -> unregisterSomething(someInstance));
}, EventListenerMode.EAGER);
A case that uses a registration could be written as a oneliner, but it would be quite confusing since the attach action would be "inside" onNextAttach
which might give the impression that it happens only when detaching:
addAttachListener(event -> event.onNextDetach(registerForPush(this, event.getUI()));
Yet another commonly needed feature is related to cleanup. Adding a regular attach listener returns a registration that can be used to remove the attach listener. In this particular case where state needed for cleanup is captured inside the listener closure, it would also be necessary trigger the cleanup action on registration removal in case the component is currently attached.
This is how that aspect would work with the originally suggested API:
Registration registration = component.whileAttached(ui -> {
System.out.println("Registering");
return () -> System.out.println("Unregistering");
});
attachedComponent.add(component); // Prints "Registering"
registration.remove(); // Prints "Unregistering"
attachedComponent.remove(component); // Doesn't print anything
This unregistration behaviour wouldn't be natural for the alternative API suggestion based on event.onNextDetach
since there's no logical connection between the attach listener and some action performed through an event passed to it.
This way of looking at the registration does on the other hand lead towards another potential API design direction by seeing this functionality as a Registration
subclass rather than as a component feature:
Registration registration = new WhileAttachedRegistration(component, ui -> {
System.out.println("Registering");
return () -> System.out.println("Unregistering");
});
This approach sacrifices some discoverability but does instead remove the need of polluting the sacred Component
namespace.
I spotted yet another typical example where this would be useful: https://cookbook.vaadin.com/ui-eventbus
Isn't that over-engineered with the Dialog::setCloseOnEsc(true);
API? ;)
Workaround for the issue that pending changes in the TextArea won't be sent to the server with setCloseOnEsc(true)
.
It's a relatively common pattern to initialize some state when a component is attached and then do the reverse when the component is detached. One typical example of this is to register the UI for background updates to be pushed out. Another example is with using
executeJs
to initialize the client-side of a component but then wanting to cancel the registration on detach to avoid double init in case the component is attached and detached again during the same round trip.The most straightforward way of implementing this is by overriding
onAttach
andonDetach
.There's a whole bunch of boilerplate code associated with this technique, and the logic does also suffer from lack of locality since the concerns are spread out over two separate instance methods and one instance field.
Another alternative is to use
addAttachListener
andaddDetachListener
to keep all logic in one method. Furthermore,unregisterListener
can be used to create a one-time detach listener which in turn allows keeping the state local instead of relying on an instance field.There's still some boilerplate in this pattern, and it's also a pattern that isn't immediately obvious.
One additional challenge is in case the logic is outside the component itself. In this case, the same technique with
addAttachListener
can be used, but there's also the need for additional boilerplate to run the attach logic immediately in case the target component is already attached.As an alternative, there could be a helper method either as an instance method in
Component
or as a static helper inComponentUtil
that takes an attach handler that is supposed to return a matching detach handler. The attach handler immediately receives theUI
instance rather than an event object that needs unpacking. The helper would also take care of running the attach handler immediately in case the method is currently attached and it would finally also take care of running the detach handler if one is present in case the registration of the attach handler itself is removed.The method signature could thus look like this if implemented as an instance method:
Usage would be like this in the happy case when the internal logic is also based on
Registration
.In case some other way of unregistering is needed, then it might be necessary to create an inline
Registration
to return.In other cases, it might be enough to convert some other no-args callback into a
Registration
as a method reference by adding e.g.::close
to the end, e.g.return someClosableThing::close
.