how can I select a tab from outside controller - angularjs

I'm using Tabs (ui.bootstrap.tabs) control\directive described here. The control creates it's own controller which sets active tab:
.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
var ctrl = this,
tabs = ctrl.tabs = $scope.tabs = [];
ctrl.select = function(selectedTab) {
angular.forEach(tabs, function(tab) {
if (tab.active && tab !== selectedTab) {
tab.active = false;
tab.onDeselect();
}
});
selectedTab.active = true;
selectedTab.onSelect();
};
Tabset child tab controls (child elements) can trigger parent's select function when clicked on them.
.directive('tab', ['$parse', function($parse) {
return {
require: '^tabset',
scope: {
onSelect: '&select',
I have my custom controller upwards the DOM which needs to trigger select function on TabsetController to set first tab active. I've read that I could use event broadcasting but I can't modify TabsetController to bind event listener so this doesn't seem to be a viable option. Any suggestions?
EDIT:
Please see Plunker for better understanding - here.

You can declare a scope attribute within the "parent" controller and it will be accessible in the child controller.
see: AngularJS - Access to child scope
Because TabsetController is set on a child DOM element while the MainController is set on a parent element, you can define and manipulate $scope.tabs in the MainController, and it will be seen and intepreted in TabsetController.

Related

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

Angular directives with a shared controller don't share the controller's $scope

If I have two different directives with isolated scopes, and they share a controller, shouldn't they share the controller's $scope?
Effectively, I have the following scenario:
I call to the server and pass it some parameters, let's call them x and y.
The server either returns some data, or it returns null which indicates that this data is not available.
If the data is returned, I need to display a button; if the user clicks the button, it will display the data in a separate div.
If the data is not returned, I do not display the button.
I've been trying to implement this as a set of linked directives. I'm trying to do it this way because this "component" is to be re-used multiple times in the application, and I'm trying to do this in two directives because I couldn't figure out any other way to make a single directive control two different elements.
In addition to there being many, they need to be linked by x and y values. So if I have a button with particular x and y, then clicking on it will only toggle the visibility of the display area with the same x and y values. Effectively a given view will have multiple of each with different x and y values.
Anyway, I have a potential working solution, but I seem to be having problems with the shared scope. When I click the button, I have logging statements which correctly show that we trigger the "show" logic. But the logging statements in the ng-if of the div consistently evaluate the same logic to false and doesn't display.
My solution is in three parts:
A directive for the button
A directive for the "display"
A controller that is shared by the two
I have a trivial working example, which I will share below. There's a Plunkr URL at the end of this post as well.
Here is the HTML. The <p> tag in the middle is just to demonstrate that the two directives are physically not adjacent.
<trigger x="apple" y="2" ></trigger>
<p>Some unrelated dom content...</p>
<display x="apple" y="2"></display>
This is the trigger directive, which is the button:
app.directive("trigger", function() {
return {
restrict: "E",
scope: {
x : "#",
y : "#"
},
transclude: false,
template: "<button ng-if='hasCalculation(x,y)' ng-click='toggle()'>Trigger x={{x}} & y={{y}}</button>",
controller: 'testController',
link: function(scope) {
scope.doSomeWork();
}
};
});
This is the display directive, which is supposed to show the data when toggled by the button:
app.directive("display", function() {
return {
restrict: 'E',
scope: {
x : '#',
y : '#'
},
require: '^trigger',
transclude: false,
controller: 'testController',
template: "<p ng-if='shouldShow(x,y)'>{{getCalculation(x,y)}}</p>"
};
});
This is the shared controller, testController:
app.controller("testController", ["$scope", function($scope) {
$scope.shouldShow = [[]];
$scope.calculatedWork = [[]];
$scope.doSomeWork = function() {
var workResult = "We called the server and calculated something asynchonously for x=" + $scope.x + " and y=" + $scope.y;
if(!$scope.calculatedWork[$scope.x]) {
$scope.calculatedWork[$scope.x] = [];
}
$scope.calculatedWork[$scope.x][$scope.y] = workResult;
};
$scope.hasCalculation = function(myX, myY) {
var xRes = $scope.calculatedWork[myX];
if(!xRes) {
return false;
}
return $scope.calculatedWork[myX][myY]
}
$scope.toggle = function() {
if(!$scope.shouldShow[$scope.x]) {
$scope.shouldShow[$scope.x] = [];
}
$scope.shouldShow[$scope.x][$scope.y] = !$scope.shouldShow[$scope.x][$scope.y];
console.debug("Showing? " + $scope.shouldShow[$scope.x][$scope.y]);
}
$scope.isVisible = function(myX, myY) {
console.debug("Checking if we should show for " + myX + " and " + myY);
var willShow;
if(!$scope.shouldShow[myX]) {
willShow = false;
} else {
willShow = $scope.shouldShow[myX][myY];
}
console.debug("Will we show? " + willShow);
return willShow;
}
$scope.getCalculation = function(myX, myY) {
if(!$scope.calculatedWork[myX]) {
return null;
}
return $scope.calculatedWork[myX][myY];
}
}]);
Here is the Plunkr.
If you go to the Plunkr, you'll see the trigger button correctly rendered. If you click the button, you'll see that the toggle() method correctly flips the value of the shouldShow to the opposite of what it was previously (there's a console debug statement in that method that shows its result). You'll also see the re-evaluation of the isVisible method which determines if the display should show -- this always returns false.
I think it has to do with the fact that I'm saving my data and visibility state relative to $scope. My understanding of this is that each individual directive has its own $scope, but since they share a controller, shouldn't they share that controller's $scope?
An isolate scope does not prototypically inherit the properties of the parent scope.
In AngularJS, a child scope normally prototypically inherits from its parent scope. One exception to this rule is a directive that uses scope: { ... } -- this creates an "isolate" scope that does not prototypically inherit.(and directive with transclusion) This construct is often used when creating a "reusable component" directive. In directives, the parent scope is used directly by default, which means that whatever you change in your directive that comes from the parent scope will also change in the parent scope. If you set scope:true (instead of scope: { ... }), then prototypical inheritance will be used for that directive.
Source: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Additionally, a controller is not a singleton it is a class...so two directives can't share a controller. They each instantiate the controller.
If you want two controllers to share the same data, use a factory.

Communication between child and parent directive

I'm trying to figure out how to make a child directive communicate with it's parent directive
I basically have this html markup
<myPanel>
<myData dataId={{dataId}}></myData>
</myPanel>
In the myData directive, if there is no data available, I want to hide the myPanel.
In the myData directive controller I've tried
$scope.$emit('HideParent');
And in the myPanel controller I've tried
$scope.$on('HideParent', function () { $scope.hide = true; });
And also
$scope.$watch('HideParent', function () { if (value) { $scope.hide = true; }});
In either situation, the myPanel directive isn't receiving the $emit
You may create controller in myPanel directive.
Then require this controller in myData directive. And when child directive has no data, call controller method to hide parent.
For example in you parent (myPanel) directive:
controller: function($scope, $element){
$scope.show = true;
this.hidePanel = function(){
$scope.show = false;
}
}
In myData directive require this controller:
require:'^myPanel'
And then, call controller function when you need
if (!scope.data){
myPanelCtrl.hidePanel();
}
Look this Plunker example
The answers above where the parent directive's controller is required is a great one if there truly is a dependency. I had a similar problem to solve, but wanted to use the child directive within multiple parent directives or even without a parent directive, so here's what I did.
Your HTML.
<myPanel>
<myData dataId={{dataId}}></myData>
</myPanel>
In your directive simply watch for changes to the attribute.
controller: function ($scope, $element, $attrs) {
$attrs.$observe('dataId', function(dataId) { $scope.hide = true; } );
}
It's explicit and simple without forcing a dependency relationship where one may not exist. Hope this helps some people.

Angular custom directive: Access parent scope property in linking function

I want to set the name of an event (click or touch) on a parent scope in angular as a string. On a child scope I want to use that string to bind an event with element.on.
Example:
angular.module('directives', [])
.directive('Sidebar', function () {
'use strict';
return {
link: function (scope) {
//determines whether or not to use click or touch events
//I want to use this to bind events with
//element.on(scope.sidebarClickEvent) in child scopes
scope.sidebarClickEvent = 'click';
},
restrict: 'E',
templateUrl: 'sidebar.html'
};
})
.directive('collapseBtn', function() {
'use strict';
return function (scope, element) {
element.on(scope.sidebarClickEvent /* undefined */ , function () {
//scope.sidebarClickEvent is available here, when the event handler executes
scope.toggleSidebarCollapseState();
element.find('i').toggleClass('icon-double-angle-right');
});
};
})
The problem is that properties defined on the parent scope aren't available when I bind the events, so scope.sidebarClickEvent is undefined when I bind the event. But if I change it to a regular click event then I can get the property in the event handler.
Can I access properties inherited by the scope at the time that the event binding occurs? I'm not even sure that I'm understanding scope inheritance properly here, so pointing out errors in my understanding would also be appreciated.
Thanks.

Creating a scope-independent fadein/fadeout directive in AngularJS

To set the stage - this is not happening within a single scope, where I can bind a simple attribute. The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope). Further, the button that's clicked is in a child scope about 3 children deep from root.
Here is how I'm currently solving this:
HTML (sitting in root scope):
<ul class="nav-secondary actions"
darthFader fadeDuration="200"
fadeEvent="darthFader:secondaryNav">
Where darthFader is my directive.
Directive:
directive('darthFader',
function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.$on(attrs.fadeevent, function(event,options) {
$(element)["fade" + options.fade || "In"](attrs.fadeduration || 200);
});
}
}
})
So here I'm creating an event handler, specific to a given element, that is calling fadeIn or fadeOut, depending on an option being passed through the event bus (or defaulting to fadeIn/200ms).
I am then broadcasting an event from $rootScope to trigger this event:
$rootScope.$broadcast('darthFader:secondaryNav', { fade: "Out"});
While this works, I'm not crazy about creating an event listener for every instance of this directive (while I don't anticipate having too many darthFader's on a screen, it's more for the pattern I would establish). I'm also not crazy about coupling my attribute in my view with an event handler in both my controller & directive, but I don't currently have a controller wrapping the secondary-nav, so I'd have to bind the secondaryNav to $rootScope, which I don't love either. So my questions:
Is there a way to do this without creating an event handler every time I instantiate my directive? (maybe a service to store a stateful list of elements?)
How should I decouple my view, controller & directive?
Any other obvious questions I'm missing?
Cheers!
You mention in your question
The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope).
I believe if I were to write this same functionality, I would put the element in its own controller--controllers are responsible for managing the intersection of the view and the model, which is exactly what you're trying to do.
myApp.controller('NavController', function($scope) {
$scope.fadedIn = false;
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="fadedIn">
myApp.directive('darthFader', function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var duration = attrs.fadeDuration || 200;
$scope.$watch(attrs.fadeShown, function(value) {
if (value)
$(element).fadeIn(duration);
else
$(element).fadeOut(duration);
});
}
};
});
If you're worried about sharing the fade in/out state between multiple controllers, you should create a service to share this state. (You could also use $rootScope and event handlers, but I generally find shared services easier to debug and test.)
myApp.value('NavigationState', {
shown: false
});
myApp.controller('NavController', function($scope, NavigationState) {
$scope.nav = NavigationState;
});
myApp.controller('OtherController', function($scope, NavigationState) {
$scope.showNav = function() {
NavigationState.shown = true;
};
$scope.hideNav = function() {
NavigationState.shown = false;
};
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="nav.shown">
<!-- ..... -->
<div ng-controller="OtherController">
<button ng-click="showNav()">Show Nav</button>
<button ng-click="hideNav()">Hide Nav</button>
</div>
Create a custom service, inject it in the controller. Call a method on that service that will do the fade-in/fade-out etc. Pass a parameter to convey additional information.

Resources