Angular how to correctly destroy directive - angularjs

I have a 'regionMap' directive that includes methods for rendering and destroying the map. The map is rendered inside of a modal and upon clicking the modal close button the 'regionMap' destroy method is called, which should remove the element and scope from the page. However, when returning to the modal page, that includes the 'region-map' element, the previous 'region-map' element is not removed, resulting in multiple maps being displayed. What is the correct way to remove the regionMap directive from the page when the modal is closed?
// directive
(function(){
'use strict';
angular.module('homeModule')
.directive('regionMap', regionMap);
function regionMap() {
var directive = {
restrict: 'E',
template: '',
replace: true,
link: link,
scope: {
regionItem: '=',
accessor: '='
}
}
return directive;
function link(scope, el, attrs, controller) {
if (scope.accessor) {
scope.accessor.renderMap = function(selectedRegion) {
var paper = Raphael(el[0], 665, 245);
paper.setViewBox(0, 0, 1100, 350, false);
paper.setStart();
for (var country in worldmap.shapes) {
paper.path(worldmap.shapes[country]).attr({
"font-size": 12,
"font-weight": "bold",
title: worldmap.names[country],
stroke: "none",
fill: '#EBE9E9',
"stroke-opacity": 1
}).data({'regionId': country});
}
paper.forEach(function(el) {
if (el.data('regionId') != selectedRegion.name) {
el.stop().attr({fill: '#ebe9e9'});
} else {
el.stop().attr({fill: '#06767e'});
}
});
}
scope.accessor.destroyMap = function() {
scope.$destroy();
el.remove();
}
}
}
}
})();
// controller template:
<region-map accessor="modalvm.accessor" region-item="modalvm.sregion"></region-map>
// controller:
vm.accessor = {};
...
function showMap() {
$rootScope.$on('$includeContentLoaded', function(event) {
if (vm.accessor.renderMap) {
vm.accessor.renderMap(vm.sregion);
}
});
function closeMap() {
if (vm.accessor.destroyMap) {
vm.accessor.destroyMap();
}
$modalInstance.dismiss('cancel');
}

The issue is related to loading a template with a directive inside of it. Fixed it by adding a var to check if the map has previously been rendered:
vm.accessor.mapRendered = false;
$rootScope.$on('$includeContentLoaded', function(event) {
if (vm.accessor.renderMap && !vm.accessor.mapRendered) {
vm.accessor.renderMap(vm.selectedRegions);
vm.accessor.mapRendered = true;
}
});

Related

How to get async html attribut

I have a list of items retreived by an async call and the list is shown with the help of ng-repeat. Since the div container of that list has a fixed height (400px) I want the scrollbar to be at the bottom. And for doing so I need the scrollHeight. But the scrollHeight in postLink is not the final height but the initial height.
Example
ppChat.tpl.html
<!-- Height of "chatroom" is "400px" -->
<div class="chatroom">
<!-- Height of "messages" after all messages have been loaded is "4468px" -->
<div class="messages" ng-repeat="message in chat.messages">
<chat-message data="message"></chat-message>
</div>
</div>
ppChat.js
// [...]
compile: function(element) {
element.addClass('pp-chat');
return function(scope, element, attrs, PpChatController) {
var messagesDiv;
// My idea was to wait until the messages have been loaded...
PpChatController.messages.$loaded(function() {
// ...and then recompile the messages div container
messagesDiv = $compile(element.children()[0])(scope);
// Unfortunately this doesn't work. "messagesDiv[0].scrollHeight" still has its initial height of "400px"
});
}
}
Can someone explain what I missed here?
As required here is a plunk of it
You can get the scrollHeight of the div after the DOM is updated by doing it in the following way.
The below directive sets up a watch on the array i.e. a collection, and uses the $timeout service to wait for the DOM to be updated and then it scrolls to the bottom of the div.
chatDirective.$inject = ['$timeout'];
function chatDirective($timeout) {
return {
require: 'chat',
scope: {
messages: '='
},
templateUrl: 'partials/chat.tpl.html',
bindToController: true,
controllerAs: 'chat',
controller: ChatController,
link: function(scope, element, attrs, ChatController) {
scope.$watchCollection(function () {
return scope.chat.messages;
}, function (newValue, oldValue) {
if (newValue.length) {
$timeout(function () {
var chatBox = document.getElementsByClassName('chat')[0];
console.log(element.children(), chatBox.scrollHeight);
chatBox.scrollTop = chatBox.scrollHeight;
});
}
});
}
};
}
The updated plunker is here.
Also in your Controller you have written as,
var Controller = this;
this.messages = [];
It's better to write in this way, here vm stands for ViewModel
AppController.$inject = ['$timeout'];
function AppController($timeout) {
var vm = this;
vm.messages = [];
$timeout(
function() {
for (var i = 0; i < 100; i++) {
vm.messages.push({
message: getRandomString(),
created: new Date()
});
}
},
3000
);
}

Multiple directives on same page execute all at once

I've built a directive to create a toggle menu and I have problem with it when using the same diretive multiple times on the same page.
This is the directive:
function menuTrigger($document) {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
var
menuOpen = false,
elButton = angular.element(document.querySelectorAll(".menu-button")),
elContent = angular.element(document.querySelectorAll(".menu-content")),
elClose = angular.element(document.querySelectorAll("[menu-close]"));
var
pos = attrs.pos,
style;
if (pos == 'tl') {
style = {top: '0', left: '0', 'transform-origin': 'top left'}
} else if (pos == 'tr') {
style = {top: '0', right: '0', 'transform-origin': 'top right'}
} else if (pos == 'bl') {
style = {bottom: '0', left: '0', 'transform-origin': 'bottom left'}
} else if (pos == 'br') {
style = {bottom: '0', right: '0', 'transform-origin': 'bottom right'}
};
element.bind('click', function(e) {
e.stopPropagation();
openMenu();
});
elClose.bind('click', function(e) {
e.stopPropagation();
closeMenu();
});
$document.on('click', function () {
if (menuOpen == true) {
closeMenu();
};
});
function openMenu() {
menuOpen = true;
elContent.removeClass('menu-hide');
elContent.css(style);
setTimeout(function(){
elContent.addClass('menu-open');
}, 100);
};
function closeMenu() {
menuOpen = false;
elContent.removeClass('menu-open');
setTimeout(function(){
elContent.addClass('menu-hide');
elContent.removeAttr('style');
}, 400);
};
}
};
}
So, for example, if I'm using 1 menu on a main view, let's say the top navbar and then in a sub view I have other menu to control a selection, when I click on one menu, both of them will open.
How can i solve this issue?
As requested an example on how to require parent directive controllers. That should enable you to use less jQuery style code.
myModule.directive('myParentDirective', function(){
return {
controller: function(){
var vm = this;
vm.foo = 'bar';
}
};
});
myModule.directive('myChildDirective', function(){
return {
require: 'myParentDirective',
link: function(scope, elem, attrs, parentController){
console.log(parentController.foo); // equals 'bar'
}
};
});
<my-parent-directive>
<my-child-directive></my-child-directive
</my-parent-directive>
You're binding multiple times to multiple elements on your page:
// This will be an array of elements that will match the class .menu-button.
// Not just the .menu-button element within your directive.
// Try typing it in your browser developer tools console to see what I mean.
angular.element(document.querySelectorAll(".menu-button"))
If you really want to grab the individual elements from within the directive, you'll need to locate them like this:
// Use the element argument from the link function
angular.element(element[0].querySelectorAll(".menu-button"));
But -- in most cases it's easier (and more elegant) to use ng-click, ng-class directives and such. Just create your click handlers on the scope object in the directive link function and wire them in the html markup.
scope.myClickHandler = function() {
// Magic goes here
};
<div my-directive ng-click="myClickHandler"></div>
Hope this helps.

