Move HTML content and retain AngularJS two-way data binding - javascript

There's something I'm not getting about Angular. I have an AngularJS page where I have to move around content but have data binding on it. I have a fiddle here that illustrates what I want to do, and the problem: http://jsfiddle.net/gbisaga/cmzBL/6/
<input type="text" size="50" ng-model="model.dentist" name="petDentist" />
If you change the "name" or "type" fields, the displayed string value below changes; but if you change the doggy dentist field, it does not. When I move the content over I want this element to continue to be bound to the model; as you can see in the fiddle, it's not.
I'm guessing what's actually happening is the field's value is filled in BEFORE my-append-children is executed, and the binding is never actually taking place. I have also played with changing to a compile function rather than a link in my directive, but that works even less well. There's something I'm clearly not getting here.

You need to add this at the end of your directive:
$compile(target.contents())(scope);
The updated fiddle: Fiddle
You need to compile the markup that was added so angular can interpret it.

You need to remove the element after you move the children to the target.
If you do element.remove() it will also remove all the bindings and eventhandlers on that element and its children
.directive('myAppendChildren', function() {
return {
restrict: 'E',
link: function (scope, element, attrs) {
var target = $(attrs.target);
target.append(element.children());
element.remove();
}
};
})

Related

Get an element in DOM hierarchy from the link function of a directive [AngularJS]

