Memory leak with angularJS and jQuery / angular bootstrap - javascript

I have a ng-repeat list that updates every minute. Its a list of cards that contains stuff like title, description, dates and so on.
In those cards there's also a angular-ui-bootstrap popover which i use to display comments on those cards.
When the list updates, the popover will keep some reference which creates a lot of detached dom elements.
Heres some of the code.
The directive i use.
.directive('mypopover', function ($compile, $templateCache) {
var getTemplate = function (contentType) {
var template = '';
switch (contentType) {
case 'user':
template = $templateCache.get("templateId.html");
break;
}
return template;
}
return {
restrict: "A",
link: function ($scope, element, attrs) {
var popOverContent;
popOverContent = getTemplate("user");
popOverContent = $compile("<span>" + popOverContent + "</span>")($scope);
var options = {
content: popOverContent,
placement: "bottom",
html: true,
trigger: "manual",
selector: '.fa-comment',
date: $scope.date,
animation: true
};
$(element).popover(options).on("mouseenter", function () {
var _this = this;
$(this).popover("show");
$('.popover').linkify();
$(".popover").on("mouseleave", function () {
$(this).popover('destroy');
$('.popover').remove();
});
}).on("mouseleave", function () {
var _this = this;
setTimeout(function () {
if (!$(".popover:hover").length) {
$(this).popover('destroy');
$('.popover').remove();
}
}, 350);
});
var destroy = function () {
$(element).popover('destroy');
}
$scope.$on("$destroy", function () {
destroy();
});
}
}
})
from the html..
The bo-something is the just a one way bind i use instead of the normal double bind from angular
<a bo-href="c.ShortUrl" target="_blank" bindonce ng-repeat="c in cards | filter:searchText | limitTo: limitValue[$index] track by c.Id">
<div class="panel detachable-card">
<div class="panel-body" bo-class="{redLabel: c.RedLabel, orangeLabel: c.OrangeLabel}">
<!-- Comments if any -->
<script type="text/ng-template" id="templateId.html">
<div ng-repeat="comment in c.Comment track by $index">
<strong style="margin-bottom: 20px; color:#bbbbbb; white-space: pre-wrap;">{{c.CommentMember[$index]}}</strong>
<br />
<span style="white-space: pre-wrap;">{{comment}}</span>
<hr />
</div>
</script>
<span bo-if="c.Comment" data-container="body" mypopover style="float:right"><i class="fa fa-comment fa-lg"></i></span>
<!-- Card info -->
<strong style="font-size:12px; color:#999999"><span bo-if="!c.BoardNameOverride" bo-text="c.NameBoard"></span> <span bo-if="c.BoardNameOverride" bo-text="c.BoardNameOverride"></span></strong>
<br />
<strong bo-text="c.Name"></strong>
<br />
<span bo-if="c.Desc" bo-text="c.Desc"><br /></span>
</div>
</div>
</a>
Heres a heap-snapshot of the site after one update.
http://i.stack.imgur.com/V4U1O.png
So Im fairly bad at javascript in general, and i have my doubts about the directive. I would have thought that the .popover('destroy') would remove the reference, but it doesnt seem to..
Any help is greatly appreciated..

Why are you constantly destroying the popup over and over? There is no need to destroy the popup every time the mouse is moved. Just show and hide the popup. It's much nicer on memory than constantly destroying and recreating the popup.
But, what you may not realize is that bootstrap components don't play well with AngularJS. Bootstrap components weren't architected in ways that allow the content within them to be updated easily which poses problems when you use them with AngularJS because the update model is built into the framework. And that's why the AngularUI project rewrote their Javascript components from the ground up in AngularJS so they behave as you would expect. I think you'll find those much easier to use.
http://angular-ui.github.io/bootstrap/
If you are using bootstrap 2.3 AngularUI v0.8 was the last version supporting bootstrap v2.3.

Related

How to fire function after v-model change?

