Difference between revisions of "JavaScript widget controllers"

From PKP Wiki
Jump to: navigation, search
(Tabs)
(Notification: Gramatical correction???)
 
(14 intermediate revisions by 2 users not shown)
Line 27: Line 27:
 
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.
 
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.
+
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 changes the data presented on 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.
 
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.
 
  
 
== Forms ==
 
== Forms ==
  
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 standard form handler ($.pkp.controllers.form.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:
 
The form currently emits three public events which other widgets can subscribe to:
Line 40: Line 39:
 
* formInvalid: The same as the previous event for a form with invalid form values.
 
* formInvalid: The same as the previous event for a form with invalid form values.
 
* formSubmitted: Emitted after the form has been successfully submitted.
 
* formSubmitted: Emitted after the form has been successfully submitted.
 +
* formCanceled: Emitted after the form's cancel button was activated.
  
 +
Please use {include file="form/formButtons.tpl"} in your form for standard buttons.
 +
 +
We also have a variant of the standard FormHandler, called ClientFormHandler, that does not send form data to the server when submitted but rather keeps them on the client for other widgets to use. The ClientFormHandler will emit the same events as the FormHandler with the small difference that when the form is submitted, the form data will be sent along with the formSubmitted event as event data rather than submitting the form to the server. The ClientFormHandler is in the same namespace/package as the FormHandler.
  
 
== Tabs ==
 
== Tabs ==
Line 46: Line 49:
 
The tab handler ($.pkp.controllers.TabHandler) implements a tabbed interface, i.e. in the case of the Information Center, the three tabs "Notes", "Notify", and "History".
 
The tab handler ($.pkp.controllers.TabHandler) implements a tabbed interface, i.e. in the case of the Information Center, the three tabs "Notes", "Notify", and "History".
  
The default implementation of the handler tracks the current tab (getCurrentTab) and current tab index (getCurrentTabIndex).
+
The default implementation of the handler tracks the current tab (getCurrentTab) and current tab index (getCurrentTabIndex). It also provides default hooks for several tab events that can be used by subclasses.
 +
 
  
 
== Wizards ==
 
== Wizards ==
Line 64: Line 68:
 
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 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.
  
 +
 +
== Site Handler ==
 +
 +
This controller is attached to the body element and it is the first one into the widgets hierarchy. The handler represents the content of the entire site, which includes the header, side bars and footer. It listen to many events, related to notifications, redirection, sidebar and header refreshing, helper system, changes not saved warning and it also apply plugins to some elements.
 +
 
  
 
== Page Handler ==
 
== Page Handler ==
Line 96: Line 105:
  
 
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 [http://www.xdebug.org/ PHP debugger] on the server side and use FireBug on the client side to step through the code.
 
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 [http://www.xdebug.org/ PHP debugger] on the server side and use FireBug on the client side to step through the code.
 +
 +
Link actions emit two events which you can use, e.g. to trigger a throbber while the link action is active:
 +
* actionStart
 +
* actionStop
 +
 +
== Multilingual inputs ==
 +
 +
Multilingual-capable fields will show all available locale fields for a data at once when the first one is focused. The first one presents always the data into the user interface locale. As soon as no other localized field is focused, all the others will be hidden and just the first one is visible again.
 +
 +
To use it just add the multilingual=true parameter inside the fbv tag, inside the template file. The js controller will be automatically attached to the html and, if you are in a multilingual scenario, you will see the other fields as soon as you focus the first one.
 +
 +
The multilingual input also works with tiny mce editor.
 +
 +
See [[multilingual input]] user interface example.
 +
 +
== Autocomplete inputs ==
 +
 +
Inputs that will give users options while they are typing. It uses ajax requests to server to query the options that matches what users already typed.
 +
 +
To work with an autocomplete field you should tell the controller which operation should be used to query the suggestions, while user is typing. You define this inside the FBV tab, using the autocompleteUrl option. You can build the url using the smarty function url (see PKPTemplateManager::smartyUrl()) or you can build it into the php handler and pass it to the template manager in a variable.
 +
 +
The server operation should return a JSON message with an array containing all the values that should be presented. You must return an array with items containing another array with two keys: label and value. The label will only be used to visualization and the value will be the data that the form will send to the server on a form submit action. Eg.:
 +
array(array('label' => 'Label 1', 'value' => 1), array('label' => 'Label 2', 'value' => 2))
 +
 +
This widget supports two modes of operation:
 +
 +
'''Sycronized with user typing''' (DEFAULT)
 +
No matter if user used an option of the result JSON query or not, the controller will always update what user types into the visible field to the hidden field value. That way you can let your users entering different data other than the options that you pre selected to them. This mode will NOT support the label and value response distinction yet. It will always use the label value, that is, what is being shown into the field.
 +
 +
'''Restricted to a set of values'''
 +
Users will not be able to submit any value other than the ones returned by the query request. They will have to select one of those that appears from the query based on what they are typing. If no option appears or if one appears but users don't select it but just continues typing, then the hidden field will not be updated and no data will be submitted to the server on form submission. Use this when you want to restrict what users can choose as data for this input. To use this option, you have to add the option disableSync=true into the FBV method, inside the template.
 +
 +
You also have to define the name of the variable that will be posted to the server on form submit, so you can get the data the user has selected or typed. You can define this using the id option, inside the autocomplete FBV method, also inside the template. A common autocomplete FBV definition inside template should be:
 +
 +
{fbvElement type="autocomplete" autocompleteUrl=$autocompleteUrl id="dataNameToBePosted" label="any.locale.key" value='anyValueAlreadyStoredByThisField'}
 +
 +
== Notification ==
 +
 +
We have two different types of notification: in place and general. The notification controller is responsible for the in place ones. It will listen to the notifyUser event and it will request to the server all notifications that matches the ones defined on its options (notification type, assoc type, etc). You have to define the in place notification controller options on the php side, pass them to the template manager as a variable and use them when you add the in place notification template file to your template. See WorkflowHandler::production() and also templates/workflow/production.tpl to understand this process.
 +
 +
The notification controller will not listen to events that are comming from the DOM hierarchy, because it will never have a widget inside of it. It is meant to just listen to the notifyUser event that is always triggered by some parent widget. The widgets that really listen to the notifyUser events that comes from the other widgets are the ModalHandler, the PageHandler and the SiteHandler.
 +
Those 3 controllers will always use a notification helper to finde and decide the best notification controller to re-trigger the notifyUser event (see NotificationHelper.js inline documentation to better understand). If none is found, then the SiteHandler will present the general notification.
 +
 +
Despite the implementation seems a little odd, the usage is simple: if you want to present in place notifications, first you have to know what type of notifications you want to show. Then you build a notification options with those parameters (like the workflow example above) and add the inPlaceNotification.tpl into the template, placing it where you want to show the notification. You also have to make sure a notifyUser event will be triggered after the action you want to show the notification. The notifyUser event is already automatically triggered by the system when dataChanged event is returned as a response to the client.
 +
 +
So, for example, if you want to present a non-trivial notification after a user deleted an item inside a grid (trivial notifications will always be handled by the site handler when the notifyUser event comes inside a grid), you just have to build the parameters, place the notification template and make sure that the notification is created into the database when the user deletes the item. TBD: a better documentation about the notification system on the server side in a new page an link it here.
 +
 +
If there is a case where the system is not already triggering the notifyUser event, you can return it as a server response to the client side. But those cases are unlikely to happen.

Latest revision as of 19:17, 15 April 2013

We have got many standard JavaScript controllers that can be used unchanged or extended to provide functionality that is in line with our UI design language and elements.

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 a JavaScript model-view-controller (MVC) framework on the client side. What we call a "widget controller" is the controller-part of the MVC framework, not to be confused with other re-usable client-side code snippets (e.g. templates). There is a lot of client-side view code (templates, HTML) that is not "active" on the client side and therefore not managed by a client-side controller. These re-usable code snippets are not documented here because they are generally or low complexity and their potential for re-use is sometimes restricted. You'll have to look these templates up in the code and read their file-level documentation for further information.

What we document here are very well-defined active client-side widgets that consist of a controller (JavaScript objects), view (mark-up/template) and model (configuration, options) part. They are an important element of our user interface standard. The following paragraphs will give a short description of the use cases for which each widget is usually appropriate and they'll document less obvious parts of their public interface which cannot be easily seen from the code.

You'll find much more detailed information in the class-level and method-level documentation of the corresponding JavaScript files.

Widget documentation

Modals

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 changes the data presented on 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.

Forms

The standard form handler ($.pkp.controllers.form.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.
  • formCanceled: Emitted after the form's cancel button was activated.

Please use {include file="form/formButtons.tpl"} in your form for standard buttons.

We also have a variant of the standard FormHandler, called ClientFormHandler, that does not send form data to the server when submitted but rather keeps them on the client for other widgets to use. The ClientFormHandler will emit the same events as the FormHandler with the small difference that when the form is submitted, the form data will be sent along with the formSubmitted event as event data rather than submitting the form to the server. The ClientFormHandler is in the same namespace/package as the FormHandler.

Tabs

The tab handler ($.pkp.controllers.TabHandler) implements a tabbed interface, i.e. in the case of the Information Center, the three tabs "Notes", "Notify", and "History".

The default implementation of the handler tracks the current tab (getCurrentTab) and current tab index (getCurrentTabIndex). It also provides default hooks for several tab events that can be used by subclasses.


Wizards

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.


Site Handler

This controller is attached to the body element and it is the first one into the widgets hierarchy. The handler represents the content of the entire site, which includes the header, side bars and footer. It listen to many events, related to notifications, redirection, sidebar and header refreshing, helper system, changes not saved warning and it also apply plugins to some elements.


Page Handler

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.


Uploader

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).


Grids

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

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.

Link actions emit two events which you can use, e.g. to trigger a throbber while the link action is active:

  • actionStart
  • actionStop

Multilingual inputs

Multilingual-capable fields will show all available locale fields for a data at once when the first one is focused. The first one presents always the data into the user interface locale. As soon as no other localized field is focused, all the others will be hidden and just the first one is visible again.

To use it just add the multilingual=true parameter inside the fbv tag, inside the template file. The js controller will be automatically attached to the html and, if you are in a multilingual scenario, you will see the other fields as soon as you focus the first one.

The multilingual input also works with tiny mce editor.

See multilingual input user interface example.

Autocomplete inputs

Inputs that will give users options while they are typing. It uses ajax requests to server to query the options that matches what users already typed.

To work with an autocomplete field you should tell the controller which operation should be used to query the suggestions, while user is typing. You define this inside the FBV tab, using the autocompleteUrl option. You can build the url using the smarty function url (see PKPTemplateManager::smartyUrl()) or you can build it into the php handler and pass it to the template manager in a variable.

The server operation should return a JSON message with an array containing all the values that should be presented. You must return an array with items containing another array with two keys: label and value. The label will only be used to visualization and the value will be the data that the form will send to the server on a form submit action. Eg.:

array(array('label' => 'Label 1', 'value' => 1), array('label' => 'Label 2', 'value' => 2))

This widget supports two modes of operation:

Sycronized with user typing (DEFAULT) No matter if user used an option of the result JSON query or not, the controller will always update what user types into the visible field to the hidden field value. That way you can let your users entering different data other than the options that you pre selected to them. This mode will NOT support the label and value response distinction yet. It will always use the label value, that is, what is being shown into the field.

Restricted to a set of values Users will not be able to submit any value other than the ones returned by the query request. They will have to select one of those that appears from the query based on what they are typing. If no option appears or if one appears but users don't select it but just continues typing, then the hidden field will not be updated and no data will be submitted to the server on form submission. Use this when you want to restrict what users can choose as data for this input. To use this option, you have to add the option disableSync=true into the FBV method, inside the template.

You also have to define the name of the variable that will be posted to the server on form submit, so you can get the data the user has selected or typed. You can define this using the id option, inside the autocomplete FBV method, also inside the template. A common autocomplete FBV definition inside template should be:

{fbvElement type="autocomplete" autocompleteUrl=$autocompleteUrl id="dataNameToBePosted" label="any.locale.key" value='anyValueAlreadyStoredByThisField'}

Notification

We have two different types of notification: in place and general. The notification controller is responsible for the in place ones. It will listen to the notifyUser event and it will request to the server all notifications that matches the ones defined on its options (notification type, assoc type, etc). You have to define the in place notification controller options on the php side, pass them to the template manager as a variable and use them when you add the in place notification template file to your template. See WorkflowHandler::production() and also templates/workflow/production.tpl to understand this process.

The notification controller will not listen to events that are comming from the DOM hierarchy, because it will never have a widget inside of it. It is meant to just listen to the notifyUser event that is always triggered by some parent widget. The widgets that really listen to the notifyUser events that comes from the other widgets are the ModalHandler, the PageHandler and the SiteHandler. Those 3 controllers will always use a notification helper to finde and decide the best notification controller to re-trigger the notifyUser event (see NotificationHelper.js inline documentation to better understand). If none is found, then the SiteHandler will present the general notification.

Despite the implementation seems a little odd, the usage is simple: if you want to present in place notifications, first you have to know what type of notifications you want to show. Then you build a notification options with those parameters (like the workflow example above) and add the inPlaceNotification.tpl into the template, placing it where you want to show the notification. You also have to make sure a notifyUser event will be triggered after the action you want to show the notification. The notifyUser event is already automatically triggered by the system when dataChanged event is returned as a response to the client.

So, for example, if you want to present a non-trivial notification after a user deleted an item inside a grid (trivial notifications will always be handled by the site handler when the notifyUser event comes inside a grid), you just have to build the parameters, place the notification template and make sure that the notification is created into the database when the user deletes the item. TBD: a better documentation about the notification system on the server side in a new page an link it here.

If there is a case where the system is not already triggering the notifyUser event, you can return it as a server response to the client side. But those cases are unlikely to happen.