I've cleaned and completed the explanations, formatted and posted it at https://www.mediawiki.org/wiki/Mobile_web/Coding_conventions/JavaScript/Views

I'd appreciate feedback / help. I plan to slowly complete that page with other sections, since lots of MF javascript is views and currently there is no docs or good practices around them.

On Wed, Dec 31, 2014 at 3:32 PM, Florian Schmidt <florian.schmidt.welzow@t-online.de> wrote:
Hello,

great work, thanks :)

I think the good practices and conventions section should be in our Coding Conventions[1], what do you think?

[1] https://www.mediawiki.org/wiki/Mobile_web/Coding_conventions

Grüße / Kind regards,
Florian

Von: mobile-l-bounces@lists.wikimedia.org [mailto:mobile-l-bounces@lists.wikimedia.org] Im Auftrag von Joaquin Oltra Hernandez
Gesendet: Mittwoch, 31. Dezember 2014 11:03
An: mobile-l
Cc: mobile-tech
Betreff: [WikimediaMobile] Declarative DOM event delegation in JS views

I'm happy to announce that we've landed on Mantle and MobileFrontend a new way
to specify DOM events on a javascript view that I think will make our views
better in several ways.

TLDR:
  * Backbone style declarative delegated DOM events handling is in Mantle's
    View class. Use it when possible.

TOC:
  * Why. Current approach to binding events
  * A different proven approach, delegated declarative DOM events map
  * Examples of this in MobileFrontend
  * Good practices and conventions
  * Gotchas
  * Next steps

Why. Current approach to binding events
---------------------------------------

Currently on JS views, when we want to bind some events to the view, we bind
them on the `postRender` method, like so:

    var CounterView = View.extend({
      // ...
      postRender: function() {
        // View display logic
        this.$('.increment').click(function(ev) {
          // ...
        })
        this.$('.reset').click(function(ev) {
          // ...
        })
      },
      // ...
    })

With the current approach, a few bad side effects follow:

* Everytime the view renders, we bind events (multiple of them), this is
  inefficient, there are better ways.
* The postRender method on views grows a lot in size, event handling
  declarations and method code is mixed with display logic.
* The complexity of the view (arguably) increases, and with it the ability to
  read and modify the source confidently.

A different proven approach, delegated declarative dom events map
-----------------------------------------------------------------

Inspired and brought from Backbone.js, we have ported their dom events map,
which brings a way of specifying event and selector, and a handler that will be
bound to the view, declaratively on the view itself, the same way we specify
defaults, for example.

http://backbonejs.org/#View-extend
http://backbonejs.org/#View-delegateEvents

The previous example, with this approach with our views would be:

    var CounterView = View.extend({
      // ...
      events: {
        'click .increment': 'increment',
        'click .reset': 'reset',
      },
      postRender: function() {
        // View display logic
      },
      increment: function() {
        this.counter++;
      },
      reset: function() {
        this.counter=0;
      },
      // ...
    })

On View, we can now specify a `events` entry, which is a map of

  `event selector`: handler

Where event is any valid jQuery event, selector is any valid jQuery selector,
and handler is either a function, or a string (name of a method on the view).

With this approach:

  * Events are bound with delegate, effectively attaching one DOM listener on
    the view's root DOM element. (Read http://api.jquery.com/on/#direct-and-delegated-events)
  * To see which DOM events the view binds and reacts to, just need to find the
    events map of the view.
  * Event handling code (usually pieces of logic of the view) gets a name, and
    it's encapsulated on its own method giving us:
    * Reuse of such logic/functionality
    * Easier and better testability
    * Cleaner postRender methods

Internally, this is just using jQuery to delegate the events and the selectors
to specified handlers bound to the view, no black magic.

Examples of this in MobileFrontend
----------------------------------

https://gerrit.wikimedia.org/r/#/c/180835/
https://gerrit.wikimedia.org/r/#/c/180836/
https://gerrit.wikimedia.org/r/#/c/180837/

Notice that the code is much less nested and comes back to View level.
In some cases the postRender method becomes unnecessary, and is deleted, and
the code that was on it, has now a name and is properly encapsulated on the
view.

Good practices and conventions
------------------------------

When creating event handlers, if they are doing DOM stuff, like preventDefault,
or extracting data from the DOM node, manipulating the DOM, etc, you should
name the handler `onEvent`, for example `onThanksClick` or `onHeaderToggle`.

If possible, extract functionality that makes sense independently into methods
with a good name and call them from the handlers as needed.

When creating methods on a view, like the `increment` example above, that
happen to be called when an event happens but make sense independently, I would
argue that a method name `increment` makes more sense than a handler name
`onIncrementClick`, but that is open to common sense and taste I guess.

If I had to also extract what to increment from a DOM element, and then
increment, then I would do both, for example:

    // ...
      events: {
        'click .increment': 'onIncrementClick'
      },
      onIncrementClick: function(ev) {
        var amount = parseFloat(this.$('.amount').val());
        this.increment(amount);
      },
      increment: function(amount) {
        this.counter += amount || 1;
      },
    // ...

Gotchas
-------

Currently, a common practice is inside a event handler to access the DOM
element that initiated the event doing `$(this)`. With the events map the
handlers/methods are at view level and are consistently bound to the view's
this, so how do we access the DOM element of the event?

Handler methods get an jQuery.Event as a parameter, so:

    onIncrementClick: function(ev) {
      var $incButton = $(ev.target);
      // ...

http://api.jquery.com/category/events/event-object/

Next steps
----------

If there is any doubts or concerns, please ask them.

If you find going along the code you spot a View that could benefit of this
approach, open a Phabricator task (prefixed with "Hygiene" and in the
MobileWeb team) and take on it ifyou want.

I think this gives us a good opportunity to get more view tests, and make the
code cleaner, more understandable and approachable for both us and possible
contributors (Backbone style views are known by everybody at this point).