Jhernandez has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/181734

Change subject: Implement in View a declarative event map for DOM events
......................................................................

Implement in View a declarative event map for DOM events

Inspired by backbone.js

Advantages:

* Event handling definitions and code are defined in the same place, and
  handlers are defined at the root view level resulting in cleaner more
  comprehensible code overall
* Uses event delegation, thus being more efficient that individual manual
  events.

Previous discussion/patch: https://gerrit.wikimedia.org/r/#/c/180834/
Examples of views with this new feature:

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

Change-Id: I22f2e9e12e28542a5b136bfbf478a47fc657ef3e
---
M javascripts/common/View.js
M tests/javascripts/common/test_View.js
2 files changed, 166 insertions(+), 3 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Mantle 
refs/changes/34/181734/1

diff --git a/javascripts/common/View.js b/javascripts/common/View.js
index 1fcec8d..c7bcd01 100644
--- a/javascripts/common/View.js
+++ b/javascripts/common/View.js
@@ -1,6 +1,21 @@
 ( function( M, $ ) {
 
-       var EventEmitter = M.require( 'eventemitter' ), View;
+       var EventEmitter = M.require( 'eventemitter' ),
+               View,
+               // Cached regex to split keys for `delegate`.
+               delegateEventSplitter = /^(\S+)\s*(.*)$/,
+               idCounter = 0;
+
+       /**
+        * Generate a unique integer id (unique within the entire client 
session).
+        * Useful for temporary DOM ids.
+        * @param {String} prefix Prefix to be used when generating the id.
+        * @returns {String}
+        */
+       function uniqueId( prefix ) {
+               var id = ++idCounter + '';
+               return prefix ? prefix + id : id;
+       }
 
        /**
         * Should be extended using extend().
@@ -28,6 +43,31 @@
         *
         * append(), prepend(), before(), after() can be used to modify $el. 
on()
         * can be used to bind events.
+        *
+        * You can also use declarative DOM events binding by specifying an 
`events`
+        * map on the class. The keys will be 'event selector' and the value 
can be
+        * either the name of a method to call, or a function. All methods and
+        * functions will be executed on the context of the View.
+        *
+        * Inspired from Backbone.js
+        * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128
+        *
+        *     @example
+        *     <code>
+        *     var MyComponent = View.extend( {
+        *       events: {
+        *             'mousedown .title': 'edit',
+        *             'click .button': 'save',
+        *             'click .open': function(e) { ... }
+        *       },
+        *       edit: function ( ev ) {
+        *         //...
+        *       },
+        *       save: function ( ev ) {
+        *         //...
+        *       }
+        *     } );
+        *     </code>
         *
         * @class View
         * @extends EventEmitter
@@ -84,6 +124,11 @@
                defaults: {},
 
                /**
+                * Default events map
+                */
+               events: null,
+
+               /**
                 * Run once during construction to set up the View
                 * @method
                 * @param {Object} options Object passed to the constructor.
@@ -109,6 +154,10 @@
 
                        this.options = options;
                        this.render( options );
+
+                       // Assign a unique id for dom events binding/unbinding
+                       this.cid = uniqueId( 'view' );
+                       this.delegateEvents();
                },
 
                /**
@@ -154,9 +203,88 @@
                 * @param {string} query A jQuery CSS selector.
                 * @return {jQuery.Object} jQuery object containing results of 
the search.
                 */
-               $: function( query ) {
-                       return this.$el.find( query );
+               $: function(query) {
+                       return this.$el.find(query);
+               },
+
+               /**
+                * Set callbacks, where `this.events` is a hash of
+                *
+                * {"event selector": "callback"}
+                *
+                * {
+                *      'mousedown .title': 'edit',
+                *      'click .button': 'save',
+                *      'click .open': function(e) { ... }
+                * }
+                *
+                * pairs. Callbacks will be bound to the view, with `this` set 
properly.
+                * Uses event delegation for efficiency.
+                * Omitting the selector binds the event to `this.el`.
+                *
+                * @param {Object} events Optionally set this events instead of 
the ones on this.
+                * @returns {Object} this
+                */
+               delegateEvents: function ( events ) {
+                       var match, key, method;
+                       // Take either the events parameter or the this.events 
to process
+                       events = events || this.events;
+                       if ( events ) {
+                               // Remove current events before re-binding them
+                               this.undelegateEvents();
+                               for ( key in events ) {
+                                       method = events[ key ];
+                                       // If the method is a string name of 
this.method, get it
+                                       if ( !$.isFunction( method ) ) {
+                                               method = this[ events[ key ] ];
+                                       }
+                                       if ( method ) {
+                                               // Extract event and selector 
from the key
+                                               match = key.match( 
delegateEventSplitter );
+                                               this.delegate( match[ 1 ], 
match[ 2 ], $.proxy( method, this ) );
+                                       }
+                               }
+                       }
+               },
+
+               /**
+                * Add a single event listener to the view's element (or a 
child element
+                * using `selector`). This only works for delegate-able events: 
not `focus`,
+                * `blur`, and not `change`, `submit`, and `reset` in Internet 
Explorer.
+                *
+                * @param {String} eventName
+                * @param {String} selector
+                * @param {Function} listener
+                */
+               delegate: function ( eventName, selector, listener ) {
+                       this.$el.on( eventName + '.delegateEvents' + this.cid, 
selector,
+                               listener );
+               },
+
+               /**
+                * Clears all callbacks previously bound to the view by 
`delegateEvents`.
+                * You usually don't need to use this, but may wish to if you 
have multiple
+                * views attached to the same DOM element.
+                */
+               undelegateEvents: function () {
+                       if ( this.$el ) {
+                               this.$el.off( '.delegateEvents' + this.cid );
+                       }
+               },
+
+               /**
+                * A finer-grained `undelegateEvents` for removing a single 
delegated event.
+                * `selector` and `listener` are both optional.
+                *
+                * @param {String} eventName
+                * @param {String} selector
+                * @param {Function} listener
+                */
+               undelegate: function ( eventName, selector, listener ) {
+                       this.$el.off( eventName + '.delegateEvents' + this.cid, 
selector,
+                               listener );
                }
+
        } );
 
        $.each( [
diff --git a/tests/javascripts/common/test_View.js 
b/tests/javascripts/common/test_View.js
index c14d796..9721bfe 100644
--- a/tests/javascripts/common/test_View.js
+++ b/tests/javascripts/common/test_View.js
@@ -170,4 +170,39 @@
        assert.ok( spy.calledOnce, 'invoke postRender' );
 } );
 
+QUnit.test( 'View#delegateEvents', 3, function ( assert ) {
+
+       var view, EventsView = View.extend( {
+               template: mw.template.compile( '<p><span>test</span></p>', 
'xyz' ),
+               events: {
+                       'click p span': function ( ev ) {
+                               ev.preventDefault();
+                               assert.ok( true, 'Span was clicked and handled' 
);
+                       },
+                       'click p': 'onParagraphClick',
+                       'click': 'onClick'
+               },
+               onParagraphClick: function ( ev ) {
+                       ev.preventDefault();
+                       assert.ok( true, 'Paragraph was clicked and handled' );
+               },
+               onClick: function ( ev ) {
+                       ev.preventDefault();
+                       assert.ok( true, 'View was clicked and handled' );
+               }
+       } );
+
+       view = new EventsView();
+       view.appendTo( 'body' );
+       // Check if events are set and handlers called
+       view.$el.find( 'span' ).trigger( 'click' );
+       view.$el.find( 'p' ).trigger( 'click' );
+       view.$el.trigger( 'click' );
+       // Check if events can be unset and handlers are not called
+       view.undelegateEvents();
+       view.$el.find( 'span' ).trigger( 'click' );
+       view.$el.find( 'p' ).trigger( 'click' );
+       view.$el.trigger( 'click' );
+} );
+
 }( mw.mantle, jQuery) );

-- 
To view, visit https://gerrit.wikimedia.org/r/181734
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I22f2e9e12e28542a5b136bfbf478a47fc657ef3e
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Mantle
Gerrit-Branch: master
Gerrit-Owner: Jhernandez <jhernan...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to