How to pass controller data to a custom directive in AngularJS? - javascript

I have a custom directive in AngularJS and I want to pass a variable to it from my controller.
Controller:
angular.
module('orderOverview').
component('orderOverview', {
templateUrl: 'home-page/order-overview/order-overview.template.html',
controller: ['Orders',
function ControllerFunction(Orders) {
var self = this;
// Order Info
Orders.getOverview().$promise.then(function(data) {
self.LineItems = data;
});
// Order Info
}
]
});
Directive
angular.
module('myApp').
directive('gvInitializeOrderStatus', function() {
return {
scope: {
field: '#',
myData: '='
},
link: function(scope, element) {
console.log('field:', scope.field);
console.log('data:', scope.myData);
}
}
});
HTML
<div gv-initialize-order-status field="InquiryDone" myData="$ctrl.LineItems">
<span class="tooltiptext">Inquiry</span>
</div>
When I load the page, field logs fine, however data is undefined.
I've tried this a lot of ways, but this is how it should work in my mind if it gives you any idea of what I'm thinking of.
At another point in the same template I pass ng-repeat data to a directive just fine, but in this case I specifically don't want to ng-repeat
ng-repeat HTML that successfully passed data
<li ng-repeat="lineItem in $ctrl.LineItems">
<div class="status-circle"
ng-click="toggleCircle($event, lineItem, 'InquiryDone')"
field="InquiryDone" item="lineItem" gv-initialize-statuses>
<span class="tooltiptext">Inquiry</span>
</div>
</li>
In my other directive, gv-initialize-statuses, I use the same concept in my scope object and have something like scope: { 'field': '=' } and it works just fine.
How can I accomplish this without using ng-repeat?

Two-way binding with = should be avoided
The directive needs to use $watch in the link function:
app.directive('gvInitializeOrderStatus', function() {
return {
scope: {
field: '#',
̶m̶y̶D̶a̶t̶a̶:̶ ̶'̶=̶'̶
myData: '<'
},
link: function(scope, element) {
console.log('field:', scope.field);
console.log('data:', scope.myData);
scope.$watch('myData', function(value) {
console.log('data:', scope.myData);
});
}
}
});
Directives such as ng-repeat automatically use a watcher.
Also for performance reasons, two-way binding with = should be avoided. One-way binding with < is more efficient.
For more efficient code, use the $onChanges life-cycle hook in the controller:
app.directive('gvInitializeOrderStatus', function() {
return {
scope: {
field: '#',
̶m̶y̶D̶a̶t̶a̶:̶ ̶'̶=̶'̶
myData: '<'
},
bindToController: true,
controllerAs: "$ctrl",
controller: function() {
console.log('field:', this.field);
console.log('data:', this.myData);
this.$onChanges = function(changes) {
if (changes.myData)
console.log('data:', changes.myData.currentValue);
};
});
}
}
});
Doing so will make the code more efficient and the migration to Angular 2+ easier.
There are different levels of watch:
The ng-repeat directive actually uses $watchCollection.
The directive may need to use the $doCheck Life-Cycle hook.
For more information, see
AngularJS Developer Guide - Scope $watch Depths
AngularJs 1.5 - Component does not support Watchers, what is the work around?
AngularJS Developer Guide - Component-based application architecture

Do this if you just want the data in your directive
Orders.getOverview().$promise.then(function(data) {
self.LineItems = data;
$rootScope.$broadcast('myData', data);
});
And in your directive just catch this event with callback function
$scope.$on('myData', function(event, args) {
var anyThing = args;
// do what you want to do
});

The problem is the $promise.
self.LineItems is not ready when the directive get active. That's why data is undefined.
Maybe ng-if could helps you:
<div ng-if="$ctrl.LineItems" gv-initialize-order-status field="InquiryDone" myData="$ctrl.LineItems">
<span class="tooltiptext">Inquiry</span>
</div>
Hope this helps. Good luck!

