I am new to angular and have a problem using my custom directive with ng-repeat. I want to display some posts I get from a rest interface and then use their _id property inside the directive for other purposes. However, it turns out that the property is always the one from the last displayed post when used from inside a function (test in the sample below). When trying to display the id directly over the viewmodel it shows the right one. Hope this makes sense. Any help would be appreciated.
//directive.js
angular.module('app').directive('gnPost', myGnPost);
function myGnPost() {
return {
restrict: 'E',
controller: 'postCtrl',
controllerAs: 'postCtrl',
bindToController: {
post: '='
},
scope: {},
templateUrl: 'template.html'
};
};
//controller.js
angular.module('app').controller('postCtrl', myPostCtrl);
function myPostCtrl(postRestService) {
vm = this;
vm.test = function () {
return vm.post._id;
};
};
// template.html
<p>{{postCtrl.post._id}}</p>
//displays the right id
<p>{{postCtrl.test()}}</p>
//displays the id of the last element of ng-repeat
//parent page.html
<gn-post ng-repeat="singlePost in posts.postList" post="singlePost"></gn-post>
In your controller, you have the following line:
vm = this;
It should be:
var vm = this;
By omitting the var, there is a vm variable created on the global scope instead of a local one per controller instance. As a result each iteration when vm.test is called, it's always pointing at the function defined on the last controller.
Fiddle - try including/omitting the var in postCtrl
It's good practice to use strict mode in Javascript to prevent that issue and others. In strict mode, it impossible to accidentally create global variables, as doing so will throw an error and you'll see the problem straight away. You just need to add this line at the start of your file or function:
"use strict";
Related
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!
Background
Hi, let's say I have an app that has 2 views and ech one of them has the same Directive repeated a few times within it (each directive has a different content though). So the structure looks like this:
<view-1>
<my-directive>
<my-directive>
<my-directive>
</view-1>
<view-2>
<my-directive>
<my-directive>
<my-directive>
<my-directive>
</view-2>
Note, the above is not actual code.
The problem
How do I launch a function once after one of myDirective has loaded/rendered (preferably after the last one has rendered)?
My Code
angular.module('app').directive('myDirective', myDirective);
function myDirective() {
var directive = {
restrict: 'E',
bindToController: true,
controllerAs: 'ctrl',
controller: myDirectiveCtrl,
link: link
};
return directive;
function link() {
function onceFunction() {
// Launch this once
}
onceFunction();
}
function myDirectiveCtrl() {
}
}
angular.module('app').controller('myViewController', myViewController);
function myViewController() {
var ctrl = this;
}
The above code obviously launches 3 times in view-1 and 4 times in view-2, which is not what I'm aiming for.
Solutions I have tried
1) Creating a service that will store a boolean variable set initially to true inside myViewController. Then, inside my link function (or myDirectiveCtr) create a function that will check if that variable is true if it is, set it to false and launch the function.
This works but I create a service that's only function is to communicate this true/false information.
2) Same as above but with the usage of $rootScope and probably $broadcast().
3) Using a $timeout function inside myViewController() which is a terrible sollution. ($timeout 0 does not work).
4) Some strange stuff like adding it inside the main app.js with mixed results.
Final notes
The function does not have to be inside any of the above controllers/link functions.
Many thanks for any help.
I'm very much trying to get my head around angularJS and directives still.
I have an existing REST service that outputs JSON data as follows (formatted for readability):
{"ApplicationType":
["Max","Maya","AfterEffects","Nuke","WebClient","Other"],
"FeatureCategory":
["General","Animation","Compositing","Management","Other"],
"FeatureStatus":
["Completed","WIP","NotStarted","Cancelled","Rejected","PendingReview"],
"BugStatus":
["Solved","FixInProgress","NotStarted","Dismissed","PendingReview"]}
I then have a service (which appears to be working correctly) to retrieve that data that I wish to inject into my directive.
(function () {
'use strict';
var enumService = angular.module('enumService', ['ngResource']);
enumService.factory('Enums', ['$resource',
function ($resource) {
return $resource('/api/Enums', {}, {
query: { method: 'GET', cache: false, params: {}, isArray: false }
});
}
]); })();
My intentions are to use the data from the json response to bind to html selector 'options' for the purposes of keeping the data consistent between the code behind REST service and the angular ( ie. the json data is describing strongly typed model data from c# eg. Enum.GetNames(typeof(ApplicationType)) )
projMgrApp.directive('enumOptions', ['Enums',
function (Enums) {
return {
restrict: 'EA',
template: '<option ng-repeat="op in options">{{op}}</option>',
scope: {
key: '#'
},
controller: function($scope) { },
link: function (scope, element, attrs) {
scope.options = Enums.query(function (result) { scope.options = result[scope.key]; });
}
};
}
]);
the intended usage would be to use as follows:
<label for="Application" class="control-label col-med-3 col">Application:</label>
<select id="Application" class="form-control col-med-3 col pull-right">
<enum-options key="ApplicationType"></enum-options>
</select>
which would then produce all of the options consistent with my c# enums.
In this case it appears the directive is never being called when the tag is used.
Note. I assume the factory is working fine, as i can inject it into a separate controller and it works as anticipated.
1) I guess projMgrApp is the main module. Have you included enumService as dependency to this module?
angular.module('projMgrApp',['enumServices'])
Otherwise your main module won't be aware of the existence of your service.
2) Are you aware how the directives are declared and used. When you declare
projMgrApp.directive('EnumOptions', ['Enums', function (Enums) {}])
actually should be used in the html code as:
<enum-options></enum-options>
I m not quite sure about the name but it should start with lowercase letter like enumOptions
3) I don't know why you use this key as attribute. You don't process it at all. scope.key won't work. You either have to parse the attributes in link function (link: function(scope, element, attributes)) or create isolated scope for the directive.
Add as property of the return object the following:
scope : {
key:'#' //here depends if you want to take it as a string or you will set a scope variable.
}
After you have this , you can use it in the link function as you did (scope.key).
Edit:
Here is a working version similar (optimized no to use http calls) to what you want to achieve. Tell me if I'm missing anything.
Working example
If you get the baddir error try to rename your directive name to enumOptions according to the doc (don't forget the injection):
This error occurs when the name of a directive is not valid.
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
Thanks to Tek's suggestions I was able to get this fully functioning as intended. Which means now my angularJS + HTML select tags/directives are completely bound to my APIs enums. I know down the line that I am going to be needing to adjust add to these occasionally based on user feedback, plus I use these enums to represent strongly typed data all over the app. So it will be a big time saver and help reduce code repetition. Thanks to everyone that helped!
The service and directive that I used is copied below, in case other people starting out with Angular run into similar issues or have similar requirements.
Service:
(function () {
'use strict';
var enumService = angular.module('enumService', [])
.service('Enums', ['$http', '$q', function ($http, $q) {
return {
fetch: function () {
var defer = $q.defer();
var promise = $http.get("/api/Enums").then(function (result) {
defer.resolve(result);
});
return defer.promise;
}
}
}]);})();
Directive:
angular.module('projMgrApp').directive('enumOptions', ['Enums', function (Enums) {
return {
restrict: "EA",
scope: {
key: "#"
},
template: "<select><option ng-repeat='enum in enumIds' ng-bind='enum'></option><select>",
link: function (scope) {
Enums.fetch().then(function (result) {
scope.enumIds = result.data[scope.key];
});
}
};
}]);
Consider
angular.module('App').directive('errors',function() {
return {
restrict: 'A',
controller:function() {
var self = this;
self.closeErrors = function() {
self.errors = [];
self.hasErrors = false;
}
},
controllerAs: 'errorsCtrl',
templateUrl: 'errors.html'
}
when called with
<div errors="otherCtrl.errors"></div>
the object errors comes from another controller.
I know i can add
scope: {errors:"="},
and then access it in my controller via
$scope.errors;
but when I assign it to
self.errors = $scope.errors.
self.errors never gets updated when it is changed in the parent.
So my question is, how can I let this work that whenerver my parentcontroller changes the errors object it is also changed in the errorsCtrl.
(Also I do know I can access errors directly in my template without the controller, but I simply want to use my errorsCtrl)
Add bindToController: true to your directive.
http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
Angular 1.3 introduces a new property to the directive definition
object called bindToController, which does exactly what it says. When
set to true in a directive with isolated scope that uses controllerAs,
the component’s properties are bound to the controller rather than to
the scope.
That means, Angular makes sure that, when the controller is
instantiated, the initial values of the isolated scope bindings are
available on this, and future changes are also automatically
available.
So basically I have a controller, which lists a bunch of items.
Each item is rendering a directive.
Each directive has the ability to make a selection.
What I want to achieve is once the selection has been made, I want to call a method on the controller to pass in the selection.
What I have so far is along the lines of...
app.directive('searchFilterLookup', ['SearchFilterService', function (SearchFilterService) {
return {
restrict: 'A',
templateUrl: '/Areas/Library/Content/js/views/search-filter-lookup.html',
replace: true,
scope: {
model: '=',
setCriteria: '&'
},
controller: function($scope) {
$scope.showOptions = false;
$scope.selection = [];
$scope.options = [];
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
};
}]);
The directive is used like this:
<div search-filter-lookup model="customField" criteria="updateCriteria(criteria)"></div>
Then the controller has a function defined:
$scope.updateCriteria = function(criteria) {
console.log("Weeeee");
console.log(criteria);
};
The function gets called fine. But I'm unable to pass data to it :(
Try this:
$scope.setCriteria({criteria: option});
When you declare an isolated scope "&" property, angular parses the expression to a function that would be evaluated against the parent scope.
when invoking this function you can pass a locals object which extends the parent scope.
It's a common mistake to think that $scope.setCriteria is the same as the function inside the attribute. If you log it you'll see it's just an angular parsed expression function which have the parent scope saved at it's closure.
So when you run $scope.setCriteria() you actually evaluate an expression against the parent scope.
In your case this expression happens to be a function but it could be any expression.
But you don't have a criteria property on the parent scope, that's why angular let you pass a locals object to extend the parent scope. e.g. {criteria: option}
Extends the parent scope
you wrote in a comment that it requires the directive to have knowledge of the parameter name defined in the controller. No it doesn't, it just extends the parent scope with a criteria option, you can still use any expression you want though you are provided with an extra property you may use.
A good example would be ngEvents, take ng-click="doSomething($event)":
ngClick provides you with a local property $event, you don't have to use but you may if you need.
the directive doesn't know anything about the controller, it's up to you to decide which expression you write, cheers.
You can pass the function in using =...
scope: {
model: '=',
setCriteria: '='
},
controller: function($scope) {
// ...
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
<div search-filter-lookup model="customField" criteria="updateCriteria"></div>