angularjs directive doesn't update when scope value update - angularjs

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<script type="text/javascript">
(function(){
'use strict';
angular.module('myApp',[])
.controller('TestCtrl',TestCtrl)
.directive('trxTablePersonTd',trxTablePersonTd);
function TestCtrl($scope){
var vm = $scope.vm = this;
vm.trxs = [
{id:"1",acctKey:"2",persons:[{name:'peter',age:20},{name:'hank',age:23}]},
{id:"2",acctKey:"3",persons:[{name:'Joe'},{name:'Jason'}]}
];
$scope.changePerson = function(){
vm.trxs[1]['persons']['age'] = 33;
vm.trxs[1]['acctKey'] = 123;
}
}
function trxTablePersonTd($compile){
return{
scope:{persons:'=persons'},
restrict:'A',
link:link,
replace:false,
//compile:compile
}
function compile(elem,attrs){
return function(scope){
var html = [];
scope.persons.map(function(person,index){
html.push('<td>'+person.name+'</td>');
html.push('<td>'+person.age+'</td>');
});
}
}
function link(scope, elem, attrs){
var html = [];
if(scope.persons){
scope.persons.map(function(person,index){
html.push('<td>'+person.name+'</td>');
html.push('<td>'+person.age+'</td>');
});
}
elem.replaceWith(html.join(''));
$compile(elem.contents())(scope);
}
}
}());
</script>
</head>
<body ng-app="myApp" ng-controller="TestCtrl">
<button type="button" name="button" ng-click="changePerson()">change person</button>
<table border="1">
<tbody>
<tr ng-repeat="trx in vm.trxs">
<td>
{{trx.id}}
</td>
<td>
{{trx.acctKey}}
</td>
<td trx-table-person-td persons="trx.persons">
</td>
</tr>
</tbody>
</table>
</body>
</html>
Blockquote
when I click the button the undefined age doesn't get update. can someone help me look at this problem
when I click the button the undefined age doesn't get update. can someone help me look at this problem

You didn't properly update your collection in $scope.changePerson method. persons is also an array, it should like
$scope.changePerson = function () {
vm.trxs[0]['persons'][0]['age'] = 33; //look ['persons'][0]
vm.trxs[0]['acctKey'] = 123;
}
As #Ed Staub suggest you need to $watch your collection to propagate further model change like
app.directive('trxTablePersonTd', function ($compile) {
return {
scope: { persons: '=persons' },
restrict: 'A',
link: link,
replace: false,
}
function link(scope, elem, attrs) {
if (scope.persons) {
scope.$watch('persons', function () {
var html = scope.persons.map(function (person, index) {
return '<td>' + person.name + '</td>' + '<td>' + (person.age ? person.age : "") + '</td>';
});
elem.empty().append(html.join(''));
$compile(elem.contents())(scope);
}, true);
}
}
});
Also if you don't have additional html except td in your directive, i think you don't need directive at all, just use ng-repeat like
<td ng-repeat="person in trx.persons">
<span>{{person.name}}</span>
<span>{{person.age}}</span>
</td>

I've been waiting to see if someone would answer this better and more authoritatively than I can. I believe the problem is because scope updates are not watched as deeply into data structures as your changes are. I think you need to implement a deep watch ("watch by value") on vm.trxs. See https://docs.angularjs.org/guide/scope, scroll down to section on "Controllers and Scopes".

Related

Enable Button After Page Load in AngularJS Directive

