Few questions regarding Angular directives - angularjs

I have few questions regarding angular directives
I'm basing my questions based on the following block of codes
<ul class='parent'>
<li class='child1'></li>
<li class='child2 active'></li>
<li class='child3'></li>
</ul>
So based on the above
How can i create a directive based on the above (assuming this comes from an Angular plugin, and there is no angular directive attribute defined within the tag)?
Assuming the above is from an Angular plugin, and i don't intend to edit the plugin, how can i use the directive to see which list is currently active?
How can i do operations through the code above using Angular? With Jquery, i can easily do it as
For checking the child length
$('.parent').children().length
For iterating through the child elements
$.each($('.parent').children(), function(){ console.log(this); });
But when i tried to do it with Angular way
angular.element(document.querySelectorAll('.parents')).children().length
Somehow this part always returned me the wrong value. I guess my Jquery mindset is the reason why i couldn't get the value but still, i read somewhere that angular.element object is the equivalent of the jquery $() object, thus both should be able to use the same functions.
If i want to do event binding on the code above i should do it inside the link function correct? If i want to watch and apply changes for the code above, then i should use the directive's scope and do it inside the link function too right?
Is there easy-to-chew guide on how Angular's directives? Somehow most of those that i read are pretty much as confusing as the directive guide in Angularjs documentation itself.

Related

angularjs ng-init json variable in ng-repeat

I have an ng-repeat loop that goes over a json array. The array is fetched via $http. Within that loop I want to add some variables to the json object that comes in handy when comparing old/changes values.
<li class="machine" ng-repeat="machine in machines>
....
<input id="machine-host" ng-model="machine.host"
ng-init="machine.editHostDisabled=true;machine.originalHost=machine.host"/>
......
</li>
I read on the angularjs documentation that you are not allowed or "should not" use ng-init. But, here it seems strange to not use this this directive. The above code is clean and easy.
Yes I can set it in the controller, but then I need to loop through the json array of the machines and set the values in the loop. Seems too much of a hassle.
This directive can be abused to add unnecessary amounts of logic into your templates. There are only a few appropriate uses of ngInit:
aliasing special properties of ngRepeat, as seen in the demo below.
initializing data during development, or for examples, as seen throughout these docs.
injecting data via server side scripting.
Besides these few cases, you should use Components or Controllers rather than ngInit to initialize values on a scope.
— AngularJS ng-init Directive API Reference - Overview
Of course I understand that an extensive function can give issues here. But this simple line of code...?
Also, I see the current way of working of many examples all over the web.
What is the rule to follow here ? Adding a controller function to ng-init? But that would be the same thing right? Or is the above code fine?

Angularjs dynamically added ng-model is not accessible by $scope, shows undefined

