-
-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wrong behaviour of modal.dismiss in bootstrap.native.js #98
Comments
Do you have an example to showcase the behavior is wrong? What do you mean by wrong: is it different than original jQuery script or it's not fast enough? |
Well, I will try to explain with pseudo-example, and if it wouldn't be understandable, will provide a fiddle. The main problem is with dismiss and keydown methods. They both create handlers within them, which are subscribed to click event of modal element through closures. The first problem is that when both dismiss and keydown methods are called, the modal element doesn't contains 'in' style class on it - it is removed right before in caller. So it doesn't matter, if you are showing or closing the window - event handlers are always attached on both operations, and it leads to memory leaks. But, even if we will subscribe or unsubscribe from events according to correct 'in' style class, another error will occur - because handlers are created every time inside methods - they are new objects, and they are not detached from event. This handlers should be a members of this. If you put a debug statements inside handlers now - you will see that the calls are accumulated as much, as you opened the dialogs. I don't know if the problem exists in jquery plugins, because I don't use it. But if so, this behavior is still incorrect in its nature, and I think that this is a good practice to make this good project even better. |
In my mind the scripts generally try to add listeners when needed and removed when not needed so that the CPU can carry less burden and really manage less memory.
I perfectly agree with you here. In this case, I am open to any code suggestion you may have. I was thinking of some new options to customize what happens with certain components' handlers like |
Here's a jsfiddle showing the accumulation of previous event listeners. After launching the Modal the first time, Then close the Modal. Now, two Launch the Modal again, now three It keeps accumulating a |
Does this happen with Modal only or is it happening to other components? I am now sure this has to be improved ASAP, and I also am sure this should not happen with Carousel. So in my mind now, I am confident we should have something like this:
Please confirm that this is the case. |
I'm sorry, but I am new to both Bootstrap and Bootstrap-native.,so I am not quite sure what you are asking. When you say "remove listeners that trigger the Then, when you What is the purpose of the |
Ok, please make another test with Carousel, check if it multiplies any If that is true, then Modal and other components have to be checked asap, and I mean ASAP. |
I will try. I have never used Carousel, so it won't be fast turnaround. |
You can simply test on the bootstrap.native demo site, I am curious of your finding. UPDATE I have already tested and I think you make a very good point with your finding. I might have to rework all handlers from all components. I think the future version is going to be even more simple code and perhaps lighter. |
The problem in the Modal code is in its use of a CSS class named All three routines which are meant to add an event listener in
use But
|
A couple other code problems in Modal:
I think you meant to put the "word boundary" on both sides of
And two problems with
|
Your findings are truly interesting. I am super curious what would you suggest about your previous comment. |
You could try moving these lines to the bottom of the function.
|
Assumptions made by Jeff17Ribbinns are right, but even this changes would not prevent accumulating, because component doesn't save original handler to unsubscribe. |
Good point. The handlers are local to a closure, so the removeEventListener isn't passed the same function as the addEventListener had been passed. |
Let's go! First changes includes adding a special toggled subscription routines for DOM events. The main purpose of it to generate a toggled subscribe/unsubscribe methods. First call of getSubscriptionToggler(...) returns a toggler, which when called subscribes/unsubscribes from listeners in a chainable manner; function subscribe(element, event, handler) {
element.addEventListener(event, handler, false);
return function() {
return unsubscribe(element, event, handler);
};
};
function unsubscribe(element, event, handler) {
element.removeEventListener(element, event, handler);
return function() {
return subscribe(element, event, handler);
};
};
function getSubscriptionToggler(element, event, handler) {
return function() {
return subscribe(element, event, handler);
};
}; Next, I've removed handlers generation from keydown and dismiss methods, and place them in common space of instance. Right after: var self = this, timer, ..., checkScrollbar = function () {...} goes our new handlers: keyHandler = function(e) {
if (self.options.keyboard && e.which == 27) {
self.close();
}
},
dismissHandler = function (e) {
if ( e.target.parentNode.getAttribute('data-dismiss') === 'modal'
|| e.target.getAttribute('data-dismiss') === 'modal'
|| e.target === self.modal ) {
e.preventDefault(); self.close()
}
}, And after that we declare a scoped variables keyListenerToggler and clickListenerToggler: keyListenerToggler = getSubscriptionToggler(document, 'keydown', keyHandler),
clickListenerToggler = getSubscriptionToggler(this.modal, 'click', dismissHandler); This will hold our togglers inside modal instance object for future use. And we are almost finished. The last step is to refactor this.keydown and this.dismiss. It's very simple, just use out listener togglers appropriate: this.keydown = function() {
keyListenerToggler = keyListenerToggler();
};
this.dismiss = function() {
clickListenerToggler = clickListenerToggler();
}; |
@oliseviche I was looking for a fork in your profile to see this code working but I couldn't find one. Please consider making a PR as soon as you see fit. |
@oliseviche I think we can export this script to UTILS and use it in all components for the event handling. |
sorry for hijacking this thread, but the @Jeff17Robbins suggested |
@oliseviche I am considering to change the way events are handled all round, no more removing and re-adding events, we will simply toggle a local variable, for instance BUT I will look into your code as well. |
This is a major release that could break backward compatibility (especially for those who rely heavily on JavaScript) as we have renamed some methods, completely revamped components, added more utilities, added original events, improved performance along with other cosmetic changes. **Utilities** * added new utilities for event delegation: `on()`, `off()` * added new utility for triggering original events `bootstrapCustomEvents()` * added `initializeDataAPI` utility * added selector `queryElement()` utility * added `hasClass()` utility as suggested #98 (comment) * fixed `getClosest()` utility and now can be used by other components as well, like Alert * `stylePopover()` and `styleTooltip()` are now one common utility called `styleTip()` **Alert** * now requires `data-dismiss="alert"` attribute in order to work, for both DATA API usage and JavaScript * the event handler is no longer attached to the `document` object **Affix** * if the offset options are set as functions, they are executed on update instead of only at the time of initialization * renamed most methods and now we expose a single public method `update()` * major code cleanup and performance improvements **Dropdown** * added ability to keep the dropdown-menu open via `persist: true/false` option or `data-persist="true"` attribute * if the trigger is a link, it will `event.preventDefault()` **Button** * events renamed from `bs.change.button` to `change.bs.button` * events no longer trigger twice **Tab** * now can animate height as well via `data-height="true"` and additional CSS * because the above, the handler is protected by a `isAnimating` private _boolean_ **Modal** * no longer initialize on `.modal` via DATA API, but on `[data-toggle="modal"]` elements, while via JavaScript we initialize on a triggering element, also not a modal * no longer resizes the backdrop, seems it's not needed * now stores the initialization (`this`) in the `element.modalInit` object (the triggering button), to be able to easily close a currently open modal * the `<div class="modal">` object stores the triggering button reference in `modal.modalTrigger`, and this updates everytime you open a modal by clicking a different trigger button * renamed methods from `.open()` to `.show()` and `.close()` to `.hide()` * added `.toggle()` method * reworked event handlers, solving #98 **Collapse** * reworked event handlers, they are not removed and readded anymore, we now have a local variable `isAnimating` to prevent click * renamed `open()` to `show()` and `close()` to `hide()` methods * triggering elements no longer target collapsible elements via class name, only parent accordion * some code improvements and major cleanup **Carousel** * reworked event handlers, they are not removed and readded anymore, we now have a local variable `isSliding` to prevent click * renamed `_slideTo()` to `slideTo()` and `_getActiveIndex()` to `getActiveIndex()` methods * if no active item is found on initialization, the first carousel item (with the 0, zero index) will be made active * events also point the `relatedTarget` like the original plugin **Popover** * renamed Popover option `dismiss` to `dismissible` in the need to eliminate conflicts with Alert `click` handler, as both handlers are bound to `document` **ScrollSpy** * reworked all about the component, now the JavaScript initialization should work as expected * major performance improvements, the component now should be one of the fastest of it's kind * fixed non-window overflowing elments active state processing, now the component will handle level 1 nested containers **All Components** * added original events to all components * all initializations are registered in `window.BootstrapNative` object * major code clean up in all components: total separation of public methods and private methods, removed alot of stuff from each component's `this` instance, much more readable code, **now the library is only ~20Kb minified** instead of 38Kb * major documentation re-write: added methods, more usage and added events usage guides MORE INFO TO COME
This is a major release that could break backward compatibility (especially for those who rely heavily on JavaScript) as we have renamed some methods, completely revamped components, added more utilities, added original events, improved performance along with other cosmetic changes. **Utilities** * added new utilities for event delegation: `on()`, `off()` * added new utility for triggering original events `bootstrapCustomEvents()` * added `initializeDataAPI` utility * added selector `queryElement()` utility * added `hasClass()` utility as suggested #98 (comment) * fixed `getClosest()` utility and now can be used by other components as well, like Alert * `stylePopover()` and `styleTooltip()` are now one common utility called `styleTip()` **Alert** * now requires `data-dismiss="alert"` attribute in order to work, for both DATA API usage and JavaScript * the event handler is no longer attached to the `document` object **Affix** * if the offset options are set as functions, they are executed on update instead of only at the time of initialization * renamed most methods and now we expose a single public method `update()` * major code cleanup and performance improvements **Dropdown** * added ability to keep the dropdown-menu open via `persist: true/false` option or `data-persist="true"` attribute * if the trigger is a link, it will `event.preventDefault()` **Button** * events renamed from `bs.change.button` to `change.bs.button` * events no longer trigger twice **Tab** * now can animate height as well via `data-height="true"` and additional CSS * because the above, the handler is protected by a `isAnimating` private _boolean_ **Modal** * no longer initialize on `.modal` via DATA API, but on `[data-toggle="modal"]` elements, while via JavaScript we initialize on a triggering element, also not a modal * no longer resizes the backdrop, seems it's not needed * now stores the initialization (`this`) in the `element.modalInit` object (the triggering button), to be able to easily close a currently open modal * the `<div class="modal">` object stores the triggering button reference in `modal.modalTrigger`, and this updates everytime you open a modal by clicking a different trigger button * renamed methods from `.open()` to `.show()` and `.close()` to `.hide()` * added `.toggle()` method * reworked event handlers, solving #98 **Collapse** * reworked event handlers, they are not removed and readded anymore, we now have a local variable `isAnimating` to prevent click * renamed `open()` to `show()` and `close()` to `hide()` methods * triggering elements no longer target collapsible elements via class name, only parent accordion * some code improvements and major cleanup **Carousel** * reworked event handlers, they are not removed and readded anymore, we now have a local variable `isSliding` to prevent click * renamed `_slideTo()` to `slideTo()` and `_getActiveIndex()` to `getActiveIndex()` methods * if no active item is found on initialization, the first carousel item (with the 0, zero index) will be made active * events also point the `relatedTarget` like the original plugin **Popover** * renamed Popover option `dismiss` to `dismissible` in the need to eliminate conflicts with Alert `click` handler, as both handlers are bound to `document` **ScrollSpy** * reworked all about the component, now the JavaScript initialization should work as expected * major performance improvements, the component now should be one of the fastest of it's kind * fixed non-window overflowing elments active state processing, now the component will handle level 1 nested containers **All Components** * added original events to all components * all initializations are registered in `window.BootstrapNative` object * major code clean up in all components: total separation of public methods and private methods, removed alot of stuff from each component's `this` instance, much more readable code, **now the library is only ~20Kb minified** instead of 38Kb * major documentation re-write: added methods, more usage and added events usage guides MORE INFO TO COME
@oliseviche @thewisenerd @Jeff17Robbins please test the new commit |
Wrong condition for removing event listeners exists in Modal.dismiss method.
After closing dialog 'in' class is removed before the code above is executed, thus leading to behaviour that every close event new click listeners are attached.
The text was updated successfully, but these errors were encountered: