Pass object to Angular directive's '&' parent scope function - javascript

How might one go about passing an object to Angular's (Angular 1.4.8) & ampersand scope binding directive?
I understand from the docs that there is a key-destructuring of sorts that needs named params in the callback function, and the parent scope uses these names as args. This SO answer gives a helpful example of the expected & functionality. I can get this to work when explicitly naming the params on the parent controller function call.
However, I am using the & to execute actions via a factory. The parent controller knows nothing of the params and simply hands the callback params to a dataFactory, which needs varied keys / values based on the action.
Once the promise resolves on the factory, the parent scope updates with the returned data.
As such, I need an object with n number of key / value pairs, rather than named parameters, as it will vary based on each configured action. Is this possible?
The closest I have seen is to inject $parse into the link function, which does not answer my question but is the sort of work-around that I am looking for. This unanswered question sounds exactly like what I need.
Also, I am trying to avoid encoding/decoding JSON, and I would like to avoid broadcast as well if possible. Code stripped down for brevity. Thanks...
Relevant Child Directive Code
function featureAction(){
return {
scope: true,
bindToController: {
actionConfig: "=",
actionName: "=",
callAction: "&"
},
restrict: 'EA',
controllerAs: "vm",
link: updateButtonParams,
controller: FeatureActionController
};
}
Child handler on the DOM
/***** navItem is from an ng-repeat,
which is where the variable configuration params come from *****/
ng-click="vm.takeAction(navItem)"
Relevant Child Controller
function FeatureActionController(modalService){
var vm = this;
vm.takeAction = takeAction;
function _callAction(params){
var obj = params || {};
vm.callAction({params: obj}); // BROKEN HERE --> TRYING
//TO SEND OBJ PARAMS
}
function executeOnUserConfirmation(func, config){
return vm.userConfirmation().result.then(function(response){ func(response, config); }, logDismissal);
}
function generateTasks(resp, params){
params.example_param_1 = vm.add_example_param_to_decorate_here;
_callAction(params);
}
function takeAction(params){
var func = generateTasks;
executeOnUserConfirmation(func, params);
}
Relevent Parent Controller
function callAction(params){
// logs undefined -- works if I switch to naming params as strings
console.log("INCOMING PARAMS FROM CHILD CONTROLLER", params)
executeAction(params);
}
function executeAction(params){
dataService.executeAction(params).then(function(data){
updateRecordsDisplay(data); });
}

I think the example below should give you enough of a start to figure out your question:
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<meta charset="utf-8">
<title>Angular Callback</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
var myApp = angular.module("myApp", []);
myApp.controller('appController', function($scope) {
$scope.var1 = 1;
$scope.handleAction1 = function(params) {
console.log('handleAction1 ------------------------------');
console.log('params', params);
}
$scope.handleAction2 = function(params, val1) {
console.log('handleAction2 ------------------------------');
console.log('params', params);
console.log('val1', val1);
}
});
myApp.controller('innerController', innerController);
innerController.$inject = ['$scope'];
function innerController($scope) {
$scope.doSomething = doSomething;
function doSomething() {
console.log('doSomething()');
var obj = {a:1,b:2,c:3}; // <-- Build your params here
$scope.callAction({val1: 1, params: obj});
}
}
myApp.directive('inner', innerDirective );
function innerDirective() {
return {
'restrict': 'E',
'template': '{{label}}: <button ng-click="doSomething()">Do Something</button><br/>',
'controller': 'innerController',
'scope': {
callAction: '&',
label: '#'
}
};
}
</script>
</head>
<body ng-controller="appController">
<inner label="One Param" call-action="handleAction1(params)"></inner>
<inner label="Two Params" call-action="handleAction2(params, val)"></inner>
</body>
</html>
In the appController I have two functions that will be called by the inner directive. The directive is expecting the outer controller to pass in those functions using the call-action attribute on the <inner> tag.
When you click on the button within the inner directive it called the function $scope.doSomething This, in turn calls to the outer controller function handleAction1 or handleAction2. It also passes a set of parameters val1 and params:
$scope.callAction({val1: 1, params: obj});
In your template you specify which of those parameters you want to be passed into your outer controller function:
call-action="handleAction1(params)"
or
call-action="handleAction2(params, val)"
Angular then uses those parameter names to look into the object you sent when you called $scope.callAction.
If you need other parameters passed into the outer controller function then just add then into the object defined in the call to $scope.callAction. In your case you would want to put more content into the object you pass in:
var obj = {a:1,b:2,c:3}; // <-- Build your params here
Make that fit your need and then in your outer controller you would take in params and it would be a copy of the object defined just above this paragraph.
It this is not what you were asking, let me know.

Related

How to pass data from directive to controller

I've built a custom directive in my app which utilizes D3.js. I want to be able to make an API call to load more data when a user clicks on a node within my D3 visualization. This will require grabbing the data associated with the node which was clicked and passing it back to my controller. The controller then handles calling a function to retrieve more data.
To get started I'm simply trying to log the data associated with the node a user clicked in my controller. My problem is that this data is undefined in my controller.
Relevant directive code:
angular.module('gameApp')
.directive('gmLinkAnalysis', gmLinkAnalysis);
gmLinkAnalysis.$inject = ['$location', 'd3'];
function gmLinkAnalysis($location, d3) {
var directive = {
restrict: 'E',
templateUrl: '/app/gmDataVis/gmLinkAnalysis/gmLinkAnalysis.directive.html',
scope: {
data: '=',
logNode: '&'
},
link: function(scope) {
...
function click(d) {
scope.logNode(d);
}
}
};
return directive;
}
HTML:
<gm-link-analysis data="connections.users" log-node="connections.logNode(d)"></gm-link-analysis>
Relevant controller code:
angular.module('gameApp')
.controller('ConnectionsController', ConnectionsController);
function ConnectionsController() {
var vm = this;
...
vm.logNode = function(d) {
console.log(d);
};
}
If I replace d in my html with a string such as "hello world" (log-node="connections.logNode('hello world')") it is properly logged. So clearly my issue lies in not properly passing my data as the parameter in my html. How would I go about doing so?
you need to specify the parameter in the call:
so in your directive it should be
function click(d) {
scope.logNode({d: d})
}
here is an example:
http://jsfiddle.net/heavyhorse/7983y06k/
You may pass a model attaching some methods into the directive but I personally prefer using $.broadcast service to keep my codebase cleaner.
Directive
function click(d) {
$rootScope.$broadcast('someEvent', d);
}
Controller
angular.module('gameApp')
.controller('ConnectionsController', ConnectionsController);
function ConnectionsController() {
var vm = this;
vm.$on('someEvent', function(event, data) {
console.log(data)
});
}
If you still think that passing methods around would make the trick for you, here is a simple example passing a method to the directive via a data model

Testing the controller passed to an Angular Material Dialog instance

First off, I am trying to unit test the controller that is being passed to an Angular Material Dialog instance.
As a general question, does it make more sense to test such a controller separately, or by actually invoking$mdDialog.show()?
I am attempting the first method, but I'm running into some issues, mostly related to how Angular Material binds the "locals" to the controller.
Here is the code that I am using to invoke the dialog in my source code, which works as expected:
$mdDialog.show({
controller: 'DeviceDetailController',
controllerAs: 'vm',
locals: {deviceId: "123"},
bindToController: true,
templateUrl: 'admin/views/deviceDetail.html',
parent: angular.element(document.body),
targetEvent: event
});
I don't believe the docs have been updated, but as of version 0.9.0 or so, the locals are available to the controller at the time the constructor function is called (see this issue on Github). Here is a stripped-down version of the controller constructor function under test, so you can see why I need the variable to be passed in and available when the controller is "instantiated":
function DeviceDetailController(devicesService) {
var vm = this;
vm.device = {};
// vm.deviceId = null; //this field is injected when the dialog is created, if there is one. For some reason I can't pre-assign it to null.
activate();
//////////
function activate() {
if (vm.deviceId != null) {
loadDevice();
}
}
function loadDevice() {
devicesService.getDeviceById(vm.deviceId)
.then(function(data) {
vm.device = data.collection;
};
}
}
I am trying to test that the device is assigned to vm.device when a deviceId is passed in to the constructor function before it is invoked.
The test (jasmine and sinon, run by karma):
describe('DeviceDetailController', function() {
var $controllerConstructor, scope, mockDevicesService;
beforeEach(module("admin"));
beforeEach(inject(function ($controller, $rootScope) {
mockDevicesService = sinon.stub({
getDeviceById: function () {}
});
$controllerConstructor = $controller;
scope = $rootScope.$new();
}));
it('should get a device from devicesService if passed a deviceId', function() {
var mockDeviceId = 3;
var mockDevice = {onlyIWouldHaveThis: true};
var mockDeviceResponse = {collection: [mockDevice]};
var mockDevicePromise = {
then: function (cb) {
cb(mockDeviceResponse);
}
};
var mockLocals = {deviceId: mockDeviceId, $scope: scope};
mockDevicesService.getDeviceById.returns(mockDevicePromise);
var ctrlConstructor = $controllerConstructor('DeviceDetailController as vm', mockLocals, true);
angular.extend(ctrlConstructor.instance, mockLocals);
ctrlConstructor();
expect(scope.vm.deviceId).toBe(mockDeviceId);
expect(scope.vm.device).toEqual(mockDevice);
});
});
When I run this, the first assertion passes and the second one fails ("Expected Object({ }) to equal Object({ onlyIWouldHaveThis: true })."), which shows me that deviceId is being injected into the controller's scope, but apparently not in time for the if clause in the activate() method to see it.
You will notice that I am trying to mimic the basic procedure that Angular Material uses by calling $controller() with the third argument set to 'true', which causes $controller() to return the controller constructor function, as opposed to the resulting controller. I should then be able to extend the constructor with my local variables (just as Angular Material does in the code linked to above), and then invoke the constructor function to instantiate the controller.
I have tried a number of things, including passing an isolate scope to the controller by calling $rootScope.$new(true), to no effect (I actually can't say I fully understand isolate scope, but $mdDialog uses it by default).
Any help is appreciated!
The first thing I would try would be to lose the 'as vm' from your call to $controller. You can just use the return value for your expect rather than testing scope.
Try this:
var ctrlConstructor = $controllerConstructor('DeviceDetailController', mockLocals, true);
angular.extend(ctrlConstructor.instance, mockLocals);
var vm = ctrlConstructor();
expect(vm.deviceId).toBe(mockDeviceId);
expect(vm.device).toEqual(mockDevice);

How to dynamically bound value to a link using AngularJS

The problem that I need to generate link on the fly since the link is set in ng-repeat. I think I need to execute custom function inside ng-repeat loop which gets data from $http and pushes link to $scope.array. Then bound href to $scope.array[someIndex]....The problem I don't know if:
it's the only way
a good design
how to implement it
Example:
HTML
<div ng-repeat-start="item in items">
the link
// here execute $scope.getUrl(item ) somehow
<div class="extra-div">
<div ng-repeat-end=""></div>
Controller:
$scope.arrayOfUrls= [];
$scope.getUrl = function(url){
$http.get(url).then(
function(data){
arrayOfUrls.push(data.link);
}
)
}
How to execute getUrl during ng-repeat cycle?
PS. I cannot bound href directly to getUrl function since there is $http which eventually result in infinite digest loop.
Also promises can be returned not in order so expecting that first call to getUrl will push link to $scope.arrayOfUrls[0] is false assumption.
UPDATE:
As #Claies suggested I trie to prefetch links like this:
Contoller executes $scope.loadFeed();
$scope.loadFeed = function() {
http.jsonp('feed url').then(function(res) {
$scope.feeds = res.data.responseData.feed.entries;
$scope.feeds.forEach(function(e) {
// prefetch content and links for each feed
//hook new entryStateUrl property to feed objects
e['entryStateUrl'] = $scope.getEntryStateUrl(e.link); // e['entryStateUrl'] is undefined
})
})
}
}
$scope.getEntryStateUrl = function(inputUrl) {
$http.get(inputUrl).then(function(data) {
// do stuff
return data.link;
});
}
}
Now seems like I am trying pre-fetch urls but getting undefined for e['entryStateUrl']...
The problem maybe about assigning scope variable when $http is not done getting results... Also it seems like there are nested promises: $http.jsonp and inside it $http.get.
How to fix it?
As this requires UI enhancement, a directive would be a good approach. How about a directive like this ( JSFiddle here ). Please note that I am calling $window.open here - you can replace this with whatever the application requires. :-
todoApp.directive('todoLinks', ['$window',function ($window) {
var directive = {};
directive.restrict = 'A';
directive.transclude = 'true';
directive.scope = { ngModel: '=ngModel', jsOnClick:'&' };
directive.template = '<li ng-repeat="item in ngModel">{{item.name}}</li>';
directive.link = function ($scope, element, attributes) {
$scope.openLink = function (idx) {
$window.open($scope.ngModel[idx].link); //Replace this with what your app. requires
if (attributes.jsOnClick) {
//console.log('trigger post jsOnClick');
$scope.jsOnClick({ 'idx': idx });
}
};
};
return directive;
}]);
When the controller fills the todo items like this:-
todoApp.controller("ToDoCtrl", ['$scope','$timeout','dbService',function($scope, $timeout, dbService)
{
$scope.todo=[{"name":"google","link":"http://www.google.com"},{"name":"bing","link":"http://www.bing.com"},{"name":"altavista","link":"http://www.altavista.com"}];
}]);
Usage of this directive is simple:-
<div todo-links ng-model="todo"></div>

How to assign variables to an aliased Controller inside a function?

(Disclaimer: I don't really know if this question fits the Stackoverflow definition of 'question', since I already have (more than) one solution for the problem, I just don't like the solutions I've found. I apologize in advance if it happens to be so, and welcome alternatives.)
I'm creating an Angularjs Directive and I'd like to use the Controller as ctrl syntax so that I can have ctrl.property in my HTML, instead of a non-contextual, possibily-shadowing property.
However, this implies that, on the Controller function, variables to be accessed in the HTML need to be bound to this. For example:
<!-- HTML file: greetings.html -->
<ul>
<li ng-repeat="item in main.items track by $index" ng-bind="item"></li>
</ul>
angular.module('GreetingModule').directive('greetings', function() {
'use strict;'
return {
restrict: 'E',
templateUrl: 'greetings.html',
controller: function () {
this.greetings = ['Hello', 'Hola', 'Salut'];
},
controllerAs: main
}
});
I'm very okay with this. But things fall apart when I start to use functions.
Let's say I need to load the greetings from a service.
angular.module('GreetingModule').directive('greetings',
['langService', function(langService) {
'use strict;'
return {
restrict: 'E',
templateUrl: 'greetings.html',
controller: function () {
this.greetings = [];
function languagesLoaded(langs) {
for (var i = 0; i < langs.length; i++) {
this.greetings.push(langs[i].greeting);
}
}
langService.load({
callback: languagesLoaded
});
},
controllerAs: 'main'
};
}]);
This will fail. At the time of the callback, when languagesLoaded is called, this is not bound and the function will throw an error, this.greetings is undefined.
I've found three ways around this, but I dislike all three (I don't really have any technical reason for disliking them, they just feel wrong, like I'm trying to do something I'm not supposed to):
Create a variable pointing to this and use that in the function:
var self = this;
// ...
self.greetings.push(langs[i].greeting);
Passing this in the object argument to langService.load():
/* In the directive */
langService.load({
target: this,
callback: languagesLoaded
})
/* In the service */
function load(config) {
// load languages, then:
config.languagesLoaded.call(target, languages);
}
Binding the array both to this and to the function scope, so that changing the scope variable also affects the this variable (since they reference the same array):
var greetings = this.greetings = [];
// ...
greetings.push(langs[i].greeting);
Is there any other way around this? Assuming there isn't one, which of the above solutions would be the most correct one?
You can bind the function's this to the controller:
langService.load({
callback: languagesLoaded.bind(this)
});
For IE < 9 a polyfill would be needed because bind is available as of ECMAScript 5.
Your approach #1 seems the best. You should be careful when using this since it changes its meaning based on its context, for example, like in a function. Sometimes you wouldn't even know this, if you are using a 3rd party library.
I find this guide useful. From the guide:
function Customer() {
var vm = this;
vm.name = {};
vm.sendMessage = function() { };
}

Callback function inside directive attr defined in different attr

So I have this directive called say, mySave, it's pretty much just this
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
element.bind("click", function() {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
}
});
}
});
the element itself looks like this
<button my-save my-save-callback="callbackFunctionInController()">save</button>
callbackFunctionInController is for now just
$scope.callbackFunctionInController = function() {
alert("callback");
}
when I console.log() attrs.mySaveCallback inside my-save directive, it just gives me a string callbackFunctionInController(), I read somewhere that I should $parse this and it would be fine, so I tried to $parse(attrs.mySaveCallback) which gave me back some function, but hardly the one I was looking for, it gave me back
function (a,b){return m(a,b)}
What am I doing wrong? Is this approach flawed from the beginning?
So what seems like the best way is using the isolated scope as suggested by ProLoser
app.directive('mySave', function($http) {
return {
scope: {
callback: '&mySaveCallback'
}
link: function(scope, element, attrs) {
element.on("click", function() {
$http.post('/save', scope.$parent.data).success(returnedData) {
// callback defined on my utils service here
scope.callback(); // fires alert
}
});
}
}
});
For passing parameters back to controller do this
[11:28] <revolunet> you have to send named parameters
[11:28] <revolunet> eg my-attr="callback(a, b)"
[11:29] <revolunet> in the directive: scope.callback({a:xxx, b:yyy})
There are a lot of ways to go about what you're doing. The FIRST thing you should know is that the $http.post() is going to be called as soon as that DOM element is rendered out by the template engine, and that's it. If you put it inside a repeat, the call will be done for each new item in the repeater, so my guess is this is definitely not what you want. And if it is then you really aren't designing things correctly because the existence of DOM alone should not dictate queries to the backend.
Anyway, directly answering your question; if you read the albeit crappy docs on $parse, it returns you an evaluation expression. When you execute this function by passing the scope to evaluate on, the current state of that expression on the scope you passed will be returned, this means your function will be executed.
var expression = $parse(attrs.mySave);
results = expression($scope); // call on demand when needed
expression.assign($scope, 'newValu'); // the major reason to leverage $parse, setting vals
Yes, it's a little confusing at first, but you must understand that a $scope changes constantly in asynchronous apps and it's all about WHEN you want the value determined, not just how. $parse is more useful for a reference to a model that you want to be able to assign a value to, not just read from.
Of course, you may want to read up on creating an isolate scope or on how to $eval() an expression.
$scope.$eval(attrs.mySave);
You can use .$eval to execute a statement in the given scope
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
scope.$eval(attrs.mySaveCallback)
}
}
});
TD: Demo
If you want to share data between a directive and a controller you can use the two way binding
app.controller('AppController', function ($scope) {
$scope.callbackFunctionInController = function() {
console.log('do something')
};
$scope.$watch('somedata', function(data) {
console.log('controller', data);
}, true);
});
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&mySaveCallback' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
scope.data = data;
scope.callback(); //calling callback, this may not be required
});
}
};
});
Demo: Fiddle
scope: {
callback: '&mySaveCallback'
}
Setting the scope explicitly could be a good solution but if you want the reach other parts of the original scope you can't because you have just overwritten it. For some reason, I needed to reach other parts of the scope too so I used the same implementation as ng-click do.
The use of my directive in HTML:
<div my-data-table my-source="dataSource" refresh="refresh(data)">
Inside the directive (without setting the scope explicitly):
var refreshHandler = $parse(attrs.refresh);
scope.$apply(function () {
refreshHandler( {data : conditions}, scope, { $event: event });
});
With this I can call the function in controller and pass parameters to it.
In the controller:
$scope.refresh= function(data){
console.log(data);
}
And it prints the conditions correctly out.
This worked for me
Inside the view script
<tag mycallbackattrib="scopemethod">
Inside the directive
$scope[attrs.mycallbackattrib](params....);
It is correctly called and params are passed, but maybe is not a best 'angular way' to work.
You should be using ng-click instead of creating your own directive.
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
if (scope.callback()) scope.callback().apply(data);
});
}
};
});

Categories

Resources