Skip to content
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

Routing events between handlers in the JS framework #2163

Closed
NateWr opened this issue Jan 9, 2017 · 82 comments
Closed

Routing events between handlers in the JS framework #2163

NateWr opened this issue Jan 9, 2017 · 82 comments
Assignees

Comments

@NateWr
Copy link
Contributor

NateWr commented Jan 9, 2017

This issue is a placeholder to track some work/experimentation to improve the way that our JS handlers can communicate between themselves. The suggested approach is to have a kind of global event router which is based on the observer pattern common in several JS frameworks.

I'd like to use this to document some of the lessons learned about how events in the JS Handlers (eg - UI "widgets) work.

Overview

As a general rule, events that are executed on a Handler do not bubble up like normal JS events. They're captured in an effort to ensure that Handlers remain encapsulated.

However, in some cases we have needed Handlers to talk to each other. This is done by white-listing certain events to be "published", using the publishEvent, trigger and triggerPublicEvent_ methods. These published events are triggered on the Handler's parent element in the DOM, from which they can bubble up.

This event is captured by a parent DOM element which has a handler attached, typically a complex Handler like a grid or the SiteHandler.

Because modals exist outside the normal DOM hierarchy, bubbling doesn't really get the job done. To accomodate this, there's a eventBridge option which can be passed to Handlers. This is used by modals to redirect events to the LinkAction which opened the modal.

The notifyUser event also gets it's own routing. Several handlers deliberately bubble the event up to the SiteHandler, which then makes a request to the server to decide what notifications to display.

So we have two ways for Handlers which are separate in the DOM (ie - not nested) to communicate with each other: an event that bubbles to SiteHandler and is then re-routed by SiteHandler, or an event bridge that reaches out to another element with a jQuery selector.

The maintenance problem

This architecture has worked to keep the Handlers highly re-usable. But it means that in cases where we do need Handlers to interact, including in cases where bubbling may not be sufficient, we have written a lot of code to reach out from one Handler to another.

The result has been that the connections between coupled Handlers tend to be ad-hoc, written into whatever method of the Handler happens to be doing that job right then. As a result, it's hard to have a clear sense of the dependencies between Handlers where they do exist, and to guard against breaking these when refactoring.

The (first) proposed solution

I looked into addressing this with the observer pattern, something I was familiar with from the Backbone.js framework. It would also give us an opportunity to start piecemeal refactoring towards a more common approach to JS-heavy applications.

The blocking problem with implementing the observer pattern

The observer pattern can’t be implemented because we lack a central registry of handlers. A handler which updates its content will remove large chunks of the DOM without tracking which handlers were caught up in the destruction. We will have references to listeners that are no longer present in the DOM (the lapsed listener problem).

