I'm trying to implement a little MVC framework and now I'm implementing the view-model binder, I mean, when the model changes, trigger a refresh/render/whatever on the model. So, I need an event listener on a object:
model.on("customEvent",appendItem);
$("#button").on("click",function(){
model.add(item);
model.trigger("customEvent");
});
function appendItem(item) {
$("#content").append(item.toHTML());
}
So how can I create my event listener on objects?
If you are already using jQuery, you can use its built-in event handling facilities.
It's a little known fact that you can also put any kind of Javascript objects into a jQuery collection, not just DOM elements. Then you can use a limited set of jQuery methods (.data(), .prop(), .on(), .off(), .trigger() and .triggerHandler()) on these objects.
//create the model - it can be any kind of object
var model = {};
//pass it to the jQuery function and you have a jQuery collection
//you can put an event handler on the object
$(model).on('customEvent', function () { console.log('hehe'); });
//and trigger it
$(model).trigger('customEvent');
Read more in the Manual
Try the Demo
For those who are not using jQuery and are interested in wiring their own stuf, here's how you may achieve similar aim:
/**
* EventfulObject constructor/base.
* #type EventfulObject_L7.EventfulObjectConstructor|Function
*/
var EventfulObject = function() {
/**
* Map from event name to a list of subscribers.
* #type Object
*/
var event = {};
/**
* List of all instances of the EventfulObject type.
* #type Array
*/
var instances = [];
/**
* #returns {EventfulObject_L1.EventfulObjectConstructor} An `EventfulObject`.
*/
var EventfulObjectConstructor = function() {
instances.push(this);
};
EventfulObjectConstructor.prototype = {
/**
* Broadcasts an event of the given name.
* All instances that wish to receive a broadcast must implement the `receiveBroadcast` method, the event that is being broadcast will be passed to the implementation.
* #param {String} name Event name.
* #returns {undefined}
*/
broadcast: function(name) {
instances.forEach(function(instance) {
(instance.hasOwnProperty("receiveBroadcast") && typeof instance["receiveBroadcast"] === "function") &&
instance["receiveBroadcast"](name);
});
},
/**
* Emits an event of the given name only to instances that are subscribed to it.
* #param {String} name Event name.
* #returns {undefined}
*/
emit: function(name) {
event.hasOwnProperty(name) && event[name].forEach(function(subscription) {
subscription.process.call(subscription.context);
});
},
/**
* Registers the given action as a listener to the named event.
* This method will first create an event identified by the given name if one does not exist already.
* #param {String} name Event name.
* #param {Function} action Listener.
* #returns {Function} A deregistration function for this listener.
*/
on: function(name, action) {
event.hasOwnProperty(name) || (event[name] = []);
event[name].push({
context: this,
process: action
});
var subscriptionIndex = event[name].length - 1;
return function() {
event[name].splice(subscriptionIndex, 1);
};
}
};
return EventfulObjectConstructor;
}();
var Model = function(id) {
EventfulObject.call(this);
this.id = id;
this.receiveBroadcast = function(name) {
console.log("I smell another " + name + "; and I'm model " + this.id);
};
};
Model.prototype = Object.create(EventfulObject.prototype);
Model.prototype.constructor = Model;
// ---------- TEST AND USAGE (hopefully it's clear enough...)
// ---------- note: I'm not testing event deregistration.
var ob1 = new EventfulObject();
ob1.on("crap", function() {
console.log("Speaking about craps on a broadcast? - Count me out!");
});
var model1 = new Model(1);
var model2 = new Model(2);
model2.on("bust", function() {
console.log("I'm model2 and I'm busting!");
});
var ob2 = new EventfulObject();
ob2.on("bust", function() {
console.log("I'm ob2 - busted!!!");
});
ob2.receiveBroadcast = function() {
console.log("If it zips, I'll catch it. - That's me ob2.");
};
console.log("start:BROADCAST\n---------------");
model1.broadcast("crap");
console.log("end :BROADCAST\n---------------\n-\n-\n");
console.log("start:EMIT\n---------------");
ob1.emit("bust");
console.log("end:EMIT\n---------------");
<h1>THE CODE IS IN THE JavaScript SECTION!</h1>
<h3>AND... THE SHOW IS ON YOUR CONSOLE!</h3>
Related
i'll get right to the description and example code as i bet many of you are as confused about the title as i am (tried my best).
Situation: I have a form that submits input data to a new instance of an object, using a constructor function.
CURRENT CODE:
// Selectors
var submit = document.getElementById('submit-btn');
var formValues = document.getElementsByClassName('input');
// ** Task Object
function Task(arrayOfValues) {
this.title = arrayOfValues[0].value;
this.deadline = arrayOfValues[1].value;
this.status = arrayOfValues[2].value;
}
submit.addEventListener('click', function(e) {
e.preventDefault();
var newTask = new Task(formValues);
}, false);
Problem: Passing the array as an argument and assigning each index manually feels incredibly brittle to me. If i were to extend the data collection of the form i would have to manually assign each new value as a separate variable. What is the syntax or pattern if you will for iterating over an array and dynamically assigning values to variables in a constructor function ? Thank you in advance. Any and all help is much appreciated.
EXAMPLE of desired CODE
var formValues = document.getElementsByClassName('input');
// ** Task Object
function Task(arrayOfValues) {
this.values = arrayOfValues;
for (var i=0;i<this.values.length;i++) {
var key = this.values[i].getAttribute('name');
// This should be used with "this" to reference key ie. "this.title, this.deadline, this.status ect...
var value = this.values[i].value;
// This will be the value associated with each key reference ie. "this"
return this[key] = value;
// is this even possible ?
}
}
submit.addEventListener('click', function(e) {
e.preventDefault();
var newTask = new Task(formValues);
}, false);
Just use the names of the form elements as the property names in the object.
function Task(arrayOfValues) {
for (var i=0;i<arrayOfValues.length;i++) {
var key = arrayOfValues[i].name;
var value = arrayOfValues[i].value;
this[key] = value;
}
}
Don't use return inside the loop, that will end the constructor after processing the first element in the array, and ignore the rest.
I would just take the three parameters separately, let the caller figure out which one is the title, deadline or status
function Task(title, deadline, status) {
this.title = title;
this.deadline = deadline;
this.status = status;
}
Now your callers can figure out which one is which by using IDs, data-attributes, or anything they want.
<input id="title-x"> <input id="deadline"> <input id="status">
submit.addEventListener('click', function(e) {
e.preventDefault();
var getVal = function(id) { return document.getElementById(id).value; }
var newTask = new Task(getVal('title-x'), getVal('deadline'), getVal('status'));
}, false);
Now your constructor doesn't rely on the order within the array, or even that they are DOM elements.
You could always have a contract where a specific input maps to a task field through data-attributes
<input data-task-field="title">
<input data-task-field="deadline">
<input data-task-field="status">
<input data-task-field="newField">
/**
* #param {object} taskDescription
* #param {string} taskDescription.title
* #param {string} taskDescription.deadline
* #param {string} taskDescription.status
* #param {string} taskDescription.newField
*/
function Task(taskDescription) {
this.task = taskDescription;
}
submit.addEventListener('click', function(e) {
e.preventDefault();
var tasks = document.querySelectorAll('input[data-task-field]');
var taskSpec = {};
for (var i=0; i < tasks.length; i++) {
taskSpec[tasks.getAttribute('data-task-field')] = tasks.value;
}
var newTask = new Task(taskSpec);
}, false);
I would use an associative array -
http://www.w3schools.com/js/js_arrays.asp
var assArray = [];
assArray["title"] = document.getElementbyID(......)
That makes your array semantically meaningful and less brittle. It doesn't matter if you expand the form, so long as you don't change the names it will continue to work.
It also means you don't have to change the call signature of your constructor each time you expand the form, you only have to alter the body to deal with the new elements in the array (or not, as you so choose).
I have a React component where it will attach a bunch of li elements to DOM. And some of them have a click Eventlistener to them. I'm trying to disable the eventlistener after the user clicked on those special li, I'm using event.currentTarget.removeEventListener('click', this.handleMouse) for that, but it's not working. Here are the relevant part of the code:
var DisplayList = React.createClass({
/* ... */
handleMouse: function (event) {
event.currentTarget.style.backgroundColor = 'white';
this.props.changeCounts(-1);
event.currentTarget.removeEventListener('click', this.handleMouse); //NOT WORKING
},
/* ... */
render: function () {
var self = this;
return(
<div id = "listing-boxes-wrapper">
{
this.props.sortedList.map(function(item, index){
if (self.state.changedHabit.indexOf(item.habitid) > -1) {
return <li key={index} style={{backgroundColor: '#ccc'}} className = "text-center" onClick={self.handleMouse}>{item.description}
</li>
}else{
return <li key={index} className =" text-center">{item.description}
</li>
}
})
}
</div>
)
}
});
reactjs uses Function.prototype.bind to bind the context to the handler (otherwise this would be undefined).
So what happens under the hood is something like:
element.addEventListener('click', this.handleMouse.bind(this));
So, as you can see it's another function added to the listener, not the this.handleMouse.
So after that - you cannot remove it, since it was not even attached.
The react-way solution would be just to re-render the element once again without the handler so that the react detached the handler itself.
Relevant (?) code in react:
/**
* Binds a method to the component.
*
* #param {object} component Component whose method is going to be bound.
* #param {function} method Method to be bound.
* #return {function} The bound method.
*/
function bindAutoBindMethod(component, method) {
var boundMethod = method.bind(component);
if (__DEV__) {
// stripped as irrelevant
}
return boundMethod;
}
/**
* Binds all auto-bound methods in a component.
*
* #param {object} component Component whose method is going to be bound.
*/
function bindAutoBindMethods(component) {
var pairs = component.__reactAutoBindPairs;
for (var i = 0; i < pairs.length; i += 2) {
var autoBindKey = pairs[i];
var method = pairs[i + 1];
component[autoBindKey] = bindAutoBindMethod(
component,
method
);
}
}
In my angular app, i use modal windows for filters/selectors and there are many components which are binded with window resize event like SVG, wordcloud etc.. Whenever i open or close the modal window, it triggers window resize event which results in reloading of all those components (Charts)..
Is there a way to stop the triggering of window resize event while opening or closing the modal window..?.. I googled and got a thread about this issue which is still open..
https://github.com/driftyco/ionic/issues/2309
The window.resize is triggered intentionally by the framework itself -- from the source: ionic.trigger('resize');.
solution
option 1
You can just comment out the above mentioned line and create your own bundle of the ionic framework.
option 2
You can use decorator to replace $ionicModal service altogether by copying and modifying the source code. Let me know if anything unclear. I personally prefer this option as it should be easier to maintain in the long run and let you customise the behaviour of the modal even further.
HERE is an example of what I mean:
.config(function($provide){
$provide.decorator('$ionicModal',function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) {
var PLATFORM_BACK_BUTTON_PRIORITY_MODAL = 200;
var delegate = (function(){
/**
* #ngdoc controller
* #name ionicModal
* #module ionic
* #description
* Instantiated by the {#link ionic.service:$ionicModal} service.
*
* Be sure to call [remove()](#remove) when you are done with each modal
* to clean it up and avoid memory leaks.
*
* Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
* scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are
* called when the modal is removed.
*/
var ModalView = ionic.views.Modal.inherit({
/**
* #ngdoc method
* #name ionicModal#initialize
* #description Creates a new modal controller instance.
* #param {object} options An options object with the following properties:
* - `{object=}` `scope` The scope to be a child of.
* Default: creates a child of $rootScope.
* - `{string=}` `animation` The animation to show & hide with.
* Default: 'slide-in-up'
* - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
* the modal when shown. Default: false.
* - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop.
* Default: true.
* - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware
* back button on Android and similar devices. Default: true.
*/
initialize: function(opts) {
ionic.views.Modal.prototype.initialize.call(this, opts);
this.animation = opts.animation || 'slide-in-up';
},
/**
* #ngdoc method
* #name ionicModal#show
* #description Show this modal instance.
* #returns {promise} A promise which is resolved when the modal is finished animating in.
*/
show: function(target) {
var self = this;
if (self.scope.$$destroyed) {
$log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.');
return;
}
var modalEl = angular.element(self.modalEl);
self.el.classList.remove('hide');
$timeout(function() {
$ionicBody.addClass(self.viewType + '-open');
}, 400);
if (!self.el.parentElement) {
modalEl.addClass(self.animation);
$ionicBody.append(self.el);
}
if (target && self.positionView) {
self.positionView(target, modalEl);
// set up a listener for in case the window size changes
ionic.on('resize',function() {
ionic.off('resize',null,window);
self.positionView(target,modalEl);
},window);
}
modalEl.addClass('ng-enter active')
.removeClass('ng-leave ng-leave-active');
self._isShown = true;
self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(
self.hardwareBackButtonClose ? angular.bind(self, self.hide) : angular.noop,
PLATFORM_BACK_BUTTON_PRIORITY_MODAL
);
self._isOpenPromise = $q.defer();
ionic.views.Modal.prototype.show.call(self);
$timeout(function() {
modalEl.addClass('ng-enter-active');
// ionic.trigger('resize');
self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self);
self.el.classList.add('active');
self.scope.$broadcast('$ionicHeader.align');
}, 20);
return $timeout(function() {
//After animating in, allow hide on backdrop click
self.$el.on('click', function(e) {
if (self.backdropClickToClose && e.target === self.el) {
self.hide();
}
});
}, 400);
},
/**
* #ngdoc method
* #name ionicModal#hide
* #description Hide this modal instance.
* #returns {promise} A promise which is resolved when the modal is finished animating out.
*/
hide: function() {
var self = this;
var modalEl = angular.element(self.modalEl);
self.el.classList.remove('active');
modalEl.addClass('ng-leave');
$timeout(function() {
modalEl.addClass('ng-leave-active')
.removeClass('ng-enter ng-enter-active active');
}, 20);
self.$el.off('click');
self._isShown = false;
self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self);
self._deregisterBackButton && self._deregisterBackButton();
ionic.views.Modal.prototype.hide.call(self);
// clean up event listeners
if (self.positionView) {
ionic.off('resize',null,window);
}
return $timeout(function() {
$ionicBody.removeClass(self.viewType + '-open');
self.el.classList.add('hide');
}, self.hideDelay || 320);
},
/**
* #ngdoc method
* #name ionicModal#remove
* #description Remove this modal instance from the DOM and clean up.
* #returns {promise} A promise which is resolved when the modal is finished animating out.
*/
remove: function() {
var self = this;
self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);
return self.hide().then(function() {
self.scope.$destroy();
self.$el.remove();
});
},
/**
* #ngdoc method
* #name ionicModal#isShown
* #returns boolean Whether this modal is currently shown.
*/
isShown: function() {
return !!this._isShown;
}
});
var createModal = function(templateString, options) {
// Create a new scope for the modal
var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
options.viewType = options.viewType || 'modal';
angular.extend(scope, {
$hasHeader: false,
$hasSubheader: false,
$hasFooter: false,
$hasSubfooter: false,
$hasTabs: false,
$hasTabsTop: false
});
// Compile the template
var element = $compile('<ion-' + options.viewType + '>' + templateString + '</ion-' + options.viewType + '>')(scope);
options.$el = element;
options.el = element[0];
options.modalEl = options.el.querySelector('.' + options.viewType);
var modal = new ModalView(options);
modal.scope = scope;
// If this wasn't a defined scope, we can assign the viewType to the isolated scope
// we created
if (!options.scope) {
scope[ options.viewType ] = modal;
}
return modal;
};
return {
/**
* #ngdoc method
* #name $ionicModal#fromTemplate
* #param {string} templateString The template string to use as the modal's
* content.
* #param {object} options Options to be passed {#link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
* #returns {object} An instance of an {#link ionic.controller:ionicModal}
* controller.
*/
fromTemplate: function(templateString, options) {
var modal = createModal(templateString, options || {});
return modal;
},
/**
* #ngdoc method
* #name $ionicModal#fromTemplateUrl
* #param {string} templateUrl The url to load the template from.
* #param {object} options Options to be passed {#link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
* options object.
* #returns {promise} A promise that will be resolved with an instance of
* an {#link ionic.controller:ionicModal} controller.
*/
fromTemplateUrl: function(url, options, _) {
var cb;
//Deprecated: allow a callback as second parameter. Now we return a promise.
if (angular.isFunction(options)) {
cb = options;
options = _;
}
return $ionicTemplateLoader.load(url).then(function(templateString) {
var modal = createModal(templateString, options || {});
cb && cb(modal);
return modal;
});
}
}
})();
console.log(delegate);
return delegate;
});
})
caution
Changing the behaviour may have some unwanted side effects, but I do not know ionic framework that much. Moreover, if there are any negative side effects then surely that is a bad design of the framework itself, as general events should not be used for framework specific purposes. If I had a bit more time I will try to make a PR to have triggering of the event optional.
The proxy function will not work, but a normal function is ok
What can i do to make it work
It's problably a scope issue, but how do I use "apply" or "call" in this context?
delegateEvents: function(){
for (var key in this.events) {
var methodName = this.events[key];
var method = this.proxy(this[methodName]);
var match = key.match(this.eventSplitter);
var eventName = match[1], selector = match[2];
if (selector === '') {
this.el.bind(eventName, method);
} else {
eventName == "click" && ( eventName = "tap click");
console.log("selector",selector);
console.log("eventName",eventName);
var eel = $$(selector, $(this.el));
Hammer(eel).on(eventName, method);
}
}
}
});
and a part of Hammer
Hammer.Instance.prototype = {
/**
* bind events to the instance
* #param {String} gesture
* #param {Function} handler
* #returns {Hammer.Instance}
*/
on: function onEvent(gesture, handler){
var gestures = gesture.split(' ');
for(var t=0; t<gestures.length; t++) {
this.element.addEventListener(gestures[t], handler, false);
}
return this;
},
proxy function
util = {
proxy: function(fn, context, args){
var argsArr = slice(arguments, 2); //curried args
return function(){
return fn.apply(context, argsArr.concat(slice(arguments)));
};
},
becomes part of the controller
result.proxy = function(func){ return util.proxy(func, this); };
so, this.proxy == result.proxy and the context is already set to this
thanks if anyone knows this
The problem was that the Minified library returned a selector that the Hammer library
could not work with.
Well, that was my first assumption
It was actually the fact that I was using one type of selector for two different functions, the "$" version, wich returns the Minified object, but Hammer just needs the node and therefore needs the "$$" version.
goog.events provides an unlistenByKey method allowing the specific deregistration of a listener by its unique identity. Is it correct that the only way to obtain this key is by calling either goog.events.listen again with each event type listened for or by calling goog.events.getListener with each event type listened for?
e.g.
var eventKeys = new Array(2);
goog.events.listen(document, ['mousedown', 'touchstart'], this.pressStartHandler_, false, this);
eventKey[0] = goog.events.getListener(document, 'mousedown', this.pressStartHandler_, false, this);
eventKey[1] = goog.events.getListener(document, 'touchstart', this.pressStartHandler_, false, this);
EDIT
Barring better advice from an answerer, I'm wrote the following wrapper to get the keys:
/**
* Wrap goog.events.listen to capture all of the event keys of the requested listeners.
*
* #param {(EventTarget |
* goog.events.EventTarget | null)} src The node to listen to events on.
* #param {(Array |
* null | string)} type Event type or array of event types.
* #param {(Object |
* null)} listener Callback method, or an object with a handleEvent function.
* #param {(boolean |
* undefined)} opt_capt Whether to fire in capture phase (defaults to false).
* #param {(Object |
* null | undefined)} opt_handler Element in whose scope to call the listener.
* #returns {Array.<number>} Array of keys for listeners
*/
wias.util.listen = function(src, type, listener, opt_capt, opt_handler)
{
var i;
var keys = new Array();
goog.events.listen(src, type, listener, opt_capt, opt_handler);
if (type instanceof Array)
{
for (i = 0; i < type.length; i++)
{
keys.push(goog.events.getListener(src, type[i], listener, opt_capt, opt_handler).key);
}
}
else
{
keys.push(goog.events.getListener(src, type, listener, opt_capt, opt_handler).key);
}
return keys;
};
You might find goog.events.EventHandler useful.
// Note that all handler callbacks receive 'this' as the opt_handler, by default.
var handler = new goog.events.EventHandler(this);
handler.listen(
document, ['mousedown', 'touchstart'], this.pressStartHandler_, false);
// Some time later...
handler.removeAll();
To answer the original question, goog.events.listen also returns the key when used with a single event type. You could be slightly more correct and concise in your above implementation by iterating over each event type, calling goog.events.listen for each, and storing the key for each.
wias.util.listen = function(src, type, listener, opt_capt, opt_handler) {
var keys = new Array();
if (type instanceof Array) {
for (var i = 0; i < type.length; i++) {
keys.push(goog.events.listen(src, type, listener, opt_capt, opt_handler));
}
} else {
keys.push(goog.events.listen(src, type, listener, opt_capt, opt_handler));
}
return keys;
};