I'm writing a lightweight jQuery plugin to detect dirty forms but having some trouble with events. As you can see in the following code, the plugin attaches an event listener to 'beforeunload' that tests if a form is dirty and generates a popup is that is the case.
There is also another event listener attached to that form's "submit" that should in theory remove the 'beforeunload' listener for that specific form (i.e. the current form I am submitting should not be tested for dirt, but other forms on the page should be).
I've inserted a bunch of console.log statements to try and debug it but no luck. Thoughts?
// Checks if any forms are dirty if leaving page or submitting another forms
// Usage:
// $(document).ready(function(){
// $("form.dirty").dirtyforms({
// excluded: $('#name, #number'),
// message: "please don't leave dirty forms around"
// });
// });
(function($) {
////// private variables //////
var instances = [];
////// general private functions //////
function _includes(obj, arr) {
return (arr._indexOf(obj) != -1);
}
function _indexOf(obj) {
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (obj, fromIndex) {
if (fromIndex == null) {
fromIndex = 0;
} else if (fromIndex < 0) {
fromIndex = Math.max(0, this.length + fromIndex);
}
for (var i = fromIndex, j = this.length; i < j; i++) {
if (this[i] === obj)
return i;
}
return -1;
};
}
}
////// the meat of the matter //////
// DirtyForm initialization
var DirtyForm = function(form, options) {
// unique name for testing purposes
this.name = "instance_" + instances.length
this.form = form;
this.settings = $.extend({
'excluded' : [],
'message' : 'You will lose all unsaved changes.'
}, options);
// remember intial state of form
this.memorize_current();
// activate dirty tracking, but disable it if this form is submitted
this.enable();
$(this.form).on('submit', $.proxy(this.disable, this));
// remember all trackable forms
instances.push(this);
}
// DirtyForm methods
DirtyForm.prototype = {
memorize_current: function() {
this.originalForm = this.serializeForm();
},
isDirty: function() {
var currentForm = this.serializeForm();
console.log("isDirty called...")
return (currentForm != this.originalForm);
},
enable: function() {
$(window).on('beforeunload', $.proxy(this.beforeUnloadListener, this));
console.log("enable called on " + this.name)
},
disable: function(e) {
$(window).off('beforeunload', $.proxy(this.beforeUnloadListener, this));
console.log("disable called on " + this.name)
},
disableAll: function() {
$.each(instances, function(index, instance) {
$.proxy(instance.disable, instance)
});
},
beforeUnloadListener: function(e) {
console.log("beforeUnloadListener called on " + this.name)
console.log("... and it is " + this.isDirty())
if (this.isDirty()) {
e.returnValue = this.settings.message;
return this.settings.message;
}
},
setExcludedFields: function(excluded) {
this.settings.excluded = excluded;
this.memorize_current();
this.enable();
},
serializeForm: function() {
var blacklist = this.settings.excludes
var filtered = [];
var form_elements = $(this.form).children();
// if element is not in the excluded list
// then let's add it to the list of filtered form elements
if(blacklist) {
$.each(form_elements, function(index, element) {
if(!_includes(element, blacklist)) {
filtered.push(element);
}
});
return $(filtered).serialize();
} else {
return $(this.form).serialize();
}
}
};
////// the jquery plugin part //////
$.fn.dirtyForms = function(options) {
return this.each(function() {
new DirtyForm(this, options);
});
};
})(jQuery);
[EDIT]
I ended up fixing this by using jQuery's .on() new namespace feature to identify the handler. The problem was that I was passing new anonymous functions as the handler argument to .off(). Thanks #FelixKling for your solution!
this.id = instances.length
[...]
enable: function () {
$(window).on('beforeunload.' + this.id, $.proxy(this.beforeUnloadListener, this));
},
disable: function () {
$(window).off('beforeunload.' + this.id);
},
Whenever you are calling $.proxy() it returns a new function. Thus,
$(window).off('beforeunload', $.proxy(this.beforeUnloadListener, this));
won't have any effect, since you are trying to unbind a function which was not bound.
You have to store a reference to the function created with $.proxy, so that you can unbind it later:
enable: function() {
this.beforeUnloadListener = $.proxy(DirtyForm.prototype.beforeUnloadListener, this);
$(window).on('beforeunload', this.beforeUnloadListener);
console.log("enable called on " + this.name)
},
disable: function(e) {
$(window).off('beforeunload', this.beforeUnloadListener);
console.log("disable called on " + this.name)
},
Related
I am having a lot of trouble trying to remove an event listener.
I have created a website that relies on JavaScript quite heavily. When you navigate on the website it is basically loading in elements dynamically without a page refresh with template literals.
I have to sometimes load in content and add infinite scroll but also be able to remove that event again.
This is the code I use to handle scroll events:
var start = 30;
var active = true;
function yHandler(elem)
{
var oHeight = selectElems('content_main', 'i').offsetHeight;
var yOffset = window.pageYOffset;
var hLimit = yOffset + window.innerHeight;
if (hLimit >= oHeight - 500 && active === true)
{
active = false;
new requestContent({
page: GET.page,
type: returnContentType(GET.page),
scroll: true,
start: start
}, (results) => {
if(results){
setTimeout(()=>{
active = true;
start = start + 30;;
}, 400);
new ContentActive();
}
});
}
}
var scrollRoute =
{
contentScroll: () =>{
yHandler();
}
};
var scrollHandler = function(options)
{
var func = options.func.name;
var funcOptions = options.func.options;
var elem = options.elem;
var flag = options.flag;
this.events = () => {
addEvent(elem, 'scroll', ()=>{
scrollRoute[func](elem, funcOptions);
}, flag);
}
this.clear = () => {
elem.removeEventListener('scroll', scrollRoute[func](), flag);
}
}
I am using this function to set events
function addEvent(obj, type, fn, flag = false) {
if (obj.addEventListener) {
obj.addEventListener(type, fn, flag);
} else if (obj.attachEvent) {
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event);
};
obj.attachEvent("on" + type, obj[type + fn]);
} else {
obj["on" + type] = obj["e" + type + fn];
}
}
I am calling this code from whatever code when I need to set the infinite scroll event
new scrollHandler({
func: {
'name':'contentScroll',
},
elem: window,
flag: true,
}).events();
I am calling this code from whatever code when I need to remove the infinite scroll event but without any luck
new scrollHandler({
func: {
'name':'contentScroll',
},
elem: window,
flag: true,
}).clear();
How do I successfully remove the event listener? I can't just name the instances, that will be so messy in the long run when setting and removing the scroll events from various different places.
Two problems:
You have to pass the same function to removeEventListener as you passed to addEventListener. (Similarly, you have to pass the same function to detachEvent as you passed to attachEvent using Microsoft's proprietary stuff — but unless you really have to support IE8 and earlier, you can ditch all that.) Your code isn't doing that.
When trying to remove the handler, you're calling scrollRoute[func]() and passing its return value into removeEventListener. As far as I can tell, that's passing undefined into removeEventListener, which won't do anything useful.
Here's the code I'm referring to above:
this.events = () => {
addEvent(elem, 'scroll', ()=>{ // *** Arrow function you don't
scrollRoute[func](elem, funcOptions); // *** save anywhere
}, flag); // ***
}
this.clear = () => {
elem.removeEventListener('scroll', scrollRoute[func](), flag);
// Calling rather than passing func −−−^^^^^^^^^^^^^^^^^^^
}
Notice that the function you're passing addEvent (which will pass it to addEventListener) is an anonymous arrow function you don't save anywhere, but the function you're passing removeEventListener is the result of calling scrollRoute[func]().
You'll need to keep a reference to the function you pass addEvent and then pass that same function to a function that will undo what addEvent did (removeEvent, perhaps?). Or, again, ditch all that, don't support IE8, and use addEventListener directly.
So for instance:
var scrollHandler = function(options) {
var func = options.func.name;
var funcOptions = options.func.options;
var elem = options.elem;
var flag = options.flag;
var handler = () => {
scrollRoute[func](elem, funcOptions);
};
this.events = () => {
elem.addEventListener('scroll', handler, flag);
};
this.clear = () => {
elem.removeEventListener('scroll', handler, flag);
};
};
(Notice I added a couple of missing semicolons, since you seem to be using them elsewhere, and consistent curly brace positioning.)
Or using more features of ES2015 (since you're using arrow functions already):
var scrollHandler = function(options) {
const {elem, flag, func: {name, options}} = options;
const handler = () => {
scrollRoute[name](elem, options);
};
this.events = () => {
elem.addEventListener('scroll', handler, flag);
};
this.clear = () => {
elem.removeEventListener('scroll', handler, flag);
};
};
I have set Editors.Text or edit.
{id: "label", name: "name", field: "label",editor: Editors.Text,width: 80},
This enables editing for field on browser.
But how can I catch when editing is finished??
I am checking the event list of slickgrid.
However can't find the appropriate event.
How can I catch the event after editing columns??
It appears that there is no generic event for this - probably not a bad idea to add one. I suspect it is expected that you would write a custom editor and add the event directly to that rather than adding it to the grid.
I assume you want to update some related data or UI when the editing is complete?
[Edit]
The editor has events encapsulated in it for doing this - the grid uses a plugin model with loadValue and applyValue to read/write the data source. I'll post an example of my personal text editor here, as it may help. Note that I have written a data provider for my personal grid to allow it to interface to several custom data objects - this isn't in the standard one you should be using, which is here.
function TextEditor(args) {
var $input;
var defaultValue;
var scope = this;
this.init = function () {
$input = $("<INPUT type=text class='editor-text' />")
.appendTo(args.container)
.on("keydown.nav", function (e) {
if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
e.stopImmediatePropagation();
}
})
.focus()
.select();
$input.width(args.container.clientWidth); /* mod */
};
this.destroy = function () {
$input.remove();
};
this.focus = function () {
$input.focus();
};
this.getValue = function () {
return $input.val();
};
this.setValue = function (val) {
$input.val(val);
};
this.loadValue = function () {
defaultValue = args.grid.getDataProvider().getValueByColName(args.rowIndex, args.column.field);
defaultValue = defaultValue || "";
$input.val(defaultValue);
$input[0].defaultValue = defaultValue;
$input.select();
};
this.serializeValue = function () {
return $input.val();
};
this.applyValue = function (state) {
args.grid.getDataProvider().setValueByColName(args.rowIndex, args.column.field, state);
};
this.isValueChanged = function () {
return (!($input.val() == "" && defaultValue == null)) && ($input.val() != defaultValue);
};
this.validate = function () {
if (args.column.validator) {
var validationResults = args.column.validator($input.val());
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
};
this.init();
}
I have a function, simplified like this:
var fooFunction = function($container, data) {
$container.data('foobarData', data);
$container.on('click', 'a', function(e) {
var data = $(e.delegateTarget).data('foobarData');
var $target = $(e.currentTarget);
if (typeof data.validateFunction === 'function' && !data.validateFunction(e)) {
return;
}
e.preventDefault();
e.stopPropagation();
// Do stuff
console.log(data.returnText);
});
};
fooFunction('.same-container', {
validateFunction: function(event) {
return $(e.currentTarget).closest('.type-companies').length ? true : false;
},
returnText: 'Hello Company!',
});
fooFunction('.same-container', {
validateFunction: function(event) {
return $(e.currentTarget).closest('.type-humans').length ? true : false;
},
returnText: 'Hello Human!',
})
I am using event delegation on the same container (.same-container) with a custom validateFunction() to validate if the code in // Do stuff should run.
For each fooFunction() initiation, I have some different logic that will get called on // Do stuff. The issue is that those two event delegations conflict. It seems that only one of them is called and overwrites the other one.
How can I have multiple event delegations with the option to define via a custom validateFunction which one should be called. I use preventDefault() + stopPropagation() so on click on a <a>, nothing happens as long as validateFunction() returns true.
The problem is that you're overwriting $(e.delegateTarget).data('foobarData') every time.
Instead, you could add the options to an array, which you loop over until a match is found.
var fooFunction = function($container, data) {
var oldData = $container.data('foobarData', data);
if (oldData) { // Already delegated, add the new data
oldData.push(data);
$container.data('foobarData', oldData);
} else { // First time, create initial data and delegate handler
$container.data('foobarData', [data]);
$container.on('click', 'a', function(e) {
var data = $(e.delegateTarget).data('foobarData');
var $target = $(e.currentTarget);
var index = data.find(data => typeof data.validateFunction === 'function' && !data.validateFunction(e));
if (index > -1) {
var foundData = data[index]
e.preventDefault();
e.stopPropagation();
// Do stuff
console.log(foundData.returnText);
}
});
}
}
In my Nativescript app I have a loop and want to display a dialog for each item being iterated over. When the dialog displays it contains "Accept" and "Reject" options, both of which when clicked I would like to call a method which I pass the iterated item into. The issue is since the option selection returns a promise I lose the reference to the iterated item. What can I do to get around this? Here's an example of my code.
EDIT: I also really don't like that I'm declaring a function in the loop after the promise returns.
function _showPendingConnections() {
for (var i = 0; i < ViewModel.pendingConnections.length; i++) {
var pendingConnection = ViewModel.pendingConnections[i];
dialog.confirm({
message: pendingConnection.PatientFirstName + " would like to share their glucose readings with you.",
okButtonText:"Accept",
cancelButtonText:"Reject"
}).then(function(result) {
if(result === true) {
ViewModel.acceptConnection(pendingConnection);
} else {
ViewModel.removeConnection(pendingConnection);
}
});
}
}
The following change worked for me (I have probably created different viewModel but however the idea is the same) - all I have done is to change when your item index is passed.
For example:
// main-page.js
"use strict";
var main_view_model_1 = require("./main-view-model");
var dialogModule = require("ui/dialogs");
var viewModel = new main_view_model_1.MyViewModel();
viewModel.pendingConnections = [{ PatientFirstName: "John" }, { PatientFirstName: "Merry" }, { PatientFirstName: "Abygeil" }];
// Event handler for Page "navigatingTo" event attached in main-page.xml
function navigatingTo(args) {
// Get the event sender
var page = args.object;
page.bindingContext = viewModel;
for (var index = viewModel.pendingConnections.length - 1; index >= 0; index--) {
connectionDealer(index);
}
}
exports.navigatingTo = navigatingTo;
function connectionDealer(index) {
var pendingConnection = viewModel.pendingConnections[index];
dialogModule.confirm({
message: pendingConnection["PatientFirstName"] + " would like to share their glucose readings with you.",
okButtonText: "Accept",
cancelButtonText: "Reject"
}).then(function (result) {
if (result === true) {
// your code follow.. pass pendingConnection[index] to your method
console.log("accepted by " + pendingConnection["PatientFirstName"]);
}
else {
// your code follow.. pass pendingConnection[index] to your method
console.log("Rejected by " + pendingConnection["PatientFirstName"]);
}
});
}
// main-view-model.js
"use strict";
var observable = require("data/observable");
var MyViewModel = (function (_super) {
__extends(MyViewModel, _super);
function MyViewModel() {
_super.apply(this, arguments);
}
Object.defineProperty(MyViewModel.prototype, "pendingConnections", {
get: function () {
return this._pendingConnections;
},
set: function (value) {
if (this._pendingConnections !== value) {
this._pendingConnections = value;
}
},
enumerable: true,
configurable: true
});
return MyViewModel;
}(observable.Observable));
exports.MyViewModel = MyViewModel;
I am writing a jQuery event plugin and I need to pass some data to a argument. So if I use it like this:
$(element).on('myevent', function(event, myargument) { console.log(myargument); });
I wan't to get the myargument object and it's properties (e.g. name), which is set in the handler.
So how would this work with the code below?
SmartScroll.myevent = {
setup: function() {
var myargumentData,
handler = function(evt) {
var _self = this,
_args = arguments;
// The data I wan't to get
myargumentData = {
name: "Hello"
};
evt.type = 'myevent';
jQuery.event.handle.apply(_self, _args);
};
jQuery(this).bind('scroll', handler).data(myargumentData, handler);
},
teardown: function() {
jQuery(this).unbind('scroll', jQuery(this).data(myargumentData));
}
};
You can modify an object passed to add.
$.event.special.foo = { add: function(h) {
var hh = h.handler;
h.handler = function(e, a) {
hh.call(this, e, a * 2);
};
} };
Now, let's bind the event:
$("body").on("foo", function(e, a) {
console.log(a);
});
Fire the event and see what happens:
$("body").trigger("foo", [ 10 ]); // 20
on: function( types, selector, data, fn, /*INTERNAL*/ one )
According to the source.
so if i understand your problem , just do :
$(element).on('myevent',{my_data:"foo"},function(event) {
console.log(event.data.my_data);
});
prints "foo"