Using directive scope to update Highcharts - javascript

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

Related

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.

AngularJS : How to update controller scope associated to directive scope's object as it changes?

Here's the explanation:
I have the current controller that creates an array of $scope.plan.steps which will be used to store every step:
.controller('PlanCtrl', function ($scope, $http) {
$scope.plan = {
steps: [{}]
};
$scope.addStep = function () {
$scope.tutorial.steps.push({});
}
}
Then I have the following directive which has an isolated scope and that is associated to the index of the $scope.plan.steps array:
.directive('planStep', function () {
return {
template: '<input type="text" ng-model="step.name" />{{step}}',
restrict: 'E',
scope: {
index: '=index'
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.index);
$element.remove();
$scope.$destroy();
}
}
};
});
These two communicate, create, and delete objects inside of the controller's scope, however, how can I allow the directive to update the controller's scope array in real time?
I've tried doing a $watch on the directive's isolated scope changes, $emit the changes to the controller, and specify the $index... But no luck.
I've created a plunker to reproduce what I currently have: Link
So far I can create and delete objects inside of the array, but I cannot get a single object to update the controller's object based on the $index.
If the explanation was not clear, by all means, let me know and I will elaborate.
Thank you
WHen you do things like this inside ng-repeat you can take advantage of the child scope that ng-repeat creates and work without isolated scope.
Here's the same directive without needing any angular events
.directive('planStep', function() {
return {
template: '<button ng-click="removeStep(step)">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
transclude: true,
controller: function($scope, $element, $transclude) {
var steps = $scope.plan.steps// in scope from main controller
/* can do the splicing here if we want*/
$scope.removeStep = function(step) {
var idx =steps.indexOf(step)
steps.splice(idx, 1);
}
}
};
});
Also note that removing the element with element.remove() is redundant since it will automatically be removed by angular when array gets spliced
As for the update, it will update the item in real time
DEMO
The way you set up 2-way binding for index you could set one up for step as well? And you really do not need index to remove the item, eventhough your directive is isolated it relies on the index from ng-repeat which probably is not a good idea.
<plan-step ng-repeat="step in plan.steps" index="$index" step="step"></plan-step>
and in your directive:
scope: {
index: '=index',
step:'='
},
Demo
Removing $index dependency and redundant element remove() and scope destroy (when the item is removed from the array angular will manage it by itself):
return {
template: '<button ng-click="removeStep()">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'='
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.step);
}
}
and in your controller:
$scope.$on('removeStep', function(event, data) {
var steps = $scope.plan.steps;
steps.splice(steps.indexOf(data), 1);
});
Demo
If you want to get rid of $emit you could even expose an api with the isolated scoped directive with function binding (&).
return {
template: '<button ng-click="onDelete({step:step})">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'=',
onDelete:'&' //Set up function binding
},
transclude: true
};
and register it on the view:
<plan-step ng-repeat="step in plan.steps" step="step" on-delete="removeStep(step)"></plan-step>
Demo

ng-click loose the function context

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.

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

isolate scope '=' not getting assigned

I have the following directive:
CorrelatorApp.directive('correlator', function ($WebApi) {
return {
restrict: 'A',
scope: {
crOptions: '=',
},
link: function (scope, element, attrs) {
var options = scope.crOptions;
}
}
});
then in my index.html I use it like this:
<form correlator cr-options="correlatorOptions" name="CorrelatorForm" ng-controller="PortalMerchantController">
and my correlatorOptions are defined in the controller:
CorrelatorApp.controller("PortalMerchantController", function
PortalMerchantController($scope, $http) {
$scope.correlatorOptions = {
dependant: {
controller: 'PortalMerchant',
model: 'portalMerchants',
nameField: 'PortalsMerchantName'
},
principal: {
controller: 'Merchant',
model: 'merchants',
nameField: 'Name'
}
};
});
when the directive links, the value of scope.crOptions is undefined. If I set crOptions to & and then call it (var options = scope.crOptions()), the code executes correctly and I get the object defined in the controller. What am I missing?
Move your ngController directive outside of the form element.
In 1.2.0 and greater, the ngController and form have sibling scopes (previously they would have shared the isolate scope). Here's the change that causes this
You want form to be a child of ngController so it can access it's scope:
<div ng-controller="PortalMerchantController">
<form correlator cr-options="correlatorOptions" name="CorrelatorForm"></form>
working fiddle

Categories

Resources