Confused refreshing Angular custom directive / communicating between adjacent directives - angularjs

I am two weeks into Angular. I have watched several Pluralsite videos and read several post and this has resulted in great progress but also some confusion. I want to notify one directive of some change in another directive. Then refresh the directive. In other words it needs to go back to the server with the selection from the first and pull the appropriate data.
I have read about eventing and things like $watch() but then I have seen others say to avoid watch and to use $emit and $on. I have even seen one article say to use transclusion.
I have access to Pluralsight and other resources. I will self educate if someone could just point my nose in the right direction.
My directive markup html:
<div class="col-md-3">
<dashboard-main-nav></dashboard-main-nav>
</div>
<div class="col-md-3">
<dash-select ng-show="vm.isDashSelectionVisible">Selections</dash-select>
</div>
My app declaration: NOTE I know I need to get the parm from scope but not sure how...
(function ()
{
"use-strict";
...snip controller setup etc..
.directive("dashboardMainNav", function () {
return {
restrict: "E",
templateUrl: "/Navigation/GetDashItems",
scope: true
}
})
.directive("dashSelect", function () {
return {
restrict: "E",
templateUrl: "/Navigation/GetDashSelections/:" + $scope.??,
scope: true
}
});
})();
routingController:
(function () {
...snip...
function routingController($http, $scope) {
var vm = this;
var isDashSelectionVisible = false;
var dashSelectionId = 0;
$scope.LetterSearch= function (dashSelId) {
vm.isDashSelectionVisible = true;
vm.dashSelectionId = dashSelId;
alert("Letters Clicked: " + dashSelId);
}
}
})();
Rendered HTML:
<dashboard-main-nav>
....snip....
Letters
</dashboard-main-nav>
..... snip.....
<dash-select>
Numbers
</dash-select>
I am not showing the $routeProvider config that wires up the routingController as that works fine. I just need to get that custom directive to grab the parm from scope..refresh then update the dom.
Thank You for your patience and knowledge sharing.

Related

angular array item not updating on scope change

