ng-model doesn't work when I create new DOM elements - angularjs

When I create a new DOM element (out of angular), angular doesn't take it under control. Why?
Exemple:
<html ng-app="gemStore">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
</head>
<body ng-controller="StoreController as store">
<button onclick="changeDom();">CreateDomElementWithDataBinding</button>
<input type='text' ng-model='store.bright'/>
<div id='placeNewElement'>
</div>
<script type="text/javascript">
angular.module('gemStore',[]).controller('StoreController', function(){
this.name = 'diamont';
this.bright = 3;
});
var changeDom = function(){
document.getElementById('placeNewElement').innerHTML = "<input type='text' ng-model='store.name'/>";
};
</script>
</body>
</html>
Here, initial DOM is created with an input that have databinding with scope.bright data model.
When you click on the button, a new element is created, with ng-model to make a databinding with scope.name.
But when new DOM element is created, angular doesn't 'managed' its element (doesn't show scope.name value, doesn't put typical angular classes (ng-valid, etc) to the element).
I searched a lot and I can't solve it :-(
I tryed to make scope.apply() to upload model, but this appears not to be the problem (because angular doesn't know this new element).
Tnaks for help! ;-)
Jordi

Use the idiomatic way of managing DOM tree (that is, don't modify it from under angular)
<html ng-app="gemStore">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
</head>
<body ng-controller="StoreController as store">
<button ng-click="store.showNameField = true">CreateDomElementWithDataBinding</button>
<input type='text' ng-model='store.bright'/>
<input type='text' ng-model='store.name' ng-if='store.showNameField' />
<script type="text/javascript">
angular.module('gemStore',[]).controller('StoreController', function(){
this.name = 'diamont';
this.bright = 3;
this.showNameField = false;
});
</script>
</body>
</html>

I founded an answer by Kevin Shay at angularJs google group
The solution for the example code was:
<html ng-app="gemStore">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
</head>
<body ng-controller="StoreController as store">
<button ng-click="recognizeNewElements();" onclick="changeDom();">CreateDomElementWithDataBinding</button>
<input type='text' ng-model='store.bright'/>
<div id='placeNewElement'>
</div>
<script type="text/javascript">
angular.module('gemStore',[]).controller('StoreController', function($scope, $compile){
this.name = 'diamont';
this.bright = 3;
$scope.recognizeNewElements = function(){
var placeNewElement = angular.element(document.getElementById('placeNewElement'));
$compile(placeNewElement.contents())($scope);
};
});
var changeDom = function(){
document.getElementById('placeNewElement').innerHTML = "<input type='text' ng-model='store.name'/>";
};
</script>
</body>
</html>
Thanks a lot for all you that waste time to help me!

Your approach towards handling new element is wrong. You can achieve this using angular directives and sharing the scope of the same angular controller with the directive. That would update the DOM at run time.
Please have a look at this fiddle example. In this I have added a textbox and dynamically added a new text box which shares the same scope as what was there for first text box and its coming under the scope of angular.
<section ng-app="myApp" ng-controller="MainCtrl">
<addbuttonsbutton></addbuttonsbutton>
<input type='text' ng-model='store'/>
<div id="space-for-buttons"></section>
</section>
var myApp = angular.module('myApp', []);
function MainCtrl($scope) {
$scope.count = 0;
$scope.store = "";
}
//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{
link: function(scope, element, attrs){
element.bind("click", function(){
angular.element(document.getElementById('space-for-buttons')).append($compile("<input type='text' ng-model='store' ng-init='scope.store'/>")(scope));
});
}
};
});
Hope this helps.

Thanks Lalit.
I paste your code with some changes for working (may be it will be useful for somebody searching this soluction).
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
</head>
<body>
<section ng-app="myApp" ng-controller="MainCtrl">
<addbuttonsbutton></addbuttonsbutton>
<input type='text' ng-model='store'/>
<div id="space-for-buttons"></div>
</section>
<script type="text/javascript">
var myApp = angular.module('myApp', []).controller('MainCtrl', function($scope) {
$scope.store = "something";
});
//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{
link: function(scope, element, attrs){
element.bind("click", function(){
angular.element(document.getElementById('space-for-buttons')).append($compile("<input type='text' ng-model='store'/>")(scope));
});
}
};
});
</script>
</body>
</html>
For me it was not :-(, because I'm looking for something to use witout changing how new DOM elements are created (may be it's not possible :-S ).

Related

angularjs - access transclude html scope from hosting directive

