ng-blur in directive template won't callback on parent controller function - angularjs

I am trying to create a directive that uses a template with a input field that makes a callback to the parent controller when the input field loses focus so that I can update other values in the parent model. The directive is implementing jQuery autocomplete. I have tried both calling the parent callback function directly as well as calling a function on the local scope that then in turn calls the callback function.
Here is the directive code and the element the directive is applied to. What am I missing?
HTML:
<div iptm-ext-lookup extension="button.destination" siteid="vm.buttonInfo.primaryExtSiteID" buttonid="button.identifier" extensionselected="vm.extensionSelected" numberlostfocus="vm.numberLostFocus(buttonid)"></div>
JS:
angular.module("iptmApp").directive("iptmExtLookup", function () {
function Controller($scope, $element, $attrs, extensionMgmtService) {
$scope.getData = function (extension, siteid) {
return extensionMgmtService.getExtensionInfo(extension, siteid);
}
}
// I bind the $scope to the DOM behaviors.
function link(scope, element, attributes, controllers) {
// Setup jquery UI
element.autocomplete({
minLength: 3,
source: [],
select: function (event, ui) {
scope.extensionselected(scope.buttonid, ui.item.value, ui.item.extId, ui.item.extLabel);
}
});
//Watcher to update autocomplete list when the input data changes
scope.$watch('extension', function (value) {
if (value != null && value.length > 2) {
scope.getData(value, scope.siteid).success(function (data) {
element.autocomplete("option", "source", data);
}).error(function (data, status, headers, config) {
alert(status);
});
}
});
}
// Return the directive confirugation.
return ({
controller: Controller,
link: link,
restrict: "EA",
replace: true,
template: '<input type="text" ng-model="extension" ng-blur="lostFocusCallback({ buttonid: $scope.buttonid })" style="width:100px; height:14px;">',
scope: {
extension: '=',
buttonid: '=',
extensionselected: '=',
lostFocusCallback: '&numberlostfocus',
siteid: '='
}
});
});

Have a closer read of the documentation...
Often it's desirable to pass data from the isolated scope via an expression to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn
You'll need the following
<div iptm-ext-lookup ... numberlostfocus="vm.numberlostfocus(buttonid)"></div>
<!-- note the argument name ^ -->
and in your directive
template: '<input ... ng-blur="lostFocusCallback({ buttonid: buttonid })" ...'
You didn't need $scope.buttonid as
The template is already bound to the directive's scope, and
There is no $scope variable defined in the template

Related

angularjs: can' t pass parameter to the parent controller method via '&' binding in isolated directive scope

In my angular application, I defined a directive foo-directive, which is placed in a parent controller as below:
<div ng-app="app">
<div ng-controller="ParentCtrl as parent">
<foo-directive data="parent.city" tab-click="parent.tClick()" tab-click2="parent.tClick2(v)"></foo-directive>
</div>
</div>
pass two method: parent.tClick() and parent.tClick2(v) into the directive and bind to tab-click and tab-click2 attributes respectively. The difference is that the second one has a parameter
and the JS code goes as below:
function ParentCtrl($timeout){
this.city= "London";
this.tClick = function(){
console.log("debugging parent tclick...");
}
this.tClick2 = function(v){
console.log("debugging parent tclick2...");
console.log(v)
}
}
function FirstCtrl() {
this.$onInit = function(){
this.click = function(){
this.tabClick();
this.tabClick2("abc");
}
}
}
function fooDirective() {
return {
scope: {
data: '=',
tabClick : "&",
tabClick2: "&"
},
controller: 'FirstCtrl',
controllerAs: 'foo',
bindToController: true,
template: '<div ng-click="foo.click()">{{ foo.data }}</div>',
link: function ($scope, $element, $attrs, $ctrl) {
//console.log($ctrl.name);
}
};
}
Now the issue comes from the second method this.tabClick2("abc"). There is TypeError message. I have reproduced this issue with this live demo:
https://jsfiddle.net/baoqger/sv4d03hk/1/
any help?
When passing the functions into your directive, you should pass the "reference" to the function, rather than the "result" of the function. Adding the parenthesis, is actually executing the function and returning the result to the directive. As neither function returns a value, they will both be passing undefined.
Given that the value of the parameter you want to pass (v) to the function is inside the directive scope, not that of the parent, you dont need to even tell the directive that the function accepts a parameter. You just pass it to the function inside the directive. ie.
<foo-directive data="parent.city" tab-click="parent.tClick" tab-click2="parent.tClick2"></foo-directive>
According to the docs, using & is then you want to evaluate the result of the attribute:
The & binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time.
Instead, we want to be able to execute the passed attribute, and in particular give it a variable. As such either = (two-way binding) or # (one-way binding) are probably more appropriate for what we're after.
tabClick: "=",
tabClick2: "="
You can also do away with your controller completely by updating your template.
template: '<div ng-click="foo.tabClick();foo.tabClick2(data)">{{ foo.data }}</div>'
Updated JSFiddle
function ParentCtrl($timeout) {
this.city = "London";
this.tClick = function() {
console.log("debugging parent tclick...");
}
}
function FirstCtrl() {}
function fooDirective() {
return {
scope: {
data: '=',
tabClick: "="
},
controller: 'FirstCtrl',
controllerAs: 'foo',
bindToController: true,
template: '<div ng-click="foo.tabClick(data)">{{ foo.data }}</div>',
link: function($scope, $element, $attrs, $ctrl) {
//console.log($ctrl.name);
}
};
}
angular
.module('app', [])
.directive('fooDirective', fooDirective)
.controller('FirstCtrl', FirstCtrl)
.controller('ParentCtrl', ParentCtrl)
<script src="https://code.angularjs.org/1.6.2/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="ParentCtrl as parent">
<foo-directive data="parent.city" tab-click=":: parent.tClick"></foo-directive>
</div>
</div>
PS. if you're concerned about performance using # or =, consider one time bindings using ::. ie. <foo-directive data="parent.city" tab-click=":: parent.tClick" tab-click2=":: parent.tClick2"></foo-directive>
Try the following snippet of code for your "FirstCtrl":
function FirstCtrl() {
this.$onInit = function(){
this.click = function(){
this.tabClick({v: this.data});
}
}
}
As you are using an expression binding (&), you need to explicitly call it with a JSON containing "v" and it's value. like the following:
this.tabClick({v: this.data});

