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.
Related
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>
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.
I have a directive i'm using to do the same search filtering across multiple pages. So the directive will be using a service and get pretty hefty with code. Because of that I want to link to a controller instead of have the controller inside the directive like this:
.directive('searchDirective', function($rootScope) {
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search'
};
});
I also want access to parent scope data inside the template, so I don't want to use a isolated scope.
Anyway here's what i'm not sure how to do. My directive looks like this:
<search-directive filter="foo"/>
How do I pass in the value in the filter attribute so that I can access it in my controller using $scope.filter or this.filter?
If I were using an isolated scope it'd be simple. If i had the controller in the same page I could use $attrs. But since i'm using a controller from another spot and don't want an isolated scope i'm not sure how to get the attrs values into the controller.
Any suggestions?
What about using the link function and passing the value to the scope?
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search',
link: function (scope, element, attr) {
scope.filter = attr.filter;
}
};
searchDirective.js
angular
.module('searchDirective', []).controller('SearchCtrl', SearchCtrl)
.directive('SearchDirective', directive);
function directive () {
var directive = {
templateUrl:'searchtemplate.html',
restrict: "E",
replace: true,
bindToController: true,
controller: 'searchCtrl as search',
link: link,
scope: { filter:'=' } // <-- like so here
};
return directive;
function link(scope, element, attrs) {}
}
SearchCtrl.$inject = [
'$scope',
'$filter'];
function SearchCtrl(
$scope,
$filter) {
/** Init SearchCtrl scope */
/** ----------------------------------------------------------------- */
var vs = $scope;
// ....
Also I highly recommend checking out this AngularJS style guide, how you are writing your directive above is how I use to do it too. John Papa shows some way better ways: https://github.com/johnpapa/angular-styleguide
Directives:
https://github.com/johnpapa/angular-styleguide#directives
Controllers:
https://github.com/johnpapa/angular-styleguide#controllers
Flip the values of bindToController and scope around.
{
....
scope: true,
bindToController: { filter:'=' }
...
}
I have just hit the same issue over the weekend, and made a simple complete example here: bindToController Not Working? Here’s the right way to use it! (Angular 1.4+)
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
Here's the explanation:
I have the current controller that creates an array of $scope.plan.steps which will be used to store every step:
.controller('PlanCtrl', function ($scope, $http) {
$scope.plan = {
steps: [{}]
};
$scope.addStep = function () {
$scope.tutorial.steps.push({});
}
}
Then I have the following directive which has an isolated scope and that is associated to the index of the $scope.plan.steps array:
.directive('planStep', function () {
return {
template: '<input type="text" ng-model="step.name" />{{step}}',
restrict: 'E',
scope: {
index: '=index'
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.index);
$element.remove();
$scope.$destroy();
}
}
};
});
These two communicate, create, and delete objects inside of the controller's scope, however, how can I allow the directive to update the controller's scope array in real time?
I've tried doing a $watch on the directive's isolated scope changes, $emit the changes to the controller, and specify the $index... But no luck.
I've created a plunker to reproduce what I currently have: Link
So far I can create and delete objects inside of the array, but I cannot get a single object to update the controller's object based on the $index.
If the explanation was not clear, by all means, let me know and I will elaborate.
Thank you
WHen you do things like this inside ng-repeat you can take advantage of the child scope that ng-repeat creates and work without isolated scope.
Here's the same directive without needing any angular events
.directive('planStep', function() {
return {
template: '<button ng-click="removeStep(step)">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
transclude: true,
controller: function($scope, $element, $transclude) {
var steps = $scope.plan.steps// in scope from main controller
/* can do the splicing here if we want*/
$scope.removeStep = function(step) {
var idx =steps.indexOf(step)
steps.splice(idx, 1);
}
}
};
});
Also note that removing the element with element.remove() is redundant since it will automatically be removed by angular when array gets spliced
As for the update, it will update the item in real time
DEMO
The way you set up 2-way binding for index you could set one up for step as well? And you really do not need index to remove the item, eventhough your directive is isolated it relies on the index from ng-repeat which probably is not a good idea.
<plan-step ng-repeat="step in plan.steps" index="$index" step="step"></plan-step>
and in your directive:
scope: {
index: '=index',
step:'='
},
Demo
Removing $index dependency and redundant element remove() and scope destroy (when the item is removed from the array angular will manage it by itself):
return {
template: '<button ng-click="removeStep()">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'='
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.step);
}
}
and in your controller:
$scope.$on('removeStep', function(event, data) {
var steps = $scope.plan.steps;
steps.splice(steps.indexOf(data), 1);
});
Demo
If you want to get rid of $emit you could even expose an api with the isolated scoped directive with function binding (&).
return {
template: '<button ng-click="onDelete({step:step})">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'=',
onDelete:'&' //Set up function binding
},
transclude: true
};
and register it on the view:
<plan-step ng-repeat="step in plan.steps" step="step" on-delete="removeStep(step)"></plan-step>
Demo