I'm using jquery-ui Tabs and I'm having a problem that occurs when a tab has been removed. The tab appears to be removed, along with its content div but when you take a look at the heap in Chrome DevTools Profiles (after a tab has been removed) you'll see that the tab li and div elements are still present, but detached. Over time, the repeated addition/removal of tabs causes these elements to accumulate. For example, if you add a tab 10 times, there will be 10 detached div elements and 10 detached li elements showing up in the heap snapshot:
I have the following views:
TabLabel = Marionette.ItemView.extend({
template: "#tab-label",
tagName: "li",
events: {
"click .ui-icon-close": "closeTab"
},
closeTab: function(e) {
this.$el.contents().remove();
this.model.collection.remove(this.model);
$("#main-container").tabs("refresh");
this.close();
}
});
TabContainer = Marionette.ItemView.extend({
template: "#tab-container",
tagName: "div",
onBeforeRender: function() {
this.$el.attr("id", "div-" + this.id);
},
onClose: function() {
// This removes the region that contains the container
App.layout.removeRegion(this.containerRegion);
}
});
TabLabels = Marionette.CollectionView.extend({
tagName: "ul"
});
TabContainers = Marionette.CollectionView.extend({
tagName: "div"
});
The views are instantiated like so:
tabs = new TabsCollection(); // Create a new collection instance
var tabLabelView = new TabLabels({
itemView: TabLabel,
collection: tabs
});
var tabContainerView = new TabContainers({
itemView: TabContainer,
collection: tabs
});
As you can see, the views both refer to the same collection; each model in the collection can be said to represent a single tab (it just so happens that the model contains the necessary information to satisfy jquery-ui tabs). The views are shown in a region via Marionette.Layout... pretty standard. Adding a tab is accomplished by clicking a link in the tab container; all this does is adds another model to the collection and then calls tabs("refresh") on the main container, which makes the new tab appear. Removing a tab is accomplished by clicking an "X" icon in the upper right-hand corner of the tab.
I've spent a lot of time trying to track down this leak and I can't figure out if it's a problem in my code (the way I'm closing views/models/etc. perhaps?) or if it's a problem in the jquery-ui tabs plugin.
Thoughts? Thanks in advance!
Update #1 As requested, here is a jsfiddle demonstrating the problem -- just close the tabs and you'll see that detached elements are left behind.
Also, a screenshot:
Update #2 This appears to be a leak in the jquery-ui tabs widget. The same behavior occurs in the widget demonstration on the jquery-ui website. I added a few tabs and then closed them out and sure enough, they persisted:
I've tested this with the latest (at the time of this writing) version of jQuery UI (1.10.3) and the previous version (1.10.2).
Is there a reason why you use this.$el.contents().remove() instead of this.$el.empty()?
Using this.$el.empty() in that jsFiddle of yours seemed to remedy the detached NodeList.
A few notes memory profiling:
watch http://www.youtube.com/watch?v=L3ugr9BJqIs
Use the 3 snapshots method, 2 is not enough. What does that mean?
Start fresh, incognito mode and refreshed
Take snapshot (forced GC will happen every time you take snapshot)
Do something, take snapshot (gc)
Do something similar, take snapshot (gc)
Compare snapshot 2 with snapshot 1, find deltas with +
Choose Summary and Objects allocated between Snapshots 1 and 2 for snapshot 3
Look for stuff that you found comparing 2 and 1 that shouldn´t be there. These are the leeks
I have found cases where jQuery seems to leek because they save the current jQuery object in .prevObject when doing some operations like calling .add(). Maybe that call to .contents() do some funky magic.
So I eventually fixed this problem (quite some time ago) by doing two things:
Ditching jQueryUI in favor of Bootstrap
Changing the closeTab function (see the original jsFiddle) to the following:
closeTab: function (e) {
e.stopPropagation();
e.preventDefault();
this.model.collection.remove(this.model);
this.close();
}
In that snippet, stopPropagation and preventDefault are really what stopped this element from remaining detached (and accumulating on subsequent adds/removes) in the DOM. To summarize: this problem was created by not calling stopPropagation/preventDefault on the event object after it was triggered. I've updated the jsFiddle so that it correctly removes the tab elements and for posterity, here's a screenshot of the result:
As you can see, none of the tab elements are remaining detached after clicking the "X" to close them. The only detached elements are the ones that come from jsFiddle's interface.
Related
I am working on a browser extension.
It has two parts:
popup - which contains checkboxes
content script - which contains the code to alter the CSS property
I am saving the states of checkboxes so that the next time I open the popup again the same checkboxes are marked as checked.
When I use the checkboxes they change the DOM as intended, however when I try to alter the DOM after the page is loaded, changes are not reflected. This is probably because the element on which I want to perform the operation is loaded slow and thus required operations fail.
I tried to use onload and ready but nothing worked
$('.question-list-table').on('load', function() {
browser.storage.local.get(["options"], modifyThenApplyChanges)
});
I also tried, but nothing changed.
$('body').on('load','.question-list-table', function() {
browser.storage.local.get(["options"], modifyThenApplyChanges)
});
Also, there is no visible error with the popup or content script as I test in both Google Chrome and Mozilla Firefox.
Update:
As suspected earlier, the target element is loaded slowly so I used setTimeout for 5 seconds and the script is working as intended.
Loading time is variable and I want to show my changes as early as possible everything in a consistent manner.
After going through MutationObserver as suggested by #charlietfl in the comment section, this is what I coded and works for me
// Mutation Observer
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function(mutation) {
if(mutation.addedNodes.length) {
//do stuff
}
});
});
el = document.getElementsById('elementId');
if(el) {
observer.observe(el, {
childList: true // specify the kind of change you are looking for
});
}
I am new to web dev, so I'm sorry if this is a silly question. I followed a very simple tutorial and made a chrome extension that replaces images on screen with that of random ones I picked from my computer (mostly crappy meme valentines cards). However I have noticed some peculiar behaviour.
1) it doesn't replace all the images. for example in the google images page, it replaces the first 5 or 6 lines perfectly, and after that replaces none
2) similarly, on netflix it also does 1), but on top of that, if I go left or right on one of the sliders that it worked on, the images go away and it goes back to the default images.
How can I fix this so it 1) replaces all images and 2) keeps those changes.
I have attached a gif demonstrating this issue below
https://i.imgur.com/4hZuzsX.gif
It will happen mainly for two reasons:
Not all the img tags exist when you perform the replacement;
The elements get rerendered when you perform some action on the page (like clicking or scrolling).
I tested in Google Image Search and yep - not all the img tags are there from the start. So you'd have to replace them, too, when they appear.
Go to Google Image Search and test this script once. Then scroll down to the bottom and test it once again to see if it changed:
// this will tell you how many img tags exist in the page
console.log(document.getElementsByTagName('img').length);
It most certainly increased as you were scrolling down. Right?
Possible workaround - observing element mutations
In days of old there were events you could listen to for changes whenever elements were added or removed, but they've been deprecated for quite some time. Let's get this out of the way: don't use mutation events!
Nowadays if you want to monitor changes in the page I suggest you take a look at Mutation Observers.
Mutation Observers
A mutation happens when an element or its contents or attributes are changed. So in short, Mutation Observers provide you a way to monitor changes in an element.
The most relevant difference (at least for extension developers) between observing mutations and listening to events is that mutations capture changes that are done programmatically, which makes it so useful since more often than not we must alter a page whose source we did not author.
Could you listen to a given event like, say, scrolling? Yes. But since there most likely are event listeners going on concurrently with your own that will cause mutations in the page, it's hard to make sure that the element you want to change yourself will already exist by the time the event is triggered (the page's native code might still be doing its thing).
Example
Here's an example borrowing from the above MDN link, plus the desired behavior you described - replacing images, whenever they are added:
EDIT: I'm going to be more descriptive in the comments and leave links to where you should browse for additional information in the MDN docs in case you need them. Note that the example is targeting a specific element (#some-id) but you could target document.body, but not in Stack Overflow's fiddle sandbox, apparently.
// Select the node that will be observed for mutations
// I'm targeting a specific node, but it COULD BE document.body,
// which would observe the entire document for changes
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
// See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit
const config = {
attributes: true, // watch for changes to attributes on the node(s)
childList: true, // watch for the addition or removal of child nodes
subtree: true // all monitoring rules apply to child elements as well
};
/*
PS: all settings are 'optional' but at least one among
childList, attributes or characterData must be true.
Again, don't forget to take a look on the docs!
*/
// Callback function to execute when mutations are observed
// See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver#The_callback_function
const callback = function(mutationsList, observer) {
// mutationsList is an array of MutationRecord objects,
// describing each change that occurred.
// See https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
// observer is the MutationObserver instance that was triggered
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
// This will be executed when a new image is added
console.log('A child node has been added or removed.');
// Iterate over the added nodes to check if there are any images
for (let node of mutation.addedNodes) {
// Replace the .src attribute if the element is an img
if (node.tagName === 'IMG') {
node.src = 'https://pm1.narvii.com/6334/121fb1f34767638906fac47cf818dc9c326bc936_128.jpg';
}
}
console.log('Garmanarnar will rule above all');
} else if (mutation.type === 'attributes') {
// This will be executed when .src is changed
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
// observer.disconnect();
<script>
function addImg() {
console.clear();
let img = document.createElement('img');
img.src = 'https://loremflickr.com/320/240';
document.getElementById('some-id').appendChild(img);
}
</script>
<button style="cursor: pointer;" onclick="addImg()">
<strong>Click me</strong>
</button>
<div id="some-id">
<img src="https://pm1.narvii.com/6334/121fb1f34767638906fac47cf818dc9c326bc936_128.jpg" alt="Garmanarnar" />
</div>
With each click, you should see only Garmanarnars (blue friendly alien doing thumbs up), not any random pic of cats or anything else. This means the new img tag got its src attribute replaced successfully.
In Javascript we use lots and lots of callback functions, so if you're not used to them, you might have a lot to digest, but it will be worth your while learning them.
Additional considerations (you may skip if you're satisfied)
I get that it's just a prank and it would be a 'passable' flaw, but if you want to target every single image in a page, looking for img tags might not be always enough, since some elements will have images included through CSS background-image property, or even svg elements.
I made a Chrome extension myself a few years ago and maybe it's also worth mentioning that you must pay attention whether an image is part of an iframe or not. They're not part of the current document - it's a page from elsewhere and it's sandboxed for security reasons. Take a look at this answer from another Stack Overflow question to see how to enable accessing iframes within a page (you still have to create rules for the target page, though).
I'm working with a provider and factory set up in angular, and in the factory is where I do all the heavy lifting of generating the templates, creating the instances, and doing all of the animations. The provider creates a very nifty slider menu from the left.
Problem
What's happening though, is that after the first instance of the slider menu, menu options start to double themselves. So I'll have the original 5, then 10, then 20, then 40... I have found a solution where we start with a null instance, and check if that instance is null, if it is null render the menu. So that forces it to only continuously render the initial 5, but then if we dynamically change the menu we won't ever see those changes and that is not what we want.
Fiddle
https://jsfiddle.net/Mr_Pikachu/chdbxt1h/351/
Broken Code
This is the chunk of code that I am most focused on, as it is the bit that is causing us the issue.
backdropScope.close = function(){
$animate.leave(menu).then(function(){
backdrop.remove();
//menuOpts.scope.$destroy();
// menu_rendered = null;
menu.remove();
});
}
// menustack object
$menuStack = {
'open': function(menuOpts){
menuOpts.scope.main = menuOpts.menu.main;
if(!menu_rendered) {
menu_rendered = menu_template(menuOpts.scope);
}
if(!backdropRendered) {
backdropRendered = backdropTemplate(backdropScope);
}
menuOpts.scope.$apply(function(){
$animate.enter(backdropRendered, body).then(function(){
$animate.enter(menu_rendered, body);
});
});
}
};
List of Attempted Fixes
setting menu_rendered = null in the $animate.leave() will work on the first instance, and re-render the menu properly, but then the backdrop instance won't recognize a click event
Using menuOpts.scope.$destory(), but it did absolutely nothing
Using the current solution of menu_rendered check. It is not optimal and looking for a solution that allows the use of dynamic content.
Updated Fiddle: https://jsfiddle.net/chdbxt1h/355/
I moved the angular.element calls into the body of the $menuStack.open method. The menu content does not get duplicated in repeated exposures. Presumably, this is because the DOM Node is created anew on each open, and garbage collected cleanly on leave and/or remove.
Both the background (menu-overlay) and menu are re-created on each open, so this should honor changes in the source menu data, though possibly not while the menu is open.
Update 9/11/13 - Here is a fully working jsFiddle demonstrating the issue... to experience the issue, expand the grid and attempt to drag the nested row over to the TreePanel. The drag element will be obfuscated by the TreePanel, as if it is behind it. Link here: http://jsfiddle.net/ZGxf5/
Here's a bit of a unique roadblock I've run into... I figured it would be best illustrated with an image:
As you can see in the picture, I am attempting to drag a div, generated via the rowBodyTpl property of the RowExpander plugin utilized in the grid shown in the bottom left of the image. I am able to "grab" the element and move it about, however it is seemingly constrained to the RowExpander generated div. I cannot drag the div any further left, nor upwards from where its original position. Attempting to move it into the panel to the right results in the dragging div being obfuscated, as shown in the picture.
I have attempted to completely eliminate all constraints in the startDrag method as you will see in the code below, but to no avail. I am basically just using the code provided in Sencha's 5 Steps to Understanding Drag and Drop with ExtJS Blog Post, but it obviously needs some tweaking for my implementation.
Below is my code for initializing the Drag on the target div..
/**
* NOTE: The following code is executed whenever
* the contents of the grid's store change
*/
var me = this, // ENTIRE left panel, including the TreePanel and lower GridPanel
divs = Ext.select('div[name=storage-item-div]', false, me.getEl().dom),
dragOverrides = {}; // provided separately, see below
Ext.each(divs.elements, function(el){
console.warn("mkaing new dd", el);
var dd = new Ext.dd.DD(el, 'storageItemDDGroup',{
isTarget: false
});
Ext.apply(dd, dragOverrides);
});
The dragOverrides object is defined as follows (note my debugging for Constrain)
dragOverrides = {
b4StartDrag : function() {
// Cache the drag element
if (!this.el) {
this.el = Ext.get(this.getEl());
}
//Cache the original XY Coordinates of the element, we'll use this later.
this.originalXY = this.el.getXY();
},
startDrag: function(){
/** DEBUGGING */
_t = this;
this.resetConstraints();
this.setXConstraint(1000,1000);
this.setYConstraint(1000,1000);
},
// Called when element is dropped not anything other than a dropzone with the same ddgroup
onInvalidDrop : function() {
// Set a flag to invoke the animated repair
this.invalidDrop = true;
},
// Called when the drag operation completes
endDrag : function() {
// Invoke the animation if the invalidDrop flag is set to true
if (this.invalidDrop === true) {
// Remove the drop invitation
this.el.removeCls('dropOK');
// Create the animation configuration object
var animCfgObj = {
easing : 'elasticOut',
duration : 1,
scope : this,
callback : function() {
// Remove the position attribute
this.el.dom.style.position = '';
}
};
// Apply the repair animation
this.el.moveTo(this.originalXY[0], this.originalXY[1], animCfgObj);
delete this.invalidDrop;
}
}
Finally, I think the rowBodyTpl portion of the lower grid's configuration may be useful in solving the issue, so here is the source for that!
rowBodyTpl : ['<div id="OrderData-{item_id}" style="margin-left: 50px;">'+
'<tpl for="order_data">'+
'<tpl for=".">' +
'<div name="storage-item-div" class="draggable" style="padding-bottom: 5px;">' +
'<b>{quantity}</b> from Purchase Order <b>{purchase_order_num}</b> # ${purchase_cost}' +
'<input type="button" style="margin-left: 10px;" name="storageViewOrderButton" orderid="{purchase_order_id}" value="View Order"/>' +
'</div>' +
'</tpl>' +
'</tpl>'+
'</div>']
I was able to get this working in a Fiddle, but I had to switch my RowExpander template to instead render an Ext.view.View rather than the div which I was previously using. Using an Ext.view.View allowed me to basically just follow the Sencha demo for using DragZone and DropZone.. kinda wish it wasn't so complicated but hey, that's just how it is sometimes, I guess.
See the very messy jsFiddle source here for a working demo using DragZone and DropZone, feel free to tweak for your own needs: http://jsfiddle.net/knppJ/
Again, the issue here was dragging a nested div from inside a RowExpander generated row inside a gridpanel to a separate adjacent panel. The issue I was encountering is thoroughly described in my question above. I was not able to get a regular div working the way I wanted it to, so I ended up using an Ext.view.View in place of the grid. This required adding a bit of extra logic in the onbodyexpand event fired by the RowExpander plugin, basically just rendering an Ext.view.View to the div generated by the RowExpander.
I have an Ember.js app that has a Checklist model, where each Checklist has many Checkitems. (A variant of the classic ToDo app, but with multiple TodoLists.)
In the top-most view, the user sees a listing of all available checklists to the left. When a checklist is selected, the corresponding checkitems appear to the right.
The checkitems on the right side are drag sortable. I'm using this html5sortable library to handle drag sorting. It's like the classic jQueryUI version, but less clunky.
Upon initial loading of the app, the sortable list works fine. However, if the list of checkitems changes (either because a checkitem is marked as complete, a new checkitem is added, changes to an existing check item are saved, or another checklist is selected on the left), the binding to html5sortable is lost.
When the app first loads, I have a view called App.CheckitemsPendingTableView:
App.CheckitemsPendingTableView = Ember.View.extend({
templateName: 'app/templates/checkitems/checkitemsPendingTable',
classNames: ['checkitems-list', 'sortable', 'list'],
tagName: 'ul',
didInsertElement: function() {
this._super();
$('ul.sortable').sortable();
console.log('CheckitemsPendingTableView has been inserted in the DOM and is bound to sortable. At this point, drag-sorting of the list is working fine.');
}
});
The corresponding template is called checkitemsPendingTable.handlebars and it looks like this:
{{#each content}}
{{view App.CheckitemSingleView checkitemBinding="this"}}
{{/each}}
And for good measure, the controller that feeds the content attribute for that view is App.checkitemsController.remainingItems:
App.checkitemsController = Ember.ArrayProxy.create({
content:[],
...snip...
remainingItems: function() {
var checkitems = this.get('content');
var sortedCheckitems = checkitems.filterProperty('isDone', false).sort(function(a,b) {
return a.get('position') - b.get('position');
});
return sortedCheckitems;
}.property('content.#each.isDone'),
...snip...
});
The content attribute of the checkitemsController is driven by the checklistsController:
App.checklistsController = Ember.ArrayProxy.create({
content: App.store.findAll(App.Checklist),
selectedChanged: function() {
var checklist = this.get('selected');
var checkitems = checklist.get('checkitems');
App.checkitemsController.set('checklist', checklist);
App.checkitemsController.set('content', checkitems);
}.observes('selected')
});
(You may have noticed that this controller pulls its data from a Rails backend via ember-data. This shouldn't matter for the current issue, though.)
The view for the left-hand side's menu is called checklistsView. It has a child view called checklistSingleView that is rendered for each of the checklists:
App.ChecklistSingleView = Ember.View.extend({
templateName: 'app/templates/checklists/checklistSingle',
classNames: ['menu-item'],
tagName: 'tr',
...snip...
chooseList: function() {
var checklists = App.checklistsController.get('content');
checklists.setEach('isActive', false);
var checklist = this.get('checklist');
checklist.set('isActive', true);
App.checklistsController.set('selected', checklist);
}
...snip...
});
And, finally, the corresponding template checklistSingle.handlebars contains a link that is tied to the chooseList by way of an action:
<a href="#" {{action "chooseList"}}>{{checklist.name}}</a>
So, everything above works brilliantly...until the user causes a change to the ul of checkitems on the right. At that point, the binding to html5sortable is lost, and I cannot find a convenient place to refresh it.
The problem is that didInsertElement is not called again for the view that generates that ul (i.e., CheckitemsPendingTableView). When the checklistController's content attribute changes, the child views dutifully adjust to reflect the currently-selected list of checkitems. However, the original binding to sortable() is lost, and there is no apparent hook for re-binding to sortable() via jQuery.
I can't re-bind on the child view of CheckitemsPendingTableView, since that would repeat for every instance of a checkitem in the currently-selected list. I can't rebind from the controllers or models, since they will attempt to bind before the DOM update is completed.
I'm sure I'm just thinking about this incorrectly. I'm new to Ember.js (if it isn't wildly obvious), and am struggling to understand how this case is properly handled.
UPDATE
I solved this problem, by adding the following function and observer to the App.CheckitemsPendingTableView:
resetSortableTable: function() {
$('.sortable').unbind('sortable');
$('.sortable').sortable();
console.log('Sort reset by CheckitemsPendingTableView');
},
itemsChanged: function() {
console.log('itemsChanged caught in CheckitemsPendingTableView');
// flush the RunLoop so changes are written to DOM
Ember.run.sync();
Ember.run.next(this, function() {
this.resetSortableTable();
});
}.observes('content.#each')
I based my solution on this answer. I'm a little worried that it's not a great solution, since it seems to be making assumptions about the DOM completing during a run loop iteration.
Whoa! Kind of detailed question...
I think your issue comes from html5sortable plugin: it uses JQuery's bind method to attach handlers directly on living elements. As those elements disappear/evolve under Ember control, the binding are lost.
The plugin should better use JQuery's on method to bind handlers, as specified in the documentation.