AngularJS - scope watch is not triggered athough ng-class has been updated - angularjs

I am trying to implement a dropdown mega-menu using Angular, where if the user clicks on Link 1, then the content for Link 1 should appear. My directives are as followed:
mobile-menu which acts as a controller and keeping track of the links and menu items states
menu-link, the actual link that user clicks on to open/close the menu items
menu-item, the menu item that should show/hide based on the scope.test value
Here is my AngularJS code:
angular.module("myApp", [])
.controller("testCtrl", function ($scope) {})
.directive("mobileMenu", function () {
return {
controller: function ($scope) {
this.menuLinks = [];
this.menuItems = [];
this.addMenuLink = function (l) {
this.menuLinks.push(l);
};
this.addMenuItem = function (m) {
this.menuItems.push(m);
};
// Function to close all other menu items if they are open.
// This is because only one menu item can be active at a time
this.closeOthers = function (selectedMenuLink) {
angular.forEach(this.menuLinks, function (l) {
if (l !== selectedMenuLink) {
l.selected = false;
}
});
angular.forEach(this.menuItems, function (m) {
if (selectedMenuLink.target == m.menuId) {
m.test = true;
} else {
m.test = false;
}
});
};
}
};
}).directive("menuLink", function () {
return {
require: "^mobileMenu",
scope: {},
link: function(scope, element, attrs, menuController) {
scope.selected = false;
menuController.addMenuLink(scope);
scope.$watch('selected', function(newValue, oldValue) {
if (oldValue === newValue) {return};
if (newValue) {
scope.target = angular.element(element[0].children[0]).attr("data-menu");
menuController.closeOthers(scope);
}
});
}
};
}).directive("menuItem", function () {
return {
require: "^mobileMenu",
scope: true,
link: function (scope, element, attrs, menuController) {
scope.test = false;
scope.menuId = attrs.id;
menuController.addMenuItem(scope);
scope.$watch('test', watchLink);
scope.$watch(attrs.collapse, watchLink);
scope.$watch(function () {
return scope.test;
}, watchLink);
var watchLink = function (newValue, oldValue) {
// Initializing for the first time, do nothing
if (newValue === oldValue) return;
// If the collapse attribute has a true value, collapse this element
if (newValue) {
collapse();
} else {
expand();
}
};
// Helper function to collapse the element
var collapse = function () {
element.css({
height: "0px"
});
};
// Helper function to show the element
var expand = function () {
element.css({
height: "200px"
});
};
}
};
});
And here is my HTML code:
<div ng-app="myApp" ng-controller="testCtrl">
<div mobile-menu>
<ul>
<li menu-link>
Link 1
</li>
<li menu-link>
Link 2
</li>
</ul>
<div id="menu0" ng-class="{'expanded' : test, 'collapsed' : !test}" menu-item collapse="!test">
<p class="text">First Menu</p>
</div>
<div id="menu1" ng-class="{'expanded' : test, 'collapsed' : !test}" menu-item collapse="!test">
<p class="promo-text">Second Menu</p>
</div>
</div>
I have an issue where if menu-link #1 is clicked, the corresponding menu-item #1's scope.test value should be updated and its' scope watch should be triggered, but it does not. If the scope watch triggered the watchLink function, then I would expect menu-item #1 would have a height of 200px.
I have also attached a jsfiddle example:
http://jsfiddle.net/EmwBP/28/
If you look at the browser console tool, the corresponding menu-item ng-class is always updated based on its scope.test value. However, even with my 3 different scope watchers set up, none of them were triggered. I am also using the ng-class directive just to show that the scope.test value does get updated and will be removed in the final implementation.
Normally, I would have put the menu-item directives as a child of menu-links, but I have a requirement where I have to put the menu-items as it is right now to achieve the slide-down effect of pushing elements below it down.
Many thanks in advance for your advices and assistance

You create a new isolated scope in menuLink by using scope: {}. So whatever scope variables you set inside that scope (like selected) are only available within that scope, and will not propagate to sibling or parent scopes.
What you need to do is de-isolate the menuLink scope by using scope: true.
That is not the problem. The problem is that watchLink is used before you define it. Fix where watchLink is triggered: http://jsfiddle.net/EmwBP/31/

