Simple Angular Directive - angularjs

I'm attempting to write my first Angular directive to add pagination functionality to my app.
I'm keeping it simple for now and want to have the directive just share the scope of the controller.
In my controller, I have a scoped var called:
$scope.pages
Inside my directive's linking function, I can do:
console.dir($scope)
to see the contents of the controller's scope object, but if I try to access the pages property, it comes back as undefined.
What am I missing?
Thanks in advance.
myDirectives.directive("mdPagination", function(){
return {
template: '',
restrict: 'A',
priority: 1000,
scope: false,
link: function($scope, el, attrs){
console.dir($scope.pages); //returns undefined
el.text('hello world');
},
}
});
Edit: I'm aware there's pagination modules out there, but my needs are modest and I thought building my own would be an easy way to start learning how to build directives.

The problem you have is directives linking functions are run before controllers are, so you typically can't access scope values during this linking phase. There are a couple other ways to do this. The most immediate way to solve your problem is with $scope.$watch. So something like this should work:
$scope.watch('pages', function(pages) {
console.dir(pages);
});
This works just fine for lost of circumstances. I would also suggest decoupling your controller from the scope it is declared in a bit by using an attribute to pass in an expression (property name basically) for your watch. A simple example might look like this:
<div my-directive="myProperty">
link: function($scope, el, attrs) {
$scope.watch(attrs.myDirective, function(value) {
//do something you need to do with the value here
});
}
However as you add complexity to your directive you may want to give your directive it's own controller to manage state without directly calling watches. This controller can either use a child scope belonging to the directive itself with scope: true or just use the parent scope with scope: false. Your choice will likely depend upon need. Then you could have:
myDirectives.directive("mdPagination", function(){
return {
template: '',
restrict: 'A',
priority: 1000,
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
console.dir($scope.pages);
}],
link: function($scope, el, attrs){
$element.text('hello world');
}
}
});

By default, the directive shares its scope with the controller. To do that, you must not having a "scope" attribute in your object, like that:
myDirectives.directive("mdPagination", function(){
return {
template: '',
restrict: 'A',
priority: 1000,
link: function($scope, el, attrs){
console.dir($scope.pages);
el.text('hello world');
},
}
});

#Sandro is right. You set the scope property in your directive to false which means that the scope will not inherit (or share) its properties with the controller. If you remove the false, it should work as you intend it.
The docs for how to use $scope are here: http://code.angularjs.org/1.2.0-rc.3/docs/api/ng.$rootScope.Scope
The developer guide for creating directives has a lot of good info about how to setup your directive so that it gives you the scope you want. Those docs are here: http://code.angularjs.org/1.2.0-rc.3/docs/guide/directive
Hope that helps.

Related

ngChange fires before value makes it out of isolate scope

