Pass object context back to controller callback from AngularJS Directive - javascript

I'm essentially trying to recreate ng-change but add some delay in it (auto-save on change frequency timeout).
So far, I have the following directive:
myApp.directive('changeDelay', ['$timeout', function ($timeout) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
callBack: '=changeDelay'
},
link: function (scope, elem, attrs, ngModel) {
var firstRun = true;
scope.timeoutHandle = null;
scope.$watch(function () {
return ngModel.$modelValue;
}, function (nv, ov) {
console.log(firstRun);
if (!firstRun) {
console.log(nv);
if (scope.timeoutHandle) {
$timeout.cancel($scope.timeoutHandle);
}
scope.timeoutHandle = $timeout(function () {
//How can I pass person??
scope.callBack();
}, 500);
}
firstRun = false;
});
}
};
}]);
With the following controller:
myApp.controller('MyCtrl', ['$scope', function ($scope) {
$scope.people = [{
name: "Matthew",
age: 20
}, {
name: "Mark",
age: 15
}, {
name: "Luke",
age: 30
}, {
name: "John",
age: 42
}];
$scope.updatePerson = function (person) {
//console.log("Fire off request to update:");
//How can I get person here??
//console.log(person);
};
}]);
And this markup should be able to define which controller scope method to call as well as the object that is passed to it:
<div ng-app='myApp'>
<div ng-controller="MyCtrl">
<div ng-repeat="person in people">
<input type="text" ng-model="person.name" change-delay="updatePerson(person)" />
</div>
</div>
</div>
Here's an failing fiddle: http://jsfiddle.net/Troop4Christ/fA4XJ/
As you can see, I can't figure out how to call the directive attribute parameter w/ the "person" parameter passed to it.
So like I said, at the begining.. just trying to recreate ng-change w/ some "tweaking". How is this done in ng-change? i.e.

Solution
Isolate scope binding should be declared with "&" instead of "=", thus resulting in scope.callBack() executing the updatePerson(person) given function.
Explanations
When isolating a scope, you work with "#", "=" and "&":
"#" tells angular to watch the result of attribute evaluation against the element scope
"=" tells angular to build the getter/setter with $parse
"&" tells angular to bind a function that will evaluate the attribute (and, as an option, provide an extension to the attribute definition scope as an argument to this function call).
So, when you choose this last option "&", it means that calling callBack() on the isolate directive scope will actually call updatePerson(person) againts the outside scope (not extended with any object coming from isolate scope).
Taking the scope extension capability into account, you could have replaced the person argument of the updatePerson(person) by calling scope.callBack({person: {a:1}}). Then person would have been {a:1} in the updatePerson call scope (function scope, not angular scope).

Related

How to call a controller function from angular directive

I want to call controller function from directive. Here is the fiddle. I have a sayHello() function in controller. And I want to call that function from angular directive. If i wall like scope.sayHello();
scope.sayHello is not a function
I am getting like the above in console.
To get your alert in your fiddle to fire, all I had to do what add the person into your template. You had the updateparent="updatePerson()", and you just needed to pass the person in that call, like this: updateparent="updatePerson(person)". Then your alert fired.
The reason for this is that you need to state in the template all of the parameters that you are passing in to the function. Since you call it like updateparent({person: mandatePerson}), you have to put the key person into your template that it will be called with that param. They have to match.
The Angular directive's link function has arguments for both scope and controller -- if the method you want to call is directly on $scope in your controller you can just call it off of the scope arg-- if you are using controllerAs syntax (which I would recommend as it is a recommended Angular pattern) you can call it off the controller argument.
So, for your specific case (methods directly on $scope) in your directive return object you add a property link:
link: function (scope, iElement, iAttrs, controller, transcludeFn)
scope.sayHello();
}
link runs once at directive creation-- if you want the scope or method to be available outside of that for some reason, assign it to a variable defined at the top level of the module.
I changed your directive a bit, but this is how you get that sort of functionality.
FIDDLE
If you're interested in AngularJS I would highly recommend the John papa styleguide.
https://github.com/johnpapa/angular-styleguide
It will get you using syntax like controllerAs and will help make your code cleaner.
HTML
<body ng-app="myApp" ng-controller="MainCtrl">
<div>
Original name: {{mandat.name}}
</div>
<my-directive mandat="mandat"></my-directive>
</body>
JS
var app = angular.module('myApp', []);
app.controller('MainCtrl', MainController);
function MainController($scope) {
$scope.mandat = {
name: "John",
surname: "Doe",
person: { id: 1408, firstname: "sam" }
};
}
app.directive('myDirective', MyDirective);
function MyDirective() {
return {
restrict: 'E',
template: "<div><input type='text' ng-model='mandat.person.firstname' /><button ng-click='updateparent()'>click</button></div>",
replace: true,
scope: {
mandat: "="
},
controller: function($scope) {
$scope.updateparent = function() {
$scope.mandat.name = "monkey";
}
}
}
}

Passing function into ng-repeat object

