I'm using ui-select plugin and I'm passing ng-model from my controller to a custom directive called richSelect but the ng-model doesn't seemed to get updated on select of any item.
<richselect ng-model="dataModel"></richselect>
Custom directive
app.directive('richselect', ['$compile', function ($compile) {
return {
restrict: 'AE',
scope: {
ngModel: '=' /* Model associated with the object */
},
link: function (scope, element, attrs, ngModel) {
scope.options = [
{
'Value' : 'value1',
'Desc' : 'Value One'
},
{
'Value' : 'value2',
'Desc' : 'Value Two'
}
]
scope.getRichSelectTemplate = function () {
return '<ui-select multiple ng-model="ngModel" theme="bootstrap" ng-disabled="disabled">' +
'{{ngModel}} <ui-select-match placeholder="Select">{{$select.selected.Desc}}</ui-select-match>' +
'<ui-select-choices repeat="option in options | filter: $select.search">' +
'<span ng-bind-html="option.Desc | highlight: $select.search"></span>' +
'</ui-select-choices>' +
'</ui-select>';
}
var linkFn = $compile(scope.getRichSelectTemplate())(scope);
element.append(linkFn);
}
}
}]);
Plnkr : http://plnkr.co/edit/Im8gpxEwnU7sgrKgqZXY?p=preview
Here, try this. I wasn't exactly sure what format or output you were trying to get, but this gets the selected options passed to the View.
EDIT - I got rid of the plunker that used to be here.
You have to use the ngModel.$setViewValue in order to change the value of ng-model in the directive in the view. Additionally, to get the value of the ui-select, you need to have ng-model pointed at the options.selected
Then it was just a matter of adding an ng-click that pointed to a function that updated the view with ngModel.$setViewValue(scope.options.selected.
Also, I believe you need to `require: 'ngModel' in your directive so you can access the ngModelController.
app.directive('richselect', ['$compile', function ($compile) {
return {
restrict: 'AE',
require: 'ngModel',
scope: {
blah: '=' /* Model associated with the object */
},
link: function (scope, element, attrs, ngModel) {
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected)
console.log(scope.options.selected)
}
scope.options = [
{
'Value' : 'value1',
'Desc' : 'Value One'
},
{
'Value' : 'value2',
'Desc' : 'Value Two'
}
]
scope.getRichSelectTemplate = function () {
return '<ui-select multiple ng-model="options.selected" theme="bootstrap" ng-click="changer()" ng-disabled="disabled">' +
'{{options.selected}} <ui-select-match placeholder="Select">{{$select.selected.Desc}}</ui-select-match>' +
'<ui-select-choices repeat="option in options | filter: $select.search">' +
'<span ng-bind-html="option.Desc | highlight: $select.search"></span>' +
'</ui-select-choices>' +
'</ui-select>';
}
var linkFn = $compile(scope.getRichSelectTemplate())(scope);
element.append(linkFn);
}
}
}]);
EDIT:
After a lot of digging and tinkering, per the comment below - getting two-way binding working has proved somewhat elusive. I found it was quite easy to do using the standard ui-select directive, as seen here (modified example code from ui-select), because we can easily get access to the scope of the directive:
Standard Directive Demo
I also came across a similar wrapper as the one in the OP, but after playing with it,that one seemed to have the same issue - it's easy to get stuff out, but if you need to push data into the directive it doesn't want to go.
Interestingly, in my solution above, I can see that the `scope.options.selected' object actually contains the data, it just never gets down the the scope of the ui-select directive, and thus never allows us to push data in.
After encountering a similar issue with a different wrapper directive in a project I am working on, I figured out how to push data down through the different scopes.
My solution was to modify the ui-select script itself, adding an internal $watch function that checked for a variable in it's $parent scope. Since the ui-select directive uses scope: true, it creates a child scope (which, if I am not mistaken, the parent would be the directive in this OP).
Down at the bottom of the link function of the uiSelect directive I added the following watch function:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
In the link function of our directve here, I added this $watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
So what happens here is that if the $viewValue changes (i.e., we assign some data from a http service, etc. to the dataModel binding, the $watch function will catch it and assign it to scope.myVar. The $watch function inside the ui-select script watches scope.$parent.myVar for changes (We are telling it to watch a variable on the scope of it's parent). If it sees any changes it pushes them to $select.selected - THIS is where ui-select keeps whatever values that have been selected by clicking an item in the dropdown. We simply override that and insert whatever values we want.
Plunker - Two-way binding
First of all dataModel is a string. Since you defined multiple the model would be an array.
What's more important is that the uiSelectdirective creates a new scope. That means that ng-model="ngModel" does no longer point to dataModel. You effectively destroy the binding.
In your controller make dataModel an object:
$scope.dataModel = {};
In your directive let the selected values be bound to a property:
return '<ui-select multiple ng-model="ngModel.selection"
Now the the selected values will be bound to dataModel.selection.
If you don't use the ngModelController you shouldn't use ng-model with your directive.
Related
I'm building a directive that needs to initialize some data based on values passed into it through the scope. The problem is that when I try and initialize the data in the link function, the passed in value isn't available yet. Is there anyway to only run the initialization when the passed in value is available? I thought about using a watch as in the following code but it seems messy (and doesn't seem to work anyway).
.directive('etMemberActivitySummary', [function () {
return {
restrict: 'E',
templateUrl: '<div>My template</div>',
transclude: false,
scope: {
memberModel: '='
},
link: function(scope, element, attrs, controller) {
var watcher = scope.$watch(
function() {
return scope.memberModel
},
function(value) {
console.log(value);
if (value != null) {
console.log('Watch');
console.log(value);
watcher();
// Perform initialization based on scope.memberModel here
}
});
}
}
}])
Is there a correct way to do this? If it helps, the passed in value is in itself retrieved from a web service.
Update 1
Turns out that if I put an ng-if="ctrl.memberModel" on the directive usage like the following and get rid of all the watch stuff, it works. Is this the best way to do this?
<et-member-activity-summary member-model="ctrl.memberModel" ng-if="ctrl.memberModel"></et-member-activity-summary>
In AngularJS the data-binding work to expose immediate data to our View !!
this stuff's due of the object scope which is the glue between the Logic Code AND The View.
Also all we know that AngularJs support the tow-way-binding !!!
My Question Is :
How the $scope can know that there object binding was changed or not??
if there while condition inside scope for auto-change detect or what?
Check angular.js file, we will get the code for ngBindDirective:
var ngBindDirective = ['$compile', function($compile) {
return {
restrict: 'AC',
compile: function ngBindCompile(templateElement) {
$compile.$$addBindingClass(templateElement);
return function ngBindLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.ngBind);
element = element[0];
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
element.textContent = isUndefined(value) ? '' : value;
});
};
}
};
}];
Note the last two line, it used watcher for attribute ngBind, for any change it apply to the element.
I've followed Angular docs precisely to get a directive working with an isolated scope containing a couple of vars from the parent Controller's scope object.
app.controller('MainCtrl', function($scope) {
$scope.name = 'Parent Name';
$scope.pokie = {
whyIs: "thisUndefined?"
};
});
app.directive('parseObject', function() {
var preLink = function($scope, el, att, controller) {
console.log('[link] :: ', $scope);
};
var postLink = function($scope, el, att, controller) {
console.log('[PostLink] :: ', $scope);
console.log('[$Parent] :: ', $scope.$parent.name);
};
return {
restrict: 'E',
scope: {
myPokie: '=pokie',
name: '=name'
},
template: [
'<div>',
'<h1>Directive does not get parent scope</h1>',
'<h1>{{ myPokie }}</h1>',
'<h2>{{ name }}</h2>',
'</div>'
].join(''),
compile: function() {
return {
pre: preLink,
post: postLink
}
}
}
});
http://plnkr.co/edit/FpQtt9?p=preview
Can anyone tell me what is wrong with my code? Why does the directive's Isolate scope return undefined values for 'myPokie' and 'name'?
I've seen other people saying you have to use $scope.watch for this.. but angular's directive docs don't say anything about that.. And I really don't want to use $scope.watch for something so trivial which should work out of the box.
As you declared isolated scope in your directive, that means you are going to provide those value to your directive using attribute.
scope: {
myPokie: '=pokie',
name: '=name'
}
This means your directive scope will not be prototypically inherited from the parent scope. pokie attribute provide the value for myPokie & name attribute will provide value of name for your directive, = indicating a two way binding if your myPokie value change in directive, the same referencing value will change in the parent controller. The same is true for the name attribute.
Your directive element markup should be:
<parse-object pokie="pokie" name="name"></parse-object>
Working Plunkr
You're using isolated scope but you're not passing the variables. The problem is in your HTML:
Change your HTML from:
<parse-object></parse-object>
To:
<parse-object pokie="pokie" name="name"></parse-object>
Isolated scope takes its parameters from the DOM element. So if you have inside the scope declaration:
myPokie: '=pokie',
That means that myPokie variable should be taken from the pokie attribute that's on the scope. Your name: "=name" can be changed to name: "=" since it is exactly the same name.
Plunker
I am building a directive that can be use in different controllers, and I would like to be able of bind the directive to a particular property of my $scope.
I would like to do something like this:
<div ng-app="myapp">
<div ng-controller="myController">
<my-directive-wrapper ng-model="mymodel">
<my-directive-inner ng-repeat="item in items" />
</my-directive-wrapper>
</div>
</div>
With this model:
$scope.mymodel = {
name : "Transclude test",
items : [
{ title : "test1" },
{ title : "test2" },
{ title : "test3" }
]
};
So the directive myDirectiveWrapper gets $scope.mymodel as scope, and nothing else. Then I may put the directive twice, pointing to a different property each.
I have a demo project with the problem here: http://jsfiddle.net/vtortola/P8JMm/3/
And the same demo working normally (without limiting the scope) here: http://jsfiddle.net/vtortola/P8JMm
The question is, how to indicate in the use of my directive that I want to use a particular property of my $scope as scope of my directive. It should be possible to bind the directive to arbitrary properties in the same $scope.
Cheers.
So the basic answer to this question is - you can do what you want to do, but it is a bit more complicated than you might think. To understand what is happening here you need to know about scopes in angular. A scope is essentially an object that contains the data accessible to the view. There are (at least) three ways scopes operate in angular:
Isolated - In this case angular basically creates a brand new scope for the directive. None of the properties are copied over.
Extended - In this case you would start with the root scope but make a shallow copy of it. Objects that are changed will be changed in the root scope but primitives will not be.
Shared - In this case you share share some or even all of the data with the root scope.
Based on your question above, what you what to do here is to extend the parent scope - copying an object to a property with a specific name in the newly created child scope. The way to get this behavior is to manually create a new child scope before the transclude. The two key lines of code to do this are:
// create a "new" scope
var childScope = scope.$new();
// extend using the model binding provided
angular.extend(childScope, scope[iAttr.myModel]);
In the context of your directive this looks like:
.directive('myDirectiveWrapper', ['$compile', function ($compile) {
return {
transclude: true,
restrict: 'E',
compile: function (element, attrs, transclude) {
var contents = element.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
var childScope = scope.$new();
angular.extend(childScope, scope[iAttr.myModel]);
if (!compiledContents) {
compiledContents = $compile(contents, transclude);
}
compiledContents(childScope, function(clone, childScope) {
iElement.append(clone);
});
};
},
template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
}])
Now you can specify any variable that you want as the "model" for the child scope and you can then access that directly in the contents of your transcluded code!
SEE THE FIDDLE: http://jsfiddle.net/P8JMm/7/
EDIT: Just for fun, I created a more complicated use case for this directive: http://jsfiddle.net/P8JMm/9/
Note - angular site also has some really good resources to understand scope better. See here.
If you want two way binding to work it's going to be a lot easier to just create a variable on your directive scope rather than apply mymodel directly onto the directive scope.
HTML
<div ng-app="myapp">
<div ng-controller="myController">
<my-directive-wrapper model="mymodel">
<my-directive-inner ng-repeat="item in mymodel.items" />
</my-directive-wrapper>
</div>
</div>
Directive
.directive("myDirectiveWrapper", function(){
return {
scope: {
model: '='
},
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, controller) {
},
template: "<div><h3>{{ model.name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
})
http://jsfiddle.net/kQ4TV/
If you don't care about two way binding I suppose you could do something like this but I wouldn't recommend it:
.directive("myDirectiveWrapper", function(){
return {
scope: {
model: '='
},
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, controller) {
angular.extend(scope, scope.model);
},
template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
}
})
http://jsfiddle.net/vWftR/
Here is an example of when that second approach can cause problems. Notice that when you enter something into the input field it will change the directive's name but not the name in the outer scope: http://jsfiddle.net/r5JeJ/
I've got an Angular directive. Inside the link function, I do this:
link: function(scope, element, attrs) {
...
element.data('startY', value);
...
}
What I'd like to do is perfix 'startY' with the name of the directive, without hard-coding the name. I'd like to dynamically get the name.
Is there any way to do this? Does Angular provide a way to reflect it? Something like:
link: function(scope, element, attrs) {
...
element.data(this.$name + '-startY', value);
...
}
If not, what are the recommended best practices for choosing data() keys to avoid collisions?
As indicated in the AngularJS source code, a directive's name is assigned in the context of the object literal where your directive options reside. The link function however cannot access the object literal's context, this, because it will be transferred to a compile function where it will be returned and invoked after the compilation process has taken place.
To get the name within your link function you can follow any of these suggestions:
[ 1 ] Create a variable that may hold reference to the object literal(directive options).
.directive('myDirective', function() {
var dir = {
link: function(scope, elem, attr) {
console.log(dir.name);
}
};
return dir;
});
[ 2 ] You can also get the directive's name by using the compile function since it is invoked in the context of the directive option.
.directive('myDirective', function() {
return {
compile: function(tElem, tAttr) {
var dirName = this.name;
return function(scope, elem, attr) { /* link function */ }
}
};
});
As far as I can tell you've answered your own question. You may prefix the name by string concatenation as you've done but it will probably be easier to add it as a separate data store.
element.data('directiveName', this.$name).data('startY', value);
I'm not sure what you mean by avoid collision as this will only apply to the element that was passed into the link function.