AngularJS - accessing parent directive properties from child directives - angularjs

This should not be too hard a thing to do but I cannot figure out how best to do it.
I have a parent directive, like so:
directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
replace: true,
transclude: true,
template: '
<div class="editable-fieldset" ng-click="edit()">
<div ng-transclude></div>
...
</div>',
controller: ['$scope', function ($scope) {
$scope.edit = ->
$scope.editing = true
// ...
]
};
});
And a child directive:
.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
template: function (element, attrs) {
'<div>
<label>' + attrs.label + '</label>
<p>{{ model.' + attrs.field + ' }}</p>
...
</div>'
},
require: '^editableFieldset'
};
});
How can I easily access the model and editing properties of the parent directive from the child directive? In my link function I have access to the parent scope - should I use $watch to watch these properties?
Put together, what I'd like to have is:
<editable-fieldset model="myModel">
<editable-string label="Some Property" field="property"></editable-string>
<editable-string label="Some Property" field="property"></editable-string>
</editable-fieldset>
The idea is to have a set of fields displayed by default. If clicked on, they become inputs and can be edited.

Taking inspiration from this SO post, I've got a working solution here in this plunker.
I had to change quite a bit. I opted to have an isolated scope on the editableString as well because it was easier to bind in the correct values to the template. Otherwise, you are going to have to use compile or another method (like $transclude service).
Here is the result:
JS:
var myApp = angular.module('myApp', []);
myApp.controller('Ctrl', function($scope) {
$scope.myModel = { property1: 'hello1', property2: 'hello2' }
});
myApp.directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
transclude: true,
replace: true,
template: '<div class="editable-fieldset" ng-click="edit()"><div ng-transclude></div></div>',
link: function(scope, element) {
scope.edit = function() {
scope.editing = true;
}
},
controller: ['$scope', function($scope) {
this.getModel = function() {
return $scope.model;
}
}]
};
});
myApp.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
scope: {
label: '#',
field: '#'
},
template: '<div><label>{{ label }}</label><p>{{ model[field] }}</p></div>',
require: '^editableFieldset',
link: function(scope, element, attrs, ctrl) {
scope.model = ctrl.getModel();
}
};
});
HTML:
<body ng-controller="Ctrl">
<h1>Hello Plunker!</h1>
<editable-fieldset model="myModel">
<editable-string label="Some Property1:" field="property1"></editable-string>
<editable-string label="Some Property2:" field="property2"></editable-string>
</editable-fieldset>
</body>

You can get access to parent controller by passing attribute in child directive link function
link: function (scope, element, attrs, parentCtrl) {
parentCtrl.$scope.editing = true;
}

Related

Angular JS - ng-change not getting triggered

I have created a directive for showing a X besides a text box for clearing the data inside the text box,
Directive JS
angular.module(appName).directive('clrTxt', function () {
return {
restrict: 'E',
replace: true,
scope: {
cntrlas: '=',
mdlval: '='
},
link: function (scope, elem, attrs, ctrl) {
scope.cleartxt = function () {
scope.cntrlas[scope.mdlval] = '';
}
},
template: '<button class="close-icon" type="reset" id="closeicon" ng-click="cleartxt()" ><img src="/resources/img/quote-tool-close.png" class="clear-icon"></button>'
};
});
HTML
<input type="text" ng-model="item.epinNumber" ng-change="numberLengthCheck(item)" >
<clr-txt cntrlas="item" mdlval="'epinNumber'"></clr-txt>
This will create a X icon at the end of the text box and will clear the data when you click on it.
The issue is, I'm triggering a function on-change , So when the X icon is clicked, the data will be cleared and so ideally. the change event should be triggered. But for some reason the change event is not triggered when the data is cleared using the X directive.
The key point is to set bindToControllerand controllerAs alias.then you can access the controller function inside link function of directive.
angular.module('plunker', []);
function MainCtrl($scope) {
$scope.name = 'Test';
$scope.numberLengthCheck = function(n){
alert('change triggered '+ n);
};
}
angular.module('plunker').directive('clrTxt', function(){
return {
restrict: 'E',
controller: 'MainCtrl',
controllerAs: 'vm',
bindToController: true,
scope: {
cntrlas: '='
},
templateUrl: 'reverse_template.html',
replace: true,
link: function(scope, elem, attr, ctrls) {
scope.cleartxt = function () {
scope.cntrlas = '';
scope.numberLengthCheck(scope.cntrlas);
}
}
};
});
In ng-change the expression is not evaluated when the value change is coming from the model. If you want to listen the model $watch is useful
And here I think no need of sending extra attributes (cntrlas,mdlval) to clrTxtdirective. You can access the parent scope in directive by making scope:false which is default.
Directive JS
app.directive('clrTxt', function () {
return {
restrict: 'E',
replace: true,
template: '<button type="reset" ng-click="cleartxt()" >X</button>',
link: function (scope, elem, attrs, ctrl) {
scope.cleartxt = function () {
scope.item.epinNumber = '';
}
}
};
});
Controller JS
app.controller('MainCtrl', function($scope) {
$scope.$watch('item.epinNumber', function(newvalue,oldvalue) {
console.log('new value is ='+newvalue+ ' and old value is ='+oldvalue);
});
});
HTML
<body ng-controller="MainCtrl">
<input type="text" ng-model="item.epinNumber">
<clr-txt ></clr-txt>
{{item.epinNumber}}
</body>
Here is the working plunker LINK

