ng-click loose the function context - javascript

I am using angularjs.
I have a directive that gets a function as parameter:
module.directive('someDirective', [function () {
return {
restrict: 'E',
template: <button ng-click="doClick()">blabla</button>,
scope: {
doClick: '='
}
};
}]);
I am using this directive like that:
<some-directive do-click="funcOnScope"></some-directive>
And everything works perfectly.
Now I have the following on my scope:
$scope.a = {
data: { name: 'myname' },
action: function() {
alert(this.data.name);
}
}
When calling to:
<some-directive do-click="a.action"></some-directive>
I am expecting to get an alert with 'myname' when clicking on the button. But I get an exception that data is undefined. This is happening because "this" is referencing to window (angular calls to the ng-click function without a context).
How can I call a.action() without loosing the context?

change the scope definition to
scope: {
doClick: '&'
}
and call it like that
<some-directive do-click="a.action()"></some-directive>
here is jsfiddle

I don't know if this is the correct answer, but it will suffice. Perform a bind on a.action in the do-click attribute:
<some-directive do-click="a.action.bind(a)"></some-directive>
This will force the function to keep the a context.

Related

Using directive scope to update Highcharts

I have two charts with different ids (#chart1 and #chart2). I've created a button, so I can change a chart's type (e.g. from column to line):
<button ng-click="updateChart(name, 'line')">Line</button>
This button calls the updateChart function:
$scope.updateChart = function(id, type) {
var chart = $('#' + id).highcharts();
chart.series[0].update({
type: type
});
};
As I need to call the button for every chart, I've created a directive passing a name value to the scope:
.directive('changeChart', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
templateUrl: "change-chart.html"
};
})
In my HTML I call the directive passing the chart id: <change-chart name="chart1"></change-chart>
However, the button isn't working. It only works if I remove the directive scope and set the id manually. Any ideas on how to solve this?
Here's a Plunker: http://plnkr.co/edit/5wmLldmXpoq9wiMACbNS?p=preview
That is because when you define a scope object in your directive, it creates an isolate scope for that directive. That means, the scope within your directive cannot access the scope properties defined outside. You have your controller defined on the outer scope where you attach the function updateChart. So, your isolate scope directive is not aware of this method.
To fix this, you can define a controller on your directive itself. And in that controller, define the method updateChart
.directive('changeChart', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
controller: function ($scope) {
$scope.updateChart = function(id, type) {
var chart = $('#' + id).highcharts();
chart.series[0].update({
type: type
});
};
},
templateUrl: "change-chart.html"
};
})

Update UI based on change of a directive attribute in AngularJs