So I found an answer that works when I was reading about $compile in the docs. I realized you can get interpolated attribute values, so I removed the myData field from the scope object and instead accessed the value through the attrs object, like so.
Directive
angular.
module('myApp').
directive('gvInitializeOrderStatus', function() {
return {
scope: {
field: '#'
},
link: function(scope, element, attrs) {
console.log('field:', scope.field);
console.log('attrs:', attrs);
attrs.$observe('lineItems', function(value) {
console.log(value);
})
}
}
});
HTML
<div gv-initialize-order-status field="InquiryDone" lineItems="{{$ctrl.LineItems}}">
<span class="tooltiptext">Inquiry</span>
</div>
Notice the added {{ }} to the lineItems attribute. The attrs.$observe block lets me get notices of changes to the value, as well.

in directive
angular.
module('myApp').
directive('gvInitializeOrderStatus', function() {
return {
scope: {
field: '#',
ngModel: '=' // <- access data with this (ngModel to ng-model in view)
},
link: function(scope, element) {
console.log('field:', scope.field);
console.log('data:', scope.ngModel);
}
}
});
in view
<div gv-initialize-order-status field="InquiryDone" ng-model="$ctrl.LineItems">
<span class="tooltiptext">Inquiry</span>
</div>

Related

Update UI based on change of a directive attribute in AngularJs