I have a simple DOM hierarchy and I want to grab a specific set of elements (I want all the canvas elements). This is the entire template for this directive:
<div id='charts-container'>
<div class='chart-wrapper' ng-repeat='chart in getNumberOfCharts() track by $index'>
<canvas id="{{'exam-chart-' + $index}}" class='chart-canvas'></canvas>
</div>
</div>
I want to create a list with all the canvas within the #charts-container element, but I just can't. This is what I'm trying to do inside the link function:
link: function (scope, element, attributes, controller) {
var look = element.find('#chart-canvas');
$log.debug(look);
}
And then I get this element, but I don't know how to get all the chart-wrapper elements from here.
I tried doing look.context.children, it returned an empty list, but at the same time showed me what I wanted as if the list were populated, here is an image. If I try to access any index of this list, it returns undefined (which is fine, because it's an empty list. But why the console is showing me these values?)
What is the best way to achieve this? To get all the canvas elements in this template? (there are 15 of them). Thanks!
--- UPDATE ---
I realized that if I remove the ng-repeat attribute I'm using in the template, it works! But I need the ng-repeat...
element.find('.chart-canvas') manage to get the canvas element, but only if there are no ng-repeat attribute.
Is this a common issue when using ng-repeat. Is there any special treatment when falling in this case?
I found a solution in this thread.
Apparently I was trying to access DOM that hasn't been rendered yet. So, wrapping my query for the canvas with
$timeout(function() {
var canvasList = element.find('.chart-canvas');
}, 0);
solved the issue, since using $timeout will wait until all $digest cycles are complete.
I would like to get to know better how the rendering pipeline works in AngularJS, to avoid falling in issues of this kind. If anyone has a link to a thread or explanation of this, it would be much appreciated.
I think it should be element.find('.chart-canvas') instead of element.find('#chart-canvas'). Does it work with that change ?

AngularJS directive wait until templates rendered

I'm trying to wrap the SharePoint People Picker in an AngularJS directive. In order to initialise a people picker I need to place a div on the page, give it an ID and pass that ID into a SharePoint function.
I have this working with a basic directive like this:
<sp-people-picker id="test"></sp-people-picker>
But I wish for the directive to be useable anywhere, including in a repeating section:
<div ng-repeat="item in dataset">
<sp-people-picker id="test-{{ $index }}"></sp-people-picker>
</div>
This fails. I stepped through the code to see what was going wrong and found that while I was happily calling the SharePoint people picker function with "test-0" it was failing to find the element. document.getElementById("test-0") returned null. The reason for this is that my div still had the id "test-{{ $index }}" and only gets "test-0" AFTER my directive has compiled.
How can I make sure my directive runs after the {{ }} has been rendered?
(Not tagging with SharePoint as the SharePoint stuff is just the context, it's not actually relevant to the issue I'm trying to solve)
You need to use attrs.$observe inside your directive link function, that will act as the same as like $watch, the difference is it can watch on the {{}} interpolation directive, Your link function will look like below. It call function whenever interpolation directive gets evaluated.
Directive(Link Function)
link: function(scope, element, attrs){
attrs.$observe(attrs.id, function(newVal, oldVal){
//here you can get new value & `{{}}` is evaluated.
});
}

Get Elements with AngularJS without referencing the document

I have been developing an advanced web-based GUI in AngularJS. Recently, I decided to use the call document.getElementsByClassName() (I hate using element collecting methods, but here I had to use one) and my boss flipped his lid for accessing the document element. He says that I "need to use only Angular calls for everything", even for element collection! Is there an "Angular way" to collect elements by class name? If so, which way is better to use within the Angular framework? Please provide reasons why. Thanks!
UPDATE: Why I need to use an element collector...
So, I really wish I didn't have to do this, but I do...
I am using a third-party directive that I found online called the Bootstrap DateTimePicker. Its pretty cool and very nice looking, yet it might have a bug...
First, I make a directive bound to an attribute, stating that the element I pass in is meant to be a "DateTimePicker". I then pass that element to the DateTimePicker function.
When invoked, this function creates a new div with absolute positioning and appends it to the body of the page.
Now, I open a dialog in my GUI that has a table in it. On each row of the table, I have two DateTimePickers: one for end-date and one for start-date.
My problem is that, once I leave my screen and the elements which the DateTimePickers were bound to are destroyed, the DateTimePickers still remain! If I open the dialog box again, it creates a ton more of these divs as well!
Until I could determine a true solution to this issue, I decided to use the element collector as a temporary quick-fix. I grab all of the elements with the datetimepicker class and perform a:
elem[i].parentNode.removeChild(elem[i]);
Not having your exact use case but knowing that you are attempting to aggregate elements by class name in your controller makes me agree with you boss. Think of the controller as an object which exposes data and and services to your declarative html page. The data is bound into the markup for presentation and possible modification. THe services are usually wrapped in functions on your controller which are then tied to event handling directives like ng-click or ng-change. These services should operate exclusively on your data and never touch the DOM. If you need to modify a DOM element in your declarative markup then that should be done through directives like ng-class etc.
In any case, It would be useful to know what you are trying to accomplish so as to give you a better idea of the "angular way" to approach the problem.
Well, I have my answer. This does not solve the question "Grab all elements with a certain class name without touching the document element" yet it does solve my problem and eliminates my need to use document.getElementsByClassName.
First of all, it turns out that every element using the DateTimePicker directive have an element.datetimepicker("remove") function.
I use a directive for each DateTimePicker:
components.directive('DateTimePicker', function() {
// Requires bootstrap-datetimestamp.js
return {
restrict: 'E',
replace: true,
scope: {
dateTimeField: '='
},
template:
'<div>' +
'<input type="text" readonly data-date-format="yyyy-mm-ddThh:ii:ssZ" data-date-time required/>'+
'</div>',
link: function(scope, element, attrs, ngModel)
{
var input = element.find('input');
input.
datetimepicker(
{
//stuff
})
.on('changeDate', function(ev)
{
//more stuff
});
...
Directive drastically shortened for the sake of your eyeballs...
I then need to remove the DateTimePicker and the input it is bound to from the DOM on destruction of the dialog box that the input is a child of. To do so, I added this to my directive:
scope.$on("$destroy",function handleDestroyEvent()
{
input.datetimepicker("remove");
input = null;
});
And it works! The DateTimePicker gets removed, the DateTimePicker's handles to the input are cleaned up, and I've marked my input for the GC! WooHoo! Thanks everybody!
If you include jQuery in your project before AngularJS, Angular will use jQuery instead of jqLite for the angular.element function. This means you should be able to use jQuery's selectors for finding / referencing DOM elements.

Validation in a custom directive that is used multiple times on the same page (AngularJS)

I've created a custom directive that has contains related inputs and dropdowns. I've also used an isolate scope to properly bind the outer scope to the inner scope to assist with two databinding, and this also allows me to use the same directive multiple times on the same page. All works well up to this point. My next question is how to handle validation within the directive.
I no longer can use something along the lines of
ng-show="formName.controlname.$invalid && !formname.controlname.$pristine"
for the following 2 reasons,
My directive should not have to worry about the external form.
Because i'm using the same directive twice on the same page, using the syntax formName.controlname would actually map to two different controls.
Some ideas on this would be helpful at this point. What am I missing here?
You should definitely not make your directive depend on the form name. What you should do instead is to provide a parent form directive dependency so you can use its controller in your directive link function:
.directive('yourDirective', function() {
return {
require: '^form',
link: function(scope, element, attrs, formController) {
// use formController.$errors object
}
};
});
Probably you should do the validation in your directive.
Create a copy of the two-way bound scope members (assuming these are bound to the form inputs) to check pristine-ness and undo-ability.
Thanks for the feedback, but I discovered that using ng-form meets my requirement.
So in my directive mark up I added:
<div ng-form="[some name]">
.......
</div>
Doing this allowed me to continue to use the ng-* attributes.
Now I can do this in my directive:
ng-show="somename.controlname.$invalid && !somename.controlname.$pristine"
It is self contained so I don't have to worry about crossing any boundaries. I can add the control over and over again and validation stays intact per directive.

directive not properly being targeted by jQuery plugin

I've been playing around a bit with this slick jQuery datpicker and wanted to wrap it up as a directive that I could use inside my angular app. The code for the directive is very simple for right now:
directive('datePicker', function() {
return {
restrict: 'E',
template: '<input type="text" class="topcoat-text-input--large full" placeholder="Select Date">',
link: function (scope, element, attrs) {
element.pickadate();
}
}
As you can see I'm simply targeting the element parameter with the necessary pickadate() jQuery call. The input is properly being targeted as when I click on it I am provided with the datepicker interface and can interact with it no problem. However, when I select a date no information is being populated into the input element. Am I missing something obvious that will allow the date being selected from the control to set the value of the input?
I've done a little bit of debugging and in the link function the element parameter seems to be wrapping the actual input in some way (there seems to be a childNodes array property that holds the <input> tag). Could this be why I'm getting the popup for the picker but the selected value isn't being set as the input's value?
element is the original element in your markup. By default it is not replaced and template is used for innerHtml.
You could use replace:true option in directive or element.find('input').pickadate()
Either of these should resolve visual issue of seeing date. One important thing to note however when you use ng-model and change a value from external code like a jQuery plugin, will need to use the plugin select ( or whatever it is called in pluginAPI) callback to trigger scope.$apply(). This infomrs angular a change was made from code external to angular and to update internals
I suppose you should add replace: true to your directive definition - Angular is targeting the <date-picker> directive element instead of the input.
Instead you can also try this: element.children().first().pickadate();

Categories

Resources