Reputation: 24548
I have developed a simple UI slider directive that has an isolated scope. It also supports registering a change
property that is fired when the value of the slider changed. The change
property would usually call a function of a parent scope with the slider value from the isolated scope, like this:
change="onValueChanged(theValue)"
where change
is an attribute, onValueChanged
is a function declared in a parent controller and theValue
is the current value of the element that is only declared in the isolated scope.
How can I $eval
this correctly? In the slider's link
function I can call $scope.$eval(attrs.change)
but $scope.onValueChanged
is not defined. The isolated scope did not inherit from its parent. At the same time, $scope.$parent.$eval(attrs.change)
correctly calls the onValueChanged
function, but theValue
will not be set because it is isolated in the child scope. Since the change
property can reference any number of parent scope properties, I cannot explicitly declare them in the isolated scope.
How can I eval
the change
property so that all variables from the different scopes will be present? Is there a way to force the isolated scope to inherit from its parent?
Upvotes: 1
Views: 1211
Reputation: 24548
I have found that there were several small issues with my approach. My final slider looks a little like this:
/**
* A jquery-UI slider with databinding.
* @see http://jsfiddle.net/vNfsm/50/
*/
app.directive('slider', function() {
var linkFun = function($scope, element, attrs) {
var $slider = jQuery(element);
var option = attrs;
var readIntOption = function(key, option) {
if (option[key]) {
option[key] = parseInt(option[key]);
}
};
// read default options
readIntOption("min", option);
readIntOption("max", option);
readIntOption("step", option);
// add `value` and `change` properties to slider for data-binding
option = jQuery.extend({
change: function(event, ui) {
if (!event.which) return; // only trigger on UI events
if (ui.value != $scope.valueModel) {
// update value
$scope.valueModel = ui.value;
// update the value of the variable that is bound to `valueModel`
if (!$scope.$$phase) {
$scope.$apply();
}
// raise callback
if ($scope.valueChanged) {
$scope.valueChanged();
}
}
}
}, option);
// data binding in the other direction
$scope.$watch("valueModel", function(val) {
if ($scope.valueModel != $slider.slider("value")) {
// update slider value
// this will not raise the `change` event above
$slider.slider("value", $scope.valueModel);
}
});
// create slider
$slider.slider(option);
};
return {
restrict: 'E',
replace: true,
transclude: false,
template: '<div />',
scope: {
valueModel: '=',
valueChanged: '&'
},
link: linkFun
};
});
This is an example instance of a slider:
<slider
value-model="ratingsOwn[category.categoryId]"
value-changed="onRatingsChange(ratingsOwn[category.categoryId])"
range="min"
step="{{category.valueStep}}"
min="{{category.valueMin}}"
max="{{category.valueMax}}"></slider>
These were the issues I had to fix:
Avoid accessing isolated scope variables from the outside. Since the isolated value model is actually bound to some variable in the parent scope, simply use that instead. This way, there is no mixing of scopes, and things become a whole lot simpler.
When binding variables, make sure to propagate changes to all bound models, by calling $apply
or similar, before calling any hook callbacks (e.g. value-changed
, in this case).
As @Rafal points out, $eval
can be avoided by simply using uni-directional data-binding (&
) and treating the $scope
's property as a function (that is how value-changed
works in the above example).
Be careful when setting up data-binding to avoid infinite loops. In this case, the $watch
binding will be triggered by the UI (by checking event.which
), but vice versa is avoided, to break the loop. When the value variable is updated programatically, the slider will be updated but its change
event will not be triggered.
Do not mess with transclude
if you want multiple directive instances in the same scope.
Upvotes: 1
Reputation: 181
Don't use eval. When defining a directive with an isolate scope you can use one way binding like this:
app.directive('myDirective', function(){
return{
//...
scope:{
functionToCall: '&'
}
//...
}}
When your change event is fired simply call the function in the link function like this:
scope.functionToCall({change:changeVal});
edit: When using the directive pass in the function from the controller like this:
<my-directive function-to-call="functionInController(change)"></my-directive>
Upvotes: 0