AngularJS Two Way Bind Property in Directive - angularjs

I have a custom directive:
.directive('test', function () {
return {
scope: {},
link: function (scope, element, attr) {
scope.$parent.$watch(attr.selectedItem, function(newValue, oldValue){
scope.selectedItem = newValue;
});
}
}
This will one way bind my directive's scope's selectedItem property to the value set in the attribute as such
<div test selectedItem="thePropertyOnTheController"></div>
But what if I want to two way bind? Is there an easy way to set this up without $watch'ing the directive's scope's selectedItem property and $parse'ing the attr.selectedItem expression and calling assign witht he parsed expression on scope.$parent?

$scope.thePropertyOnTheController might have some value like "Hello"
HTML
<div ng-repeat="photosets in userPhotoSetList">
<photosets photosetsarray="photosets.photosetDetail">
</div>
script :
.directive('photosets', function () {
return {
scope: {
photosetslist : "=photosetsarray"
},
link: function (scope, element, attr) {
console.log(scope.photosetslist);
//"Hello" is output
}
}
If you see photosetsarray="photosets.photosetDetail"" photosetsarray and
scope: {
photosetslist : "=photosetsarray" **//this name is same as assignee attr**
},
leftside variable name in html must = right side variable name in directive

Be careful with variable naming in these situations. Binding to an attribute that is declared as camel case in the directive cannot be accessed as such from the DOM.
.directive('test', function () {
return {
scope: {
item : "=selectedItem"
},
link: function (scope, element, attr) {
//do some stuff
}
}
So to correctly bind this attribute to a variable on the controller:
<div test selected-item="thePropertyOnTheController"></div>

Related

AngularJS: how do I create custom directive that takes in a parameter?

I want to create a custom directive that is an attribute that requires an attribute value similar to how ng-repeat takes a list of items. For example,
<div myDir="{{someList}}"></div>
How is this done?
You should do it like this
app.directive('myDir', function () {
return {
scope: {
'myDir' : '#', //'#' for evaluated value of the DOM attribute,
//'=' a parent scope property
},
link: function (scope, element, attrs) {
scope.$watch('myDir', function (newVal) {
console.log('myDir', newVal);
});
}
};
});
usage for evaluated value (with '#')
<div my-dir="{{someList}}"></div>
usage for property from a scope (with '=')
<div my-dir="someList"></div>
to understand difference between '#' and '=' look here

Directive within another directive - scope var undefined

I'm trying to generate a smart-table directive from within a custom directive I've defined:
<div ng-controller="myContrtoller">
<containing-directive></containing-directive>
</div>
The directive definition:
angular.module('app')
.directive('containingDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<table st-table="collection" st-pipe="scopeFun"></table>',
link: function(scope, elem, attrs) {
scope.scopeFun = function () {
// solve the misteries of life
}
}
}
});
As you can see my directive tries to replace the element by the template generated by the st-table directive, using the st-pipe directive depending on the first, briefly:
ng.module('smart-table')
.controller('stTableController' function () {
// body...
})
.directive('stTable', function () {
return {
restrict: 'A',
controller: 'stTableController',
link: function (scope, element, attr, ctrl) {
// body
}
};
})
.directive('stPipe', function (config, $timeout) {
return {
require: 'stTable',
scope: {
stPipe: '='
},
link: {
pre: function (scope, element, attrs, ctrl) {
var pipePromise = null;
if (ng.isFunction(scope.stPipe)) { // THIS IS ALWAYS UNDEFINED
// DO THINGS
}
},
post: function (scope, element, attrs, ctrl) {
ctrl.pipe();
}
}
};
});
Problem:
The st-pipe directive checks the scope var stPipe if it is defined or not by: if (ng.isFunction(scope.stPipe)). This turns out to be ALWAYS undefined. By inspecting I found two things:
From the stPipe directive, the value supposed to be scope.stPipe that is my scopeFun defined within my containingDirective is undefined on the scope object BUT defined within the scope.$parent object.
If I define my $scope.scopeFun within the myContrtoller I don't have any problem, everything works.
Solution:
I did find a solutions but I don't know what really is going on:
Set replace: false in the containingDirective
Define the scope.scopeFun in the pre-link function of containingDirective
Questions:
Why is the scopeFun available in the stPipe directive scope object if defined in the controller and why it is available in the scope.$parent if defined in the containingDirective?
What is really going on with my solution, and is it possible to find a cleaner solution?
From the docs: "The replacement process migrates all of the attributes / classes from the old element to the new one" so what was happening was this:
<containing-directive whatever-attribute=whatever></containing-directive>
was being replaced with
<table st-table="collection" st-pipe="scopeFun" whatever-attribute=whatever></table>
and somehow st-table did not enjoy the extra attributes (even with no attributes at all..).
By wrapping the containingDirective directive template within another div fixed the problem (I can now use replace:true):
<div><table st-table="collection" st-pipe="scopeFun"></table></div>
If someone has a more structured answer would be really appreciated

Angular scroll directive

I try to create a directive with two params, class and pageYOffset. I would like to check the scrolling position of my element and add a class name if the pageYOffset is bigger than the number in the attr. Somehow I cant't trigger the class changes.
HTML
<div scroll offset="1500" ng-class="{active : scrollClass}"></div>
Directive
app.directive('scroll', function($window) {
return {
scope: {
offset: "#offset"
},
link: function (scope, element, attr) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= scope.offset) {
scope.scrollClass = true;
}
scope.$apply();
});
}
}
});
As you are using isolated class, your directive scope is different scope than you controller scope, controller scope will not get inherited to directive scope because scope: {..}, As you want to change flag of parent scope you need to pass that value inside your directive with = which will perform two way binding as inner scope variable change will affect to the parent scope variable value.
Makrup
<div scroll offset="1500" scroll-class="scrollClass" ng-class="{active : scrollClass}"></div>
Directive
app.directive('scroll', function($window) {
return {
scope: {
offset: "#offset",
scrollClass: '='
},
link: function (scope, element, attr) {
angular.element($window).bind("scroll", function(event) {
if (element.pageYOffset >= scope.offset) {
//this will change the parent scope variable value to true
scope.scrollClass = true;
}
scope.$apply(); //need full to run digest cycle
});
}
}
});
You can try something like tihs:
app.directive('scroll', function($window) {
return {
scope: {
offset: "#offset",
toggleScroll: "&toggle"
},
link: function (scope, element, attr) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= scope.offset) {
toggle();
}
//scope.$apply();
});
}
}
});
<div scroll offset="1500" toggle-scroll="changeActiveClass()" ng-class="{active : scrollClass}"></div>
Then define changeActiveClass in your parent scope:
$scope.changeActiveClass = function() {
$scope.scrollClass = !scrollClass;
}