I am creating form, where few fields are dynamic, ng-model is added dynamically.
Ex.:
form.append("<input type='hidden' name='paymillToken' value='" + token + "' data-ng-model = 'formdata.token'/>");
This fields shows undefined while I try to access using $scope.formdata.token
Following is another scenario where I am adding fields via ajax.
angular.forEach(data.data, function(obj, key) {
list+='<div class="items text-center"><img src="assets/uploads/discs/'+obj.image+'" class="img-circle"><br><input type="radio" id="chkDisc'+obj.id+'" name="disc_id" value="'+obj.id+'" required data-ng-model="formdata.disc_id" /></div>';
});
$scope.discslist = $sce.trustAsHtml(list);
This model disk_id is not accessible too.
Okay, to expand on my comment and a bit more on what everyone else here is saying to you, the main issue you're having is inherent in your approach. The way you're trying to manipulate the DOM is very un-AngularJS.
In AngularJS, when you want to change what is displayed to the user (the view), you make changes to your model (your controller scope). That means, you have to set up your view to be able to respond to those changes. We do that with directives and expressions in Angular.
You're probably already using directives to respond to changes in your model whether you realize it or not. ngRepeat, ngModel, ngShow, ngIf, ngInclude, are a handful you're probably familiar with, and even forms and form elements like inputs are actually directives in Angular. When you use these, a change in your model (such as loading data into the controller scope) signals to Angular that it should check whether that change affects any of the directives in your view, and if so, respond to it by updating the view.
In order to do this, Angular needs to know which parts of the model are connected to which parts of the view. It makes these connections when it compiles the html elements that are added to the page. This compile process happens automatically when you load an Angular app. After that, it's up to us to tell Angular when to compile html that is added to the page.
More often than not, we do this without even realizing it. For example, when you use the ngView directive, it will compile the template for each route that it loads, so that all of the directives in your template are properly linked with their associated model.
I know this is a long explanation, but there are two very important points here that are essential to learning AngularJS:
To change the view, you change your model and let the directives (and expressions) on your page respond to those changes.
When you add html elements to the page, if you want AngularJS to be able to use them in your view, they must be compiled first. The compile process should be done via a directive (either a built in one or a custom one).
So, how does all of this apply to your question?
First, I'm guessing that you're breaking both rules by trying to manipulate the DOM via a controller. Even if it is possible to use $compile in a controller, using a controller to change the DOM is bad practice and simply wrong to do (read the part in that link to the doc that specifically states: Do not use controllers to: Manipulate DOM...). A good rule to remember when you're learning AngularJS is that the only time you should ever be using JQuery or JQLite inside Angular is when you are creating a custom directive.
Okay, so how do you solve your question? Use a directive. It looks like you've got a case where you're trying to iterate over an object called data and add some inputs that correspond to the data.data property. This sounds like a job for ngRepeat.
The first thing you need to do is add your data to your controller and make sure it is accessible to the view. The easiest way to do this is by injecting $scope into your controller and setting the data on a scope variable. In its simplest form, that might look something like this:
angular.module('MyApp', [])
.controller('MyController', ['$scope', function($scope){
$http.get('/some/url/that/returns/the/data').
success(function(data) {
$scope.data = data;
});
}]);
Now that we have the data somewhere that we can access from the view, we can use it with the ngRepeat directive in our html, something like this:
<div ng-controller="MyController">
<div class="items text-center" ng-repeat="disc in data.data">
<img ng-src="assets/uploads/discs/{{disc.image}}" class="img-circle"><br>
<input type="radio" id="{{'chkDisc' + disc.id}}" name="{{disc.disc_id}}" value="{{disc.disc.id}}" required data-ng-model="formdata[disc.disc_id]" />
</div>
</div>
This is a common issue. By updating the value and not the model angular has no idea that the value in the field has been updated. As the first commenter said updating in this manner is completely unnecessary when using ng-model.

Angular directive failing silently (directive rendered too soon?)

I have an Angular app with a custom element directive (<uber-table>). <uber-table> is supposed to take a collection of objects, render them into a <table> and add some functionality (click row to toggle the underlying object as selected, search box for live filtering, action links on each row with customized click callback(s) for each object). I created a Plunker with the relevant code. Please note that Plunker is giving an error about map (Object [object Object] has no method 'map'), but locally I am not getting any errors.
The post-link function's element parameter is not the <uber-table> element as I expected. Instead it is the template's <div class="uber-table"> element. This is preventing me from extracting data from <uber-table>. What am I doing wrong? Any help will be much appreciated.
Here's a rundown on some of the issues.
First main issue is you want existing content already within the uber-table markup to exist, as well as a new template. Unless told otherwise the existing content ( columns) in this case will be overwritten. In order to include existing content in a directive that has a template, you need to set transclude:true then identify within template where this existing content needs to be placed using ng-transclude on element that will be parent of the content.
Here's demo with transclude fixed
New problems arise now where you are trying to use jQuery to loop over columns and return angular attrs => column.attrs . This throws undefined error.
I haven't tried to unravel this enough to sort out the columns issues yet. They should likely be handled by directive of their own
UPDATE: here's slightly revised error free version using jQuery to parse column count. I'm afraid am still confused a bit by structure of this. I don't see need to use jQuery to parse colunms, this could be converted to directive or pass column definitions into main directive from controller scope
After trying several things and looking at the documentation again, I finally got it working. The solution was to put the post-link function inside the compile function. Also I had to update my isolated scope to use =, set replace to true and set transclude to 'element'.
I updated Plunker if anybody wants to see the changes. The Plunker version isn't working, but since it is working locally, I'm not going to spend too much time on it.

Tricky scope binding in angularjs directives