I am struggling with data binding in AngularJs.
I have the following piece of markup in .html file that includes the custom directive:
<my-directive ng-repeat="i in object" attr-1="{{i.some_variable}}"></my-directive>
Note: 'some-variable' is being updated every 10 seconds(based on the associate collection and passed to template through controller).
The directive's code includes:
myApp.directive('myDirective', function () {
scope: {
'attr-1': '=attr1'
which throws this exception because of the brackets in attr-1(see html code above).
It works though if I use read-only access(note at sign below):
myApp.directive('myDirective', function () {
scope: {
'attr-1': '#attr1'
I use scope.attr-1 in directive's HTML to show its value.
The problem is that with read-only access UI is not reflecting the change in attribute change.
I've found solution with $parse or $eval(couldn't make them work tho). Is there a better one there?
You'll need only two-way binding and I think $parse or $eval is not needed.
Please have a look at the demo below or in this fiddle.
It uses $interval to simulate your updating but the update can also come from other sources e.g. web socket or ajax request.
I'm using controllerAs and bindToController syntax (AngularJs version 1.4 or newer required) but the same is also possible with just an isolated scope. See guide in angular docs.
The $watch in the controller of the directive is only to show how the directive can detect that the data have changed.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('myDirective', myDirective);
function MainController($interval) {
var self = this,
refreshTime = 1000; //interval time in ms
activate();
function activate() {
this.data = 0;
$interval(updateView, refreshTime);
}
function updateView() {
self.data = Math.round(Math.random()*100, 0);
}
}
function myDirective() {
return {
restrict: 'E',
scope: {
},
bindToController: {
data: '='
},
template: '<div><p>directive data: {{directiveCtrl.data}}</p></div>',
controller: function($scope) {
$scope.$watch('directiveCtrl.data', function(newValue) {
console.log('data changed', newValue);
});
},
controllerAs: 'directiveCtrl'
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
model value in ctrl. {{ctrl.data}}
<my-directive data="ctrl.data"></my-directive>
</div>
I've come to the following solution(in case somebody runs into the the same problem):
// Directive's code
myApp.directive('myDir', function () { return {
restrict: 'E',
templateUrl: function () {
return 'my-dir.html';
},
scope: {
'id': '#arId',
'x': '#arX',
'y': '#arY',
//....
},
link: function ($scope, element, attrs) {
// *** SOLUTION ***
attrs.$observe('arId', function (id) {
$scope.id = id;
});
//...
}
Update: somebody sent me this answer, they have the same problem and came up with a very similar if not exact same solution:
Using a directive inside an ng-repeat, and a mysterious power of scope '#'
It is useful to read because they explain what's the idea behind it.

Angular controllerAs and parameter

I m actually creating a little directive and I m facing a problem with the scope object and controllAs.
In fact, I have this result :
angular.module('app')
.directive('historyConnection', function () {
return {
templateUrl: 'views/directives/historyconnection.html',
restrict: 'E',
scope: {
idUser: '#iduser'
},
controller:function($scope){
console.log(this.idUser); // gives undefined
console.log($scope.idUser); // gives the good value
},
controllerAs:'history'
};
});
From the html code :
<history-connection iduser="55"></history-connection>
I dont know how to make controllerAs work when passing parameters to directive. Can you help me ?
Important informations are commented in the javascript code above
If you want the scope properties to be bound to the controller you have to add bindToController: true to the directive definition.

Bind model to function call return value

I am working on an angular project and I use a directive to create an isolated scope. The directive looks like this:
var directive = module.directive('question', function () {
return {
restrict: 'E',
templateUrl: 'question.html',
transclude: true,
scope: {
quiz: '=quiz'
},
link: function (scope, attr, element) {
scope.$watch(function () {
return scope.quiz;
},
function (oldVal, newVal) {
scope.currentQuestion = scope.quiz;
});
}
};
});
For I do not want to bind to a property (or field) in my Controller, I created a function and call the directive this way:
<question quiz="quiz.getCurrentQuestion()">... (transcluding stuff)</question>
Please note that quiz is my Controller using the as-Syntax.
The way I process the directive is working, but I don't like to create a two-way-binding ( to an R-value?).
Now I tried to just pass the function using &-binding but this just turns out odd results in the link-function and breaks everything.
Can I use the function-binding using & and somehow call the function (in my template or in the link-function) to get the result I need to make it work like two-way-binding?
Thank you for your help.
EDIT
The return value of the getCurrentQuestion-function is an object which looks like
{
questionNumber: 1,
answers: [],
getQuestionText() : function(...),
...
}
So nothing to special, I hope...
EDIT 2
When I use
...
scope: {
quiz: '&quiz'
}
then in the $watch-function I get
function(locals) { return parentGet(scope, locals); } for scope.quiz
And if I call the function like scope.quiz() I get undefined as result.
Couldn't find any way to watch a function in scope binding. However, there are other solutions. If you want single way binding you can use '#', but that means that you would have to parse the JSON in the watch ( working example):
var directive = module.directive('question', function () {
return {
restrict: 'E',
templateUrl: 'question.html',
transclude: true,
scope: {
quiz: '#'
},
link: function (scope, attr, element) {
scope.$watch('quiz', function (newVal, oldVal) {
scope.currentQuestion = angular.fromJson(newVal);
});
}
};
});
It works, but if you have a high rate of updates, the overhead can be annoying. What I would do, is use a service that holds all the questions, and both controller and directive can talk to. When the current question is changed, the controller should pass to the directive only the id of the new question (using simple # bind), and the directive would query the service for the question.

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

Categories

Resources