How to bind content of tag into into directive's scope?

Say I have a directive like such:
<my-directive>This is my entry!</my-directive>
How can I bind the content of the element into my directive's scope?
myApp.directive('myDirective', function () {
return {
scope : {
entry : "" //what goes here to bind "This is my entry" to scope.entry?
},
restrict: "E",
template: "<textarea>{{entry}}</textarea>"
link: function (scope, elm, attr) {
}
};
});
I think there's much simpler solution to the ones already given. As far as I understand, you want to bind contents of an element to scope during initialization of directive.
Given this html:
<textarea bind-content ng-model="entry">This is my entry!</textarea>
Define bind-content as follows:
directive('bindContent', function() {
return {
require: 'ngModel',
link: function ($scope, $element, $attrs, ngModelCtrl) {
ngModelCtrl.$setViewValue($element.text());
}
}
})
Here's a demo.
I may have found a solution. It relies on the transclude function of directives. It works, but I need to better understand transclusion before being sure this is the right way.
myApp.directive('myDirective', function() {
return {
scope: {
},
restrict: 'E',
replace: false,
template: '<form>' +
'<textarea ng-model="entry"></textarea>' +
'<button ng-click="submit()">Submit</button>' +
'</form>',
transclude : true,
compile : function(element,attr,transclude){
return function (scope, iElement, iAttrs) {
transclude(scope, function(originalElement){
scope.entry = originalElement.text(); // this is where you have reference to the original element.
});
scope.submit = function(){
console.log('update entry');
}
}
}
};
});
You will want to add a template config to your directive.
myApp.directive('myDirective', function () {
return {
scope : {
entry : "=" //what goes here to bind "This is my entry" to scope.entry?
},
template: "<div>{{ entry }}</div>", //**YOU DIDN'T HAVE A TEMPLATE**
restrict: "E",
link: function (scope, elm, attr) {
//You don't need to do anything here yet
}
};
});
myApp.controller('fooController', function($scope){
$scope.foo = "BLAH BLAH BLAH!";
});
And then use your directive like this:
<div ng-controller="fooController">
<!-- sets directive "entry" to "foo" from parent scope-->
<my-directive entry="foo"></my-directive>
</div>
And angular will turn that into:
<div>THIS IS MY ENTRY</div>
Assuming that you have angular setup correctly and are including this JS file onto your page.
EDIT
It sounds like you want to do something like the following:
<my-directive="foo"></my-directive>
This isn't possible with ELEMENT directives. It is, however, with attribute directives. Check the following.
myApp.directive('myDirective', function () {
return {
template: "<div>{{ entry }}</div>", //**YOU DIDN'T HAVE A TEMPLATE**
restrict: "A",
scope : {
entry : "=myDirective" //what goes here to bind "This is my entry" to scope.entry?
},
link: function (scope, elm, attr) {
//You don't need to do anything here yet
}
};
});
Then use it like this:
<div my-directive="foo"></div>
This will alias the value passed to my-directive onto a scope variable called entry. Unfortunately, there is no way to do this with an element-restricted directive. What is preventing it from happening isn't Angular, it is the html5 guidelines that make what you are wanting to do impossible. You will have to use an attribute directive instead of an element directive.

Angular Isolate Scope breaks down?

I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview

Resources