Background
I've been working with Polymer for a while. I've been converting from .5 and building new elements for a production app. We are currently using Polymer 1.0.6, and this particular issue is also using jQuery 2.x.x and typeahead.js.
Issue
We have an element that builds a dynamic list of label and inputs provided by a data source. in the ready function we get a list of input data, and set that to a local list variable that is bound to a foreach template to create the labels and inputs.
I was unable to find a Polymer element I really liked for typeahead, for Polymer 1.0. So I defaulted to using typeahead.js. my problem is that I cannot find a lifecycle event or workaround, to call the typeahead function after the dom has processed setting the bound list in the ready function.
Code
The easiest way to demonstrate this issue, was to create a HEAVILY trimmed down version in a jsbin. I know the element looks bad, it was cut down as much as possible to demo the core issue I'm facing.
http://jsbin.com/zivano/edit?html,output
What Have I Tried?
I've tried using the attached event, and while it does process after the ready function, the dom changes from ready have not taken effect. I found similar issues on SO domReady vs ready - Migrating to Polymer 1.0 I've tried both suggestions, the second is still being used in the jsbin, without success.
I have also bound the click event of my inputs to a function calling the typeahead setup code, to prove that if the calls are made after the dom is rendered it will work correctly.
Summary
If update a data bound, local variable in the ready function, is there a lifecycle event I can call that will guarantee that those dom changes will be rendered, so I can make a dom query against those new items? Or is there a work around that will let me call a js function on a dom element, one time after the element dom fully renders?
my problem is that I cannot find a lifecycle event or workaround, to
call the typeahead function after the dom has processed setting the
bound list in the ready function.
I think I had a problem like this . For my problem I found a solution using the following :
var self = this;
window.addEventListener('WebComponentsReady', function(e) {
// imports are loaded and elements have been registered
/*Example*/
console.log('Components are ready');
var p = self.getElementsByTagName("paper-item");//paper-item created dynamically
console.log(p);//can access and use this paper-item
/*Finish example*/
//here you can call typeahead because the dom has been processed
});
Sorry for my English or if I dont understand your question, my English is bad.
The Issue I had was that the data-bound list was populated through an ajax function, which was completed after the attached function, even if I made an async call inside of the attached function, it would still fail because of race conditions.
It's worth noting the answer by Flavio Ochoa, will work. I personally preffered to not have my custom elements add listeners to the Window. So i went a different route.
Since my issues we're predicated on guaranteeing that the bound list was updated, I wrapped the ajax call in a Promise, and added the typeahead init logic to the then statement. That solution appears to be working.
I do have some concerns whether the promise can guarantee that the bound list will have propagated to the DOM by the time the then statement is processed. But so far it has worked consistently. I'll edit this answer if I can prove otherwise.
Related
One of the things I'm still hung up on with Angular is understanding all of the lifecycle hooks and when to use what when.
I often need to stick a little plain'ol JS into a component to deal with some DOM issue that, alas, I can't handle via Angular (usually because I'm working within a component that needs to access some elements from a parent component's rendered DOM that we have no access to the Angular code for...I do realize this isn't the 'proper' Angular way but...).
An example right now is a few pages I'm working on that use a component need that needs to hide a DOM element on the page that isn't a part of this component. I need to use JS for this (a whole other story why CSS isn't the solution for this one).
But I only want to do this once the DOM is fully rendered.
Sometimes this seems to work when inserted into ngAfterViewInit -- but sometimes not. It seems that there's no guarantee the full DOM is ready using that lifecycle.
Moving that logic into ngAfterViewChecked does work. However, the issue with ngAfterViewChecked is that it's getting called dozens of times on some pages--and the first few times it's called, the DOM isn't even ready. Not the end of the world, but there's no reason for me to be attempting to grab the same DOM object 40 times per page render. I somewhat remedy this by adding a boolean flag to tell this bit of JS to stop running once it finds the DOM elements but that's hacky.
So my question is: What is the proper (if there is one) way to handle JS manipulation of the DOM after the DOM is fully rendered in the context of an Angular component? Is it to use one of the angular lifecycle events? Something else? Or this whole idea of manipulating DOM objets outside of the component I'm working in just anathema to the 'Angular way' so just isn't something accommodated within Angular?
I am trying to reset the 'selected' item after the user makes a selection. This is calling to the Vue function below, tS(). The first call $('#sel1').prop('selectedIndex',0); only works if I execute it in the console however the second one $('#current-schedule-holder').text('Current : '+scheduleToTime(msg)); works fine, meaning that jquery is working. Also my console.log($('#sel1')) shows that the element from the first call, the one that is not working, is already in the DOM when the function executes.
tS: function(msg){
$('#sel1').prop('selectedIndex',0);
console.log($('#sel1'))
$('#current-schedule-holder').text('Current : '+scheduleToTime(msg));
toggleScheduler(this.query, msg);
},
I wouldn't recommend mixing jQuery and Vue. Vue uses a virtual DOM while jQuery does not. What's probably happening is that when the tS function runs, it updates the DOM correctly, but then Vue overrides those changes based on the info it has in its virtual DOM.
See https://vuejsdevelopers.com/2017/05/20/vue-js-safely-jquery-plugin/ for instructions on how to get jQuery to work with Vue. You're going to have to wrap your jQuery functions in a Vue component which is fine if your needing some complicated functionality in a jQuery plugin, but for basic DOM manipulation like this, it's much easier to stick to doing this inside of Vue.
I'm working with a large template of charts and other widgets. I also manually implemented some ajax tabs. Now whenever those tabs load new content (charts), the problem is that all the template scripts in the head tag won't work with those ajax-loaded elements anymore.
I know, normally you would use .live for this kind of problem, but this would mean to go into the whole 50k lines-js template and change everything to .live calls... Not really able to do that.
Is there instead a jquery way of reloading/reactivating all the scripts within the head-tag?
First off .live() has long since been deprecated and even removed from the latest versions of jQuery. You should never be thinking of using .live().
Second, as it sounds like you already know, the "right" way to fix this is to change your code to use the delegated form of .on() which is what replaced .live(). Yes, change all the code that does it the wrong way. Here's a post on using the delegated form of .on() instead of .live().
Third, a work-around would be to put all your initialization code that hooks up these event handlers into a single function (or called by a single function). Then, you call that single function upon initialization and then you can call that single function any time later after you reload your content. The trick is that you can only put code into that initialization function that can be called or should be called more than once after you content has been reloaded. If you put some event handlers in there that should not be in there, then you may get duplicate event handlers installed. So, only event handler initialization that applies to the replaced content should go in this function.
Suppose that function was called initDynamicContent, then it could look like this:
// init event handlers on the original version of the dynamic content
$(document).ready(initDynamicContent);
Then, sometime later after you replace the dynamic content, you can just do:
// code here that replaces the dynamic content with new content
initDynamicContent();
There is no magic jQuery way for this to happen automatically. jQuery has absolutely no way of knowing which code should be run again and which code should not.
I got to refactor big one-page application with complex UI.
There is following code in document.ready function
$('table.datatable').dataTable({ ... params ... });
$('div.tooltip').tooltip({ ... params ... });
$('ul.dropdownMenu').menu({ ... params ... });
As you can see in this code we search for different HTML controls and call to appropriate jQuery plugins that implement these controls' behavior.
However, since the page is very dynamic new datatables, tooltips and menus are added all the time and since JS functions were called at very beginning of application - those elements have no needed functionality unless I manually call appropriate plugin for them.
I'd like to eliminate the need to call jQuery plugin after each DOM change but don't know how to do it better.
One option is to fire an event each time I am adding anything to DOM and re-call this plugins in event's listener, however I don't like this solution because of need to remember fire the event each time.
I read about jQuery's on function which attach events also for not yet exists elements, but what event do I need? AFAIK there is no domchange event.
Any advises?
Your only option is to hook up your plug-ins on new items after they are added to the DOM. The best cross browser option would be to hook into each place that adds these types of elements to the DOM and simply deal with the new items there by calling some additional function on them. What you need to look for is where you can easily hook into your existing code after new elements have been added to the DOM and then fix up those new elements at that point. Without knowing your code and where the DOM is modified, we can't really advise the best way to do that.
Generically monitoring the DOM for new items with a single method is not possible in a cross browser fashion. Mutation observers are the latest standard way to do this, but it is not supported in many browsers yet. Mutation events came before mutation observers, but is now deprecated.
jQuery's .on() will not do what you want here - it can be used to handle dynamically added DOM elements, but not in the way you want. Your plug-ins could have been designed to handle their events with use .on(), but unless they were designed that way and you can take advantage of that, there isn't a simple way for you to use it to get your desired behavior without rewriting a portion of the plugin.
I am building a single page webapp. This means over a period of time I get new DOM elements, remove unneeded ones. For example when I fetch a new form I just replace the contents of a specific div with that form HTML and also set up listeners unique to this form's elements. After some period I replace the contents of this form with a new instance of form (having different ID's).
I set up the event listeners again for this new form. Now the previous form is no longer part of the DOM so I the DOM elements should be automatically garbage collected. I am also expecting the listener functions pointing to the elements removed from the DOM to disappear.
However the following profile gathered from Chrome suggests that my listener count is increasing over time. Can you tell me why this is so? I tried clicking on the "Collect Garbage" button. But this is the profile I get. Is there something wrong with the way I am building my application? Is there a problem and if so how should I fix it?
In case it matters I am using JSP templating language with jquery, jquery-ui and some other plugins.
This is how the dynamic fragments that I add/remove on my page look like.
<script>
$(document).ready(function() {
$("#unique_id").find(".myFormButton").button().click(
function() {
$.ajax({url: "myurl.html",
success: function(response) {
console.log(response);
}
});
});
});
</script>
<div id="unique_id">
<form>
<input name="myvar" />
<button class="myFormButton">Submit</button>
</form>
</div>
Update
If you want to have a look at the actual code here is the relevant portion.
This link shows that when clear button is pressed the function clearFindForm is called which effectively refetches content (HTML fragment) using an ajax request and replaces the entire div in this jsp with the content fetched.
The refetchContent function works as below: Here is the link to the code in case that helps in giving a better answer.
function refetchContent(url, replaceTarget) {
$.ajax({
url: url,
data: {},
type: "GET",
success: function (response) {
replaceTarget.replaceWith(response);
},
error: function (response) {
showErrorMessage("Something went wrong. Please try again.");
}
});
}
While jQuery is very good at removing event listeners to DOM elements that are removed via it's methods (including .html() - just read the API: http://api.jquery.com/html/) - it won't remove event listeners to DOM elements that may still have a reference to them in a detached DOM tree.
For example, if you do something like this:
$.ajax({
....
})
.done(function(response,status,jqXHR) {
//create a detached DOM tree
form = $(response)
//add an event listener to the detached tree
form.find('#someIDInTheResponse').on('submit',function() {
});
//add the form to the html
$('#someID').html(form);
});
//at some other point in the code
$('#someIDInTheResponse').remove();
Note that in the above example, despite the fact that you removed the element from the DOM, the listener will not be removed from memory. This is because the element still exists in memory in a detached DOM tree accessible via the global variable "form" (this is because I didn't create use "var" to create the initial detached DOM tree in the scope of the done function....there are some nuances and jQuery can't fix bad code, it can only do it's best.
2 other things:
Doing everything inside callbacks or event listeners (like do this on a button click) turns into real bad spaghetti code really fast and becomes unmanageable rather quickly. Try and separate application logic from UI interaction. For example, don't use callbacks to click events to perform a bunch of logic, use callbacks to click events to call functions that perform a bunch of logic.
Second, and somewhat less important, (I welcome feedback on this perspective via comments) I would deem 30MB of memory to be a fairly high baseline for a web app. I've got a pretty intensive google maps web app that hits 30MB after an hour or so of intensive use and you can really notice start to notice it's sluggishness when you do. Lord knows what it would act like if it ever hit 60MB. I'm thinking IE<9 would become virtually unusable at this point, although, like I said, I welcome other people's feedback on this idea.
I wonder if you are simply not unbinding/removing the previously bound event listeners when you replace fragments?
I briefly looked at the specific sections of code you linked to in your updated question, but didn't see any event listener binding other than what you are doing in document ready, so I'm guessing you are doing some additional binding when you replace the document fragments. I'm not a jQuery expert, but in general binding or assigning additional event listeners does not replace previously bound/assigned event listeners automatically.
My point is that you should look to see if you are doing binding via "click()" (or via some other approach) to existing elements without unbinding the existing event listener first.
You might take a look at moff's answer to this question, which provides an example for click, specifically.
I can't add a comment because of reputation but to respond to what Adam is saying...
To summarise the case Adam presents, it's potentially nothing to do with jQuery, the problem may be within normal Javascript. However you don't present enough code for anyone to really get to the bottom of the problem. Your usage of scoped encapsulation may be perfectly fine and the problem may be else where.
I would recommend that you search for tools for finding the cause of memory leaks (for example, visualising/traversing the entire object/scope/reference/function tree, etc).
One thing to watch out for with jQuery are plugins and global insertions into the DOM! I've seen many JS libs, not just jQuery plugins, fail to provide destroyers and cleanup methods. The worst offenders are often things with popups and popouts such as date pickers, dialogs, etc that have a nasty habit of appending layer divs and the like into body without removing them after.
Something to keep in mind if that a lot of people just get as far as to make things construct but don't handle destruct especially in JS because they expect still even in this day and age that you will be serving normal webpages. You should also check plugins for destroy methods because not all will hook onto a remove event. Events are also used in a messy fashion in jQuery by others so a rogue handler might be halting the execution of following cleanup events.
In summary, jQuery is a nice robust library but be warned, just because someone depends on it does not mean it inherits jQuery's quality.
Out of curiosity... have you checked the listeners on document.ready? Maybe you need to manually GC those.