I want to write 'edit in place' directive in angularjs.
I want that directive is reusable, therefore I have following requirements on the directive:
it must be an attirbute that can deocorate any element, that makes sense (div,span,li)
it must support edit button, clicking on that will change set ot displayd elements into input fileds. Typically properties of one object, e.g. contact (number, name)
I disocvere trickery behaviour of scope visibility in the directive that can be seen in this fiddle http://jsfiddle.net/honzajde/ZgNbU/1/.
Comenting out in the directive: template and scope -> contact.number and contact.name are displayed
Comenting out in the directive: scope -> contact.number only is displayed
Not commenting out anything -> nothing is displayed
=> when both are commented out just adding template to the directive makes it render contact.number even though template is not used.
I am asking what are the rules of the game?
<div>
<div ng-controller="ContactsCtrl">
<h2>Contacts</h2>
<br />
<ul>
<li ng-repeat="contact in contacts">
<span edit-in-place="" ng-bind="contact.number"></span> |
<span edit-in-place="" >{{contact.name}}</span>
</li>
</ul>
<br />
<p>Here we repeat the contacts to ensure bindings work:</p>
<br />
<ul>
<li ng-repeat="contact in contacts">
{{contact.number}} | {{contact.name}}
</li>
</ul>
</div>
</div>
var app = angular.module( 'myApp', [] );
app.directive( 'editInPlace', function() {
return {
restrict: 'A',
//scope: { contact:"=" },
template: '<span ng-click="edit()" ng-bind="value"></span><input ng-model="value"></input>',
link: function ( $scope, element, attrs ) {
// Let's get a reference to the input element, as we'll want to reference it.
var inputElement = angular.element( element.children()[1] );
// This directive should have a set class so we can style it.
element.addClass( 'edit-in-place' );
// Initially, we're not editing.
$scope.editing = false;
// ng-click handler to activate edit-in-place
$scope.edit = function () {
$scope.editing = true;
// We control display through a class on the directive itself. See the CSS.
element.addClass( 'active' );
// And we must focus the element.
// `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
// we have to reference the first element in the array.
inputElement[0].focus();
};
// When we leave the input, we're done editing.
inputElement.prop( 'onblur', function() {
$scope.editing = false;
element.removeClass( 'active' );
});
}
};
});
app.controller('ContactsCtrl', function ( $scope ) {
$scope.contacts = [
{ number: '+25480989333', name: 'sharon'},
{ number: '+42079872232', name: 'steve'}
];
});
You are running into problems because you are misusing angular.
First, a directive should be self-contained, but you are pulling functionality out of it, which makes it less universal and less reusable. In your code, you have functionality in the DOM and in the controller that belongs in the directive. Why?
Second, it's also unclear from your markup and javascript specifically want you want to accomplish when all these pieces are strung together.
Third, in most cases, directives should have their own isolated scope, which is done by declaring a scope object with attributes it should bind. You shouldn't be passing an expression (i.e. {{contact.name}}) inside the directive as it will break the binding and your contact will not be updated when the edit-in-place finishes. The proper way is to establish bi-directional binding through an = property on the scope. ng-bind isn't what you want here: that's scope-specific, so we use it inside the directive's scope. As Valentyn suggested, you could do some magic to get around this, but it's not a good idea and it's super-simple to set it up the right way. What's the issue with doing this by an attribute?
This is all bad Ju-ju.
As I pointed out in your other question on this same topic, you must make your directive self-contained and work with angular, rather than against it. Here's an attribute-based version of the fiddle I gave you previously, meeting the first of your requirements. Please let me know what is wrong specifically with this implementation and we can talk about the angular way of fixing it.
Lastly, if you provide further context on what you need in terms of a "button", I'll incorporate that into the fiddle too.
[update]
It is possible to make the directives work your way, but you will run into problems eventually (or right now, it would seem). All components in an angular app (or any app for that matter) should be as self-contained as is feasible. It's not a "rule" or limitation; it's a "best practice". Similarly, communication between directive components can occur through a controller, but it shouldn't. Ideally, you shouldn't reference the DOM in a controller at all - that's what directives are for.
If your specific purpose is a row that is editable, then that is your directive. It's okay to have a lower-level generic edit-in-place directive that the larger directive uses, but there is still the higher-level directive too. The higher-level directive encapsulates the logic between them. This higher-level component would then require a contact object.
Lastly, no, there isn't necessarily a big difference between ng-bind="var" and {{var}}. But that's not the issue; the issue was where that binding takes place. In your example, a value was passed to the directive instead of a bi-directionally-bound variable. My point was that the directive needs access to the variable so it can change it.
Summary: You are coding in a very jQuery-style way. That's great for coding in jQuery, but it doesn't work so well when coding in Angular. In fact, it causes a lot of problems, like the ones you're experiencing. In jQuery, you would, for example, dynamically insert DOM elements, declare and handle events, and manually bind variables all within a single code block, all manually. In Angular, there is a clean separation of concerns and most of the binding is automatic. In most cases, it leads to javascript code at least two-thirds smaller than the jQuery alternative. This is one of those cases.
That said, I have created a Plunker that contains a more sophisticated version of both the edit-in-place as well as a new higher-level directive to incorporate additional features: http://plnkr.co/edit/LVUIQD?p=preview.
I hope this helps.
[update 2]
These are the answers to your new round of questions. They may be good for your edification, but I already gave you the "angular way" to fix your problem. You will also find that I already addressed these questions (in broader strokes) earlier in my original answer as well as in my update. Hopefully, this makes it more apparent.
Question: "Comenting out in the directive: template and scope -> contact.number and contact.name are displayed"
My Reply: When you do not specify a scope, the directive inherits its parent scope. You bound and interpolated the name and number within the context of the parent, so it "works". Because the directive will alter the value, however, this is not a good way way to solve it. It really should have its own scope.
Question: "Comenting out in the directive: scope -> contact.number only is displayed"
My Reply: You bound a scope property of the parent to the "contact.number" directive, so it will get placed inside during the $digest loop - after the directive has been processed. On the "contact.name", you put it inside the directive, which can only work if the directive codes for transclusion.
Question: "Not commenting out anything -> nothing is displayed"
My Reply: Right. If the directive has its own scope (and this one definitely should), then you must use a defined directive scope property to communicate values, as my several code samples demonstrate. Your code, however, tries to use the parent scope in the directive when we explicitly forbid that by using the scope property in its definition.
Summary: While this second update may be informative (and I hope that it is), it doesn't answer the question beneath your questions: how do I use angular components correctly so that the scope I'm using is always what I think it is? My first post and the subsequent update, answer that question.
Here is little bit updated your fiddle, but it need further improvements to meet full list of your requirements: http://jsfiddle.net/5VRFE/
Key point is:
scope: { value:"=editInPlace" },
Some notes: its better to use ng-show ng-hide directivies for visual appearing-hiding instead of changing css classes. Also its better to spread functionality into different directives to have better separation of concerns (check ngBlur directive)
About your confusion of scope check guide about scopes paragraph "Understanding Transclusion and Scopes": Each directive have separate isolated scopes, if you want to have access from directive's template to controller's scope use directive scope binging ("scope" field of directive definition object). And also transcluded elements have a scope of from where you defined transcluding template.
From the first view those isolated scope sounds little bit strange, but when you have good structured directives (note also that one directive can require another and share bindings) you can find it extremly usefull.

