I'm giving a first try at AngularJS custom directives.
I'm having trouble using (or understanding ...) the isolated scope in the link function of the directive.
Here is the code of this part of my app :
view.html
...
<raw-data id="request-data" title="XML of the request" data="request">See the request</raw-data>
...
request is a variable published in the scope of the viewCtrl that contains the xml-string of a request.
rawData.js
directives.directive('rawData', function() {
return {
restrict : 'E',
templateUrl : 'partials/directives/raw-data.html',
replace : true,
transclude : true,
scope : {
id : '#',
title : '#',
data : '='
},
link : function($scope, $elem, $attr) {
console.log($scope.data); //the data is correclty printed
console.log($scope.id); //undefined
}
};
});
raw-data.html
<div>
<!-- Button to trigger modal -->
<a href="#{{id}}Modal" role="button" class="btn" data-toggle="modal" ng-transclude></a>
<!-- Modal -->
<div id="{{id}}Modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="{{id}}Modal" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">{{ title }}</h3>
</div>
<div class="modal-body">
<textarea class="input-block-level" rows="10">{{ data }}</textarea>
</div>
<div class="modal-footer">
<!-- <button class="btn" ng-click="toggleTagText('')">{{'cacher'}} l'image</button> -->
<button class="btn btn-primary" data-dismiss="modal" aria-hidden="true">Fermer</button>
</div>
</div>
</div>
I don't understand why the ID is correclty shown when the modal pops, but when I try to console.log() it, its value is undefined.
Maybe i'm wrong with the isolated scope value (= and #).
Thank you for reading. :)
Isolate scope properties bound with # are not immediately available in the linking function. You need to use $observe:
$attr.$observe('id', function(value) {
console.log(value);
});
Your template works properly because Angular automatically updates isolate scope property id for you. And when it does update, your template automatically updates too.
If you are just passing strings, you can simply evaluate the values once instead of using # binding:
link: function($scope, $elem, $attr) {
var id = $attr.id;
var title = $attr.title
console.log(id, title);
}
However, in your case, since you want to use the properties in templates, you should use #.
If you weren't using templates, then # is useful when attribute values contain {{}}s – e.g., title="{{myTitle}}". Then the need to use $observe becomes more apparent: your linking function may want to do something each time the value of myTitle changes.
This is intentional and has to do with compilation order and the difference between '#' and '='.
Some excerpts from this Google Groups discussion with input from Misko:
# and = do very different things. One copies the attribute value
(which may be interpolated) and the other treats the attribute value
as an expression.
#attrs are not $interpolated until later, so they are not available at
link time. If you want to do something with them in the link function
you either need to $interpolate yourself manually
well, none of the answers above actually mentioned one key aspect, even with '=', it doesn't seem to me you can access the scope inside link function directly like the following,
scope: {
data: '=',
},
link: function(scope, elem, attrs) {
console.debug(scope.data); // undefined
but you can access the scope in the internal function,
link: function(scope, elem, attrs) {
scope.saveComment = function() {
console.debug(scope.data);
so it seems to me there might be a time lag in when scope.data can be available.
You can visit the JSFiddle created by me here: http://jsfiddle.net/7t984sf9/5/:
link: function($scope, $elem, $attr) {
console.log($scope.onewayObject);
$attr.$observe('onewayObject', function(value) {
console.log(value);
});
}
Or some more detailed explanation here: What is the difference between & vs # and = in angularJS
Related
I'm trying to pass some data from directive into a function addTrackFromPicker in my controller.
$scope.addTrackFromPicker = function (message) {
console.log("addTrackFromPicker", message);
};
Here what I have in my directive
dir.directive('youtubeList', function($http, $timeout, YT_event){
return {
restrict: 'E',
scope: {
search: '=',
dial: '&'
},
templateUrl: 'youtube-list.html',
...
Here I want to call controllers function from my template and pass it item.id.$t as argument:
<div class="media list-group-item" ng-repeat="item in entries">
<a type="button" ng-click="dial(item.id.$t)">
<img ng-src="{{item.media$group.media$thumbnail[0].url}}">
</a>
But I don't know how to pass it into my tag
<youtube-list search="search" dial="addTrackFromPicker(???)"></youtube-list>
I also tried $parent.addTrackFromPicker but it didnt work
In order to pass in your data from your directive, you will need to do it like this:
<youtube-list search="search" dial="addTrackFromPicker(data)"></youtube-list>
Then, in your template:
<div class="media list-group-item" ng-repeat="item in entries">
<a type="button" ng-click="dial({data: item.id.$t})">
<img ng-src="{{item.media$group.media$thumbnail[0].url}}">
</a>
</div>
You can use an "argument name" other than data if something else makes more sense for your situation. See Angular's documentation on scope for detailed info on how this works.
Isolated Scope: pass some values from the parent scope to the directives
There’re 3 types of prefixes AngularJS provides
"#" ( Text binding / one-way binding )
"=" ( Direct model binding / two-way binding )
"&" ( Behaviour binding / Method binding )
All these prefixes receives data from the attributes of the directive element.
class="directive"
name="{{name}}"
color="color"
When the directive encounters a prefix in the scope property, it will look for an attribute ( with same property name ) on directive’s html element
scope : {
name: "#"
}
I've followed this linkhttp://jsfiddle.net/shidhincr/pJLT8/10/light/
I have two directives:
window.app.directive('placeholder', function ($compile, $route, $rootScope) {
return {
restrict: 'AC',
link: function (scope, element, attr) {
// Store the placeholder element for later use
$rootScope["placeholder_" + attr.placeholder] = element[0];
// Clear the placeholder when navigating
$rootScope.$on('$routeChangeSuccess', function (e, a, b) {
element.html('');
});
}
};
});
window.app.directive('section', function ($compile, $route, $rootScope) {
return {
restrict: 'AC',
link: function (scope, element, attr) {
// Locate the placeholder element
var targetElement = $rootScope["placeholder_" + attr.section];
// Compile the template and bind it to the current scope, and inject it into the placeholder
$(targetElement).html($compile(element.html())(scope));
element.html('');
}
};
});
I use them to basically swap out one section with html in another.
If I have the following html:
<div placeholder="footer"></div>
<div section="footer">
<ul ng-model="items">
<li ng-repeat="item in items"> {{item.Description}}</li>
</ul>
</div>
The ng-repeat doesn't seem to be working. If I simply output {{items}} below the , it displays fine. Also, I know binding is working because I can change items and it will update.
Lastly, if I move the ul outside the section it works fine.
So, my question is why does this not work (compile ng-repeat inside directive).
Am I missing something?
EDIT:
What is confusing me, is I can do this:
<div section="footer">
<!-- This Works -->
{{items}}
<!-- This also works -->
<input type="text" ng-model="items[0].Description" />
<!-- This doesn't work -->
<ul ng-model="items">
<li ng-repeat="item in items"> {{item.Description}}</li>
</ul>
</div>
This isn't going to work. It can't evaluate something from another scope without having an exact copy of it in its scope. If you want two directives to communicate use require and setup a way for them to do that if they aren't in a parent child relationship.
A couple of things you should think about. Essentially what you are doing is called transclusion. Section directive would use ng-transclude to capture the client's defined code. Use transclusion and maybe you can evaluate the template into html in the scope of section then using directive communication allow it to pass the HTML block (already evaluated) to the other directive. The only problem is making sure this happens when things change through binding. You're probably going to need some $watches on variables in section in order for placeholder to be notified when things change.
You will probably need a 3rd directive so allow section and placeholder to communicate through. In this example say I have a 3rd directive called broadcaster. Then section and placeholder will require broadcaster (ie require: '^broadcaster'), and it will define some interface for each of the directives to send HTML from section -> placeholder. Something like this:
<div broadcaster>
<div placeholder="footer"></div>
<div section="footer">
<ul>...transcluded content</ul>
</div>
</div>
So this is a bit complex, and I will try to explain it thoroughly. Ultimately the question I am trying to answer is, "How can I print the model, and not the value of the model, in the HTML?"
I believe the answer to that will solve my problem, but I'm going to discuss my problem in full so you all can tell me that I'm probably doing it wrong. In which case I have a lot of re-writing to do, so I hope not.
Scenario:
Here is the initial HTML page, where-in 'field' is a custom directive.
<field model="model1" placeholder="Model 1" type="input"></field>
<field model="model2" placeholder="Model 2" type="input"></field>
<field model="model3" placeholder="Model 3" type="input"></field>
Here is the custom directive.
app.directive("Field", function(){
return{
restrict: "E",
scope: { model: '=', placeholder:'#', type: '#'},
templateUrl: function(tElement, tAttrs){
return 'views/common/' + tAttrs.type + '.html';
}
};
});
Here is the Template for the directive. The type attribute just feeds in different templates, this way I don't have to do a complicated if then structure. Bear with me, it's might start to get a bit complex.
<field-label model="model">{{placeholder}}</field-label>
<div class="col-sm-2 no-right-pad">
<input type="text" ng-model="model.value" placeholder="{{placeholder}}" class="form-control input-sm"></input>
</div>
<div class="col-sm-2 no-pad">
<field-options field-options-model="model" class="field-options"></field-options>
</div>
As you should be able to tell, this is a form. Each initial directive call creates a form group containing a label, an input, and a directive with which options can be added to the model. For the sake of this exercise, lets say I want to be able to add a comment to each input. I get a little dirty when you go down into the field-options directive, and I change the model name, it's legacy from previous attempts and isn't hurting anything but I still might change that. I don't necessarily need it because the directives are nested and thus, share $scopes. Here's that directive:
app.directive("FieldOptions", function() {
return {
restrict: "E",
scope: { field: '=FieldOptionsModel' },
templateUrl: 'views/common/field-options.html'
};
});
And that template:
<div class="dropdown">
<a ng-href class="dropdown-toggle btn btn-bars" data-toggle="dropdown" tabindex="-1"><i class="fa fa-bars"></i></a>
<ul class="dropdown-menu pull-right">
<li><a data-toggle="modal" href="#{{field}}-comment-modal">Add Comment</a></li>
</ul>
</div>
<div id="{{field}}-comment-modal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3>Submit Comment</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label class="control-label">Add Comment</label>
<textarea ng-model="field.comments[0].comment" class="form-control"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" data-dismiss="modal">Cancel</button>
<button class="btn btn-primary" data-dismiss="modal" href="#">Add Comment</button>
</div>
</div>
</div>
</div>
OK, In case you didn't know, my field-options is a button with a dropdown that chooses a popup. Because we have repeating directives, you can tell instantly that there are essentially 3 identical copies of this popup being stored in the DOM structure right now. 1 for Model1, 1 for Model2, and 1 for Model3. We now have an issue with being able to call up the proper popup for each field group. Since it's built with the same ID usually, there are 3 identical ID's and browsers can't handle that so they only open the first. My solution, as you can see here, is to call the model name and inject it into the ID for the comment popup, thus making it unique per each field group. The problem is that id="{{field}}-comment-modal" does not print out id="model1-comment-modal" like I want it to, it instead prints out id="{"value" = "whatever the value might be set to, or blank if we click to add a comment before we fill out the input."}-comment-modal".
I return to my original question, "How can I print the model string, not the data it represents?" or, what am I doing wrong, or how to stop worrying and get a new job.
Note: Please be gentile, my example is dumbed down, actual use is way more complex and repetitive.
Not sure it will be possible to output the variable name but your issue can be resolve with one of the following solutions :
Add a 'name' property to your model object and use it to generate the ID or
Pass a new parameter called 'name' to the first diretive and propagate it to the inner one. Anyway you already use standard attributes such as placeholder and type or
Create a factory that will be in charge of generating unique IDs for the whole application or
Simply use a timestamp, Date.now(), to prefix your modal ID.
Hope this will help.
Update : After a bit of digging it may be possible to retrieve the string model1, model2 from the first directive by using the third attributes of the directive link function :
<field model="model1" placeholder="Model 1" type="input"></field>
angular.module('app', []).directive("field", function() {
return {
restrict: "E",
scope: { model: '=', placeholder:'#', type: '#'},
templateUrl: function(tElement, tAttrs){
return 'views/common/' + tAttrs.type + '.html';
},
link: function(scope, elem, attrs) {
console.log(attrs.model);
//Will output model1
}
}
});
By forwarding this string to the inner directive you may be able to generate unique IDs
I'm trying to create a directive that will work against xml that i am injecting into the dom via a service.
Here is my a relatively reduced example, having removed the async service call for data and also the template: jsBin - transforming elements using directive
Everything works well with regard getting my post elements' header attribute into an h2 tag but it is leaving a element within my page which will fail for some browsers.
to clarify, this is what i get:
<post class="ng-isolate-scope ng-scope" heading="test heading">
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
</post>
and this is what i would expect:
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
I think Adam's answer is the better route, but alternatively and easier if you include jquery you can do this in your link function:
var e =$compile(template)(scope);
element.replaceWith(e);
You aren't using template correctly in your directive. Your link function should not applying your template as you are in the example code.
var linker = function(scope, element, attrs) {
var template = getTemplate();
element.html(template);
$compile(element.contents())(scope);
};
Instead of that, just do this:
return {
restrict: 'E',
replace: true,
scope: {
heading: '#'
},
template: '<div><h2>{{heading}}</h2><div>test</div></div>'
};
In your post directive. 'replace: true' will not impact anything if you are compiling and manipulating the DOM yourself.
At the same time, though, I have no idea what your compile directive is for and why you have a factory that returns an HTML string. All of that code looks like it could be reduced to a single directive. I can't really comment on what you're trying to do, but once you start using $compile all over the place, odds are you aren't doing things the 'Angular way'.
I have the following code:
<div modal="modal.shouldBeOpen" close="close()" options="opts">
<div class="modal-body">
<form novalidate name="itemForm" style="margin-bottom: 0px;">
Which is contained inside the included file modal.html
<div data-ng-controller="AdminController">
<ng-include src="'/Content/app/admin/partials/grid-subject.html'"></ng-include >
<ng-include src="'/Content/app/admin/partials/modal.html'"></ng-include>
</div>
In my AdminController controller I am trying to use the following code to reset the form to pristine:
$scope.itemForm.$setPristine();
When I do this it tells me that "itemForm" is undefined.
Is there a way I can set the contents of the form to pristine. I assume this is a scope problem but I am not sure how to fix it. I
tried the one solution of removing the second include and pasting the code in directly. This solution works.
However we want to be able to reuse code
so I would like to be able to do this with an include for modal.html
Note that the reason we would like to do this is because we have something like the following on our modal.html:
<button
class="btn float-right"
data-ng-disabled="itemForm.$pristine"
data-ng-click="modalReset()"
data-ng-show="modal.resetButton">
Reset</button>
</form>
So we are actually inside of the itemForm and would like to set it to $pristine from the button inside.
This answer will break all the rules (i.e., DOM traversal inside a controller), but here it is anyway...
.controller('AdminController', ['$scope','$element',
function($scope, $element) {
$scope.$on('$includeContentLoaded', function() {
var childFormController = $element.find('form').eq(0).controller('form');
console.log(childFormController);
childFormController.$setPristine();
});
}]);
We wait for the ng-included content to load, then from the $element where AdminController is defined, we look for form elements, pick the first one, then get its FormController.
Plunker
If you are only calling $setPristine() as a result of some user interaction, you won't need to look for the $includedContentLoaded event – I only had to do that because I didn't want to create any UI component to trigger the operation, and when the controller first runs, the form doesn't exist yet.
See also AngularJS: Access formController of a form placed inside transcluded directive from parent controller which deals with the similar problem of trying to access a child from a parent.
A cleaner solution: define a directive (use it on the ng-include element) and pass it an AdminController function as an attribute. In the directive's link function, call that method and pass the FormController as a parameter. Then the AdminController will have a reference to the desired FormController. (I did not bother coding this up, as I'm not sure you want a solution where you have to use a directive along with ng-include.)
Well, one way to do it is to broadcast an event, like so:
angular.module('myApp',[])
.controller('AdminCtrl',function($scope){
$scope.modalReset = function(){
$scope.$broadcast('modal-reset');
};
})
.controller('ModalCtrl', function($scope){
$scope.$on('modal-reset', function(){
$scope.itemForm.$setPristine();
});
});
This way you don't have to traverse the dom.
Do not break the rules :) Just define the variable (empty object) in the controller and use it while defining your form. Since angular JS uses scope prototypes under the hood, when form will try to access the inner scope (to bootstrap the variable), it will first go via scope chain and try to find the same variable in the parent's scope.
<!—- The vars should live in the controller. I placed them here for the example. -—>
<div ng-controller=“controllerName” ng-init="form={}; model={}" >
<div ng-include=“ ‘path-to-the-template’ ”></div>
</div>
<!—- Inside path-to-the-template -—>
<form name="form.createUser">
<input name="name" ng-model="model.name" />
<input name="email" ng-model="model.email" />
</form>
Link for reference http://blog.152.org/2014/07/angular-form-element-not-attaching-to.html
If you want to achieve this as the result of some user interaction, in my opinion a much more cleaner and 'angular' way of doing it would be to use a custom directive which will set the form to pristine (i.e. when the user wants to clear the form by pressing esc or clicking a button or whatever).
app.directive("formCleaner",
function () {
return {
restrict: 'E',
require: '^form',
scope: {
callback: '&',
defaultText:'#'
},
template: '<button type="button" ng-click="setFormToPristine()" class="btn btn-warning" >{{defaultText}}</button>',
link: function (scope, element, attrs, formCtrl) {
scope.setFormToPristine = function () {
formCtrl.$setPristine();
scope.callback();
};
}
};
});
and simply hook it up to some button in your form:
<form name="testForm">
<input type="text" ng-model="someModel" />
<hr/>
<input type="button" value="submit form" class="btn btn-primary" ng-disabled="testForm.$pristine"
ng-click=submitForm(testForm) />
<form-cleaner callback="resetFormCallback(testForm)" default-text="Clear Form"></form-cleaner>
</form>
And if you're looking to set the form to pristine directly from the controller, (not as a result of some user interaction) such as success response from a POST, then one way would be to assign a callback to the directive which will be responsible for clearing the form and then invoking that callback from the controller. In your view:
<form-cleaner callback="resetFormCallback(testForm)" default-text="Clear Form"></form-cleaner>
and the controller:
$scope.resetFormOnSubmitCallback=function(cb){
$log.warn("simulating $http POST call.....");
$timeout(function() {
cb();
$scope.someModel=null;
}, 3000)
}
and the directive:
return {
restrict: 'E',
require: '^form',
scope: {
callback: '&',
defaultText:'#',
ngDisabled:'='
},
template: '<button type="button" ng-disabled="ngDisabled" ng-click="submitForm()" class="btn btn-primary" >{{defaultText}}</button>',
link: function (scope, element, attrs, formCtrl) {
var setFormToPristine=function(){
$log.log("setting form to prsitine....");
formCtrl.$setPristine();
};
scope.submitForm = function () {
scope.callback({
onFormSubmittedCallback:setFormToPristine
});
};
}
};
See plunk