//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function () {
return {
scope: {
searchModel: '=ngModel',
searchChange: '&ngChange',
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel" ng-change="searchChange()"/>',
restrict: 'E'
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
Here is a simplified directive to illustrate. When I type into the input, I expect the console.log in loadResults to log out exactly what I have already typed. It actually logs one character behind because loadResults is running just before the searchFilter var in the main controller is receiving the new value from the directive. Logging inside the directive however, everything works as expected. Why is this happening?
My Solution
After getting an understanding of what was happening with ngChange in my simple example, I realized my actual problem was complicated a bit more by the fact that the ngModel I am actually passing in is an object, whose properties i am changing, and also that I am using form validation with this directive as one of the inputs. I found that using $timeout and $eval inside the directive solved all of my problems:
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function ($timeout) {
return {
scope: {
searchModel: '=ngModel'
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel.subProp" ng-change="valueChange()"/>',
restrict: 'E',
link: function ($scope, $element, $attrs, ngModel)
{
$scope.valueChange = function()
{
$timeout(function()
{
if ($attrs.ngChange) $scope.$parent.$eval($attrs.ngChange);
}, 0);
};
}
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
The reason for the behavior, as rightly pointed out in another answer, is because the two-way binding hasn't had a chance to change the outer searchFilter by the time searchChange(), and consequently, loadResults() was invoked.
The solution, however, is very hacky for two reasons.
One, the caller (the user of the directive), should not need to know about these workarounds with $timeout. If nothing else, the $timeout should have been done in the directive rather than in the View controller.
And two - a mistake also made by the OP - is that using ng-model comes with other "expectations" by users of such directives. Having ng-model means that other directives, like validators, parsers, formatters and view-change-listeners (like ng-change) could be used alongside it. To support it properly, one needs to require: "ngModel", rather than bind to its expression via scope: {}. Otherwise, things would not work as expected.
Here's how it's done - for another example, see the official documentation for creating a custom input control.
scope: true, // could also be {}, but I would avoid scope: false here
template: '<input ng-model="innerModel" ng-change="onChange()">',
require: "ngModel",
link: function(scope, element, attrs, ctrls){
var ngModel = ctrls; // ngModelController
// from model -> view
ngModel.$render = function(){
scope.innerModel = ngModel.$viewValue;
}
// from view -> model
scope.onChange = function(){
ngModel.$setViewValue(scope.innerModel);
}
}
Then, ng-change just automatically works, and so do other directives that support ngModel, like ng-required.
You answered your own question in the title! '=' is watched while '&' is not
Somewhere outside angular:
input view value changes
next digest cycle:
ng-model value changes and fires ng-change()
ng-change adds a $viewChangeListener and is called this same cycle.
See:
ngModel.js#L714 and ngChange.js implementation.
At that time $scope.searchFilter hasn't been updated. Console.log's old value
next digest cycle:
searchFilter is updated by data binding.
UPDATE: Only as a POC that you need 1 extra cycle for the value to propagate you can do the following. See the other anwser (#NewDev for a cleaner approach).
.controller('mainCtrl', function ($scope, $timeout){
$scope.loadResults = function (){
$timeout(function(){
console.log($scope.searchFilter);
});
};
});

AngularJS passing json to directive

I'm still new in Angular but i'm doing some progress.. i think :)
I have problem passing json file from controller to directive using isolated scope.
This is my controller which talk to factory "dataArchive":
.controller('graphCtrl', ['$scope', 'dataArchive', function($scope, dataArchive){
dataArchive.get().then(function(data){
$scope.getData = data;
});
}]);
Then i have directive which is using isolated scope.
.directive('dataGraph', function($compile){
return {
restrict: 'AE',
replace: true,
scope: {
getArchiveData: '='
},
template: '<div id="chartdiv"></div>',
link: function (scope, element, attrs){
var dati = scope.getArchiveData;
console.log(dati);
};
};
});
And this is my HTML:
<div ng-controller="graphCtrl">
<data-graph get-archive-data="getData"></data-graph>
</div>
In console i always get 'undefined'.
Where i am wrong and is this a good way?
Thanks you all.
Since this code is async:
dataArchive.get().then(function(data){
$scope.getData = data;
});
The link function will run before the data is set on getData, and therefore the isolated scope variable will not be set at this time. So, I believe you are seeing a timing issue.
To make sure that your directive binding is correct. Try to set $scope.getData to a static value (e.g. $scope.getData = [{ data: 'value'}]). This should work.
Also, Angular checks for changes (to rebind) based on object reference. So, you might need to define $scope.getData in the controller (outside of the async call). Then you might want to push all the data in (instead of replace the entire object with the assignment).
Hope this helps.

AngularJS : Child input directive needs to compile in the scope of its parent for ng-model to bind

We have a contact form we use in many applications. There are many default values, validation rules, structure, etc, that are repeated. We're working on a set of directives in order to make the view more semantic and less verbose.
There are a few targets we're shooting for.
Defining the contact form model once in a parent directive like this: <div my-form model='formModel'>. Associated children directives would be able to get the base model from the model attribute.
Supply the default configuration (size, validation rules, placeholders, classes, etc) for each input, but allow the possibility for attributes to be overwritten if necessary. Thus, we are creating child directives using the my-form directive's controller for communication. We also want these child directives to bind to the application controller's model formModel.
I'm having some trouble with implementing this.
formModel is exposed through the parent directive's controller, but I'm having to manually $compile the child directive using scope.$parent in the link function. This seems smelly to me, but if I try to use the child directive's scope the compiled HTML contains the correct attribute (it's visible in the source), but it isn't bound to the controller and it doesn't appear on any scope when inspected with Batarang. I'm guessing I'm adding the attribute too late, but not sure how to add the attribute earlier.
Although I could just use ng-model on each of the child directives, this is exactly what I'm trying to avoid. I want the resulting view to be very clean, and having to specify the model names on every field is repetitive and error-prone. How else can I solve this?
Here is a jsfiddle that has a working but "smelly" setup of what I'm trying to accomplish.
angular.module('myApp', []).controller('myCtrl', function ($scope) {
$scope.formModel = {
name: 'foo',
email: 'foo#foobar.net'
};
})
.directive('myForm', function () {
return {
replace: true,
transclude: true,
scope: true,
template: '<div ng-form novalidate><div ng-transclude></div></div>',
controller: function ($scope, $element, $attrs) {
$scope.model = $attrs.myModel;
this.getModel = function () {
return $scope.model;
};
}
};
})
.directive('myFormName', function ($compile) {
return {
require: '^myForm',
replace: true,
link: function (scope, element, attrs, parentCtrl) {
var modelName = [parentCtrl.getModel(),attrs.id].join('.'),
template = '<input ng-model="' + modelName + '">';
element.replaceWith($compile(template)(scope.$parent));
}
};
});
There is a much simpler solution.
Working Fiddle Here
Parent Form Directive
First, establish an isolated scope for the parent form directive and import the my-model attribute with 2-way binding. This can be done by specifying scope: { model:'=myModel'}. There really is no need to specify prototypical scope inheritance because your directives make no use of it.
Your isolated scope now has the 'model' binding imported, and we can use this fact to compile and link child directives against the parent scope. For this to work, we are going to expose a compile function from the parent directive, that the child directives can call.
.directive('myForm', function ($compile) {
return {
replace: true,
transclude: true,
scope: { model:'=myModel'},
template: '<div ng-form novalidate><div ng-transclude></div></div>',
controller: function ($scope, $element, $attrs) {
this.compile = function (element) {
$compile(element)($scope);
};
}
};
Child Field Directive
Now its time to setup your child directive. In the directive definition, use require:'^myForm' to specify that it must always reside within the parent form directive. In your compile function, add the ng-model="model.{id attribute}". There is no need to figure out the name of the model, because we already know what 'model' will resolve to in the parent scope. Finally, in your link function, just call the parent controller's compile function that you setup earlier.
.directive('myFormName', function () {
return {
require: '^myForm',
scope: false,
compile: function (element, attrs) {
element.attr('ng-model', 'model.' + attrs.id);
return function(scope, element, attrs, parentCtrl) {
parentCtrl.compile(element);
};
}
};
});
This solution is minimal with very little DOM manipulation. Also it preserves the original intent of compiling and linking input form fields against the parent scope, with as little intrusion as possible.
It turns out this question has been asked before (and clarified) here, but never answered.
The question was also asked on the AngularJS mailing list, where the question WAS answered, although the solution results in some smelly code.
Following is Daniel Tabuenca's response from the AngularJS mailing list changed a bit to solve this question.
.directive('foo', function($compile) {
return {
restrict: 'A',
priority: 9999,
terminal: true, //Pause Compilation to give us the opportunity to add our directives
link: function postLink (scope, el, attr, parentCtrl) {
// parentCtrl.getModel() returns the base model name in the parent
var model = [parentCtrl.getModel(), attr.id].join('.');
attr.$set('ngModel', model);
// Resume the compilation phase after setting ngModel
$compile(el, null /* transclude function */, 9999 /* maxPriority */)(scope);
}
};
});
Explanation:
First, the myForm controller is instantiated. This happens before any pre-linking, which makes it possible to expose myForm's variables to the myFormName directive.
Next, myFormName is set to the highest priority (9999) and the property of terminal is set true. The devdocs say:
If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined).
By calling $compile again with the same priority (9999), we resume directive compilation for any directive of a lower priority level.
This use of $compile appears to be undocumented, so use at your own risk.
I'd really like a nicer pattern for follow for this problem. Please let me know if there's a more maintainable way to achieve this end result. Thanks!

Is it ok watch the scope inside a custom directive?

I'm trying to write a directive to create a map component so I can write something like:
<map></map>
Now the directive looks like this:
angular.module('myApp')
.directive('map', function (GoogleMaps) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.$watch('selectedCenter', function() {
renderMap(scope.selectedCenter.location.latitude, scope.selectedCenter.location.longitude, attrs.zoom?parseInt(attrs.zoom):17);
});
function renderMap(latitude, longitude, zoom){
GoogleMaps.setCenter(latitude, longitude);
GoogleMaps.setZoom(zoom);
GoogleMaps.render(element[0]);
}
}
};
});
The problem is that 'watch' inside the directive doesn't looks very well thinking in the reusability of the component. So I guess the best thing is being able to do something like:
<map ng-model="selectedCenter.location"></map>
But I don't know if it's even a good thing using angular directives inside custom directives or how can I get the object indicated in the ng-model attribute in the custom-directive's link function.
You will need to do something like that
angular.module('myApp')
.directive('map', function (GoogleMaps) {
return {
restrict: 'E',
scope: {
ngModel: '=' // set up bi-directional binding between a local scope property and the parent scope property
},
as of now you could safely watch your scope.ngModel and when ever the relevant value will be changed outside the directive you will be notified.
Please note that adding the scope property to our directive will create a new isolated scope.
You can refer to the angular doc around directive here and especially the section "Directive Definition Object" for more details around the scope property.
Finally you could also use this tutorial where you will find all the material to achieve a directive with two way communication form your app to the directive and opposite.
Without scope declaration in directive:
html
<map ng-model="selectedCenter"></map>
directive
app.directive('map', function() {
return {
restrict: 'E',
require: 'ngModel',
link: function(scope, el, attrs) {
scope.$watch(attrs.ngModel, function(newValue) {
console.log("Changed to " + newValue);
});
}
};
});
One easy way you can achieve this would be to do something like
<map model="selectedCenter"></map>
and inside your directive change the watch to
scope.$watch(attrs.model, function() {
and you are good to go

Call controller method from directive without defining it on the directive element - AngularJS

I'm sure there's a simple answer to this that i've just missed.
http://jsfiddle.net/jonathanwest/pDRxw/3/
Essentially, my directive will contain controls that will always call the same method in a controller which is external to the directive itself. As you can see from the above fiddle, I can make this work by defining the attribute with the method on the control directive, but as that method will always be called from the same button within the directive, I don't want to have to define the method to call. Instead the directive should know to call the controller edit method when that button is pressed. Therefore, the definition of the control would be:
<control title="Custom Title" />
How can I achieve this?
Actually I think doing that straightway using $parent is not a recommended way how directives should be defined. Because actually there is no visible dependency on what functions could be called from parent controller, making them little bit harder to re-use.
I do not know actual use case why you need this, but I assume that you use control several times and you do not want to copy-paste bunch of attributes that defines some common behavior.
In this case I would recommend little bit other approach: add some directive-container that will define that behavior, and control will require this directive as dependency:
myApp.directive('controlBehavior', function() {
return {
restrict: 'E',
scope: {
modifyfunc: '&'
},
controller: function($scope, $timeout) {
this.modifyfunc = $scope.modifyfunc;
}
};
});
myApp.directive('control', function() {
return {
restrict: 'E',
require: '^controlBehavior',
replace: true,
scope: {
title: "#"
},
template : '<div>{{title}}<button ng-click="edit()">Edit</button></div>',
link: function(scope, element, attr, behavior) {
scope.edit = behavior.modifyfunc;
}
}
});
Here is a fiddle to demonstrate this approach: http://jsfiddle.net/7EvpZ/4/
You can access the parent scope by using $parent property of the current scope.
http://jsfiddle.net/XEt7D/

Resources