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