AngularJS - How to pass html attribute variable value to directive?

...
...
UPDATE
HTML
<my-directive ng-repeat="item in items = ( data | filter: {isExists: true})">
something
</my-directive>
<my-second-directive counter="{{items.length}}"></my-second-directive>
JS
angular.module('directives')
.directive('myDirective', function () {
...
})
.directive('mySecondDirective', function () {
return {
restrict: 'EA',
transclude: false,
replace: true,
scope: {
counter: '#'
},
template: '',
link: function (scope, element, attrs) {
alert(scope.counter);
}
});
Excuse me I've not described my question well. My first directive should be ngRepeated, with filter. But in my second directive, I would like to allow to show a counter, how many first directive is currently instantiated, because the user will be able to add and remove instances. So I would like to get the value of the items.length by fly with the second directive. But the link() method of the second directive is fired prior the ngRepeat, so the value of the counter will be an empty string.
Thanks in advance
UPDATE 2
.directive('cardGroupHeader', function($templateCache){
return {
restrict: 'EA',
transclude: true,
replace: true,
require: '^cardGroup',
scope: {
cbiscounter: '=?',
cbcounter: '=?',
cbisarrow: '#?'
},
template: $templateCache.get('card-group-header-tpl.html'),
link: function(scope, $element, $attrs, cardGroupController) {
scope.rowId = cardGroupController.getCurrentId();
console.log(scope.cbcounter);
scope.toggle = function () {
cardGroupController.toggle(scope.rowId)
}
angular.element(document).ready(function () {
console.log(scope.cbcounter);
});
scope.$watch('scope.cbcounter', function (n, o) {
if(n && n != o) {
console.log(n);
}
});
//scope.cbcounter++;
}
};
})
HTML
<card-group-header cbiscounter="true" cbarrow="true" cbcounter="data.length">Waiting for Approval</card-group-header>
<card-group-row cbCollapsed="false">
<card ng-repeat="approveItem in data = (approveItems | filter: {isApproved: false, isRejected: false})">
TEMPLATE
$templateCache.put('card-group-header-tpl.html', '<div class="card-group-header" ng-click="toggle()"><span ng-transclude></span><span class="card-group-counter" ng-if="cbiscounter">{{cbcounter}}</span></div>');
When I change the data.length to 2, this is transferred well. If I use the data.length the scope.cbcounter is always undefined. In case of 2 I've got it back on the console.log(scope.cbcounter);
The counter: '#' means that you are accepting a string value. If you wanted to pass an expression you could either use:
<my-second-directive counter="{{ items.length }}"></my-second-directive>
Or:
.directive('mySecondDirective', function () {
return {
restrict: 'EA',
transclude: false,
replace: true,
scope: {
counter: '=' // Accept two ways binding
},
template: '',
link: function (scope, element, attrs) {
alert(scope.counter);
}
});
EDIT:
I finally quite understand the problem! It's because of attributes are not interpolated until after the link phase. You have two following options:
The first option is wrapping every in the link inside $timeout to take it away from the event loop and be executed after DOM finished manipulating:
.directive('mySecondDirective', function ($timeout) {
return {
restrict: 'EA',
transclude: false,
replace: true,
scope: {
counter: '=' // Accept two ways binding
},
template: '',
link: function (scope, element, attrs) {
$timeout(function() {
alert(scope.counter);
});
}
});
Secondly, using $observe:
attrs.$observe('counter', function(value){
console.log(value);
});
or using $watch as #jusopi suggested.
I think this would be what you want.
Html
<div ng-app="testapp" ng-controller="testctrl">
<div ng-repeat="item in filtereditems">
{{item}}
</div>
<testdir counter="filtereditems.length" />
</div>
Javascript
angular.module('testapp', [])
.directive('testdir', function(){
return {
restrict: 'E',
scope:{
counter: '='
},
link: function(scope, element, attrs) {
alert(scope.counter);
}
}
})
.controller('testctrl', function($scope, $filter){
$scope.items = [
{name: 'A', isExists: true},
{name: 'B', isExists: false},
{name: 'C', isExists: true},
{name: 'D', isExists: false}
];
$scope.filtereditems = $filter('filter')($scope.items, {isExists: true});
})
My jsfiddle is here.
In addition to #LVarayut's answer about the scope's binding expression, the reason the alert is undefined is because linking is not part of the $digest cycle. So binding and data hasn't be effected yet (don't quote me on that, it's the best way I could verbalize what I'm showing in the code below).
Instead you need to use a watcher to trigger the alert
link: ($scope, elem, attrs)->
#undefined because linking isn't part of the $digest cycle
#alert $scope.count
$scope.$watch 'count', (n, o)->
if n and n isnt o
true
#alert n
http://plnkr.co/edit/xt95gb3cTXfUEHgpWK1W?p=preview

How to make a template repeat

I have a directive, I want that template to show all the rows from data set
app.directive('exampleDirective', [ 'TestProvider', 'TestFactory', function (TestProvider, TestFactory) {
return {
restrict: 'E',
template: '<tr><td>{{Name}} </td><td>{{Surname}}</td></tr>',
replace: true,
link: function(scope, elm, attrs) {
var dat= TestFactory.dataReturn();
for (var i = 0; i < dat.length; i++) {
scope.Name = dat[i].Name;
scope.Surname = dat[i].Surname;
console.log(dat[i]);
}
// alert("hah");
}
};
}]);
How can I make it repeat like ng-repeat ?
Assuming your service returning a promise. Here is the simple code to repeat your data in table.
app.directive('exampleDirective', [ 'TestProvider', 'TestFactory', function (TestProvider, TestFactory) {
return {
restrict: 'E',
template: '<table ng-repeat="person in persons"><tr><td>{{person.Name }} </td><td>{{person.Surname}}</td></tr></table>',
replace: true,
link: function(scope, elm, attrs) {
TestFactory.dataReturn().then(function(resp){
scope.persons = resp.data;
});
}
};
}]);
Alternatively, you could redefine your element directive to represent a single object, and then ng-repeat the directive itself. Obviously, this would require moving the location where you use the factory. Your application architecture may or may not be able to accommodate this change.
directive:
app.directive('exampleDirective', [function () {
return {
restrict: 'E',
template: '<tr><td>{{person.Name}} </td><td>{{person.Surname}}</td></tr>',
replace: true,
scope: {
person: '='
}
};
}]);
And usage:
<example-directive ng-repeat="data in dataFromFactory" person="data"></example-directive>

Calling service method from a directive template

Let's say I have a directive:
<component>
<img ng-src='{{something}}' />
</component>
defined as:
app.directive("component", function() {
return {
scope: {},
restrict: 'E',
transclude: true,
template: "<a href='' ng-click='MyService.doThings()' ng-transclude></a>"
}
});
Despite all my efforts, I fail to understand how to accomplish two tasks:
How do I access the inner image source path?
How can I pass this path to service MyService? (think of a lightbox wrapper)
Update with solution:
app.directive("component", function(LightboxService) {
return {
restrict: 'E',
transclude: true,
replace: true,
template: "<a href='' ng-click='lb()' ng-transclude></a>",
link: function (scope, element, attrs) {
scope.lb = function () {
var src = $(element).find("img").attr("src");
LightboxService.show(src);
}
}
}
});
You can access the source path either by binding it to your controller scope or from a link method using attributes.
You can not access Service from a template. You should inject your service into a controller and define a function in $scope to call from the template.
Check your directive below:
app.directive("component", function() {
return {
scope: {
ngSrc: "#", //Text Binding
},
controller: function($scope, MyService) {
$scope.doThings = function() {
MyService.doThings();
}
},
restrict: 'E',
transclude: true,
template: "<a href='{{ng-src}}' ng-click='doThings' ng-transclude></a>"
}
});
You can learn more about directives with isolated scope here:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/

AngularJS - Share ngModel with nested directives and transcluded directives

I am trying to create 3 directives:
.directive('dirOne', function () {
return {
restrict: 'E',
transclude: true,
replace: true,
controller: function ($scope, $element, $attrs) {
this.add = function (tag) {
tag && $scope.tags.push(tag);
};
},
template: '<div><p>Bucket from directive: {{tags}}</p><div ng-transclude></div></div>'
};
})
.directive('dirTwo', function () {
return {
restrict: 'A',
replace: true,
require: '^dirOne',
link: function (scope, element, attrs, dirOne) {
scope.add = function (tag) {
dirOne.add(tag);
};
},
template: '<div>'+
' <input type="text" ng-model="query" datum="sugestions" dir-three>' +
' <button ng-click="add(query)">add</button>' +
'</div>'
};
})
.directive('dirThree', ['$compile', function ($compile) {
var itemsTemplate = '<span class="sugestions"><span ng-repeat="item in datum|filter:query">{{item.name||item}}</span></span>';
return {
restrict: 'A',
transclude: true,
replace: true,
require: 'ngModel',
scope: {
datum: '=',
query: '=ngModel'
},
link: function (scope, element, attrs) {
var parent;
element.wrap('<span>');
parent = element.parent();
parent.append(angular.element($compile(itemsTemplate)(scope)));
}
};
}])
In the dirTwo and dirThree, I have an input <input type="text" ng-model="query" datum="sugestions" dir-three> with ngModel, this input needs to access and modify the content of ngModel, so that the scope is not isolated.
http://jsfiddle.net/joaoneto/hbABU/3/
Update
I Updated version, fix some mistakes I had committed, the content that was being transcluded in dirTwo, should not have the "ADD" function, and belongs to dirTree, hope it helps someone and apologize for peopl take to update this entry... see in http://jsfiddle.net/hbABU/4/

Resources