LivelyKernel / LivelyKernel/lively.next
(Again) Concrete architecture for signals/events/connect/bindings
- Dominant language
- JavaScript
- Stars
- 90
- Forks
- 20
- PR merge metrics
- No merged PRs in 30d
Description
**What would you like to achieve?**
This architecture (despite years of programming with it) is still not clear to me, and even if (as I suspect is the case) there is a clear architecture underneath, it isn't well-enough explained or articulated. I may not be the quickest or most acute study, but I believe I'm not the slowest, either, so if I'm having problems others will, too.
I just spent a couple of days debugging signals not being propagated from
```
$world.execCommand("open browser", {moduleName: "studio/inputs/slider.cp.js", packageName: "engageLively--galyleo-dashboard", codeEntity: "DoubleSliderModel"});
```
to
```
$world.execCommand("open browser", {moduleName: "studio/inputs/slider.cp.js", packageName: "engageLively--galyleo-dashboard", codeEntity: "DoubleSliderWithValuesModel"});
```
After _much_ experimentation, it turned out that the fix was to add the `rangeChanged` event to the `expose` list in `DoubleSliderModel`.
But this still left me with questions:
- In `DoubleSliderWithValuesModel`, using `model` in the binding for `rangeChanged` didn't work, but `target` did. Should I have used the model name and `model`? If so, what name should I use (since binding is by name, not object)
- As I understand `bindings`, the fields are:
-- `model`: model of the object generating the event
-- `target`: name of the object generating the event
-- `handler`: function to call to handle the event
Is this correct?
- Since `bindings` is a static property, how does one handle the case where there are multiple items which generate events which are added dynamically? A good example of this is a non-static list.
- Similarly, how does one dynamically connect to an event (e.g., list selection)?
**How are you trying to achieve that**
The inputs and helpers classes in https://github.com/engageLively/galyleo-dashboard/tree/main/studio try basically all of the above, as well as `connect` (and I don't understand that one well, either). They're something of a mess; in part, that's me, but in part it's also due to the fact that the architecture here is really unclear to me. Filters, for example, don't use the view/model architecture but use Morphs, and so they use `connect` a lot. See:
```
$world.execCommand("open browser", {moduleName: "studio/filters.cp.js", packageName: "engageLively--galyleo-dashboard", codeEntity: "BooleanFilterMorph"});
```
**Alternative solutions**
This is primarily a plea for documentation/examples/cookbook, and a _lot_ of those are already in Lively. What we (principally @merryman) did for Galyleo was to take the studio components from lively.ide and adapt them. This actually gives a pretty good start. Here is what I'd recommend:
- The Galyleo UI objects subclassed Lively UI components that themselves changed dramatically. That was the risk of being an early adopter, and all in it paid off. In future, people (including me) should build on a standard, well-documented toolkit, which will have components like
- - Lists
- - Buttons (push, toggle, radio)
- - Menus (OK, really a list of buttons, but still...)
- - Sliders (realistically, a minor UI element, that I overuse)
- - **_TEXT!!_** In of course multiple forms
- A _standard_, _well-documented_ way to react (to coin a phrase) to UI events. And it should be based on high-level, semantic events associated with each widget, not roll-it-from-the-ground up mechanisms. I get signals are very general; but they are also very basic. Frankly, my list interface ought to be at the level of new selection; button, an action on press; etc.
- One of the strengths of Lively is the flexibility of the low-level interface, so I'd recommend building on top of the existing mechanism, not replacing it. That way, when people want to dive deeper and do things that aren't easy with the high-level interface, they can.
Here's one idea:
Each UI element offers a set of _events_. An `event` is an object which offers the following methods:
- `subscribe(handler)`: A `handler` is an object with a `handleEvent(obj)` method. `handler.handleEvent(event)` gets called on each new event
- `unsubscribe(handler)`: remove `handler` from the list of handler objects for this event
- `subscribers`: a read-only property (no `set` method) for the subscriber list
- `notifySubscribers(obj)`: call `handler.handleEvent(obj)` for each handler on `subscribers`
The `obj` argument to `handleEvent` is event-specific; it captures data specific to the event. The existing `evt` construct, which we use for mouse events, might be re-used for this (in general, I'd like to add as little new as possible)
Implementing this on top of the existing `signal` architecture gives the following notional methods, based on a `LivelyEvent` class with the methods given above.
A simple example of this is given with the current `action` property of `ButtonModel`, and we have this method:
```
trigger () {
try {
signal(this.view, 'fire');
typeof this.action === 'function' && this.action();
} catch (err) {
const w = this.world();
if (w) w.logError(err);
else console.error(err);
}
}
```
(see
```
$world.execCommand("open browser", {moduleName: "buttons.js", packageName: "lively.components", codeEntity: "trigger"});
```
Except with events, `action` is replaced with a list of subscribers
```
init() {
...
this.events.action = LivelyEvent();
'''
}
trigger() {
signal(this.view, `fire`);
this.events.action.notifySubscribers(null); // no data to pass for a button press
}
```
The defensive code is gone. Checking if there's really an `action` is gone, because the event and its associated subscriber list was done on `init`, and logging an exception in the `handler.handleEvent` code is done in `notifySubscribers`
**Additional Resources**
The Galyleo code could be a guinea pig for this. I'd do most of the implementation but I'd like to work with @linusha and @merryman as well, since they understand what exists much better and also for figuring out how we could turn this into a reusable toolkit
**Version**: Please paste the lively.next commit on which the problem occurred here (use the copy button of the Version Checker in the bottom left corner).
130b0864109f7f2ba2930ca96821a30df8066c28
Contributor guide
Research direction
Read the examples named in studio/inputs/slider.cp.js, studio/filters.cp.js, and buttons.js, focusing on signal, bindings, connect, and the ButtonModel trigger method. Use the Galyleo components as the documented cases, and define done as a coherent explanation or cookbook covering propagation, binding fields, dynamic items, and event connections.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- documentation
- Issue type
- Documentation
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100