so I have a problem with ng-repeat directive. In my code I have a parent controller which have data stored as array of objects.
$scope.queue = [
{
name: 'Mark',
sex: 'Male',
age: 21
},
{...}
];
$scope.changePositionInQueue = function (currIndex, targetIndex) {
// move up / down person in queue
};
What I want to do is pass parent controller's function to my directive's ('person') isolated scope and at the same time be able to use '$index', '$first', '$last' variables.
<person data-change-position="changePositionInQueue" data-person="person" ng-repeat="person in queue"></person>
Directive scope declaration:
scope: {
person: '=',
changePosition: '&'
}
The problem is that when I create isolated scope inside ng-repeat loop I lose ng-repeat properties. On the other hand when I create default scope by ng-repeat and I have access to all wanted properties I can't use parent function.
This is my solution to your challenge:
In view:
<my-directive dir-func="fnc($index)" data="data" ng-repeat="data in datas"><span>{{data.id|json}}</span><br></my-directive>
In Direct call parent function in link:
myApp.directive('myDirective', function() {
return {
//require: 'ngModle',
scope: {
data: "=",
dirFunc: "&"
},
link: function(scope, elem, attr, ngModel) {
scope.dirFunc() // parent scope.func is called here where you get the alert
}
}
See the Plunker for detail

How to pass an object from a nested directive with isolated scope to parent controller scope in angular

I have a directive treeview which contains a nested directive (being the branches) of each item rendered.
In the scope of both directives I have declared two parameters that should be talking to the parent controller.
filter: '&' //binds the method filter within the nested directive (branch) to the method doSomething() in the tree directive attribute which is bound to the html directive that binds to the controller.
iobj: '=' is the two way binding paramter that should be passing the scoped object to the controller. (but currently isn't)
Directive:
app.directive('tree', function () {
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&',
iobj: '='
},
controller: 'treeController',
template: '<ul><branch ng-repeat="c in t.children" iobj="object" src="c" filter="doSomething()"></branch></ul>'
};
});
app.directive('branch', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&',
iobj: '='
},
template: '<li><input type="checkbox" ng-click="innerCall()" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
var has_children = angular.isArray(scope.b.children);
scope.visible = has_children;
if (has_children) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (has_children) {
element.toggleClass('collapsed');
}
});
scope.innerCall = function () {
scope.iobj = scope.b;
console.log(scope.iobj);
scope.filter();
}
}
};
});
HTML:
<div ng-controller="treeController">
<tree src="myList" iobj="object" filter="doSomething()"></tree>
<a ng-click="clicked()"> link</a>
</div>
Controller:
app.controller("treeController", ['$scope', function($scope) {
var vm = this;
$scope.object = {};
$scope.doSomething = function () {
var item = $scope.object;
//alert('call from directive');
console.log(item);
}
$scope.clicked = function () {
alert('clicked');
}
...
Currently I can invoke the function $scope.doSomething from the directive to the controller. So I know that I have access to the controllers scope from the directive. What I cannot figure out is how to pass an object as a parameter from the directive back to the controller. When I run this code, $scope.object
is always an empty object.
I'd appreciate any help or suggestions on how to go about this.
The & directive binding supports parameter passing. Given your example
scope.filter({message: 'Hello', anotherMessage: 'Good'})
The message and anotherMessage become local variables in the expression bound to directive:
<tree src="myList" iobj="object" filter="doSomething(anotherMessage, message)"></tree>
Here's a sample plunker where the callback parameters are set inside a template.
The documentation clearly states that:
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. For
example, if the expression is increment(amount) then we can specify
the amount value by calling the localFn as localFn({amount: 22}).

Test directive using an expression wrapper function

I have to test a directive depending on a parent scope function for its initialisation:
.directive('droppedSnippet', function () {
return {
templateUrl: 'views/dropped-snippet.html',
restrict: 'E',
scope: {
id: '#',
get: '&'
},
link: function postLink(scope, element, attrs) {
var s = scope.get({id: attrs.id});
element.find('.title').text(s.title);
}
};
});
Context, skip if in a hurry: In order to make it easier to imagine (and to discuss the whole idea if you want), on a drop event this directive is added to the document. The directive represents an embed code. During linking the directive, knowing only its id, should fetch its content from a controller and fill its markup.
In order to mock the parent scope created by the controller, i set up the following mock:
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.foo = function() {
return {
title: 'test title',
code: 'test <code>'
};
};
spyOn(scope, 'foo').andCallThrough();
element = angular.element('<dropped-snippet id="3" get="foo(id)"></dropped-snippet>');
element = $compile(element)(scope);
}));
it('calls the scope function', function() {
expect(scope.foo).toHaveBeenCalledWith(3);
});
The test fails, scope.foo is not called. The code works on the server though. I can not find similar examples around. Is this the right way to mock a function in the parent scope?
Try expect(scope.foo).toHaveBeenCalledWith("3");
Or cast it as a number, the # treats it as a String

In a directive, handle calls to a user defined method name

Is there a way to allow the user to give a method name to a directive, let the directive create that method on the scope, and then handle calls to that method?
So, first I let the user define a method name HELLO, and then I let the user call HELLO from elsewhere (still in the same scope)
<div ng-controller="AppController">
<div mydirective="" mydirective-data="MyJson" mydirective-fx="HELLO" />
<button ng-click="HELLO()">Click me</button>
</div>
Internally, the directive should see the HELLO and map it to its own method. In the directive, I am looking at the method name being passed in and assigning it
app.directive('mydirective', function() {
return {
restrict: 'A',
scope: {
data: '=mydirectiveData',
fx: '=mydirectiveFx'
},
link: function(scope, element, attrs) {
scope.fx = function () { console.log(scope.data); } ;
}
}
}
);
as you can see, I am assigning scope.fx, which should be HELLO, to a function which should read scope.data, defined in the controller.
Attempting this does not do anything nor does it throw an error. It makes me wonder if I am doing this the wrong way.
For clarity, I have created a plunker. Remember to open the console.
Use # instead of = then scope[scope.fx] to create the property:
app.directive('mydirective', function() {
return {
restrict: 'A',
scope: {
data: '=mydirectiveData',
fx: '#mydirectiveFx'
},
link: function(scope, element, attrs) {
scope[scope.fx] = function () { console.log(scope.data); };
}
}
}
);
http://plnkr.co/edit/a2c14O?p=preview

Categories

Resources