The usual way to resolve this is with a central registry of handlers which deregister their events when removed (the dispose pattern). But because a handler doesn’t know what handlers are caught up in it’s refresh operations, it can’t trigger deregistration on embedded handlers (eg - a LinkActionHandler that's part of a GridHandler).

A new proposal

Without a significant refactor, we can’t have truly decoupled handlers. Instead, I’d suggest using a modified observer pattern in which the relationships between events and listeners are stored explicitly in the observer’s code. So handlers would not subscribe to events on the observer. Instead, the observer would have explicit instructions for passing specific events to specific handlers.
The typical observer pattern looks like this:

Sender.init() {
	Observer.trigger(‘changeEvent’);
}
Listener.init() {
	Observer.listenTo(‘changeEvent’, Listener.respondToChangeEvent);
}

The Observer can be completely ignorant of the events that are passing through it. The modified observer pattern I’m proposing would nest lookup logic in the Observer.

Sender.init() {
	Observer.trigger(‘changeEvent’, eventData);
}
Listener.init() {
	this.bind(‘changeEvent’, Listener.respondToChangeEvent);
}
Observer.trigger(event, eventData) {
	if (event === ‘changeEvent’) {
		$(‘.listener’).trigger(‘changeEvent’, eventData);
	}
}

The Observer would not store references to handlers. Instead, it would look up any specific handlers by their associated DOM elements, performing a fresh lookup each time an event was triggered.
This would provide decoupling of a sort, though the coupling would still have to be specified in a central Observer. It’s only marginally better than the current practice of triggering an event from one handler on another handler’s DOM element directly.

But is it really an improvement?

Relying on DOM lookups is not something I feel good about, to be honest. All this pattern really does is restructure the hacky DOM-based lookups away from the Handler into a central location (example). I think it will help (a little) with the maintenance issue by exposing these relationships more clearly. But the same could be done in theory with the existing eventBridge technique.

I don't see a way around these DOM lookup approaches without a pretty significant refactor of our Handlers. I've been wracking my brain but I haven't come up with anything.

One very simple refactor that I have made which I think is genuinely useful is that, on the PHP side, the JSONMessage object now supports multiple events. So you can register more than one event to return in a JSON response. Example: when publishing an issue you can return the dataChanged event that the grid is programmed to respond to, as well as a issuePublished event that can be broadcast separately.

The global event router, combined with multiple events in a JSONMessage, could be a good pattern to adopt more broadly (triggering notifications, tasks, etc from the PHP side). But I'm not sure it gets us very far down the road I originally had in mind of an event-driven UI with stronger decoupling of Handlers.

Examples

pkp-lib: https://github.com/NateWr/pkp-lib/commits/i2163_event_router
ojs: https://github.com/NateWr/ojs/tree/i2163_event_router

@NateWr NateWr self-assigned this Jan 9, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 9, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 9, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Jan 9, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Jan 9, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 9, 2017
This option was related to the old jQueryUI dialogs.
NateWr added a commit to NateWr/ojs that referenced this issue Jan 11, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Jan 11, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 11, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 11, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 11, 2017
This option was related to the old jQueryUI dialogs.
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 11, 2017
Previously, a user tab handler performed a partial tab refresh
to refresh the user grid when a user merge occurred. This
causes problems for the handler event deregistration process
I'm working on implementing. This commit refactors away from
that approach in favor of a modal approach. It also fixed
pkp#2171 in the process.
NateWr added a commit to NateWr/ojs that referenced this issue Jan 11, 2017
See comment in pkp-lib commit 6ed3c58bb6aaf0030fe1f7883b4e98ca9fee325f
@asmecher
Copy link
Member

Thanks, @NateWr, good reading.

Do you remember when you first started working with the PKP JS code that you proposed a change to the handler lifecycle? IIRC you wanted static calling or something. Would that be a useful thing to revisit in mangling an observer pattern into our DOM usage?

@NateWr
Copy link
Contributor Author

NateWr commented Jan 12, 2017

I dug up my old doc on that when I was looking into this. I'm not sure it would solve the problems we're facing in this case, although I think questions about how we initialize components will keep coming up if we start to introduce parts of an existing framework.

I've come up with an ad-hoc way to get a sort-of registry of handlers. Basically, we have every handler crawl up the DOM hierarchy and register itself with the first handler it finds. So each handler gets a reference to handlers attached to elements within the DOM.

This works because in the majority of cases we do full-handler refreshes all at once. So when refreshing, a handler can loop through it's child handlers and de-register event listeners.

There are a few cases where we do partial refreshes and I'm working on those at the moment. Some I'm refactoring away. Others (like replacing single grid elements) I'll probably write something special to handle those cases. But it's a pretty small group of cases (maybe half a dozen) which I've been able to identify.

I'm hoping this will be pretty reliable, though I'll have to see once it's in place if we end up with memory leaks.

@asmecher
Copy link
Member

@NateWr, thanks, that sounds like a decent short-term plan.

NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
…lers.

This commit implements a pattern for binding global events
on handlers. Each handler registers itself with any parent
handlers, so that when a handler is refreshed, event
listeners bound to any child handlers are also
de-registered.

It does this by routing HTML replace functions through
wrappers attached to the JS Handler object, which unbind
listeners before performing the HTML replacement.
~
This refactor is still partial. The information center
, tab handlers and modal handlers have been updated, but
instances of HTML replacement that doesn't properly
unbind events still exist. These will be refactored in
subsequent commits.
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
This option was related to the old jQueryUI dialogs.
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
Previously, a user tab handler performed a partial tab refresh
to refresh the user grid when a user merge occurred. This
causes problems for the handler event deregistration process
I'm working on implementing. This commit refactors away from
that approach in favor of a modal approach. It also fixed
pkp#2171 in the process.
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
…lers.

This commit implements a pattern for binding global events
on handlers. Each handler registers itself with any parent
handlers, so that when a handler is refreshed, event
listeners bound to any child handlers are also
de-registered.

It does this by routing HTML replace functions through
wrappers attached to the JS Handler object, which unbind
listeners before performing the HTML replacement.
~
This refactor is still partial. The information center
, tab handlers and modal handlers have been updated, but
instances of HTML replacement that doesn't properly
unbind events still exist. These will be refactored in
subsequent commits.
NateWr added a commit to NateWr/ojs that referenced this issue Jan 18, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Jan 18, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Jan 18, 2017
See comment in pkp-lib commit 6ed3c58bb6aaf0030fe1f7883b4e98ca9fee325f
NateWr added a commit to NateWr/pkp-lib that referenced this issue Jan 18, 2017
NateWr pushed a commit to NateWr/pkp-lib that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/omp that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/omp that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
- Moves coverImage props into OJS service classes.
- Delivers full URLs to cover images.
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
NateWr added a commit to NateWr/ojs that referenced this issue Oct 19, 2017
@NateWr
Copy link
Contributor Author

NateWr commented Oct 19, 2017

@kaschioudi I think I've worked out the test issues. Do you have time to code review the PRs in this comment? #2163 (comment)

@kaschioudi
Copy link
Contributor

@NateWr Everything looks fine to me 👍

NateWr added a commit to pkp/ui-library that referenced this issue Oct 19, 2017
pkp/pkp-lib#2163 Integrate with API changes from entity property serv…
NateWr added a commit that referenced this issue Oct 19, 2017
#2163 Implement entity property list for service classes
NateWr added a commit to pkp/ojs that referenced this issue Oct 19, 2017
pkp/pkp-lib#2163 Implement entity property list for service classes
NateWr added a commit to pkp/omp that referenced this issue Oct 19, 2017
pkp/pkp-lib#2163 Implement entity property list for service classes
@NateWr
Copy link
Contributor Author

NateWr commented Oct 19, 2017

🎉 Really happy to have this merged in. Ready to start build more API stuff. :)