How to bind events in angular directive

I am new to angular and having problem to bind an click event to my directive.
I have an ul-list with a links in each li. When I click a link I want to do a service call that adds or removes the clicked items ID and refresh the list.
When refreshed, each list items will show if an id is "marked" or not.
Can anyone help me?
html view:
<a href="#" class="showbooklist" qtip="12568">
<img src="image.png">
</a>
Directive:
listControllers.directive('qtip', ['boklistorservice', function (boklistorservice) {
return {
restrict: 'A',
controller: ["$scope", "$attrs","$element", "boklistorservice", function ($scope, $attrs,$element, boklistorservice) {
boklistorservice.getdata().then(function (data) { //serice call to gett data
$scope.booklist = data;
$element.qtip({ //use the jquery.tip2.js tooltip plugin
content: {
text: getcurrentbooklist($attrs.qtip, data.barnenskrypin.booklistor)
},
position: {
my: 'bottom center',
at: 'top center'
},
hide: {
fixed: true,
delay: 300
}
});
})
}]
};
}]);
//function returns a string. An ul to show in the toolbox
var getcurrentbooklist = function (bookid, arr) {
var rettext = "<ul>";
$.each(arr, function (item, val) {
item;
var inlist = false;
$.each(val.bookitems, function (i, v) {
if (v.bookid == bookid) {
inlist = true;
return false;
} else {
inlist = false;
}
});
if (inlist) {
rettext += "<li><a (NEED A CLICK EVENT HERE and pass bookid) > " + val.booklistnamn + "-- MARK </a></li>";
} else {
rettext += "<li><a (NEED A CLICK EVENT HERE and pass bookid) >" + val.booklistnamn + "--</a></li>";
}
});
rettext += "</ul>";
return rettext;
};
Use the link function of a directive (code untested):
listControllers.directive('qtip', ['boklistorservice', function (boklistorservice) {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
element.bind('click', function () {
//Do your work here
});
},
controller: ["$scope", "$attrs","$element", "boklistorservice", function ($scope, $attrs,$element, boklistorservice) {
boklistorservice.getdata().then(function (data) { //serice call to gett data
$scope.booklist = data;
$element.qtip({ //use the jquery.tip2.js tooltip plugin
content: {
text: getcurrentbooklist($attrs.qtip, data.barnenskrypin.booklistor)
},
position: {
my: 'bottom center',
at: 'top center'
},
hide: {
fixed: true,
delay: 300
}
});
})
}]
};
}]);
jquery.tip2 adds the html to body so you need to find the content in body and add the click event
$element.qtip();
angular.element(document.body).find('[qtip container id or class or element] a').bind('click', function () {
//Do your work here
});

