Please make sure that you think twice before you introduce a non-standard widget controller. Often slight deviations in the UI specification from one of the standard handlers are not intended. Restricting elements to one of the standard widgets will considerably reduce our initial development effort as well as future maintenance.
What is a widget controller?
We implement two basic types of modals:
- simple confirmation modals ($.pkp.controllers.modal.ConfirmationModalHandler)
- AJAX-driven modals ($.pkp.controllers.modal.AjaxModalHandler)
The AJAX-driven modal exists as a wizard modal variant ($.pkp.controllers.modal.WizardModalHandler) which provides default bindings for wizard events (see Wizards below).
We typically use modals for static confirmation messages, to manage grid data, to show forms in general and wizard-type workflows where these are subordinated to some parent page.
Our modals are based on the jQueryUI dialog widget. Unfortunately jQueryUI is not easy to extend because it mixes view and controller elements. We have to go to some lengths to integrate jQueryUI into our more extensible framework. Most importantly jQueryUI places modals outside of the DOM which breaks UI encapsulation rules and means that events do not correctly bubble up to the calling context of the modal. jQueryUI also introduces mark-up dynamically on initialization of the dialog which makes customization of dialogs difficult without breaking the MVC pattern ourselves.
In order to being able to use the modal without creating too close of a coupling to other unrelated widgets, we introduced an "event bridge" that effectively forwards well-defined events to the calling context. It would badly break encapsulation if a modal would update a grid directly, for example. The modal would have to "know" about specific grids which would drastically reduce it's potential for re-use.
The event bridge resolves reduces the necessary coupling. If, for example, you open the modal from a link action in a grid then modal events can be forwarded via the event bridge to the link action and thereby bubble into the grid which makes it possible for the grid to react to events generated by the modal. If the modal updates an element of the grid, then a grid refresh event can be forwarded to the grid so that the grid "knows" it has to update itself. This is a much looser coupling of modal and grid than if the modal had to update the grid itself.
For performance reasons we cannot forward all events triggered inside the modal, though. This means that if you need an event generated inside the modal in the calling context then you have to "publish" it. While this still creates a certain amount of unwanted "upwards" coupling between the modal, it seems an acceptable compromise between performance and maintenance requirements. Coupling is kept to a minimum so that in practice we can maintain very broad re-usability of our modal widget. You'll have to remember, though, to publish your events if you want them to be forwarded. Please have a look at the ModalHandler object's constructor for examples (page redirection, grid refresh, ...) how to do that.
The standard form handler ($.pkp.controllers.FormHandler) should be bound to all forms, either unchanged or extended by a custom form class (e.g. $.pkp.controllers.files.form.FileUploadFormHandler). It provides automatic form validation and event binding and therefore reduces the probability for validation glitches or inconsistent form user experience.
The form currently emits three public events which other widgets can subscribe to:
- formValid: This event will be generated whenever the form is checked and considered valid for submission. Forms are usually validated after every keyup or focusout event on one of the form elements.
- formInvalid: The same as the previous event for a form with invalid form values.
- formSubmitted: Emitted after the form has been successfully submitted.
The standard wizard handler ($.pkp.controllers.WizardHandler) provides a framework to implement wizards with a consistent user experience throughout the application. The wizard handler also deals with the details of wizard navigation.
The wizard handler emits events that other widgets within the wizard or outside of the wizard can subscribe to for their own purposes:
- wizardAdvance: Triggered when the wizard advances to the next step.
- wizardClose: Triggered when wizard advance is requested and the last step was reached.
- wizardCancel: Triggered when the wizard's "cancel" button is clicked. The default implementation triggers a wizardClose event (see above).
The wizard also lets you do validation prior to canceling or advancing. If you want to do so then subscribe to the following events:
- wizardAdvanceRequested: Triggered when the continue button is clicked.
- wizardCancelRequested: Triggered when the cancel button is clicked.
If you call event.preventDefault() on one of these events, the original action (wizard advance or wizard cancellation) will not be executed.
The default implementation of the wizard does not do any validation checks. It provides default event handlers that automatically advance the wizard to the next step when the user clicks the "continue" button and closes the wizard after the last step.
The page handler is bound to the main content element of the page, typically a div with the class pkp_structure_main_contentPanel. The handler represents the content of the page that changes as users navigate the site (as opposed to the header and sidebars which typically stay static as you navigate the site).
This handler currently listens to only one event:
- redirectRequested: Redirects the user to the URL specified as the first parameter.
This provides a multi-file uploader that can be used inside forms. It wraps the jQuery plupload plug-in. For more documentation about events and methods to extend or use that class please read the documentation there.
You can find an example of the uploader in the file upload wizard used in most of our file grids (e.g. during the submission process).
The grid handler manages the client-side of our default grid implementation (see GridHandler.inc.php for the server side implementation). The grid handler's most prominent function is to refresh the grid upon request from the client-side (e.g. after an element of the grid has been added, edited or deleted on the server side) via AJAX without having to reload the whole page.
The grid subscribes to an "elementsChanged" event which is usually generated on the server side via the JSON::setEvent() method upon addition, editing or deletion of rows and then triggered on the client side. If the grid catches such an event it will update a single grid row or the whole grid depending on the event data. Please see the method documentation of the GridHandler::elementsChanged() method for more information and search for this method in server side code to see examples how it is being used. Events sent via JSON::setEvent() will be triggered automatically when received on the client side (see Handler::handleJson() in Handler.js).
Link actions are active buttons that trigger some dynamic action on the client side (e.g. follow a dynamically changing link, open up a form or wizard modal, open up a confirmation dialog, etc.). Link actions are most often used in grids as grid actions, row actions or cell actions. You'll find many examples of such actions and how they are configured on the server side in our different grid implementations. Watch out for "new LinkAction(...)" code snippets there.
Link actions themselves are not specialized. They need to be configured with a "link action request" that tells the link action what to do when it is being clicked or otherwise activated. Link actions are usually configured on the server side rather than on the client side. Please look for PHP classes extending the LinkActionRequest PHP class for all link action types currently available. If you search for "new SomeLinkActionRequest(...)" in the code you'll find examples of their use.
While the day-to-day use of link actions is very simple, it's back-end implementation is relatively abstract and complex. This was necessary so that we can accommodate a broad reach of different grid action types without having to create specialized grid implementations depending on different grid actions which would cause us a lot of extra effort in our day-to-day development work. We therefore chose to err on the complexity of the back-end code rather than increasing the amount of work to use link actions.
The downside of this flexibility is that you have to understand quite a bit of abstract code to introduce new link actions if you cannot use one of the existing action types. The easiest way to do so is to configure some PHP debugger on the server side and use FireBug on the client side to step through the code.