Related

Enable service watching while the data changes between controller and directive

I'm trying to toggle a class by setting a Boolean value with ng-class. Now while using a custom directive while clicking outside of my focusing element needs to change the Boolean to false.
app.factory('collapsed', function($rootScope){
var scope = $rootScope.$new(true);
scope.isCollapsed = true
return scope;
})
app.controller('optSideNav', optSideNav);
function optSideNav(collapsed) {
var self = this;
this.isCollapsed = collapsed.isCollapsed;
}
app.directive('offFocusing', ['$document', 'collapsed', function ($document, collapsed) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
console.log(element)
function elementClick(e) {
e.stopPropagation();
scope.isCollapsed = collapsed.isCollapsed;
}
element.on('click', elementClick);
// remove event handlers when directive is destroyed
scope.$on('$destroy', function () {
element.off('click', elementClick);
});
}
};
}]);
<!--controller area -->
<navng-class="sideNav.isCollapsed ? 'opt-sideNav' : 'opt-sideNavXl'" ng-controller="optSideNav as sideNav" ng-model="sideNav.isCollapsed">
<a ng-click="sideNav.isCollapsed = !sideNav.isCollapsed""><i class="material-icons md-16">menu</i></a>
</nav>
<!--directive area-->
<section class="opt-appArea" flex layout="column" off-focusing>
</section>
How do I asynchronously obtain the value changes while click element to controller?

AngularJS : all directives from controller if array has changed

An area i have some points. By default all disabled to draggable. If user click on edit link the point with 43 position should be draggable. but point still have disable state and watch function not executing..
jsFiddle
HTML
<div ng-app="myapp" ng-controller="MainController">
Make point 43 draggable
<ul class="court">
<li ng-repeat="point in courtPoints" droppable data-location="{{point.location}}">
{{point.location}}
<div class="draggable-point draggable-point-location"
location-point-draggable ng-show="point.marker==true"></div>
</li>
</ul>
</div>
JS
var myapp = angular.module('myapp', []);
myapp.directive('locationPointDraggable', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.draggable({
containment: '.court',
cursor: 'move',
cancel: 'a',
revert: 'invalid',
snap: 'true',
stop: function (event, ui) {
}
});
}
};
});
myapp.controller('MainController', ['$scope', function ($scope) {
Array.range = function (start, end) {
var arr = [];
for (var i = start; i < end; i++) {
var point = {};
point.location = i + 1;
point.marker = false;
point.allowDrag = false;
arr[i] = point;
}
return arr;
};
$scope.init = function () {
$scope.courtPoints = Array.range(0, 50);
$scope.courtPoints[42].marker = true; //42 because start from zero
};
$scope.edit = function (id) {
$scope.courtPoints[42].allowDrag = true;
$scope.courtPoints[42].location = '2014';
};
$scope.init();
}]);
angular.bootstrap(document, ['myapp']);
I think you want to enable the dragging element when user click on the "Make point 43 draggable".
On your fiddle you forgot to add jQueryUI so the draggable function is not working and return undefined.
And one more thing, your draggable function need to specify which element to disable dragging on the cancel attribute, in this case you specify a tag "a" which is not correct because you want to disable the dragging on the yellow circle which is a "div" inside "li" tag. I have used:
ng-class
which add a "disable" class when your allowDrag attribute object is false. You can change the draggable function as follow:
myapp.directive('locationPointDraggable', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.draggable({
containment: '.court',
cursor: 'move',
cancel: '.disable',
revert: 'invalid',
snap: 'true',
stop: function (event, ui) {
}
});
}
};
});
And change your markup html like this :
<div class="draggable-point draggable-point-location"
location-point-draggable
ng-show="point.marker==true"
ng-class='{"disable" : !point.allowDrag, "":point.allowDrag}'
</div>
Button 43 should be dragging disabled initially.
And now when you click on
Make point 43 draggable
Button 43 should be dragging enabled.
Working fiddle : http://jsfiddle.net/themyth92/gd2kzps2/

ng-click toggle variable and update entire page