I am struggling with data binding in AngularJs.
I have the following piece of markup in .html file that includes the custom directive:
<my-directive ng-repeat="i in object" attr-1="{{i.some_variable}}"></my-directive>
Note: 'some-variable' is being updated every 10 seconds(based on the associate collection and passed to template through controller).
The directive's code includes:
myApp.directive('myDirective', function () {
scope: {
'attr-1': '=attr1'
which throws this exception because of the brackets in attr-1(see html code above).
It works though if I use read-only access(note at sign below):
myApp.directive('myDirective', function () {
scope: {
'attr-1': '#attr1'
I use scope.attr-1 in directive's HTML to show its value.
The problem is that with read-only access UI is not reflecting the change in attribute change.
I've found solution with $parse or $eval(couldn't make them work tho). Is there a better one there?
You'll need only two-way binding and I think $parse or $eval is not needed.
Please have a look at the demo below or in this fiddle.
It uses $interval to simulate your updating but the update can also come from other sources e.g. web socket or ajax request.
I'm using controllerAs and bindToController syntax (AngularJs version 1.4 or newer required) but the same is also possible with just an isolated scope. See guide in angular docs.
The $watch in the controller of the directive is only to show how the directive can detect that the data have changed.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('myDirective', myDirective);
function MainController($interval) {
var self = this,
refreshTime = 1000; //interval time in ms
activate();
function activate() {
this.data = 0;
$interval(updateView, refreshTime);
}
function updateView() {
self.data = Math.round(Math.random()*100, 0);
}
}
function myDirective() {
return {
restrict: 'E',
scope: {
},
bindToController: {
data: '='
},
template: '<div><p>directive data: {{directiveCtrl.data}}</p></div>',
controller: function($scope) {
$scope.$watch('directiveCtrl.data', function(newValue) {
console.log('data changed', newValue);
});
},
controllerAs: 'directiveCtrl'
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
model value in ctrl. {{ctrl.data}}
<my-directive data="ctrl.data"></my-directive>
</div>
I've come to the following solution(in case somebody runs into the the same problem):
// Directive's code
myApp.directive('myDir', function () { return {
restrict: 'E',
templateUrl: function () {
return 'my-dir.html';
},
scope: {
'id': '#arId',
'x': '#arX',
'y': '#arY',
//....
},
link: function ($scope, element, attrs) {
// *** SOLUTION ***
attrs.$observe('arId', function (id) {
$scope.id = id;
});
//...
}
Update: somebody sent me this answer, they have the same problem and came up with a very similar if not exact same solution:
Using a directive inside an ng-repeat, and a mysterious power of scope '#'
It is useful to read because they explain what's the idea behind it.

angular directive scope not binding data

I have a question, code like this:
HTML:
<div class="overflow-hidden ag-center" world-data info="target"></div>
js:
.directive('worldData', ['$interval', function($interval) {
return {
scope: {
chart: '=info'
},
template: '<div>{{chart.aaa}}</div>',
link: function($scope, element, attrs) {
$scope.target = {'aaa': 'aaa'};
aaa = $scope.chart;
}
}
}])
The chart value is undefined, and template no value, but when I declare $scope.target within controller, the code works, why?
This should be generally the pattern:
.controller('myController', function($scope){
$scope.target = {'aaa': 'aaa'}; //In reality, you'd normally load this up via some other method, like $http.
})
.directive('worldData', [function() {
return {
scope: {
chart: '=info'
},
template: '<div>{{chart.aaa}}</div>'
}
}])
--
<div ng-controller="myController">
<div class="overflow-hidden ag-center" world-data info="target"></div>
</div>
Alternatively, the directive could be responsible for going and fetching the data, and not pass in anything to it. You'd only want to consider that if you don't need the data in multiple places.

My directive is not firing

I want to recreate nsClick behavior with my directive ( changing priority).
So this is my code:
angular.module('MyApp').directive('nsClickHack', function () {
return {
restrict: 'E',
priority: 100,
replace: true,
scope: {
key: '=',
value: '=',
accept: "&"
},
link: function ($scope, $element, $attrs, $location) {
$scope.method();
}
}
});
and the line I'm trying to bind to:
<li ng-repeat="item in items" ns-click-hack="toggle(); item.action()">
toggle and item.action are from other directives.
Can you point me where I was making mistake?
If you are trying to re-create ng-click, then it's probably better to look at the source of the ngClick directive.
For example, it does not create an isolate scope since only one isolate scope can be created on an element and it tries to be accommodating towards other directives. The alternative is to $parse the attribute value, which is what the built-in implementation is doing.
If you are just creating a "poor's man" version of ngClick, then, sure, you could use a callback function "&" defined on the scope, and invoke it when the element is clicked:
.directive("nsClickHack", function(){
return {
restrict: "A",
scope: {
clickCb: "&nsClickHack"
},
link: function(scope, element){
element.on("click", function(e){
scope.clickCb({$event: e}); // ngClick also passes the $event var
});
}
}
});
The usage is as you seem to want it:
<li ng-repeat="item in items" ns-click-hack="toggle(); item.action()">
plunker

Directive relies on template to work

I'm trying to use Angular in a more web-component style. So I have created an http-request directive that has a url and a response attribute. It works quite well but my directive is reliant on a template and I would like to remove that as it is hacky and the directive doesn't need a template. Here is my code
<div>
<http-request url="http://jsonplaceholder.typicode.com/posts" response="items"></http-request>
<ul>
<li ng-repeat="item in items">{{ item.id }}</li>
</ul>
</div>
var myApp = angular.module('myApp', []);
myApp.directive('httpRequest', ['$http', function ($http) {
return {
restrict: 'E',
replace: true,
scope: {
response: '='
},
template: '<input type="text" ng-model="response" style="display:none"/>',
link: function (scope, element, attributes) {
$http.get(attributes.url)
.then(function (response) {
scope.response = response.data;
});
}
}
}]);
Fiddle: http://jsfiddle.net/HB7LU/9558/
Update your directive to the following:
myApp.directive('httpRequest', ['$http', function ($http) {
return {
restrict: 'E',
replace: true,
scope: {
response: '='
},
link: function (scope, element, attributes) {
//create response object if it doesn't exist
scope.response = scope.response || {};
$http.get(attributes.url)
.then(function (response) {
//write to items property of response object
scope.response.items = response.data;
});
}
}
}]);
Then loop over your response.items where you use the directive:
<http-request url="http://jsonplaceholder.typicode.com/posts" response="response">
</http-request>
<ul>
<li ng-repeat="item in response.items">{{ item.id }}</li>
</ul>
Updated fiddle.
The way you were doing it (with the template inside the directive) was reassigning the reference inside the isolate scope to be the $http data. This was then being bound to the ng-model="response" (through the watch) and published back out through the two way binding. You are also using an old version of angular. Newer versions look like you don't need to do this work around, just remove the template.
Newer angular fiddle.
Edit:
Since you said you didn't like binding to an items property. You can change your directive to look like this (uses $parse service to set the value on scope). This works with the older version of angular too:
myApp.directive('httpRequest', ['$http', '$parse', function ($http, $parse) {
return {
restrict: 'E',
replace: true,
link: function (scope, element, attributes) {
//use $parse to assign this value to scope
var responseSetter = $parse(attributes.response).assign;
$http.get(attributes.url)
.then(function (response) {
//call the "setter" against scope with the data
responseSetter(scope, response.data);
});
}
}
}]);
Demo.
Your directive doesn't have to have a template, since there's nothing you need to render visually. All you're doing is setting scope variables to encapsulate the state of the request as it's being made and then reacting to the response status and data.
Have a look at https://github.com/coding-js/directives/tree/solutions/datasource from a recent JS meetup I helped run.

Angular Directive refresh on parameter change

I have an angular directive which is initialized like so:
<conversation style="height:300px" type="convo" type-id="{{some_prop}}"></conversation>
I'd like it to be smart enough to refresh the directive when $scope.some_prop changes, as that implies it should show completely different content.
I have tested it as it is and nothing happens, the linking function doesn't even get called when $scope.some_prop changes. Is there a way to make this happen ?
Link function only gets called once, so it would not directly do what you are expecting. You need to use angular $watch to watch a model variable.
This watch needs to be setup in the link function.
If you use isolated scope for directive then the scope would be
scope :{typeId:'#' }
In your link function then you add a watch like
link: function(scope, element, attrs) {
scope.$watch("typeId",function(newValue,oldValue) {
//This gets called when data changes.
});
}
If you are not using isolated scope use watch on some_prop
What you're trying to do is to monitor the property of attribute in directive. You can watch the property of attribute changes using $observe() as follows:
angular.module('myApp').directive('conversation', function() {
return {
restrict: 'E',
replace: true,
compile: function(tElement, attr) {
attr.$observe('typeId', function(data) {
console.log("Updated data ", data);
}, true);
}
};
});
Keep in mind that I used the 'compile' function in the directive here because you haven't mentioned if you have any models and whether this is performance sensitive.
If you have models, you need to change the 'compile' function to 'link' or use 'controller' and to monitor the property of a model changes, you should use $watch(), and take of the angular {{}} brackets from the property, example:
<conversation style="height:300px" type="convo" type-id="some_prop"></conversation>
And in the directive:
angular.module('myApp').directive('conversation', function() {
return {
scope: {
typeId: '=',
},
link: function(scope, elm, attr) {
scope.$watch('typeId', function(newValue, oldValue) {
if (newValue !== oldValue) {
// You actions here
console.log("I got the new value! ", newValue);
}
}, true);
}
};
});
I hope this will help reloading/refreshing directive on value from parent scope
<html>
<head>
<!-- version 1.4.5 -->
<script src="angular.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<my-test reload-on="update"></my-test><br>
<button ng-click="update = update+1;">update {{update}}</button>
</body>
<script>
var app = angular.module('app', [])
app.controller('Ctrl', function($scope) {
$scope.update = 0;
});
app.directive('myTest', function() {
return {
restrict: 'AE',
scope: {
reloadOn: '='
},
controller: function($scope) {
$scope.$watch('reloadOn', function(newVal, oldVal) {
// all directive code here
console.log("Reloaded successfully......" + $scope.reloadOn);
});
},
template: '<span> {{reloadOn}} </span>'
}
});
</script>
</html>
angular.module('app').directive('conversation', function() {
return {
restrict: 'E',
link: function ($scope, $elm, $attr) {
$scope.$watch("some_prop", function (newValue, oldValue) {
var typeId = $attr.type-id;
// Your logic.
});
}
};
}
If You're under AngularJS 1.5.3 or newer, You should consider to move to components instead of directives.
Those works very similar to directives but with some very useful additional feautures, such as $onChanges(changesObj), one of the lifecycle hook, that will be called whenever one-way bindings are updated.
app.component('conversation ', {
bindings: {
type: '#',
typeId: '='
},
controller: function() {
this.$onChanges = function(changes) {
// check if your specific property has changed
// that because $onChanges is fired whenever each property is changed from you parent ctrl
if(!!changes.typeId){
refreshYourComponent();
}
};
},
templateUrl: 'conversation .html'
});
Here's the docs for deepen into components.

Categories

Resources