I have input which I use to filter my array of objects in Vue. I'm using Salvattore to build a grid of my filtered elements, but it doesn't work too well. I think I have to call rescanMediaQueries(); function after my v-model changes but can't figure how.
Here is my Vue instance:
var articlesVM = new Vue({
el: '#search',
data: {
articles: [],
searchInput: null
},
ready: function() {
this.$http.get('posts').then(function (response) {
this.articles = response.body;
});
}
});
And here is how I have built my search
<div class="container" id="search">
<div class="input-field col s6 m4">
<input v-model="searchInput" class="center-align" id="searchInput" type="text" >
<label class="center-align" for="searchInput"> search... </label>
</div>
<div id="search-grid" v-show="searchInput" data-columns>
<article v-for="article in articles | filterBy searchInput">
<div class="card">
<div class="card-image" v-if="article.media" v-html="article.media"></div>
<div class="card-content">
<h2 class="card-title center-align">
<a v-bind:href="article.link">{{ article.title }}</a>
</h2>
<div class="card-excerpt" v-html="article.excerpt"></div>
</div>
<div class="card-action">
<a v-bind:href="article.link"><?php _e('Read More', 'sage'); ?></a>
</div>
</div>
</article>
</div>
I did get the grid system working by adding watch option to my Vue, but every time I wrote something to my input and then erase it my filterBy method wouldn't work at all. It didn't populate any data even if I tried to retype the same keyword as earlier. Here is the watch option I used:
watch: {
searchInput: function (){
salvattore.rescanMediaQueries();
}
}
I think your problem is with the scoping of this in your success handler for http. Your articles object in Vue component is not getting any values from your http.get(..) success handler.
Inside your ready function, your http success handler should be as follows:
this.$http.get('posts').then(response => {
this.articles = response.body; // 'this' belongs to outside scope
});`
Alternatively you can also do:
var self = this; // self points to 'this' of Vue component
this.$http.get('posts').then(response => {
self.articles = response.body; // 'self' points to 'this' of outside scope
});`
Another similar issue: https://stackoverflow.com/a/40090728/654825
One more thing - it is preferable to define data as a function, as follows:
var articlesVM = new Vue({
el: '#search',
data: function() {
return {
articles: [],
searchInput: null
}
},
...
}
This ensures that your articles object is unique to this instance of the component (when you use the same component at multiple places within your app).
Edited after comment #1
The following code seems to work alright, the watch function works flawlessly:
var vm = new Vue({
el: '#search',
template: `<input v-model="searchInput" class="center-align" id="searchInput" type="text" >`,
data: {
searchInput: ""
},
watch: {
searchInput: function() {
console.log("searchInput changed to " + this.searchInput);
}
}
})
The input in template is an exact copy of your version - I have even set the id along with v-model, though I do not see the reason to set an id
Vue.js version: 2.0.3
I am unable to see any further, based on details in the question. Can you check if your code matches with the one above and see if you can get the console debugging messages?
Edited after comment #4, #5
Here is another thought which you need to verify:
Role of vue.js: Render the DOM
Role of salvattore plugin: Make the DOM layouts using CSS only
Assuming the above is true for salvattore plugin, and hopefully it does not mess with vue.js observers / getters / setters, then you can do the following: provide a time delay of about 50 ms so that vue.js completes the rendering, and then call the salvattore plugin to perform the layouts.
So your watch function needs to be as follows:
watch: {
searchInput: function (){
setTimeout(function(){
salvattore.rescanMediaQueries();
}, 50);
}
}
Alternatively you may also use Vue.nexttick() as follows:
Vue.nextTick(function () {
// DOM updated
})
The nextTick is documented here: https://vuejs.org/api/#Vue-nextTick
I do not know if you may need to provide a little bit of extra time for salvattore plugin to start the layouts, but one of the above should work out.
Let me know if it works!

How to pass a DOM element to "ng-disabled"

All,
How can I pass the current DOM element to the Angular directive "ng-disabled"?
I do know that messing w/ the DOM in Angular is bad practice. But I can't think of another - simple - way to do this. Here is my problem:
I have a button that updates a scope variable when clicked:
<button ng-click="target_model.display_detail=true">click me</button>
Elsewhere in my template there is code that watches "target_model.display_detail" - when it is true it displays a modal-dialog which includes an Angular directive which gets some data from the server and populates a form which includes another button like the one above.
The data structure that I am working w/ is potentially recursive; there are loads of nested "target_models". So it is possible for a button in a modal-dialog to point a target_model whose form has already been created. In that case, I just want to disable the button. I'd like todo something like:
<button ng-disabled="ancestor_model_exists(the_current_element, target_model.some_unique_id)">click me</button>
Where "ancestor_model_exists" is a function that would check the DOM to see if there is an ancestor element with a particular id. But how do I know which element to start from?
You're approaching DOM manipulations imperatively - the jQuery way, not declaratively - the Angular way.
DOM manipulation is fine... inside directives. You don't do it in controllers, where you presumably defined that function.
When you get a chance, try to get away with 0 calls to $ in a sandbox to force you to learn how to do things the Angular way - not because it's "better" in an absolute way - it's just generally better to first learn the toolkit and recommended approaches before doing it your way, anyway.
This should do what you want, except maybe searching beyond multiple ancestors (but I mention how to do that if you need that):
https://plnkr.co/edit/7O8UDuqsVTlH8r2GoxQu?p=preview
JS
app.directive('ancestorId', function() {
return {
restrict: 'A',
controller: 'AncestorIdController',
require: ['ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// If you wanted to use an expression instead of an
// interpolation you could define an isolate scope on this
// directive and $watch it.
attrs.$observe('ancestorId', function(value) {
ancestorIdController.setId(value);
});
}
}
});
app.controller('AncestorIdController', function() {
this.getId = _getId;
this.setId = _setId;
var id;
function _getId() {
return id;
}
function _setId(value) {
id = value;
}
});
app.directive('disableForAncestorId', function() {
return {
restrict: 'A',
require: ['?^ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// Check to make sure the ancestorId is a parent.
if (ancestorIdController) {
scope.$watch(function() {
var watch = {
target: ancestorIdController.getId(),
actual: attrs.disableForAncestorId
};
return watch;
}, function(value) {
if (value.target === value.actual) {
element.attr('disabled', 'disabled');
} else {
element.removeAttr('disabled');
}
}, true /* Deep watch */ );
}
}
}
});
HTML
<!-- The simple happy path. -->
<div ancestor-id="A">
<button disable-for-ancestor-id="A">'A' === 'A' ?</button>
</div>
<!-- require will match the 'B' before the 'A' because it's closer.
if you need to match any parent you could use a third coordinating
directive. -->
<div ancestor-id="A">
<div ancestor-id="B">
<button disable-for-ancestor-id="A">'B' === 'A' ?</button>
</div>
</div>
<!-- require lets you freely change the DOM to add extra elements separating
you from what you're looking for.-->
<div ancestor-id="B">
<div>
<div>
<button disable-for-ancestor-id="B">'B' === 'B' ?</button>
</div>
</div>
</div>
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div>
<button disable-for-ancestor-id="B">'B' === undefined ?</button>
</div>
<br>
Dynamic AncestorId test (it will be disabled if the text fields are equal):
<br>
Target AncestorId <input ng-model="targetAncestorId">
<br>
Actual Ancestor <input ng-model="actualAncestorId">
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div ancestor-id="{{ targetAncestorId }}">
<button disable-for-ancestor-id="{{ actualAncestorId }}">'{{ actualAncestorId }}' === '{{ actualAncestorId }}' ?</button>
</div>
It never fails... posting a question on Stack Overflow always makes me realize the answer just minutes later.
The following code gets me pretty close:
template.html:
<button ng-click="display_if_ancestor_model_exists($event, target_model)">click me</button>
app.js:
$scope.display_if_ancestor_model_exists = function($event, target_model) {
var ancestor_form = $($event.target).closest("#" + target_model.some_unique_id);
if (ancestor_form.length) {
/* don't show the modal-dialog */
display_msg("This form already exists!");
}
else {
target_model.display_detail = true;
}
};
Of course, I would rather the button just be disabled but I can live w/ this solution for now.

Angulartics to track impressions on Bootstrap carousel items

In my Angular app, we have a bootstrap carousel (using bootstrap carousel rather than ui bootstrap carousel for some reasons), items structure as follows
<div class="item" analytics-on analytics-event="IMPRESSIONS" analytics-category="{{--}}" analytics-label="{{--}}" ng-repeat="banner in vm.bannerList">
<a ng-href="{{--}}" analytics-on analytics-event="CLICK" analytics-category="{{--}}" analytics-label="{{--}}">
<div class="fill" style="background-image: url({{--}});"></div>
</a>
</div>
The click event working fine. But how to track the IMPRESSIONS. The impression event need to trigger when a carousel item becomes active.
I tried to watch the 'active' class using a custom directive but the watch only worked on load time.
Tried and succeeded,
Following custom directive did the job.
//ng-track-carousel-impressions
angular.module('app').directive('ngTrackCarouselImpressions', ['$analytics',function (analytics) {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
// create an observer instance
var observer = new MutationObserver(function (mutations) {
scope.$apply(function () {
if (element.hasClass('active')) {
//console.log(element.attr('analytics-label'));
// emit event track (with category and label properties for GA)
analytics.eventTrack(element.attr('analytics-event'), {
category: element.attr('analytics-category'), label: element.attr('analytics-label')
});
}
});
});
// configuration of the observer:
var config = {
attributes: true
};
// pass in the target node, as well as the observer options
var node = element.get(0);
observer.observe(node, config);
}
}
}]);
Usage
<div class="item" analytics-event="IMPRESSIONS" analytics-category="{{--}}" analytics-label="{{--}}" ng-repeat="banner in vm.bannerList" ng-track-carousel-impressions>
<a ng-href="{{--}}" analytics-on analytics-event="CLICK" analytics-category="{{--}}" analytics-label="{{--}}">
<div class="fill" style="background-image: url({{--}});"></div>
</a>
</div>