I have a global variable called RecruiterDashboard.IsColdList. It is a boolean that decides rather to run one directive or another. When the page is loaded the variable is made either true or false through normal scripting.
What I want to do is allow the user to toggle through an ng-click rather to show this other directive or not, so I made an ng-click and set RecruiterDashboard.IsColdList from 'false' to 'true'. Problem is this doesn't reload the angular page and fire the controllers off. How do make the page run through the controllers again?
This is what I have so far:
$scope.showColdList = function (projId) {
RecruiterDashboard.isColdList = true;
};
I want to point out that I am not using angular routing. I am using C# MVC.
My logic looks like so:
callPanelControllers.controller('callPanelController', function($scope) {
$scope.isColdList = RecruiterDashboard.isColdList;
});
callPanelControllers.controller('incomingCall', function ($scope) {
$scope.showColdList = function () {
RecruiterDashboard.isColdList = true;
};
});
<div ng-click="showColdList()" ng-controller="incomingCall"></div>
<div ng-controller="callPanelController">
<cold-list ng-show="isColdList"></cold-list>
</div>
i had developed sort of a hack, so that the entire content inside of a given element is reloaded on change of a certain variable.
csapp.directive("csReloadOn", ["$timeout", function ($timeout) {
var getTemplate = function () {
return '<div ng-if="doRefreshPageOnModeChange"><div ng-transclude=""></div></div>';
};
var linkFunction = function (scope, element, attrs) {
scope.doRefreshPageOnModeChange = true;
scope.$watch(attrs.csReloadOn, function (newVal, oldVal) {
if (newVal === oldVal) return;
scope.doRefreshPageOnModeChange = false;
$timeout(function () { scope.doRefreshPageOnModeChange = true; }, 100);
});
};
return {
restrict: 'A',
transclude: true,
template: getTemplate,
link: linkFunction
};
}]);
you can use it like
<div cs-reload-on="{{pagemode}}">
<!-- your html here-->
</div>
so it just removes and re-renders the complete content inside of the div, so everything is reinitialized etc etc.

Dynamically disable all ng-clicks within an element

I have a directive disable-ng-clicks and under certain conditions, I want to prevent all ng-clicks that are children of the directive. Here is some example markup:
<div disable-ng-clicks> <!-- directive -->
<a ng-click="someAction()"></a>
<div ng-controller="myController">
<a ng-click="anotherAction()"></a>
<a ng-click="moreActions()"></a>
</div>
</div>
If these were normal hyperlinks, I could do something like this in the link function:
function(scope, iElement, iAttrs) {
var ngClicks = angular.element(iElement[0].querySelectorAll('[ng-click]'));
ngClicks.on('click', function(event) {
if(trigger) { // a dynamic variable that triggers disabling the clicks
event.preventDefault();
}
});
}
But this does not work for ng-click directives. Is there another way to accomplish this?
Here is the best I could come up with. I created a new directive to replace ng-click:
directive('myClick', ['$parse', function($parse) {
return {
restrict: 'A',
compile: function($element, attrs) {
var fn = $parse(attrs.myClick);
return function (scope, element, attrs) {
var disabled = false;
scope.$on('disableClickEvents', function () {
disabled = true;
});
scope.$on('enableClickEvents', function () {
disabled = false;
});
element.on('click', function (event) {
if (!disabled) {
scope.$apply(function () {
fn(scope, { $event: event });
});
}
});
};
}
}
}]);
So in a different directive, I can have:
if (condition) {
scope.$broadcast('disableClickEvents');
}
and when I want to re-enable:
if (otherCondition) {
scope.$broadcast('enableClickEvents');
}
I don't like having to use a different directive for ng-click, but this is the best plan I could think of.
You are catching 'click' event on parent only because of JS events bubbling, so if you want to intercept it on all descendants, so your directive should get all descendants of current element, listen their 'click' event and prevent it if necessary.
This directive will iterate over all child elements, check to see if they have an ng-click attribute, and if they do, it will disable any registered click event handlers:
directive('disableNgClicks', function(){
return {
restrict: 'E',
link: function(scope, elem, attrs){
angular.forEach(elem.children(), function(childElem) {
if (childElem.outerHTML.indexOf("ng-click") > -1) {
angular.element(childElem).off('click');
}
});
}
}
})
Plunker demo
I know this is 2 years ago but I needed to do something similar and came up with a rather simple solution.
The object:
items: {
item1 : {
selected: 0,
other: 'stuff'
},
item2 : {
selected : 1,
other: 'stuff'
}
}
The HTML:
<div ng-repeat="item in items" ng-model="item.selected" ng-click="selectParent($event)">
<div ng-click="item.selected ? selectChild($event) : null">Child</div>
</div>
The functions:
$scope.selectParent = function($event) {
var itemScope = angular.element($event.currentTarget)scope().item;
itemScope.selected = !itemScope.selected;
}
$scope.selectChild = function($event) {
$event.stopPropagation;
console.log('I only get triggered if parent item is selected');
}
This is a pretty raw example of what I did. You should probably be using a directive that gives you $scope rather than angular.element($event.currentTarget).scope... either way the simplistic inline if logic is what I was really getting at. You can call a function or not based on some value.

