I am adding an element dynamically in Angular. The code is as follows.
myelement.after('<input ng-model="phone" id="phone">');
The element gets added and all works fine. However I think I'm missing a step and Angular does not recognize the new element because when I collect data later, the value of the dynamically added element is not defined.
The reason for adding the element on fly is that the number of inputs is not known from the beginning and user can add as many of them as they want. The actual code is like:
myelement.after('<input ng-model="phone"' + counter + ' id="phone' + counter + '">');
I'm sorry, I should have provided complete sample from the beginning. Please see the following jsfiddle, add some phones (with values) and list them: http://jsfiddle.net/kn47mLn6/4/
And please note that I'm not creating any new directive. I'm only using Angular standard directives, and I prefer not to create a custom directive for this work (unless required).
Assuming that you are adding element to the DOM using directive. You can use built in angular service $compile.
Firstly inject the $compile service to your directive. Then in your link function of the directive, get your myElement after which you want to append your element. Then create your element using angular.element(). Then compile it using $compile service and pass the scope in which you are in now.(Here this scope is the directive scope). After getting the compiled dom element you can just append it after your myElement.
here is an example about how you can add element dynamically from directive:
var elementToBeAdded = angular.element('<input ng-model="phone" id="phone">');
elementToBeAddedCompiled = $compile(elementToBeAdded)(scope);
myElement.append(elementToBeAddedCompiled);
If you add your element using $compile service in your directive, angular will recognize your dynamically added element.
I was trying to accomplish this without using directives, but it seems the best way (and probably the only proper way) of adding multiple elements to DOM in Angular is by defining a custom directive.
I found a very nice example here http://jsfiddle.net/ftfish/KyEr3/
HTML
<section ng-app="myApp" ng-controller="MainCtrl">
<addphone></addphone>
<div id="space-for-phones"></section>
</section>
JavaScript
var myApp = angular.module('myApp', []);
function MainCtrl($scope) {
$scope.count = 0;
}
//Directive that returns an element which adds buttons on click which show an alert on click
myApp.directive("addbuttonsbutton", function(){
return {
restrict: "E",
template: "<button addbuttons>Click to add buttons</button>"
}
});
//Directive for adding buttons on click that show an alert on click
myApp.directive("addbuttons", function($compile){
return function(scope, element, attrs){
element.bind("click", function(){
scope.count++;
angular.element(document.getElementById('space-for-buttons')).append($compile("<div><button class='btn btn-default' data-alert="+scope.count+">Show alert #"+scope.count+"</button></div>")(scope));
});
};
});
//Directive for showing an alert on click
myApp.directive("alert", function(){
return function(scope, element, attrs){
element.bind("click", function(){
console.log(attrs);
alert("This is alert #"+attrs.alert);
});
};
});
in the angular mind you should manipulate the dom from the JS code.
and use ng-* directive
So i don't know you code but i think you just need to do something like :
View
<button ng-click="showAction()">Show the phone input</button>
<input ng-show="showPhone" ng-model="phone" id="phone">
Controller
app.controller('ctrl', [function(){
$scope.showPhone = false;
$scope.showAction = function() {
$scope.showPhone = true;
};
}]);
Outside of Angular you would need to use the event handler to recognize new elements that are added dynamically. I don't see enough of your code to test, but here is an article that talks about this with $.on():
Jquery event handler not working on dynamic content
Here is a good article that will help with directives and may solve your problem by creating your own directive:
http://ruoyusun.com/2013/05/25/things-i-wish-i-were-told-about-angular-js.html#when-you-manipulate-dom-in-controller-write-directives
Related
I am very new to AngularJS. I am facing difficulty in changing the inner HTML of a table. I am storing a string in a variable, the string include 3 types of tags strong tag this works well and two tags tr and td when I replace the inner value of table with JavaScript variable then it does not convert the tr and td tags into the DOM elements.
Here is my JavaScript code:
var app = angular.module('myApp', ['ngSanitize']);
app.controller('personCtrl', function($scope,$http)
{
$scope.fun = function()
{
varible="<tr><td><strong>Id</strong></td><td><strong>ahmed</strong></td><td><strong>45kg</strong></td></tr>";
$scope.bind=varible;
};
});
Here is my HTML Code
<div ng-app="myApp" ng-controller="personCtrl">
<table ng-bind-html="bind">
</table>
<button ng-click="fun()">check</button>
</div>
You need to use $sce to trust that string as html otherwise Angular won't use them because unsafe:
JSFiddle
app.controller('dummy', function($scope,$sce)
{
$scope.fun = function()
{
varible="<tr><td><strong>Id</strong></td><td><strong>ahmed</strong></td><td><strong>45kg</strong></td></tr>";
$scope.bind = $sce.trustAsHtml(varible);
};
});
It should be better to use a directive, this is a really simple example:
Directive JSFiddle
HTML:
<div ng-app="app" ng-controller="dummy">
<table><custom-table ng-show="bind"></custom-table></table>
<button ng-click="bind=true">check</button>
</div>
JS:
app.directive('customTable',
function(){
return {
restrict: 'AE',
template: '<tr><td><strong>Id</strong></td><td><strong>ahmed</strong></td><td><strong>45kg</strong></td></tr>',
link: function(scope, element, attr) {
//If you need do something here
}
};
});
When manipulating the DOM by adding elements to it or anything, try using an Angular Directive.
Directives have a special property called link that allows you to interact with the DOM.
This should be used whenever possible.
Also, to do things like this, look into ng-repeat & two-way Data Binding in Angular.
Angular shines here, so get to know it a bit more.
I am manipulating DOM from the link function to add custom css using the directive attributes.
So here, first I will append one div with a class name, then find the div and add another div inside.
(function(){
"use strict";
var directive = function() {
return {
restrict: 'E',
link: function (scope, element, attrs) {
// Configuration
var id = Math.floor(Math.random()*100);
attrs.id = attrs.id || 'my-directive-'+id;
element.attr('id', attrs.id);
element.append('<h1>Here</h1>');
element.append('<div class="my-directive-background">');
element.find('.my-directive-background').css({'background-color':attrs.bColor});
// add css
element.find('.my-directive-background').append('<div class="my-directive-foreground">');
// add css
element.find('.my-directive-foreground').css({'background-color':attrs.fColor});
}
};
}
var directives = angular.module("directives");
directives.directive("myDirective",[directive]);
})();
<my-directive bColor="gray" fColor="red" />
<my-directive bColor="gray" fColor="green" />
The problem is, when I use more than one same directive, the find method detects previous element also.
How to find only the div belongs to current directive element?
Update: (Simple Solution)
#Shripal's solution works fine. But the problem was because of the element closing syntax.
It's simply fixed when I changed
<my-directive bColor="gray" fColor="red" />
to
<my-directive bColor="gray" fColor="red"></my-directive>
If jQuery is not added to your project, then find with class selector will not work.
I have created a working plunker using angular only with element.children()
Edit:
Updated plunker with jquery. I have optimized the code to first prepare the DOM and then appeded it to the element.
I have an AngularJS directive that includes an ngIf and I would like to modify some of the DOM inside the ngIf in the directive link function. Unfortunately it seems that ngIf prevents me from finding DOM elements within it in the link function.
Here is the code for the directive:
directive('column', function () {
return {
templateUrl: 'views/column.html',
restrict: 'E',
scope: {
column: '='
},
controller: ['$scope', function ($scope) {
$scope.editing = true;
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
};
}],
link: function postLink(scope, element) {
var select = element.find('select');
console.log(select); // See if it can find the select element
// var types = scope.column.types();
// add types as options to the select element
}
};
});
And here is the simplified html of the directive:
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-if="editing">
<select></select>
</form>
</div>
Here is the link to the jsFiddle example http://jsfiddle.net/dedalusj/Y49Xx/1/
The element.find call in the link function returns an empty array but as soon as I remove the ngIf from the form it returns the proper select DOM element. I have the feeling that I'm doing this the wrong way.
UPDATE
Thanks for the answers but I found another solution. I simply created another directive that encapsulate the form, added it to the column directive template with ng-if="editing".
The form directive doesn't have it's own scope so it effectively operates out of the column directive scope and has always access to the select element because it's inside its DOM tree. I pay the cost of an extra directive but I don't have to use the $timeout hack. I created a new jsFiddle to illustrate the solution http://jsfiddle.net/dedalusj/nx3vX/1/
Thanks #Michael but I can't simply use the ng-option because the types array comes from an XML file and its elements are other angular.element objects which cannot be inserted easily with ng-option.
The ngIf directive works by using Angular's transclusion feature. What happens during the compile/link cycle is:
The content inside the ngIf is removed from the DOM when it is compiled
Angular runs the link functions. The ngIf's link function is run before
the link function of the directive using it. When ngIf's link function
runs, it uses $scope.$watch() to watch the value of the ng-if
attribute.
Your directive's link function runs, at this point the content of the ngIf is not part of the DOM
The watch set up in step (2) is called, and ngIf will then call the $transclude function to insert the contents of the ngIf into the DOM if the ng-if attribute value is truthy.
Any watch functions, $timeout calls or use of $scope.$evalAsync that you registered in your directive's link function will run.
So if you want to access elements inside the ngIf's content, the code needs to run after step 4 above. This means that any functions registered with $scope.$watch, $timeout or $scope.$evalAsync in your directive's link function will work. For a one-time piece of setup code, I would probably opt for $scope.$evalAsync:
angular.directive('yourDirective', function () {
return {
...
link: function(scope, elem) {
scope.$evalAsync(function () {
// code that runs after conditional content
// with ng-if has been added to DOM, if the ng-if
// is enabled
});
}
};
});
As #moderndegree has said, ngIf removes the element it's applied to from the DOM, so you won't be able to find it when it's not there. But, you could write your directive in a way to workaround that:
controller: function ($scope, $element, $timeout) {
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
$timeout(function() {
var select = $element.find('select');
select.append('<option>Value 1</option>')
.append('<option>Value 2</option>')
.append('<option>Value 3</option>');
});
};
}
Updated jsFiddle here.
The trick here is to delay the find() call by using $timeout with a 0 interval in order to wait for Angular to update the DOM.
UPDATE
After giving some more thought to your code, I realize that perhaps you can let Angular do the hard work for you:
Javascript
directive('column', function () {
return {
templateUrl: 'views/column.html',
restrict: 'E',
scope: {
column: '='
},
controller: ['$scope', function ($scope) {
$scope.editing = true;
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
};
}],
};
});
HTML
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-if="editing">
<select ng-model="type" ng-options="type for type in column.types"></select>
</form>
</div>
jsFiddle
Now you don't need to worry about finding the select element at the right time and populating it. Angular does all of that for you. :)
You can put your code from the link function inside $timeout.
$timeout(function(){
var select = element.find('select');
console.log(select);
});
Don't forget to inject $timeout in your directive
directive('column', function ($timeout) {
I was facing this same issue and i was able to resolve it using ng-show, this prevents this issue because ngIf removes the element it's applied to the DOM, so you won't be able to find it when it's not there.
so in your case:
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-show="editing">
<select></select>
</form>
will work OK.
Cheers.
I've got a directive that I'm using to validate form fields. I want to dynamically add the validation with the directive. Here's what my directive looks like:
app.directive('validatedInput', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var constraints = {
'ng-maxlength' : 10,
'data-required': ''
}
angular.forEach(constraints, function(value, key) {
element.attr(key, value);
}
$compile(element)(scope);
}
}
}
And here is my markup:
<input type='number' name='fieldName' ng-model='data.test' validated-input></input>
So basically what I want is for the directive to add ng-maxlength=10 and data-required='' to my input element so that the validation can work (this is a trivial case; in the future I plan on getting the constraints object from the back-end using a service). The problem is, the directive seems to add the attributes to the DOM, but the validation doesn't work. In other words, if I inspect the HTML using my browser's dev tools, the markup looks correct, but the functionality isn't there. Any thoughts?
Much appreciated
I only have a bit of Angular experience (~6 weeks), but everything I've seen in regards to Directives to do DOM in Angular is clunky as fu...nky chicken...
I'm doing this currently with jQuery (which has zero chicken funk DOM manip. wise) to do a $compile after dynamically adding ng-attributes (in this case so the INPUT name is calculated for the crappy bootstrap/angular datepicker control I'm using currently):
//# Hook the .calendars object via the required Angular attributes
$('I.icon-calendar').each(function (i, obj) {
var $s, $p = $(this).parent();
//# Set the ng-click of the .parent button
$p.attr('ng-click', "calendars.open($event)");
//# Setup the calendar INPUT
$s = $p.siblings('INPUT[datepicker-options]');
$s.attr('is-open', "calendars.isOpen['" + $s.attr('name') + "']");
//# Re-$compile the DOM elements so all of the above added Angular attributes are processed
$compile($s.get(0))($scope);
$compile($p.get(0))($scope);
});
$compile needs to be "injected" (I hate that term, why can't we just say "passed"?) into the controller, a'la:
myApp.factory("Tools", function ($http, $q, $timeout, $compile) {...}
The only issue I've had thus far is some controls/plugins/etc. add DOM elements on $compile, so currently I'm wrestling the dragon of multiple datepicker UL.dropdown-menu's being added to the DOM, to which I have no answer (but I did find this question on the way, so there's that).
I made a custom directive that had two ng-repeats inside of it. One ng-repeat was nested inside the other one. I got the code to work, and it performs well in chrome but on the iPad and iPhone it is sluggish.
There are 10 sections with 5 rows each and it needs to be very fast when it comes to scrolling and changing the bindings. I think the slowdown comes from all the loops through the bindings but only one array needs to be changed on user input. The rest of the bindings never change after the page loads.
So I am trying to figure out a way to load nested unordered lists while only binding one variable. This is pseudo code for my directive.
.directive('myDirective', function($compile) {
return {
restrict: 'A'
link: function(scope, elm, attrs) {
outerList = '<ul><li>statically generated content that does not change'
outerList += '<ul><li ng-bind="I only need to bind one thing"><li></ul>'
outerList += < /ul>'
elm[0].innerHTML = outerList
}
}
});
As you can see I am trying to generate the html content, and then insert it with innerHTML. The problem is the ng-bind isn't working when I do it this way. I tried to $compile it again but that didn't change anything.
Does anyone have a better way? I don't care how hideous the solution is I just really need this part of the app to be super fast. The main thing is I don't want ng-repeat unless there is a way to make it do its thing on load and then never loop through anything again.
I would like to do this in the most Angular way possible but I realize I might have to do something that goes completely against Angular philosophy
Here is an example of how to modify your code in order to bind some variable in a directive from a scope outside of it. I've used $compile to ensure that your directive DOM manipulation has its own directives compiled. I've used replaceWith to replace the directive element with your compiled DOM:
HTML
<div ng-app="myApp">
<div ng-controller="ctrlMain">
<div my-directive="bindMe"></div>
</div>
</div>
JavaScript
var app = angular.module('myApp',[]);
app.controller('ctrlMain',function($scope){
$scope.bindMe = {id:1,myvar:"test"};
});
app.directive('myDirective', function($compile){
return{
restrict: 'A',
scope: {
varToBind: '=myDirective'
},
link: function(scope, elm, attrs){
outerList = '<ul><li>statically generated content that does not change'
outerList += '<ul><li ng-bind="varToBind.myvar"><li></ul>'
outerList += '</ul>';
outerList = $compile(outerList)(scope);
elm.replaceWith(outerList);
}
}
});
Here is a demo