How to pass object to directive through attribute in AngularJS?

I want to pass object (reference - two way binding) through ATTRIBUTE not by isolated scope. How can I do this? Because code bellow passing string instead of object:
HTML
<tr ng-form="rowForm" myDirective="{{row.data}}">
Directive
angular.module("app").directive("myDirective", function () {
return {
require: ["^form"],
restrict: "A",
link: function (scope, element, attrs, ctrls) {
scope.$watch(function () {
return attrs.myDirective;
}, function (newValue, oldValue) {
// .....
Directives can do two-way data binding without parsing or compile anything manually, sorry for not delivering the plunker but it's rebelius and won't save for me
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.myObj = {name: 'Tibro', age: 255}
})
.directive('myDirective', function(){
return {
scope: {
'myAttribute': '='
},
template: '{{myAttribute}}',
link: function(scope){
scope.myAttribute.age= 31
}
}
})
HTML
<body ng-controller="MainCtrl">
controller: {{myObj}} <br/>
directive: <my-directive my-attribute="myObj"></my-directive>
</body>
OUTPUT
controller: {"name":"Tibro","age":31}
directive: {"name":"Tibro","age":31}
you can see from the output that passed object has been binded two-way and change made in directive is reflected on controller level
The result of {{ }} interpolation is a string. An object can't be passed like that.
Bindings are idiomatic here and thus preferable. The whole thing becomes messy when the directive is forced to use parent scope. However, it can be done by parsing scope properties manually with $parse:
$scope.$watch(function () {
var myDirectiveGetter = $parse($attrs.myDirective);
return myDirectiveGetter($scope);
}, ...);
This is a job for binding (< or =, depending on the case). If isolated scope isn't desirable, this can be done with inherited scope and bindToController:
scope: true,
bindToController: {
myDirective: '<'
},
controllerAs: `vm`,
controller: function ($scope) {
$scope.$watch('vm.myDirective', ...);
}
Notice that directive attribute is my-directive, not myDirective.
Couple of things:
myDirective in the tr should be my-directive, as per Angular's
conventions.
{{row.data}} prints the variable, you need to pass it without the
{{}} for it to go through as an object.

inherit Angular parent directive attribute with isolate scope

I'm trying to inherit an attribute from a parent custom directive with isolate scope. In the example below, I want to be able to access the api attribute on myParent from the myChild controller or link function. My end goal is to inject an instance of the api that can be accessed by the children and from the view controller.
<my-parent api="parentInstance1">
<my-child ng-repeat="field in ::data"
ng-attr-src="{{field.src||undefined}}"
</my-child>
</my-parent>
<my-parent api="parentInstance2">
<my-child ng-repeat="field in ::data"
ng-attr-src="{{field.src||undefined}}"
</my-child>
</my-parent>
A simplified version of both directives looks like this
app.directive('myParent', function () {
return {
transclude: true,
restrict: "E",
scope: {
api: '=?'
},
template: '...',
controller: function ($scope, $attrs ) {
// foo is injected from a factory instance
function foo ( ) {
}
$scope.api = {
foo: foo
}
},
link: function ($scope, $element, $attr) {
}
}
});
app.directive('myChild', function () {
return {
require: "^myParent",
restrict: "E",
scope: {
api: '=?'
},
template: "...",
controller: function ( $scope ) {
// I want to access $scope.api in link or controller
},
link: function ($scope, $element, $attr) {
// I want to access $scope.api in link or controller
}
}
});
I can't access $scope.api from the child directive but $scope.parentInstance1 and $scope.parentInstance2 are visible. I realise I can just explicitly declare but I'd rather understand how to do it correctly.
I dont know why you are referencing parentInstance1 and parentInstance2 on my-parent but the attributes on my-child are in myParent's $scope so you can reference the actual $scope.api object that is on myParent's $scope in the attributes of the my-child directive tag and then reference the name of the attribute in the isolate scope definition of the myChild directive.
<my-child inner-api="api"></my-child>
.. and then in the child directive...
app.directive('myChild', function () {
...
scope: {
innerApi: '=?'
}
...
controller: function($scope) {
$scope.innerApi // <- accessible in the controller
}
Heres a simplified fiddle...

How to bind a method in a custom directive to the parent scope in angularjs

First foray into custom directives and getting a bit stuck on binding to a method in the parent scope.
So when I use my custom directive in my app:
<dropdown x-label="Stuff" x-divider="-"
x-list = "listOfStuff"
x-ng-model="id"
x-change-select="controllerMethodToBeCalled(id)">
</dropdown>
The template is as follows and triggers the 'update' function in the directive controller on ng-change.
<div class="dropdown_container">
<div class="select_label">{{label}}</div>
<div class="select_divider">{{divider}}</div>
<select class="dropdown_select" id="dropdownList"
ng-model="ngModel"
ng-options="option.id as option.name for option in list | orderBy:'name'"
ng-change="update(ngModel)">
</select>
</div>
The directive code is below and I can call the method in parent scope directly whcih works fine but I'd like to trigger the 'change-select' that I've bound function 'controllerMethodToBeCalled' passing the id. Otherwise the component is not truly self contained.
angular.module('myApp.component.dropdown', [])
.directive('dropdown', function() {
return {
restrict: 'E',
require: "^ngModel",
replace: true,
scope: {
list: "=",
ngModel: '=',
changeSelect: '&'
},
templateUrl: 'component/dropdown/dropdown.tpl.html',
link: function ($scope, element, attrs) {
attrs.$observe('label', function (value) {
$scope.label = value;
});
attrs.$observe('divider', function (value) {
$scope.divider = value;
});
},
controller: function($scope){
$scope.update = function(id){
//replace this line with call to changeSelect passing id
$scope.$parent.controllermethodToBeCalled(id);
};
}
};
});
Probably very easy to fix but I just can't see it. Any ideas/suggestions?
You only need to call $scope.changeSelect(). See this Plunker. If you don't want to use the id from a parent scope, but rather pass the id argument from the function, you can do that do:
$scope.update = function(id){
$scope.changeSelect({id: id});
};
It basically says use the value of the local variable id for id in the expression controllerMethodToBeCalled(id).

Two way binding, shallow $watch, isolate scope not working together

Please refer to this fiddle for the questions. http://jsfiddle.net/AQR55/
1) Why a watch that is attached to an isolate scope property - which is bidirectionally bound to a parent property, is not triggering on changing the parent scope property.
In the fiddle, the below metioned watch is not getting triggered, on changing the parent scope property to which it is bound.
$scope.$watch('acts', function(neww ,old){
console.log(neww)
})
2) ng-click="addaction()" addaction="addaction()". Can this code be put in more elegant way? Because, to perform an action in isolated scope, it seems we need to set bidirectional binding and the attach to ng-click.
3)Can i declare methods inside the isolated scope like shown below? If i do like this, I'm getting .js error.
<isolate-scope-creating-cmp ng-click="isolateCmpClickHandler()"></isolate-scope-creating-cmp>
scope:{
isolateCmpClickHandler:function(){
//If i do like this, I'm getting .js error
}
}
Question 1.
Since you are adding a item to the acts array, you need to set the third parameter in $watch() to true
$scope.$watch('acts', function (neww, old) {
console.log(neww)
}, true);
Demo: Fiddle
Question 2.
Since there is an isolated scope, you need to call the $parent scope's function
<input type="button" bn="" acts="acts" ng-click="$parent.addaction()" value="Add Action" />
Demo: Fiddle
Question 3.
Yes you can, but you need to use a controller
animateAppModule.directive('bn', function () {
return {
restrict: "A",
scope: {
acts: '='
},
link: function ($scope, iElement, iAttrs) {
$scope.$watch('acts', function (neww, old) {
console.log(neww)
}, true)
},
controller: function($scope){
$scope.dosomething = function(){
console.log('do something')
}
}
}
})
Demo: Fiddle
An overall solution could look like
<input type="button" bn="" acts="acts" addaction="addaction()" value="Add Action" />
JS
animateAppModule.controller('tst', function ($scope) {
$scope.acts = [];
$scope.addaction = function () {
$scope.acts.push({
a: "a,b"
})
}
})
animateAppModule.directive('bn', function () {
return {
restrict: "A",
scope: {
acts: '=',
addaction: '&'
},
link: function ($scope, iElement, iAttrs) {
$scope.$watch('acts', function (neww, old) {
console.log(neww)
}, true);
iElement.click(function(){
$scope.$apply('addaction()')
})
}
}
})
Demo: Fiddle

Resources