AngularJS: Indicate the scope of a directive - javascript

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/

Related

Pass JSON object from Angular service to directive

I'm trying to pass a JSON object from an Angular service to a directive. Actually I just want to $compile a directive on-the-go and pass an object to the directive.
It should look something like this:
var template = '<gmap-info-window layer="layer" marker="marker"></gmap-info-window>',
content = $compile(template)(searchScope);
Whereas the directive looks like this:
.directive('gmapInfoWindow', [function() {
scope: {
marker: '=',
layer: '='
},
link: function(scope, element, attrs) {
// access objects in attrs
}
}]);
That doesn't work. All I get in the attrs.marker and attrs.layer is plain strings.
Now what I've tried and accomlished is using the transcludeFn function of the $compile function. It works, but I don't feel it being the right way to do what I'm trying to accomplish.
var template = '<gmap-info-window></gmap-info-window>',
content = $compile(template)(searchScope, null, {
parentBoundTranscludeFn: function() {
return {
marker: _marker,
layer: _layer
};
}
});
Whereas the directive looks like this:
.directive('gmapInfoWindow', [function() {
scope: {},
link: function(scope, element, attrs, controller, transcludeFn) {
var objects = transcludeFn();
// The marker and layer are in objects now!
}
}]);
I can't imagine that there's no other way to do what I wanna do. This looks kinda dirty. Thanks for your insight!
All I get in the attrs.marker and attrs.layer is plain strings.
You need to understand that attribute is always a string by definition. It not possible that you have an object there. What Angular does is it evaluates values of those attributes (strings) in proper context (scope of compilation) according to scope configuration of the directive. Then the result of this evaluation is available in scope object of the link function. This is what you need to use:
link: function(scope, element, attrs) {
console.log(scope.marker, scope.layer);
}

Custom directive scope vs attrs

I have one concern when creating a custom directive in angular.
When I'm using a link function, I'm not sure what is the real difference when accessing attributes with attrs or scope.
Take this piece of code for example:
myApp.directive('someDirective', function() {
return {
restrict: 'E',
replace: true,
scope: {
title: '=title'
},
template: '<img/>',
link: function(scope, element, attrs) {
if (scope.title) {
// do something here
}
if (attrs.title){
// do something here
}
},
}
From my observations accessing 'title' attribute from attrs and by scope has a similar effect. What is the real difference?
The difference is that attribute is of a String type by definition. Always. In your case attrs.title will be literally string equal to whatever you pass into attribute in HTML.
However, scope.title is parsed and evaluated result of the attribute attr.title.
Ex. If you use something like this in HTML
<some-directive title="name"></some-directive>
where $scope.name = "Thomas Mann" defined in the scope, then attr.title will be string "name", while scope.title will be "Thomas Mann".

Why is my angular directive not getting Parent controller scope inside isolated scope?

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

Unable to pass/update ngModel from controller to directive

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.

Call function on directive parent scope with directive scope argument

I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk

Categories