I have a simple directive with transcluded html.
I want to be able to inject directive scope params to the transclude.
I wrote a simple example in plunker :
https://plnkr.co/edit/jqyiQdgQxbeTrzyidZYF?p=preview
I know in angular 4 it can be done, but I can't find a good way to do it in angularjs.
// Code goes here
var app = angular.module("app", []);
app.controller("mainCtrl", function($scope) {
$scope.users = ["tal", "oren", "orel", "shluki"];
$scope.deleteUser = (user) => {alert("trying to delete", user);}
});
app.directive('myList', function myList() {
return {
restrict: 'E',
transclude: true,
template: "<div><table><tr ng-repeat='item in collection'><td> This is inside myList - user name: {{item}} <ng-transclude></ng-transclude></td></tr></table></div>",
scope: {
collection: "="
},
replace: true
};
});
<!DOCTYPE html>
<html>
<head>
<script data-require="angularjs#1.6.2" data-semver="1.6.2" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="app" ng-controller="mainCtrl">
<h1>Hello Plunker!</h1>
<my-list collection="users">
<h2>This is transclude</h2>
<button ng-click="deleteUser(user)">Delete user: {{user ? user : "User name should be here"}}</button>
</my-list>
</body>
</html>
Will really appreicate some help.
plunker: https://plnkr.co/edit/jqyiQdgQxbeTrzyidZYF?p=preview
Here's a working plunker with your example.
http://plnkr.co/edit/BjSowyQdLXd0xoCZFqZ6?p=preview
The idea is to pass it as contents and not html as string. $compile is here because the link is done after ng-repeats already has transcluded its own template.
var template = '<h1>I am foo</h1>\
<div ng-repeat="item in users">\
<placeholder></placeholder>\
<hr>\
</div>';
var templateEl = angular.element(template);
transclude(scope, function(clonedContent) {
templateEl.find("placeholder").replaceWith(clonedContent);
$compile(templateEl)(scope, function(clonedTemplate) {
element.append(clonedTemplate);
});
});
If you want a proper explanation of what the problem was you should check the detailed answer here : Pass data to transcluded element
Hope this helped you out

Angular JS. What the order of actions in HTML when creating a directive?

I have just a problem rightly ask a question).
I created angularJS app 'myApp'. And app get list of two groups. And app should to represent on browser two checkboxes: first - checked, second - empty.
But maybe I didn't count the order of action of building of angular's directives.
This is example
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="MainController">
<div ng-repeat="group in groups">
<mygroups info='group'></mygroups>
</div>
</div>
<script type="text/javascript">
var groups = [
{
"id":"769", "spam_status":"checked"},
{
"id":"1262", "spam_status":""}
];
var app = angular.module("myApp", []);
app.controller("MainController", function($scope){
$scope.groups = groups;
});
app.directive("mygroups", function(){
return{
scope: {info: "="},
template: "<input type='checkbox' id='{{group.id}}'> {{info.id}}",
//WORK FOR FIRST CHECKBOX IF TYPE: //BUT IT IS NO SENSE//
// template: "<input type='checkbox' id='769'> {{info.id}}",
controller: function(){
for (var i = 0; i < groups.length; i++){
$('#' + groups[i].id).attr(groups[i].spam_status,'');
}
}
};
});
</script>
</body>
</html>
I get a right 'id' of element on front-end when I inspected code on browser. If I put on template number '769' instead {{info.id}}, the first checkbox will checked.
But I think that controller of directive acts before assignment 'id' of elements.
But I need realize this by that method. Because I try to explain on scratches the situation which I don't understand.
Thanks a lot.
Two issues :
First point
Your template is :
<input type='checkbox' id='{{group.id}}'> {{info.id}}
It should be :
<input type='checkbox' id='{{info.id}}'> {{info.id}}
Second point
You must call your dom modification in a $timeout function. On the first execution of your controller, your html does not exists yet.
$timeout will trigger the code at the next digest. So, your html will exists.
Of course, do not modify your html in your controller (even in a directive). The link function is the place to modify the html.
Corrected snippet
Here is the snippet with the correction. There is a jquery error, I didn't watch what it is. Do not hesitate to edit to throw it away.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="MainController">
<div ng-repeat="group in groups">
<mygroups info='group'></mygroups>
</div>
</div>
<script type="text/javascript">
var groups = [
{
"id":"769", "spam_status":"checked"},
{
"id":"1262", "spam_status":""}
];
var app = angular.module("myApp", []);
app.controller("MainController", function($scope){
$scope.groups = groups;
});
app.directive("mygroups", function(){
return{
scope: {info: "="},
template: "<input type='checkbox' id='{{info.id}}'> {{info.id}}",
//WORK FOR FIRST CHECKBOX IF TYPE: //BUT IT IS NO SENSE//
// template: "<input type='checkbox' id='769'> {{info.id}}",
controller: function($timeout){
$timeout(function() {
for (var i = 0; i < groups.length; i++){
$('#' + groups[i].id).attr(groups[i].spam_status,'');
}
});
}
};
});
</script>
</body>
</html>
Looks like you want to hook the checkbox up
template: "<input type='checkbox' ng-model='info.spam_status' ng-true-value='\'checked\'' id='{{group.id}}'> {{info.id}}"