Spent a few hours on this already, sifted through numerous stack posts and blogs but can't seem to get this to make my model update. More specifically, I am trying to update an array item (ng-repeat). In the simple case below, I iterate over venues list, and upon toggling a "like" button, I update the server appropriately, and reflect the change on the venues item on the $scope.
in my search.html I have a directive:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
and search controller I have:
app.controller('bleh', function(Service) {
...
$scope.venues = [{ id: 1, name: 'Venue1', like: false },{ id: 2, name: 'Venue2', like: false }];
...
});
Nothing unusual there. Then in my venues-list directive:
app.directive('venues-list', function() {
function venueListController($scope, Service) {
$scope.likeToggle = function(venue, $index) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venues[$index].like= !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueListController,
scope: {
venues: '='
}
}
});
then in my venue.html I have:
<div ng-repeat="venue in venues">
<p>{{venue.name}}</p>
<button ng-click="likeToggle(venue, $index)">Like</button>
</div>
I have tried many different options:
$scope.$apply() // after updating the $scope or updating scope within apply's callback function;
$scope.$digest()
$timeout(function() { // $scope.venues[$index] .... }, 0);
safe(s,f){(s.$$phase||s.$root.$$phase)?f():s.$apply(f);}
so
safe($scope, function() { $scope.venues[$index].like = !venue.like });
I haven't yet used the link within the directive, but my venues.html template is obviously a little more elaborate than presented here.
EDIT:
Just to keep the discussion relevant, perhaps I should have mentioned - the data is coming back from the server with no issues, I am handling errors and I am definitely hitting the portion of the code where the $scope is to be updated. NOTE: the above code is a small representation of the full app, but all the fundamental pieces are articulated above.
Search Controller
Venues Service
venue-list directive and venue.html template to accompany the directive
directive controller
EDIT #2
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
Just to keep it even simpler, the above doesn't work - so really, the bottom line is my items within an array are not reflecting the updates ...
EDIT #3
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
My apologies - just to re-iterate what I was trying to say above - the above is changing the scope, however, the changes are not being reflected on the view.
Perhaps the issue is with your service and promise resolution.. Can you put a console.log there and see if the promise resolution is working fine? or Can you share that code bit. Also where are you checking for scope update? within directive or outside
OK after some refactoring I finally got it working.
The "fix" (if you want to call it that) to my specific problem was:
instead of passing an array of venues, I was iterating over the array on the parent controller, passing in a venue as an element attribute that would bind (two-way) on the isolated scope of the directive.
so, instead of:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
I now have:
<ion-content>
<venues-list ng-repeat="venue in venues" venue="venue"></venues-list>
</ion-content>
and my directive now looks like:
app.directive('venues-list', function() {
function venueController($scope, Service) {
$scope.likeToggle = function(venue) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venue.like = !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueController,
scope: {
venue: '='
}
}
});
This did the trick!

Directive inside ng-repeat

I'm trying to get a directive inside an ng-repeat to work.. it worked when it was hardcoded in HTML but after switching to an ng-repeat certain aspects of it stopped working.
<div ng-repeat="section in filterSections" filter-tray>
Test {{section.label}}
</div>
I have a module with a controller that emits events:
controller: function($scope, $element) {
this.activateTray = function(trayID) {
$scope.$emit('filterTray::show', {
tray: trayID
});
};
};
I have a directive on the page - it should receive events from the controller. Since switching to using ng-repeat receiving the event has stopped working. It still initialises, it just doesn't listen for the event.
.directive('filterTray', function() {
return {
restrict: 'A',
require: '^filter',
link: function($scope, $element, attrs, filterNavController) {
console.log('this debug statement works');
$scope.$on('filterTray::show', function(e, data) {
console.log('this debug statement never runs');
});
}
};
})
Since adding the repeat has the $scope variable been affected? Perhaps $on isn't listening to the correct thing anymore? Any ideas / tips would be appreciated!
Yes - ngRepeat creates a new scope.
Not sure if events are the right design choice, but if so, use broadcast instead of emit, and the events will reach your directive's scope.
controller: function($scope, $element) {
this.activateTray = function(trayID) {
$scope.$broadcast('filterTray::show', {
tray: trayID
});
};
};
emit shoots events up the scope tree to all ancestors
broadcast reaches all descendants.
When creating a directive that will be reused (this includes ngRepeat) it is best practice to create an isolate scope for your directive. This way you can send the trayID without having to use events. Check out the section on isolate scope in the AngularJS docs on directives here.

Is one of these the more "Angular" way of communicating with a directive?

I asked this question on the Programmer's stack exchange, but didn't get any replies, so I thought I'd try my luck here...
I am working on a project where I would like to encapsulate a directive library and distribute it to other developers to use. I would like to keep the changes to the model within this encapsulated code, so I don't really want the dev's changing the scope variables outside of the lib.
In my code, I have 2 different approaches to communicating with my lib from the parent controller.
The first theory is to create a lib that contains a directive and a service. The parent controller would call the service, which would handle all of the changes to the lib model and the directive would react depending on these changes.
The second theory is to put all the functions to change the model in the directive itself and call the directive scope on the parent to make the changes.
Here is a plunker that shows what I'm asking in better detail. It's a simple example, but illustrates the 2 different methods.
http://plnkr.co/edit/CR350Vx7NiHs5tkjNWZL?p=preview
I'm leaning towards the second method as it just seems cleaner to implement from a development scenario.
Any advice from the Angular experts out there?
Plunker Html:
<body ng-app="myApp">
This is Example 1 - Using a service to modify directive
<div ng-controller="Example1Ctrl">
<example1-directive ng-model='example1Model'></example1-directive>
<br>
<br>
<input type="button" value="Change Example 1" ng-click='changeExample1()' />
</div>
<br>
<br>
This is Example 2 - Modifying directive in the scope of the directive
<div ng-controller="Example2Ctrl">
<example2-directive ng-model='example2Model'></example2-directive>
<br>
<br>
<input type="button" value="Change Example 2" ng-click='changeExample2()' />
</div>
</body>
Plunker js
var app = angular.module("myApp", []);
//--------------------------------------------------
//-------- This is example 1
//--------------------------------------------------
app.controller("Example1Ctrl", function($scope, example1Svc) {
$scope.example1Model = {
value: "Example 1 - Original Value"
}
$scope.changeExample1 = function() {
example1Svc.change($scope.example1Model, "Example 1 - Changed Value");
}
});
/// This part would be encapsulated in a lib
app.directive("example1Directive", function() {
return {
restrict: "E",
scope: {
model: "=ngModel"
},
template: "{{model.value}}"
}
});
app.service("example1Svc", function() {
this.change = function(example1Model, newValue) {
example1Model.value = newValue;
}
})
// End lib
//--------------------------------------------------
//-------- This is example 2
//--------------------------------------------------
app.controller("Example2Ctrl", function($scope, example1Svc) {
$scope.example2Model = {
value: "Example 2 - Original Value"
}
$scope.changeExample2 = function() {
$scope.example2Model.change("Example 2 - Changed Value");
}
});
/// This part would be encapsulated in a lib
app.directive("example2Directive", function() {
return {
restrict: "E",
scope: {
model: "=ngModel"
},
template: "{{model.value}}",
controller: function ($scope) {
$scope.model.change = function(newValue) {
$scope.model.value = newValue;
}
}
}
});
// end lib
I'm somewhat confused by your example #1. What does exampleSvc.change do?
Example #2 definitely goes against MVVM best practice, as it couples the controller with the view. Controllers (of views) should be view-agnostic. They should only change the ViewModel to reflect the current state of the app. The View would then react (however the View chooses to) to changes in the ViewModel.
In particular, these lines "offend" the best practice in my mind:
$scope.model.change = function(newValue) {
$scope.model.value = newValue;
}
Now your controller relies on the view to define what the function does (or whether it is defined to begin with). Also, what if another directive decides to change the .change function?
EDIT:
Take a look at this SO question, and in particular an answer by Mark.
EDIT #2:
There is an interesting case for when some event needs to reach whichever directives or child controllers that could be interested. Use $scope.$broadcast (in controller) and $scope.$on (in directive) to handle. Here's a plunker
I'm going to agree with #New Dev and add a couple more thoughts. If you're building a directive library you don't want to also bundle controllers that the consumer of the library has to use. Your directives should be more or less self contained and provide enough of an api to be extensible and potentially used in other directives.
What does this mean? Your directives may want to define a controller so that they can be injected into other directives. E.g.
//your library
directiveModule.directive("example1Directive", function() {
return {
controller: function($scope, $element, $attrs) {
...
},
...
}
});
-
//application
app.directive("appDirective", function() {
return {
require: '?example1Directive',
link: function(scope, element, attrs, example1Directive) {
...
}
});
You may also want to specify various options that can be set on your directive with parameters e.g.
<div example1-directive="{opt1: 'val', opt2: scopeProp}"></div>
Your directive would then need to parse the the attribute and execute on the scope to generate the options. There's lots more you can do, I'd suggest taking a look at ngmodules.org and see what other people are doing.

Angular.js -- Directive to controller communication

I am very new to angular so please excuse my lack of understanding.
I have a directive called "draggable" which I want to be able to track the x position of and perform some logic on it in the controller. When the user drags the element (a stick figure) to the right, additional stick figures should appear directly behind it. The controller should know the x position and based upon where it is, increment a counter which will dictate how many stick figures appear behind the draggable element.
This code does not currently work as the controller does not have receive the value of x.
My directive:
app.directive('draggable', function() {
return {
restrict: 'A',
scope: "=x",
link: function (scope, element, attrs) {
$(element).draggable({
containment: "parent",
axis: "x",
drag: function(){
scope.x = $(this).offset().left;
}
});
}
};
});
My controller:
app.controller("main-controller", function($scope) {
$scope.range = function(n) {
return new Array(figures);
};
$scope.$watch("x", function(){
console.log($scope.x);
figures = x / (stick_figure_height)
});
});
My HTML:
<div class="human-slider" ng-controller="main-controller">
<div draggable class="human-draggable">
<img src="images/stickfigure.png"/>
</div>
<div ng-repeat="i in range()">
<img src="images/stickfigure.png"/>
</div>
</div>
The reason the controller was not picking up the updated value of x from the draggable directive was because of where the value of x is being updated. X is updated in a turn that has been created in a method outside of the angularJS library (the drag event handler). The solution to this problem was to use $.apply which will update the binding.
The updated code:
// Create our angular app
var app = angular.module('celgeneApp',[]);
// Main controller
app.controller("main-controller", function($scope) {
$scope.x = 0;
$scope.figures = 0;
$scope.range = function(n) {
return new Array($scope.figures);
};
$scope.$watch('x', function(){console.log($scope.x);});
});
// Draggable directive
app.directive('draggable', function() {
return {
restrict: 'A',
scope: false,
link: function (scope, element, attrs) {
$(element).draggable({
containment: "parent",
axis: "x",
drag: function(){
// Need to use $apply since scope.x is updated
// in a turn outside a method in the AngularJS library.
scope.$apply(function(){scope.x = element.offset().left;});
}
});
}
};
});
You can communicate between a directive and a controller through a service. A directive can also access a controller's scope variables via parameters. You can access the variables in different ways, depending on your needs:
As just text with the # prefix
With a one way binding with the & prefix
With a two bay binding with the = prefix
Check out this excellent article about directives, especially the scope section
Take a look at this directive I made, it is just a wrapper around jQuery's draggable just like yours, maybe you can get some ideas:
angular-draggable
Check my this for how parent controller and directive communicates :)
http://plnkr.co/edit/GZqBDEojX6N87kXiYUIF?p=preview plnkr

Angularjs: redraw directive with different template when user logs in

in our general layout we have a sidebar which contains a few user details like name and thumbnail. but also a few different things.
we had the idea to load the sidebar with a directive, and inside of that, have 2 templates, 1 when logged in and 1 when logged out.
to display and not display the user details.
however, we have a directive called sidebar working with either of the 2,
App.directive('sidebar', ['UserService', '$compile', function(User, $compile) {
var getTemplate = function() {
console.warn(User.isLogged);
var templateUrl = "#sidebar" + ((User.isLogged) ? '_loggedin' : '') + "_template";
console.log('requesting template: [%s]', templateUrl);
return $(templateUrl).html();
};
return {
restrict: 'A',
link: function link(scope, element, attrs) {
var tmpl = getTemplate();
element.html(tmpl);
element.replaceWith($compile(element.html())(scope));
},
template: getTemplate()
};
}]);
and we load our user details in a service
App.factory('UserService', [function userService() {
var User = {
isLogged: false,
username: ''
};
return User;
}]);
the login form accepts this UserService as dependency, and sets it's isLogged to true
but how can I let the directive redraw the sidebar, when the isLogged is changed?
are we doing this the right way?
There is a stage known as the compile phase where angular
walks the DOM to identify all the registered directives in the
template. For each directive, it then transforms the DOM based on the directive’s
rules (template, replace, transclude, and so on), and calls the compilefunction
if it exists. The result is a compiled templatefunction, which will invoke the link
functions collected from all of the directives.
Basically, you can't conditionally load templates - it will compile the first one you give it. If you want to dynamically render your view, you might use two divs with the ng-show directive inside the template:
<div ng-show="user.isLogged">// logged in content</div>
<div ng-show="!user.isLogged">// logged out content</div>
You might think you need to inject a factory into your directive - I have just tried this and it does not work! I believe this is because directives can only set up one and two-way binding's with those on the scope chain. With that in mind, I brought the user object into application wide scope:
App.controller("AppCtrl", function($rootScope, UserService) {
$rootScope.user = UserService;
})
And then used an open scope from the directive:
app.directive("sidebar", function() {
return {
restrict: "A",
template: '<div>' +
'<div ng-show="user.isLogged">Logged In</div>' +
'<div ng-show="!user.isLogged">Logged Out</div></div>',
}
})
The user object here is being found through scope chaining. Because the directive does not have an isolated scope (I haven't declared a scope property), it is finding it through the scope chain - right up to the root scope. This is of course a "global" in a sense, and could easily be hidden:
$scope.user = somethingElse
Not the prettiest solution, but it does the job.
You could also conditionally manipulate the DOM in the link function, or better yet - off load this kind of logic to the router making use of nested templates and resolve.

Resources