AngularJS - ng-bind-html-unsafe and ng-model Problems

I have the following line in my html:
<div ng-bind-html-unsafe="departmentConfig" class="control-group"></div>
and I use a $resource get to retrieve the HTML, assign the HTML to $scope.departmentConfig, and then the view is updated perfectly. The HTML that is assigned to $scope.departmentConfig contains form elements, with ng-model attributes in it, but when I change the values in these input elements, they don't update the $scope model at all.
This is what I have tried, based on a lot of other internet posts, and it isn't working:
$resource('resources/sources/departments/:mappedName', {
mappedName:departmentKey
}).get(function(departmentConfig) {
// This will call another function that will build a chunk of HTML
$scope.departmentConfig = $scope.buildDepartmentConfigHtml(departmentConfig);
// This is my feeble attempt to access the element, and bootstrap it to include the items in the $scope model.
var $departmentConfigContainer = $('#departmentConfig');
angular.bootstrap($departmentConfigContainer, ['sourcemanager']);
I have even seen some jsFiddle examples where this appears to be working, but mine isn't. Am I calling bootstrap too soon? I also tried putting a $watch on $scope.departmentConfig like this:
$scope.$watch('departmentConfig', function() {
var $departmentConfigContainer = $('#departmentConfig');
angular.bootstrap($departmentConfigContainer);
});
but it didn't work either. I bet there is an easy explanation to this, I just can't seem to get the input elements with ng-model that are loaded after page compile to get bound to the model. Any help is appreciated, this is the last piece of functionality I need to get working on my page. Let me know if you need more information about my configuration as well.
So, simply put, how can I force a section of the DOM to recompile after I know it has been loaded?
UPDATE
Here is a jsfiddle outlining what I would like to do: http://jsfiddle.net/j_snyder/ctyfg/. You will notice that property two and three don't update the model, and I am calling bootstrap on the outer div, hoping that will include those in the model binding. This is the first time I have posted to jsfiddle, please let me know if you can't see the example.
ng-bind-html is made for regular HTML, not compiling new angular elements.
You will have use the $compile service.
Here is how you would edit your current example to work: http://jsfiddle.net/andytjoslin/ctyfg/21/. But this way ends up being bad, because you have to do DOM manipulation in your controller.
You just need to create a directive that will basically do what you wanted ng-bind-html to do: http://jsfiddle.net/andytjoslin/ctyfg/22/

Resources