I am trying to enable a button after page load in an AngularJS directive. I applied ng-disabled for all my buttons DURING load and I would like to keep certain buttons disabled AFTER load.
I need some direction/advice on:
manipulating the DOM: from ng-disabled="!newAnimal.isDisabled" to ng-disabled="newAnimal.isDisabled"
I appreciate the help. Thanks.
HTML:
<a href="#/{{animal.id}}">
<button class="btn btn-success" ng-disabled="!newAnimal.isDisabled" id="add-animal" loading-animals>
Add Animal
</button>
</a>
FACTORY:
var animalFactory = angular.module('app.myFactory',[])
animalFactory.factory('newAnimal',function(){
var newAnimal = function(){
this.animal = "";
this.totalAnimals = 0;
this.totalAdopted = 0;
this.isDisabled = false;
};
return newAnimal
});
CONTROLLER (Modal):
.controller('InformationCtrl', function($scope, $modalInstance, $http) {
$scope.ok = function(){
//check if button successfully clicked
$modalInstance.dismiss('success');
//the code below was from a directive ('LoadingAnimals') that I was working on
//check if all data has been loaded from backend
var loadingPage = function(){
return $http.pendingRequests.length > 0;
//when all objects are loaded, manipulate DOM
//make ng-disabled = "!isDisabled" to "isDisabled"
element.attr("!newAnimal.isDisabled", "newAnimal.isDisabled);
}
loadingPage();
}
DIRECTIVE:
app.directive('loadingAnimals', ['$http', function($http) {
return {
restrict: 'A',
link: function (scope, element, attrs) {

var addButton = attrs.ngDisabled;
//console.log(element.attr('ng-disabled'));
scope.pageLoad = function() {
return $http.pendingRequests.length > 0;
};
scope.$watch(scope.pageLoad(), function (value) {
if (value) {
element.attr("ng-disabled", "newAnimal.isDisabled");
}
else {
element.removeAttr("ng-disabled");
}
})
}
}
}]);
UPDATE:
I updated my directive and it works, not the best way of achieving the results but it's one way.
(I would have preferred not to disable the button for 3 seconds but rather to listen to the $http request but since it's a workaround, I won't complain)
Thanks for all the answers. I'll update in the future if I figure out a more efficient way.
DIRECTIVE:
.directive('loadingAnimals', function() {
return {
restrict: 'A',
link: function (scope, element) {
var disableLink = (function() {
element.removeClass('disabled');
});
setTimeout(disableLink, 3000);
}
}
}
]);

Not sure if I'm correct but to do something after page is completely load, you can use angular.element(document).ready() (as you can see in this answer).
So you can have a <button> structured like this:
<button type="button" class="btn btn-primary" ng-disabled="!isDisabled || !animal.totalAnimals">Add animal</button>
See the example below:
(function() {
'use strict';
angular
.module('app', [])
.controller('MainCtrl', MainCtrl);
function MainCtrl($scope) {
var vm = this;
vm.animals = [
{
"name":"hamster",
"totalAnimals": 20,
"totalAdopted": 5,
},
{
"name":"turtle",
"totalAnimals": 0,
"totalAdopted": 0,
},
{
"name":"cat",
"totalAnimals": 9,
"totalAdopted": 6,
},
{
"name":"dog",
"totalAnimals": 7,
"totalAdopted": 2,
},
{
"name":"tiger",
"totalAnimals": 0,
"totalAdopted": 0,
}
];
vm.isDisabled = true;
angular.element(document).ready(function () {
console.log('completely load!');
vm.isDisabled = false;
});
}
})();
<!DOCTYPE HTML>
<html ng-app="app">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body ng-controller="MainCtrl as main">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>#of Animals Added</th>
<th>#of Animals Adopted</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="animal in main.animals track by $index">
<td ng-bind="animal.name"></td>
<td ng-bind="animal.totalAnimals"></td>
<td ng-bind="animal.totalAdopted"></td>
<td>
<button type="button" class="btn btn-primary" ng-disabled="!main.isDisabled || !animal.totalAnimals">Add animal</button>
</td>
</tr>
</tbody>
</table>
</body>
</html>
I hope it helps.
You can also use a directive to make changes post page load
<button id="submit" ng-click="test()">Submit</button>
<disable-button></disable-button>
This is how the directive will look like
.directive('disableButton', function () {
return {
restrict: 'E',
compile: function (el, attr) {
return {
pre: function (scope, el, attr) {
},
post: function (scope, el, attr) {
$('#submit').attr("disabled", true);
//add your logic here
}
}
}
}
})
Refer the demo

Generate directive content dynamically on ngclick

I have one table. in that i have declared my custom directive
<table ng-show="dataset.length" ng-table="tableParams" class="table">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="server in $data" ng-class-odd="'odd-row'" ng-class-even="'even-row'">
<td width="30" class="text-center">
<i class="ion-plus-round **toggle-icon**" group-row></i>
</td>
</tr>
</tbody>
</table>
While clicking on toggle-icon class
i need to generate one more tr data in next row.
custom directive is
app.directive('groupRow', function(){
return {
restrict: 'EA',
transclude: true,
controller: 'groupRowDirCtrl',
templateUrl: 'views/directives/templates/group-row.html',
link: function( scope, element, attrs, groupRowDirCtrl ) {
element.bind('click', function() {
$compile(el)(scope);
element.parent().parent().after(el);
});
}
};
})
.controller('scrollableTableviewDirCtrl',
function($scope) {
});
data have to fetch it from html page and append into next row.
How to do this?
if i understand you correctly, the on click function is in the directive.
so, i would add a service for the $data array that is being repeated, and on click add another item to that array by using the service.
like so
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, dataService) {
dataService.setData([1, 2, 3]);
$scope.data = dataService.getData();
});
app.directive('dir', function(dataService) {
return {
restrict: 'E',
replace: true,
template: '<tr><td ng-click="addMore()">One More</td></tr>',
link: function($scope, elem, attrs) {
$scope.addMore = function() {
dataService.addRow();
}
}
}
});
app.service('dataService', function() {
var _data = [];
var _service = {};
var _cb;
_service.getData = function() {
return _data;
}
_service.setData = function(data) {
_data = data;
}
_service.onUpdate = function(cb) {
_cb = cb;
}
_service.addRow = function( /* attibutes here */ ) {
_data.push({});
if (angular.isFunction(_cb)) _cb();
}
return _service;
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="myCtrl">
<table>
<tbody>
<dir ng-repeat="d in data"></dir>
</tbody>
</table>
</div>
</div>

Angularjs, set focus on an input in a table

I have a table with input in each cell, I can add records to that table by clicking on a button.
I would like to set focus on the first input of the last record created.
I don't know if that's possible.
If anyone can help me on this...
function Ctrl($scope) {
$scope.adresses = [];
$scope.add = function() {
var adr = {ville:null, codePostal:null}
$scope.adresses.push(adr);
};
}
<!doctype html>
<html ng-app>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="Ctrl" >
<button ng-click="add()"> Add </button>
<table>
<tr>
<th>ville</th>
<th>adresse</th>
<tr>
<tr ng-repeat="adresse in adresses">
<td>
<input ng-model="adresse.ville"/>
</td>
<td>
<input ng-model="adresse.codePostal"/>
</td>
<tr>
</table>
</div>
</body>
</html>
Try this approach.
Controller
controller('AppCtrl', function ($scope, $timeout, $element) {
$scope.adresses = [];
$scope.add = function() {
var adr = {ville:null, codePostal:null}
$scope.adresses.push(adr);
$timeout(function () {
$element[0].querySelector('[is-last=true]').focus();
})
};
});
Markup
<input ng-model="adresse.ville" is-last="{{$last}}"/>
Working Plnkr
Yes, very easily doable with a directive:
My example with your code: http://plnkr.co/edit/aDNdjjBKZHVfTXnLy2VZ?p=preview
// the directive I use
.directive('focusOnMe', ['$timeout', '$parse',
function($timeout, $parse) {
return {
//scope: true, // optionally create a child scope
link: function(scope, element, attrs) {
var model = $parse(attrs.focusOnMe);
scope.$watch(model, function(value) {
// console.log('value=',value);
if(value === true) {
$timeout(function() {
element[0].focus();
});
}
});
}
};
}
]);
The HTML: the condition for focus is a boolean value, here it is whether the element is last. So each time you add a new row, the condition is re-evaluated and focus is assigned.
<td>
<input ng-model="adresse.ville" focus-on-me="$last"/>
</td>

Angular accordion doesn't update UI when data source has been changed

I start learning Angular and faced with some strange behaviour.
I want to add a new header to accordion dynamically but I accordion doesn't reflect it on UI till I explicitly click on some of his items. By some reason he doesn't react on items changes before it starts load itserlf aggain durnig DOM rendering.
var mainApp = angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'ngResource']);
mainApp.factory('teamSharedObj',['$rootScope', function($rootScope) {
return {
teams: [],
peopleInTeam: [],
addNewTeam: function(item) {
console.log("add new team: " + item);
this.teams.push(item);
$rootScope.$broadcast('team.new');
},
addTeamMembers: function(team, teamMembers) {
for (var i = 0; i < teamMembers.length; i++) {
var temp;
// put in team as key-value pair
temp[team] = teamMembers[i]
console.log("add new team member: " + temp);
peopleInTeam.push(temp);
}
if (teamMembers.length != 0) {
$rootScope.$broadcast('teamMember.new');
}
}
}
}]);
mainApp.directive("addNewTeam", ['teamSharedObj', 'teamSharedObj', function (teamSharedObj) {
return {
restrict: 'A',
link: function( scope, element, attrs ) {
element.bind('click', function() {
console.log(scope.teamName)
teamSharedObj.addNewTeam(scope.teamName)
});
}
}
}])
mainApp.controller('teamListCtrl', ['$scope', 'teamSharedObj', function($scope, teamSharedObj) {
$scope.$on('team.new', function(event) {
console.log('new team ' + event);
$scope.items = teamSharedObj.teams;
}
);
$scope.oneAtATime = true;
$scope.items = ['new', 'another one'];//teamSharedObj.teams;
}]);
<!DOCTYPE html>
<html>
<head lang="en">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
<script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular-resource.js"></script>
<script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.2.js"></script>
<script src="js/test.team.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body ng-app="ui.bootstrap.demo">
<div id="teamBlock">
<input type="text" ng-model="teamName" >
<input add-new-team type="submit" value="Add new team" >
<!--<button add-book-button>Add data</button>-->
</div>
<div>
{{teamName}}
</div>
<div ng-controller="teamListCtrl">
<accordion close-others="oneAtATime" >
<accordion-group heading="{{d}}" ng-repeat="d in items">
This content is straight in the template.
</accordion-group>
</accordion>
<div ng-repeat="item in items">{{item}}</div>
</div>
</body>
</html>
Can you suggest me please a right way to notifu component about changes in its datasource?
bind is jqLite/jQuery method and does not automatically trigger the digest loop for you. This means no dirty checking will take place and the UI will not be updated to reflect the model changes.
To trigger it manually wrap the code in a call to $apply:
element.bind('click', function() {
scope.$apply(function () {
teamSharedObj.addNewTeam(scope.teamName);
});
});
And since teamSharedObj contains a reference to the array the controller can reference it directly. Then you do not need to use $broadcast:
addNewTeam: function(item) {
this.teams.push(item);
},
And:
mainApp.controller('teamListCtrl', ['$scope', 'teamSharedObj',
function($scope, teamSharedObj) {
$scope.oneAtATime = true;
$scope.items = teamSharedObj.teams;
}
]);
Demo: http://plnkr.co/edit/ZzZN7wlT10MD0rneYUBM?p=preview