AngularJs: Run function when any element in section is blurred

I've created the following directive:
.directive('onSectionBlur', function ($parse) {
return {
restrict: 'A',
controller: function ($scope, $element, $attrs) {
$element.focusout(function (event) {
if (!jQuery.contains($element[0], event.relatedTarget)) {
$scope.$apply($parse($attrs.onSectionBlur)($scope));
}
});
}
};
})
My goal here is if a user tabs out of a section of a form (or clicks elsewhere), I want to display a read-only version of that data: http://jsfiddle.net/uZBXw/3/
So this works from what I can tell, but I feel like I was just mashing buttons on this line:
$scope.$apply($parse($attrs.onSectionBlur)($scope));
Is this the correct way to run code and wire it into the angular lifecycle?
I think you should use an isolated scope with an attribute marked with &. This will give you access to a function that will run on the parent scope and is the exact use case of what you're trying to do.
app.directive('onSectionBlur', function () {
return {
restrict: 'A',
scope: {
'notify': '&onSectionBlur' // reuse the directive name for easier handling
},
link: function (scope, element) {
element.on('focusout', function (evt) {
if (!angular.element.contains(element[0], evt.relatedTarget)) {
scope.$apply(scope.notify); // let $apply call the notify-callback
}
});
}
};
});
demo: http://jsbin.com/diwetaje/1/
from the Developer Guide:
Best Practice: use &attr in the scope option when you want your directive to expose an API for binding to behaviors.
I was having issues with clicking on various items in the section (i.e. checkbox labels), so if anyone else runs across this issue I've added a potential enhancement to Yoshi's version:
.directive('onSectionBlur', function ($document) {
return {
restrict: 'A',
scope: {
'notify': '&onSectionBlur'
},
link: function (scope, element) {
var hasFocus = false;
element.on('focusin', function (evt) {
hasFocus = true;
});
$document.on('click focusin', function (evt) {
if (hasFocus && !angular.element.contains(element[0], evt.target)) {
hasFocus = false;
scope.$apply(scope.notify);
}
});
}
};
});
EDIT: Here's the butchered up version I ended up with, that takes into account buttons that weren't clickable (if they were outside the section and below it) as well as not firing the event if the user has a modal window open:
link: function (scope, element) {
var hasFocus = false;
var lostFocus = function () {
hasFocus = false;
scope.$apply(scope.notify);
};
element.on('focusin', function (evt) {
hasFocus = true;
});
element.on('keydown', function (evt) {
if (hasFocus && evt.keyCode == 9) {
//Using timeout to give the browser time to process what it should have been doing (i.e. focusing next item)
if (evt.shiftKey && element.find(':focusable:first').is(evt.target)) {
$timeout(lostFocus);
} else if (element.find(':focusable:last').is(evt.target)) {
$timeout(lostFocus);
}
}
});
var docHandler = function (evt) {
//If the click came from inside of a modal window, ignore it
if (angular.element(evt.target).closest('.modal').length == 0) {
if (hasFocus && !angular.element.contains(element[0], evt.target)) {
lostFocus();
}
}
};
$document.on('click', docHandler);
scope.$on('$destroy', function () {
$document.off('click', docHandler);
});
}

