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

(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() { };
}

Related

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

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.

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);

AngularJS - Creating a compile function

I'm trying to create a custom compile function, to make it easier to dynamically add HTML to a page.
The argument htmlStr is the incoming HTML to compile. The argument value is a variable that can be added to the scope. The argument compiledHTMLFunc is a function that will be executed with the compiled object. Here's my code:
function compileHTML (htmlStr, value, compiledHTMLFunc)
{
var $injector = angular.injector (["ng", "angularApp"]);
$injector.invoke (function ($rootScope, $compile)
{
$rootScope.value = value;
var obj = angular.element (htmlStr);
var obj2 = $compile (obj)($rootScope);
if (compiledHTMLFunc != null)
compiledHTMLFunc (obj2);
});
}
Here's how I use the function:
compileHTML ("<button class = \"btn btn-primary\">{{ value }}</button>", "Ok", function (element)
{
$(document.body).append (element);
});
Whenever I try to compile the following HTML, the inline {{ value }} doesn't get compiled. Even if I simply change it to {{ 1+1 }}. Why is this?
Update: I dunno why I didn't create a fiddle earlier, here's an example: http://jsbin.com/vuxazuzu/1/edit
The problem appears to be pretty simple. Since you invoke compiler from outside of angular digest cycle you have to invoke it manually to boost the process, for example by wrapping compiledHTMLFunc into $timeout service call:
function compileHTML (htmlStr, scope, compiledHTMLFunc) {
var $injector = angular.injector(["ng", "angularApp"]);
$injector.invoke(function($rootScope, $compile, $timeout) {
$rootScope = angular.extend($rootScope, scope);
var obj = $compile(htmlStr)($rootScope);
if (compiledHTMLFunc != null) {
$timeout(function() {
compiledHTMLFunc(obj);
});
}
});
}
compileHTML('<button class="btn btn-primary">{{value}}</button>', {value: 'Ok'}, function(element) {
angular.element(document.body).append(element);
});
I also improved your code a little. Note how now compileHTML accepts an object instead of single value. It adds more flexibility, so now you can use multiple values in template.
Demo: http://plnkr.co/edit/IAPhQ9i9aVVBwV9MuAIE?p=preview
And here is your updated demo: http://jsbin.com/vuxazuzu/2/edit

Exporting methods from an angular factory or directive to use later

I have developed a web application using Angular.js (It's my first). The application features a collection of interactive graphics (seat maps); so I created a module to handle the Raphael stuff, including a directive, like so:
angular.module('raphael', [])
.factory('fillData', function() {
return function(paper, data) {
var canvas = $(paper.canvas);
// Do fill the data and more ...
canvas.on('click', '[id]', function(e) {
this.classList.toggle('selected');
});
};
})
.directive('raphael', ['fillData',
function(fillData) {
return {
scope: {
raphael : '&',
seatData: '&'
},
link: function(scope, element, attrs) {
var paper = null;
var updateSeatData = function() {
if(scope.seatData()) fillData(paper, scope.seatData());
};
scope.$watch(scope.raphael, function() {
element.empty();
paper = new Raphael(element[0], '100%', '100%');
paper.add(scope.raphael());
updateSeatData();
});
scope.$watch(scope.seatData, function() {
updateSeatData();
});
}
};
}
]);
Everything works fine, until it get to the point where we need to interact with the vector in another level. Let's say, getting a count of selected seats, or deselecting all (triggered by some random element in the document).
I don't seem to be able to find a reasonable way of implementing it.
What do you suggest?
Is there any other approach to using a second library inside angular?
From what I understand you want to have directive which have certain internal state but you would like to access it's state from outside (other directive, service, etc.).
If so, then it seems that you could use service as state holder. In such case your directive will not hold state but it will be accessing it.
What do you mean by a reasonable way of implementing it? It looks good, although I would prefer to bind to the attribute seatData instead of passing function like
scope: {
seatData: '='
}
And then watch it
scope.$watch('seatData', function() {
fillData(paper, scope.seatData);
});
Is this you issue or I haven't understood it?
OK, here is the solution I came up with; I accessed the parent scope and put essential methods there.
Adding this line to the fillData factory:
return {
deselectAll: function() { ... }
};
And changed updateSeatData method to:
var updateSeatData = function() {
if(scope.seatData) {
var result = fillData(paper, scope.seatData[scope.level]);
angular.extend(scope.$parent, result);
}
};
p.s. Still open to hearing moreā€¦

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