Bind Angularjs to newly created html element dynamically

I have a tab page with multiple tabs that once clicked on call a service to return some data. Some of that data returns html forms and are very random. I want to collect those values that are entered and send the data via a service back to the server. The problem I have is that I can't get the data from the input elements in the html I'm creating dynamically.
I've created a Plunker to show what the issue is. Note that the html value can change at any time so hard-coding the html won't work. Here the code from the plunker, but please look at the plunker for the best view of whats going on.
app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $sce, $compile) {
$scope.name = 'World';
$scope.html = "";
$scope.htmlElement = function(){
var html = "<input type='text' ng-model='html'></input>";
return $sce.trustAsHtml(html);
}
});
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.3/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<div ng-bind-html="htmlElement()"></div>
{{html}}
</body>
</html>
One solution would be to use ngInclude with $templateCache, as demonstrated in this Plunker.
There are a couple things to note.
The first is that you can fetch your template using a service and add it to the $templateCache, as described here (example copied):
myApp.service('myTemplateService', ['$http', '$templateCache', function ($http, $templateCache) {
$http(/* ... */).then(function (result) {
$templateCache.put('my-dynamic-template', result);
});
}]);
Then you can include it in your template as follows:
<div ng-include="'my-dynamic-template'"></div>
ngInclude will allow databinding on the html string, so you don't need ngBindHtml.
The second is that, as ngInclude creates a new scope, accessing the html property outside of the newly created scope won't work properly unless you access it via an object on the parent scope (e.g. ng-model="data.html" instead of ng-model="html". Notice that the $scope.data = {} in the parent scope is what makes the html accessible outside of the ngInclude scope in this case.
(See this answer for more on why you should always use a dot in your ngModels.)
Edit
As you pointed out, the ngInclude option is much less useful when using a service to return the HTML.
Here's the edited plunker with a directive-based solution that uses $compile, as in David's comment above.
The relevant addition:
app.directive('customHtml', function($compile, $http){
return {
link: function(scope, element, attrs) {
$http.get('template.html').then(function (result) {
element.replaceWith($compile(result.data)(scope));
});
}
}
})
Based on Sarah's answer, i created a structure to put the directive
.directive('dynamic', function(AmazonService, $compile) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
AmazonService.getHTML()
.then(function(result){
element.replaceWith($compile(result.data)(scope));
})
.catch(function(error){
console.log(error);
});
}
};
});
And in the html:
<dynamic></dynamic>
Thanks Sarah, helped a lot!!!
I have a dynamic table with some ng-repeat's, then when I tried to fill one column with javascript callback function, it give me just in html text like
<td class="tableList_"+myValue> "span class=someclass> some_text /span>" </td>
<td class="tableList_"+myValue> "span class=someclass> some_text /span>" </td>
<td class="tableList_"+myValue> "span class=someclass> some_text /span>" </td>
So I resolved my problem with jquery:
$(".tableListFilas td").each(function() {
var td_class = $(this).attr("class");
if(td_class == 'tableList_'+titulo)
{
var toExtraHtml = $(this).text();
$(this).html(toExtraHtml);
}
});
then the final output was good:
<td class="tableList_COLORS"> <span class=someclass>some_text</span> </td>
<td class="tableList_COLORS"> <span class=someclass>some_text</span> </td>
<td class="tableList_COLORS"> <span class=someclass>some_text</span> </td>

Resources