Simple Angular Directive for Bootstrap Modal

Anyone have a simple directive to automatically show a Bootstrap modal? In Bootstrap 3 they took away the ability to automatically show the modal so I can't use a angular ng-if show block. Any help would be great.
Updated for angular 1.2 & Bootstrap 3.1.1: http://embed.plnkr.co/WJBp7A6M3RB1MLERDXSS/
I extended Ender2050's answer so the directive does not have an isolated scope. This means the modal contents can contain references to scope objects. Also reuse the directive attribute so only one attribute is needed.
app.directive("modalShow", function ($parse) {
return {
restrict: "A",
link: function (scope, element, attrs) {
//Hide or show the modal
scope.showModal = function (visible, elem) {
if (!elem)
elem = element;
if (visible)
$(elem).modal("show");
else
$(elem).modal("hide");
}
//Watch for changes to the modal-visible attribute
scope.$watch(attrs.modalShow, function (newValue, oldValue) {
scope.showModal(newValue, attrs.$$element);
});
//Update the visible value when the dialog is closed through UI actions (Ok, cancel, etc.)
$(element).bind("hide.bs.modal", function () {
$parse(attrs.modalShow).assign(scope, false);
if (!scope.$$phase && !scope.$root.$$phase)
scope.$apply();
});
}
};
});
Usage:
<div modal-show="showDialog" class="modal fade"> ...bootstrap modal... </div>
Here's an Angular directive that will hide and show a Bootstrap modal.
app.directive("modalShow", function () {
return {
restrict: "A",
scope: {
modalVisible: "="
},
link: function (scope, element, attrs) {
//Hide or show the modal
scope.showModal = function (visible) {
if (visible)
{
element.modal("show");
}
else
{
element.modal("hide");
}
}
//Check to see if the modal-visible attribute exists
if (!attrs.modalVisible)
{
//The attribute isn't defined, show the modal by default
scope.showModal(true);
}
else
{
//Watch for changes to the modal-visible attribute
scope.$watch("modalVisible", function (newValue, oldValue) {
scope.showModal(newValue);
});
//Update the visible value when the dialog is closed through UI actions (Ok, cancel, etc.)
element.bind("hide.bs.modal", function () {
scope.modalVisible = false;
if (!scope.$$phase && !scope.$root.$$phase)
scope.$apply();
});
}
}
};
});
Usage Example #1 - this assumes you want to show the modal - you could add ng-if as a condition
<div modal-show class="modal fade"> ...bootstrap modal... </div>
Usage Example #2 - this uses an Angular expression in the modal-visible attribute
<div modal-show modal-visible="showDialog" class="modal fade"> ...bootstrap modal... </div>
Another Example - to demo the controller interaction, you could add something like this to your controller and it will show the modal after 2 seconds and then hide it after 5 seconds.
$scope.showDialog = false;
$timeout(function () { $scope.showDialog = true; }, 2000)
$timeout(function () { $scope.showDialog = false; }, 5000)
I'm anxious to see what other solutions people come up with. Cheers!

Resources