Rich tabs and transcluded content

I currently have this problem: I made 3 directives (check plunkr for a 'reduced' test case, dont mind the closures they come from Typescript) that control tabs, using a controller to keep them grouped, since I can have more than one tabbed content on the current view. The problem happens when the tab itself has some bindings that are located outside of the scope, and when the tab is 'transcluded' in place, the binding never updates because the scope is different.
Here is the plunkr http://plnkr.co/edit/fnw1oV?p=preview and here is the "tab transclude" part that is important
this.link = function (scope, element, attr) {
var clickEvent, el, item;
item = scope.item;
console.log(scope);
el = item.element.filter('.is-tab');
el.addClass('picker-tabs-item');
clickEvent = 'tabs.select(item);';
if (el.is('[ng-click]')) {
clickEvent += el.attr('ng-click') + ';';
}
el.attr('ng-click', clickEvent);
el.attr('ng-class', 'tabs.classes(item)');
item.element = $compile(item.element)(scope);
element.replaceWith(item.element);
};
The current approach feels hacky (keeping the original scope and element in an array). Plus in my app, the data is loaded after the tabs were loaded, so it can't even retain some initial state. The tabs look like this right now:
and the way it should look like (but doesn't work, as you can see clicking one tab select all of them):
A real tab code from my app:
<div class="clearfix" login-space user-space>
<div class="picker clearfix" ng-class="{'to-hide': user.data.get('incomplete') || checkout.checkoutForm.$invalid}">
<div class="pick-title icon icon-pay">Forma de Pagamento</div>
<div class="for-hiding">
<div tabs="pagamento">
<div tab="/templates/tabs/plans/credito" selected="true">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('credito')" type="button">
Cartão
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('credito')"></span>
</button>
</div>
<div tab="/templates/tabs/plans/debito">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('debito')" type="button">
Débito
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('debito')"></span>
</button>
</div>
<div tab="/templates/tabs/plans/boleto">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('boleto')" type="button">
Boleto
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('boleto')"></span>
</button>
</div>
</div>
</div>
</div>
</div>
login-space and user-space are directives just to assign it to the login and user controllers. checkout is the current controller inside ui-view.
$stateProvider.state('planos.checkout', {
url: '/checkout',
templateUrl: '/templates/partials/plans/checkout',
controllerAs: 'checkout',
controller: Controllers.Checkout,
data: {
allowed: false,
enclosed: true
}
});
since the checkout controller must be instantiated only once, I can't reinstantiate it, but still need to access it's functions and bound data.
'/templates/partials/plans/checkout' contains the tab code above (so yes, technically it's in the same scope as checkout controller)
In your plunker, changing your tabs to this:
<span data-subctrl="">{{ subctrl.sum('credito') }}</span>
Showed the sum. I looked at what subctrl was and you have it as a directive, that's why subctrl.sum is not working. Plunker with it working: http://plnkr.co/edit/qwEHMqfzJ6pC79hM8cDj?p=preview
If that's not what's wrong with your application, then please describe it a little more.
Solved this by removing the html content of the tab, and applying a different scope to the inner content, then reattaching to the original element.
this.link = function (scope, element, attr) {
var clickEvent, el, item;
item = scope.item;
console.log(scope);
el = item.element.filter('.is-tab');
var contents = el.html(); //added
el.empty(); // added
el.addClass('picker-tabs-item');
clickEvent = 'tabs.select(item);';
if (el.is('[ng-click]')) {
clickEvent += el.attr('ng-click') + ';';
}
el.attr('ng-click', clickEvent);
el.attr('ng-class', 'tabs.classes(item)');
item.element = $compile(item.element)(scope);
item.element.append($compile('<div>' + contents + '</div>')(item.scope)); //added
element.replaceWith(item.element);
};

Creating elements Dynamically in Angular

I have very little javascript experience. I need to add a menu on click of an item. We have been asked to build it from scratch without using any library like bootstrap compoments or JQuery.
We are using Angularjs. In angular I want to know the correct method to create new elements. Something like what we did not document.createElement.
I am adding some of the code for you guys to have a better idea what I want to do.
Menu Directive
.directive('menu', ["$location","menuData", function factory(location, menuData) {
return {
templateUrl: "partials/menu.html",
controller: function ($scope, $location, $document) {
$scope.init = function (menu) {
console.log("init() called");
console.log("$document: " + $document);
if (menu.selected) {
$scope.tabSelected(menu);
}
}
$scope.creteMenu = function(menuContent){
//This is to be called when the action is an array.
}
$scope.tabSelected = function(menu){
$location.url(menu.action);
$scope.selected = menu;
}
$scope.click = function (menu) {
if (typeof (menu.action) == 'string') {
$scope.tabSelected(menu);
}
}
},
link: function (scope, element, attrs) {
scope.menuData = menuData;
}
};
}])
Menu data in service.
.value('menuData', [{ label: 'Process-IDC', action: [] }, { label: 'Dash Board', action: '/dashboard', selected: true }, { label: 'All Jobs', action: '/alljobs', selected: false }, { label: 'My Jobs', action: '/myjobs', selected: false }, { label: 'Admin', action: '/admin', selected: false }, { label: 'Reports', action: '/reports', selected: false }]);
If you notice the action of Process-IDC menu is an array it will contain more menu with actions in it and it should be opened in a sub menu.
Menu.html (partial)
<ul class="menu">
<li ng-class="{activeMenu: menu==selected}" ng-init="init(menu)" data-ng-click="click(menu)" data-ng-repeat="menu in menuData">{{menu.label}}</li>
</ul>
A few things come to mind. First of all, are you sure you need to actually create the element on click? If you are doing to to show a fixed element on click then the better approach would be to generate the element as normal, but not show it until you click. Something like:
<div ng-click="show_it=true">Show item</div>
<div ng-show="show_it">Hidden until the click. Can contain {{dynamic}} content as normal.</div>
If you need it to be dynamic because you might add several elements, and you don't know how many, you should look at using a repeat and pushing elements into a list. Something like this:
<div ng-click="array_of_items.push({'country': 'Sparta'})">Add item</div>
<div ng-repeat="item in array_of_items"> This is {{item.country}}</div>
Each click of the "Add item" text here will create another div with the text "This is Sparta". You can push as complex an item as you want, and you could push an item directly from the scope so you don't have to define it in the template.
<div ng-click="functionInControllerThatPushesToArray()">Add item</div>
<div ng-repeat="item in array_of_items"> This is {{item.country}}</div>
If neither of those options would work because it is a truly dynamic object, then I would start looking at using a directive for it like others have suggested (also look at $compile). But from what you said in the question I think a directive would be to complicate things needlessly.
I recommend you read the ngDirective and the angular.element docs.
Hint: angular.element has an append() method.
This is both really simple, but some what complex if you don't know where to start - I really recommend looking at the Tutorial, and following it end to end: http://docs.angularjs.org/tutorial/ - As that will introduce you to all the concepts around Angular which will help you understand the technical terms used to describe the solution.
If you're creating whole new menu items, if in your controller your menu is something like:
// An Array of Menu Items
$scope.menuItems = [{name: 'Item One',link: '/one'},{name: 'Item Two',link:'/two'}];
// Add a new link to the Array
$scope.addMenuItem = function(theName,theLink){
$scope.menuItems.push({name: theName,link:theLink});
}
And in the template, use the array inside ng-repeat to create the menu:
<ul>
<li ng-repeat="menuItem in menuItems">{{menuItem.name}}</li>
</ul>
If you just want to toggle the display of an item that might be hidden, you can use ng-if or ng-show
Assuming that you are doing it in a directive and you have angular dom element, you can do
element.append("<div>Your child element html </div>");
We can use $scope in App Controller to create Div Elements and then we can append other Div elements into it similarly.
Here's an Example:
$scope.div = document.createElement("div");
$scope.div.id = "book1";
$scope.div.class = "book_product";
//<div id="book1_name" class="name"> </div>
$scope.name = document.createElement("div");
$scope.name.id = "book1_name";
$scope.name.class= "name";
// $scope.name.data="twilight";
$scope.name.data = $scope.book.name;
$scope.div.append($scope.name);
console.log($scope.name);
//<div id="book1_category" class="name"> </div>
$scope.category = document.createElement("div");
$scope.category.id = "book1_category";
$scope.category.class= "category";
// $scope.category.data="Movies";
$scope.category.data=$scope.book.category;
$scope.div.append($scope.category);
console.log("book1 category = " + $scope.category.data);
//<div id="book1_price" class="price"> </div>
$scope.price = document.createElement("div");
$scope.price.id = "book1_price";
$scope.price.class= "price";
// $scope.price.data=38;
$scope.price.data=$scope.book.price;
$scope.div.append($scope.price);
console.log("book1 price = " + $scope.price.data);
//<div id="book1_author" class="author"> </div>
$scope.author = document.createElement("div");
$scope.author.id = "book1_author";
$scope.author.class= "author";
// $scope.author.data="mr.book1 author";
$scope.author.data=$scope.book.author;
$scope.div.append($scope.author);
console.log("book1 author = " + $scope.author.data);
//adding the most outer Div to document body.
angular.element(document.getElementsByTagName('body')).append($scope.div);
For more illustration, Here each book has some attributes (name, category, price and author) and book1 is the most outer Div Element and has it's attributes as inner Div elements.
Created HTML element will be something like that

Categories