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
Related
I have a directive in the form of a dropdown, pretty simple. The user can click a button to add as many as they need to in a ul, make their selections, and save it off. This is all inside of several ng-repeats.
I'm having trouble mastering the scope. As I expected, this works:
<div ng-repeat="group in groups" question-group="group" class="question-group">
<div ng-repeat="question in questions">
<ul>
<li ng-repeat="case in question.cases"></li>
<li><new-case group='group'></new-case></li>
</ul>
</div>
</div>
When I say "works", I mean that group is properly scoped (the data of the entire group is necessary for the resulting input).
When I switch it to "click to add":
<div ng-repeat="group in groups" question-group="group" class="question-group">
<div ng-repeat="question in questions">
<ul>
<li ng-repeat="case in question.cases"></li>
<li>add case</li>
</ul>
</div>
</div>
group is undefined in the scope. Here is my createNewCase function:
function createNewCase($event) {
var thisLi = angular.element($event.target).closest('li');
var listItem = $compile('<li><new-case group=\'group\'></new-case></li>');
var html = listItem($scope);
thisLi.before(html);
}
$scope.createNewCase = createNewCase;
And the newCase directive:
angular.module('groups.directives.newCaseDirective', [])
.directive('newCase', ['$window', function() {
return {
restrict: 'EA',
scope: { group: '=' },
templateUrl: 'groups/views/newcase.tpl.html'
};
}]);
I've been reading for days and I've tried a few other derivatives but I'm ultimately just not getting it. Help is greatly appreciated.
Thanks!
The issue is that group is created by ng-repeat and is only available in child scopes of ng-repeat.
Each repeated element is in it's own child scope. So your directive version works but your other one doesn't because the controller doesn't see those child scopes.
You would have to pass group as argument of the function if you want to access it in controller
<a href="#" ng-click="createNewCase($event, group)">
I have some DIVs that appear on the bottom of each of my pages:
<div id="message" ng-show="test.stateService.displayMessage">
<div>
<div>
<div>
{{ test.stateService.message }}
</div>
</div>
</div>
</div>
Is there a way that I can use Angular to simplify this code so that I do not need to add the same code block to every page?
Yes, you could use a directive like so:
app.directive('codeblock', function() {
return {
scope: true, // use a child scope that inherits from parent
restrict: 'A',
replace: 'false',
template: '<div>\
<div>\
<div>\
{{ test.stateService.message }}\
</div>\
</div>\
</div>'
};
});
You'd call it like this:
<div codeblock id="message" ng-show="test.stateService.displayMessage">
Check https://docs.angularjs.org/api/ng/directive/ngInclude
You can save this div as a file, say footer.htm and add the below code to the bottom of all pages.
<div ng-include="footer.htm"></div>
But this code also needs to be given at the bottom of all pages. So I am not sure how it can save your time.
I don't think you can get rid of adding at least some code on each of the pages. Using an html template with ng-include would be one of the approaches.
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
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
Alright so, what I need is a way to forward keypress (or keydown, whatever) event that occurred on textarea element, and what I've tried to do is this:
<div id="wrapper" ng-controller="MyTestCtrl">
<div id="text" ng-click="DivClick()">
<ul>
<li ng-repeat="item in items">
<textarea ng-click="InnerClick()" ui-keypress="TextKeypress()" autofocus></textarea>
</li>
</ul>
</div>
</div>
But it seems to be acting funny and I want to avoid using any particular key-code since I need it to updates that textarea's height every time a user types something in it (which is what TextKeypress functions is doing).
You could use uiEvent: <textarea ui-event=" { keypress: 'whatever($event)' } ">
UPDATE:
The core now include ng-keypress too! (no idea which version)
Alright, I've overcome this by creating a custom directive that suits my needs and I've bound that directive to the element with 'keypress'. Something like this:
.directive('mydKeypress', function(){
return function(scope, elm, attrs){
elm.bind('keypress', function(e){
//Whatever code;
alert('it bloody works!');
});
};
});