Unable to watch/observe scope?

<button-large color="green" click="createWorkstation()" busy="disableSave()" busyLabel="Saving...">Save</button-large>
I'm not able to watch changes to the output of disableSave(). The console.log()'s shown in my directive are never triggered when the output of .busy is changed. What am I doing wrong?
directive('buttonLarge', function () {
return {
scope: {
busy: '&',
click: '&'
},
replace: true,
restrict: 'E',
transclude: true,
template: '<button class="buttonL" ng-transclude/>',
link: function (scope, element, attrs) {
//when the button is busy, disable the button
if (angular.isDefined(scope.busy())) {
scope.$watch(scope.busy(), function () {
console.log('watched');
});
attrs.$observe(scope.busy(), function () {
console.log('observed');
});
}
//setup click event - https://groups.google.com/forum/#!topic/angular/-uVE5WJWwLA
if (angular.isDefined(scope.click)) {
element.bind('click', scope.click);
}
}
}
})
Controller
$scope.newWorkstationDialog = function (workflowProcess) {
var d = $dialog.
dialog({
resolve: {
workflowProcess: function () {
return workflowProcess;
}
}
}).
open('/partials/admin/'+workflowProcess.entity.slug+'/setup.htm', ['$scope', 'dialog', ..., function ($scope, dialog, ...) {
$scope.saving = false;
/* Create the workstation */
$scope.createWorkstation = function () {
console.log('saving');
$scope.saving = true;
$timeout(function () {
$scope.saving = false;
console.log('stopped saving');
}, 1000);
}
//Should the save button be disabled?
$scope.disableSave = function () {
return $scope.saving;//|| $scope.form.$valid;
}
$scope.cancel = function () {
dialog.close();
}
}]);
}
Your syntax of watching in not correct .You should be not using scope when doing watch because internally it use $parse service which internally attach scope . So you need to modify your code as below
1st option
scope.$watch(function(){
return scope.busy()
}, function (newvalue,oldvalue) {
console.log('watched');
});
2nd option
scope.$watch('busy()', function (newvalue,oldvalue) {
console.log('watched');
});

Resources