AngularJS How to add / remove directive dynamically in page when button click

suppose i have two button and when we click on button 1 then show function call in controller and same way when we click on button 2 then hide function call in controller.
from show function how could i dynamically load and add directive in page and as well as how could i hide or remove directive from page when hide function will be called.
hence i am new in angular so not sure the below code will add directive at run time if i call it from show function ?
$('body').append($compile("<my-angular-directive />")(scope));
scope.$apply();
i do not know how to remove directive from page if it exist in page from controller function. give me suggestion how to achieve this. thanks
you can use directive as class and you can load it with ng-class directive
var jimApp = angular.module("mainApp", []);
jimApp.controller('mainCtrl', function($scope){
$scope.showMyDir = true;
$scope.buttonClcik = function(){
$scope.showMyDir = !$scope.showMyDir;
};
});
jimApp.directive("customDir1", function() {
return {
restrict:"AEC",
scope:{
value:"="
},
link: function(scope, element, attrs) {
}
}
});
jimApp.directive("customDir2", function() {
return {
restrict:"C",
link: function(scope, element, attrs) {
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
<div ng-app="mainApp" ng-controller="mainCtrl">
<button ng-click="buttonClcik();">Click</button>
<div ng-class="{'custom-dir1': showMyDir, 'custom-dir2': !showMyDir}" value="showMyDir">Hai</div>
</div>
Use ng-if. So basically if you have a scope variable let's say $scope.showElement you can then use it like this:
In your controller:
$scope.showElement = true;
HTML:
<this-directive ng-if="showElement"></this-directive>
Originally the directive will show up but when you change the value of $scope.showElement it will be removed.
EDIT based on your comment:
Set your $scope.showElement to false and on ng-click set it to true like this:
angular.module('myApp', [])
.controller('testController', function($scope) {
$scope.showElement = false;
$scope.toggleElement = function() {
console.log('toggle element');
$scope.showElement = $scope.showElement ? false : true;
};
});
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
</head>
<body ng-controller="testController">
<button type="button" ng-click="toggleElement()">Show directive</button>
<div ng-if="showElement" style="width:100%;height:40px;background-color:red;">This element will be shown/removed</div>
</body>
</html>
EDIT 2:
You can also use css declarations for better performance if you don't mind that the element still stays on the page but is hidden. With css it would go like this:
angular.module('myApp', [])
.controller('testController', function($scope) {
$scope.showElement = false;
$scope.toggleElement = function() {
console.log('toggle element');
$scope.showElement = $scope.showElement ? false : true;
};
});
.hidden {
display: none;
}
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
</head>
<body ng-controller="testController">
<button type="button" ng-click="toggleElement()">Show directive</button>
<div ng-class="{'hidden' : showElement === false}" style="width:100%;height:40px;background-color:red;">This element will be shown/removed</div>
</body>
</html>
You could easily use ng-if directive on the wrapper of your custom directive
<div ng-if="showMyDir()">
<my-angular-directive />
</div>
<div ng-if="showMyDirTwo()">
<my-angular-directive-two />
</div>

Angularjs 1.3.15 ng-model in directive not binding on IE but works on Chrome

I am using angular 1.3.15 version with my own directive to create form elements. The model values get updated when i run this on chrome and firefox but ie10 the forms values dont get updated on the model.
The same code works on an older version of Angular. I tried with 1.2.1 and it works as expected.
Angular 1.3.15 Version of the code here http://goo.gl/Qzk5iA
Angular 1.2.1 Version of the code here http://goo.gl/WYE6pG
var myAppModule = angular.module('myApp', []);
myAppModule.controller("TodoCtrl",function($scope) {
$scope.tree = [];
$scope.nodeMap = [];
$scope.items = [{name:"Sudha",group:"",visibility:"visible",value:{value:"as"}},{name:"Gautam",group:"",visibility:"visible",value:{value:"as"}}];
$scope.foo = function()
{
for(var i=0;i<$scope.items.length;i++)
{
var item = $scope.items[i];
console.log(item.name+" "+item.value.value);
}
}
}).directive("ppField",["$compile", function($compile) {
return {
replace:true,
require:'ngModel',
scope:{
parameter :'=parameter',
ngModel : '='
},
restrict: 'AE',
link : function($scope, $element, $attrs) {
var field = '<input id=\''+$scope.parameter.name+'\' name=\''+$scope.parameter.name+'\' type="text" ng-model="parameter.value.value" class="form-control input-sm">';
var newInput = angular.element(field);
var $e =$compile(newInput)($scope);
$element.append($e);
}
};
}]);
The html is a simple ng-repeat with the directive inside of it. On clicking the done button the model values are printed to the console. Chrome works as expected but IE shows the initial values.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-app="myApp">
<div ng-controller="TodoCtrl">
<button ng-click="foo()">Done</button>
<div data-drag-enabled="false">
<div ng-repeat="parameter in items">
<label for="{{parameter.name}}" class="col-sm-4 control-label labelBreak" data-ng-click="showTooltip(node)">
{{parameter.name}}
</label>
<div class="col-sm-8 fieldDiv" ng-model="fieldCount">
<pp-field ng-model="parameter.value" parameter="parameter" disabled="disabled"></pp-field>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
remove the disabled="Disabled" from the html, IE disables all child elements if the parent is disabled, that is probably why the binding does not work. Chrome does not disable like IE.
Try this

AngularJS - Create a directive that adds a sibling element

I'm creating a my-validate directive that looks something like this
<input my-validate="customValidation" ng-model="model" />
What I want to do is to attach a sybling element to the directive like this
Error template:
<ul class"errors">
<li ng-repeat="for error in errors">{{error}} not valid</li>
</ul>
errors is defined in the scope of the directive.
I've added the error template in the compile function, but the problem I have is that the scope in the link function is not the same as the attached template.
Here is a plunker to illustrate the issue: http://plnkr.co/edit/ghdtdYruQaaO0Yxxlrt1?p=preview
'world' is seen in the directive template, but not on the added element :S.
That's because your div "2 hello" is outside the container where your scope is visible.
you can use element.append() instead of element.after() to have the scope available.
Directive
var app = angular.module('plunker', []);
app.directive('myValidate', function($compile) {
return {
template: '<span>1. Hello {{world}} my scope is {{$id}} (parent: {{$parent.$id}})<span/>',
replace: true,
restrict: 'A',
scope: true,
compile: function (element) {
element.append('<div>2. Hello {{ world }}, my scope is {{$id}} (parent: {{$parent.$id}})</div>');
return function(scope) {
scope.world = 'World';
//$compile()(scope);
};
}
};
});
HTML
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script data-require="angular.js#1.1.5" data-semver="1.1.5" src="http://code.angularjs.org/1.1.5/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="app.js"></script>
</head>
<body>
<input my-validate="" />
</body>
</html>
http://plnkr.co/edit/dU3holBCePKe0ZAwQKh1?p=preview
I was reading and checking the example because I was in the same situation to display validation messages but under the input field and the message can change according to what kind of validation is required.
So I came up with this solution
var app = angular.module('app', []);
app.controller('ctrl', function($scope, CONSTANTS) {
$scope.title = "title";
$scope.CONSTANTS = CONSTANTS;
});
app.constant('CONSTANTS', {
LENGHT_1: 3,
LENGHT_2: 4
});
app.directive('dir', function($compile) {
return {
scope: true,
restrict: 'A',
require: '?ngModel',
link: function(scope, elem, attrs, ngModel) {
scope.maxLength = false;
scope.required = false;
scope.max = scope.$eval(attrs['ngMaxlength']);
var tpl = '<div ng-if="maxLength" ng-include="\'length.tpl.html\'"></div>' +
'<div ng-if="required" ng-include="\'required.tpl.html\'"></div>';
var el = $compile(tpl)(scope);
elem.after(el);
scope.$watch(attrs['ngModel'], function(newValue, oldValue, scope) {
if (ngModel.$error !== null && ngModel.$error.maxlength) {
scope.maxLength = true;
} else {
scope.maxLength = false;
}
if (ngModel.$error !== null && ngModel.$error.required && ngModel.$dirty) {
scope.required = true;
} else {
scope.required = false;
}
});
}
}
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
<script type="text/ng-template" id="length.tpl.html">
max length {{max}}
</script>
<script type="text/ng-template" id="required.tpl.html">
required
</script>
</head>
<body ng-controller="ctrl">
<h1>Input Validation</h1> {{title}}
<br><br>
<form name="form" novalidate>
<input dir name="input_one" ng-model="bar" ng-maxlength="CONSTANTS.LENGHT_1" required>
<br>
input one: {{form.input_one.$error}}
<br>
<br>
<input dir name="input_two" ng-model="foo" ng-maxlength="CONSTANTS.LENGHT_2">
</form>
<br>
input two: {{form.input_two.$error}}
</body>
</html>
On Plunkr
Hope it helps.
I think you're on the right track by using the form errors to toggle display. That's exactly how it's recommended in the standard Angular documentation.
If you'd like to show multiple errors for a single input, however, and possible even control the error messages from there, I'd recommend utilizing a service, such as implemented at http://plnkr.co/edit/iNcNs2ErrOnYf9I7whdu?p=preview.
Right now you can have one message per token, but as many tokens as you want per input. If you want multiple messages per token, just use an array of messages instead of single string value (note, unset does become more complicated with that method).
Hope that helps,
Alex

Resources