I'm using ng-grid for data display and I want to dynamically adjust height of the grid itself depending on the number of returned results and user's monitor resolution.
Here's the angular code:
angular.module('modulename', [])
.controller('ctrl', function($scope, $http) {
$scope.gridResult = [];
$scope.gridOptions = {
data: 'gridResult'
};
$scope.listData = function() {
//Async call using $http.get which on success callback asigns response.data to $scope.gridResult
}
//Get data when page loads
$scope.listData();
})
.directive('tableheight', function() {
return {
restrict: 'A',
scope: {
},
controller: 'ctrl',
link: function(scope, elem, attrs) {
scope.$watchCollection('gridResult', function(n, o) {
console.log(n); //Shows empty array
if (n.length > 0) {
console.log(n) //Never displayed
//Calculate new size for the grid
...
}
});
}
};
});
HTML portion:
<div data-ng-grid="gridOptions" tableheight></div>
As you can see 'gridResult' is always empty array even after 'listData' success callback. If I move whole $watchCollection method to controller body everything is working as expected. What I'm trying to achieve is somehow run directive's linking function after DOM has been rendered which includes rendering data from async call.
I guess I'm doing something wrong here or my approach is wrong, however I would appreciate if someone could offer a solution to this.
On a subject of calling directive's linking function after DOM is rendered and ready I tried putting the code inside angular's $timeout with 0 delay but that didn't do anything for me. And speaking of this is there any way to call directive's linking function after DOM rendering since I believe some of my problems are coming from this issue?
Just now there is no communication between the controller and directive. Here you find basic scenarios how such communication can be organized.
The most secure and generic option would be to create data bind like that:
angular.module('modulename', [])
.controller('ctrl', function($scope, $http) {
$scope.gridResult = [];
...
})
.directive('tableHeight', function() {
return {
require: 'dataGrid', // it would be better to use this directive only alongside data-grid
restrict: 'A',
scope: {
gridResult: '=tableHeight'
},
// controller: 'ctrl', this is unnecessary -- it points to directive's controller
link: function(scope, elem, attrs) {
scope.$watchCollection('gridResult', function(n, o) {
...
});
}
};
});
<div data-ng-grid="gridOptions" table-height="gridResult"></div>
Related
I'm building a directive that needs to initialize some data based on values passed into it through the scope. The problem is that when I try and initialize the data in the link function, the passed in value isn't available yet. Is there anyway to only run the initialization when the passed in value is available? I thought about using a watch as in the following code but it seems messy (and doesn't seem to work anyway).
.directive('etMemberActivitySummary', [function () {
return {
restrict: 'E',
templateUrl: '<div>My template</div>',
transclude: false,
scope: {
memberModel: '='
},
link: function(scope, element, attrs, controller) {
var watcher = scope.$watch(
function() {
return scope.memberModel
},
function(value) {
console.log(value);
if (value != null) {
console.log('Watch');
console.log(value);
watcher();
// Perform initialization based on scope.memberModel here
}
});
}
}
}])
Is there a correct way to do this? If it helps, the passed in value is in itself retrieved from a web service.
Update 1
Turns out that if I put an ng-if="ctrl.memberModel" on the directive usage like the following and get rid of all the watch stuff, it works. Is this the best way to do this?
<et-member-activity-summary member-model="ctrl.memberModel" ng-if="ctrl.memberModel"></et-member-activity-summary>
I'm creating a custom directive that inherits the scope of the parent controller. For the most part, I can access the directive's "scope" object that I set in the controller's "$scope" except for properties I set inside an $http.get. Whether those properties came from the API or were just literally defined does not matter.
What's very weird to me is that if I just log the directive's "scope" I can definitely see the properties set by $http.get, but if I try to access them directly they are undefined.
I can access everything in the HTML view perfectly.
var myApp = angular.module('myApp');
myApp.controller('getData', function($scope, $http) {
var myData='myData.json';
$scope.helloFromController = "Hello! This is the controller!";
$scope.getTheData = function() {
$http.get(myData).
success(function(data, status) {
$scope.helloFromHttp = "Hello! This is the http!";
});
};
$scope.getTheData();
});
myApp.directive('useData', function ($parse) {
var directiveObj = {
link: function (scope, element, attrs) {
console.log(scope);
// Returns the scope object that clearly includes the helloFromController and helloFromControllerHttp properties
console.log(scope.helloFromController);
// returns "Hello! This is the controller!"
console.log(scope.helloFromHttp)
// returns undefined
}
};
return directiveObj;
});
It looks like there's something about how $scope works with $http that I don't understand. Help is appreciated.
The linking function executes before the callback from the asynchronous http call gets invoked.
Sample code: http://jsfiddle.net/k54ubb9L/
<div ng-controller="MyCtrl" use-data>
Hello, {{ userData.username }}!
</div>
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope, $http) {
function getTheData() {
$http.get("http://jsonplaceholder.typicode.com/users/1").
success(function(response, status) {
$scope.userData = response;
});
};
getTheData();
});
myApp.directive('useData', function($parse) {
return {
link: function(scope, element, attrs) {
scope.$watch("userData", function(newValue, oldValue) {
console.log(newValue, oldValue);
});
},
restrict: "A"
};
});
Your directive is sharing the same scope of the controller. Your directive's link function loads when the directive is found in your template. $scope.helloFromController is just a string. So, you are seeing this by logging in console. But, $scope.helloFromHttp is set after the promise is resolved for $http. So, you don't see the value instantly as console.log(scope.helloFromHttp) before it is set in the controller. You can try using $timeout to see the changed value of scope.helloFromHttp.
myApp.directive('useData', function ($parse, $timeout) {
var directiveObj = {
link: function (scope, element, attrs) {
// Returns the scope object that clearly includes the helloFromController and helloFromControllerHttp properties
console.log(scope);
// returns "Hello! This is the controller!"
console.log(scope.helloFromController);
// returns undefined
console.log(scope.helloFromHttp)
// set $timeout to trigger after 10 sec, you can reduce or increase the time
$timeout(function() {
console.log(scope.helloFromHttp);
}, 10000)
}
};
return directiveObj;
});
Hope, this helps.
Actually your code works fine without any changes; the problem is that you displayed it in the console, but if you display it in HTML, it gets refresh as expected. Here's the example: http://plnkr.co/edit/WrqO7FeF5d5JdVas9jiL?p=preview
directive('useData', function($parse) {
return {
template: "From directive: {{helloFromHttp}}",
link: function(scope, element, attrs) {
console.log(scope);
// Returns the scope object that clearly includes the helloFromController and helloFromControllerHttp properties
console.log(scope.helloFromController);
// returns "Hello! This is the controller!"
console.log(scope.helloFromHttp);
// returns undefined
}
};
});
Say that I have a straight forward directive:
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
function someEmbeddedFunction()
{
// More embedding
// This code is really getting nasty
}
}
}
}
]);
The code above has so much indentation and crowded that at least to me, it is very hard to read and unenjoyable to work with.
Instead, I want to move the link and someEmbeddedFunction out and just call them. So something along the lines of this:
function link(scope, element, attrs, MyService1, MyService2)
{
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
}
function someEmbeddedFunction()
{
// This is much nicer and less tabbing involved
}
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: link // This is line that I need to get it to work
}
]);
The problem is that MyService1 and MyService2 are not passed to the link function (i.e. if I only had a link function with scope, element, attrs then the code above would work just fine). How can I pass those variables as well?
I tried to call the function as link: link(scope, element, attrs, MyService1, MyService2) but then it says scope, element, attrs are undefined.
Note I realize that the someEmbeddedFunction can right now be moved out without a problem. This was just for demonstrating purposes.
Edit
The only way I can think to get this to work is call the link function from the directive this way:
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
As you observed, the only way to call your non-standard link function is to do so manually within a "standard" link function.
i.e.
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
This is because the link function doesn't get injected like other functions in Angular. Instead, it always gets the same series of arguments (regardless of what you call the function parameters):
The scope
The element (as an angular.element() instance)
The attrs object
An array or single controller instance that you required
A transclude function (if your directive uses transclusion)
Nothing else.
I use this scheme to keep it simple & readable:
var awesomeDir = function (MyService, MyAnotherService) {
var someEmbeddedFunction = function () {
MyService.doStuff();
};
var link = function ($scope, $elem, $attrs) {
someEmbeddedFunction();
};
return {
template: '<div>...</div>',
replace: true,
restrict: 'E',
link: link
};
};
awesomeDir.$inject = ['MyService', 'MyAnotherService'];
app.directive('awesomeDir', awesomeDir);
Here are mine two directives. I basically want to share the scope between them. However with this I get an undefined $http error. I know I need to put $http somewhere, but where?
aresAnalytics.directive('market', function($http) {
return {
restrict: 'E',
controller: function ($scope, Data) {
$scope.data = Data;
},
link: function(scope, element, attrs) {
element.bind('click', function() {
console.log("A1 " + attrs.market);
scope.temp = attrs.market;
$http.get('get_markets').success(function(markets) {
Data.markets = markets;
});
})
}
}
});
aresAnalytics.directive('events', function($http) {
return {
restrict: 'E',
controller: function ($scope) {
scope = $scope;
},
link: function(scope, element) {
element.bind('click', function() {
console.log(scope.temp);
});
}
}
});
HTML:
<market ng-repeat="market in data.markets" market="{{ market }}">
{{ market }}
</market>
Also, I think the way I am doing this
$http.get('get_markets').success(function(markets) {
Data.markets = markets;
});
is not correct, what can I replace with it.
And, should I use Isolate Scope '#' instead? How will that look like?
Thanks for reading this!
You need to inject the $http service like so.
app.directive('something', function( $http ) { ... });
Angular Dependency Injection
To access attributes like that there are many ways, but this is a simple way.
change html to
<market ng-repeat="market in data.markets" market="market">
{{ market }}
</market>
Require parse like this
app.directive('something', function( $http, $parse ) { ... });
And get your attribute like so
scope.temp = $parse(attrs.market)(scope);
This way you're getting it directly from the scope while the other way, angular hasn't rendered the attribute yet.
The problem is with your dependency injection. Try this:
aresAnalytics.directive('market', ['$http', function($http) {
// (you code)
}]);
or if you don't use code minifiers/uglifiers:
aresAnalytics.directive('market', function($http) {
// (you code)
});
I don't know, but I simply had to append a $parent to my scope to always use the parent scope. (like use scope.$parent instead of scope).
REF: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>