NateWr added a commit to NateWr/omp that referenced this issue Oct 23, 2017
asmecher added a commit to pkp/omp that referenced this issue Oct 23, 2017
@asmecher
Copy link
Member

@NateWr and @kaschioudi, is this one ready to be closed now that the OMP warning fix has been merged?

@NateWr
Copy link
Contributor Author

NateWr commented Oct 23, 2017

💥

@NateWr NateWr closed this as completed Oct 23, 2017
orazionelson pushed a commit to ojs-omp-dev-ita/pkp-lib-locale-it that referenced this issue Jun 14, 2018
…ents.

ReviewRound and ReviewAssigment objects get defined statuses. This commit
extends those statuses, makes sure they're recalculated at the appropriate
times, and moves that calculation into the objects instead of grids.

IN the process, it removes the all-reviews-are-in task. (This will get
surfaced as a review round status in the submissions list.) It also
removes the now obsolete replaced state for ReviewAssignments.

Whenever a ReviewAssignment is updated, the ReviewRound status should get
updated automatically. So the _updateReviewRoundStatus() helper method in
PKPReviewGridHandler has been removed. (A similarly named method in
EditorDecisions is still in play, as it manually overrides the status when
an EditorDecision is invoked.)

Also fixes a couple ofbugs with the reviewer grid and round status not
updating in response to actions.
JackBlackLight pushed a commit to cul/ojs-plugin-doi that referenced this issue Sep 15, 2021
JackBlackLight pushed a commit to cul/ojs-plugin-doi that referenced this issue Sep 15, 2021
pkp/pkp-lib#2163 Implement entity property list for service classes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants