Nested ng-repeat data not correctly binding to directive - javascript

[Edited to reflect comments in #shaunhusain answer]
I have a nested ng-repeat construct for which I want to instantiate a new instance of a directive for every inner item, passing to that directive the data from that inner item. In the code below, I've included two nested loops. The one that works uses one-way binding via the {{ }} notation, and the other appears to work as well...until you hit the Refresh button. Even though $scope.frames changes (and the {{ }} binding continues to reflect the changes), the binding for the directive is not triggered.
What am I missing?
var myApp = angular.module('myApp', []);
myApp.directive("myDirective", function () {
return {
restrict: 'E',
scope: {
boundData: '=data'
},
link: function (scope, element) {
angular.element(element.find("div")[0])
.html('')
.append("<p>" + scope.boundData.val + "</p>");
}
}
});
myApp.controller('FooCtrl', ['$scope', function ($scope) {
$scope.clear = function () {
$(".item-list").empty();
};
$scope.getData = function () {
var frames = [];
for (var i = 0; i < 2; i++) {
var items = [];
for (var j = 0; j < 4; j++) {
items.push({ val : Math.floor(Math.random() * 5000) });
};
frames.push(items);
}
$scope.frames = frames;
};
$scope.getData();
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div ng-app="myApp">
<div ng-controller="FooCtrl">
<div>
<button ng-click="getData()">Refresh</button>
<button ng-click="clear()">Clear</button>
</div>
<div style="float: left">
<b>{} binding</b>
<div ng-repeat="frame in frames track by $index">
<div ng-repeat="item in frame track by $index">
{{item.val}}
</div>
</div>
</div>
<div style="margin-left: 120px">
<b>Using directive</b>
<div ng-repeat="frame in frames track by $index">
<div ng-repeat="item in frame track by $index">
<my-directive data="item">
<div class="item-list"></div>
</my-directive>
</div>
</div>
</div>
</div>
</div>

"Thinking in AngularJS" if I have a jQuery background?
var myApp = angular.module('myApp', []);
myApp.directive("myDirective", function () {
return {
restrict: 'E',
scope: {
boundData: '=data'
},
link: function (scope, element) {
angular.element(element.find("div")[0])
.html('')
.append("<p>" + scope.boundData.val + "</p>");
}
}
});
myApp.controller('FooCtrl', ['$scope', function ($scope) {
$scope.clear = function () {
$(".item-list").empty();
};
$scope.getData = function () {
var frames = [];
for (var i = 0; i < 2; i++) {
var items = [];
for (var j = 0; j < 4; j++) {
items.push({ val : Math.floor(Math.random() * 5000) });
};
frames.push(items);
}
$scope.frames = frames;
};
$scope.getData();
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.js"></script>
<div ng-app="myApp">
<div ng-controller="FooCtrl">
<div>
<button ng-click="getData()">Refresh</button>
<button ng-click="clear()">Clear</button>
</div>
<div style="float: left">
<b>{} binding</b>
<div ng-repeat="frame in frames track by $index">
<div ng-repeat="item in frame track by $index">
{{item.val}}
</div>
</div>
</div>
<div style="margin-left: 120px">
<b>Using directive</b>
<div ng-repeat="frame in frames track by $index">
<div ng-repeat="item in frame track by $index">
<my-directive data="item">
<div class="item-list"></div>
</my-directive>
</div>
</div>
</div>
</div>
</div>
Believe your problem is because of using a jquery selector without a specified parent being element. Typically you won't need jQuery when using Angular, good to drop it initially, search on SO for how to think in AngularJS with a jQuery background for a good write up. Will add a sample here shortly.

The flaw here was the use of track by $index in my ng-repeat (specifically the inner one).

Related

function inside ngRepeat executed too often

I have three tabs that has different html inside ng-include. These tabs are shown using ng-repeat. Only one HTML template contains function call, but it's executed 3 times (once per ng-repeat iteration). What is wrong here and how to fix it?
var app = angular.module('myApp', [])
app.controller('myCtrl', [
'$scope',
function($scope){
$scope.randomFnc = function (i) {
console.log(i);
return "Placeholder text";
}
$scope.tabs = [
"a",
"b",
"c"
];
}
])
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="myCtrl">
<div ng-repeat="tab in tabs">
<div ng-if="$index == 1">
{{$index}}<input type="text" value="" placeholder="{{randomFnc($index)}}"/>
</div>
<div ng-if="$index != 1">{{$index}}</div>
</div>
</div>
</div>
You can use ng-init though it is not highly recommended to achieve this. The reason why your function call is being executed thrice is because angular doesn't know if any $scope value has changed during each digest cycle. So the function will get executed for each digest cycles. In your case, it will get executed when the ng-if conditions become true as well as during the two digest cycles accounting a total of three. This is the reason why it gets executed 3 times with the value 1 regardless of the number of items in the array.
var app = angular.module('myApp', [])
app.controller('myCtrl', [
'$scope',
function($scope) {
$scope.x = {};
$scope.randomFnc = function() {
console.log("once");
$scope.placeholderText = "Placeholder text";
}
$scope.tabs = [
"a",
"b",
"c"
];
}
])
app.directive('trackDigests', function trackDigests($rootScope) {
function link($scope, $element, $attrs) {
var count = 0;
function countDigests() {
count++;
$element[0].innerHTML = '$digests: ' + count;
}
$rootScope.$watch(countDigests);
}
return {
restrict: 'EA',
link: link
};
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="myCtrl">
<div ng-repeat="tab in tabs">
<div ng-if="$index == 1" ng-init="randomFnc()">
{{$index}}<input type="text" value="" placeholder="{{placeholderText}}" />
</div>
<div ng-if="$index != 1">{{$index}}</div>
</div>
</div>
<track-digests></track-digests>
</div>
Call a method for 3 times because placeholder attribute or other attributes like this as class or ... can't define the ng- in angularjs, for solution we can use ng-init to handle it.
when you run the first you have repeat and then binding elements and then your angular attributes runs.
for best solution i refer to use model as object to binding placeholder on it, it's easily.
var app = angular.module('myApp', [])
app.controller('myCtrl', [
'$scope',
function($scope){
$scope.placeholder = "";
$scope.randomFnc = function (tab) {
$scope.placeholder = "Placeholder text";
}
$scope.tabs = [
"a",
"b",
"c"
];
//----2
$scope.randomFnc2 = function (tab) {
tab.placeholder = "Placeholder text";
}
$scope.tabs2 = [
{name: "a"},
{name: "b"},
{name: "c"},
];
}
])
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="myCtrl">
<h1>better if you use model</h1>
<div ng-repeat="tab in tabs2">
<div ng-if="$index == 1" ng-init="randomFnc2(tab)">
{{$index}}
<input type="text" value="" placeholder="{{tab.placeholder}}"/>
</div>
<div ng-if="$index != 1">{{$index}}</div>
</div>
<h1>also you can</h1>
<div ng-repeat="tab in tabs">
<div ng-if="$index == 1" ng-init="randomFnc(tab)">
{{$index}}
<input type="text" value="" placeholder="{{placeholder}}"/>
</div>
<div ng-if="$index != 1">{{$index}}</div>
</div>
</div>
</div>

AngularJS ng-repeat and controllerAs

I using simple syntax to show, hide element in ng-repeat
<div ng-controller='MainController as vm'>
<ul>
<li ng-repeat="item in vm.items" ng-click="vm.showItem()">
{{item}}
<span ng-show="show">test</span>
</li>
</ul>
When i was using scope everythink worked fine
$scope.showItem = function(){
this.show=!this.show;
}
But same code with controllerAs doesn't work
vm = this;
vm.showItem = function(){
this.show=!this.show;
}
How can i access to show property of current item in ng-repeat?
Controller as http://plnkr.co/edit/Dbp5fO9OEpV6lFRySYUK?p=preview
$scope http://plnkr.co/edit/ptuySNRXSrA64K1IAng3?p=preview
Get the current this instance from the html like
<li ng-repeat="item in vm.items" ng-click="vm.showItem(this)">
And use it in your controller
vm.showItem = function(_this){
_this.show=!_this.show;
}
The problem you faced is the change of this context
Updated plunker http://plnkr.co/edit/pkxI68sGMXonDOA49EUq?p=preview
Proof of concept:
var app = angular.module('myApp', []);
app.controller('MainController', function() {
vm = this;
vm.items = [];
for (var i = 0; i < 10; i++) vm.items.push({
value: i,
show: false,
showItem: function() {
this.show = !this.show
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MainController as vm">
<ul>
<li ng-repeat="item in vm.items" ng-click="item.showItem()">
{{item.value}}
<span ng-show="item.show">test</span>
</li>
</ul>
</div>
</div>
Than we are going to improve it by extracting showItem method to the Item prototype
Second step - using prototype to less memory consume
var app = angular.module('myApp', []);
app.controller('MainController', function() {
vm = this;
vm.items = [];
for (var i = 0; i < 10; i++) vm.items.push(new Item(i));
function Item(value) {
this.value = value
this.show = false
}
Item.prototype.showItem = function() {
this.show = !this.show
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MainController as vm">
<ul>
<li ng-repeat="item in vm.items" ng-click="item.showItem()">
{{item.value}}
<span ng-show="item.show">test</span>
</li>
</ul>
</div>
</div>
Next step will be extracting Item to angular factory
You need to use vm.show = !vm.show;
vm = this;
vm.showItem = function(){
vm.show=!vm.show;
}
Replace the function definition with.
vm.show = !vm.show;
I have attached the sample code snippet.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script data-require="angular.js#1.5.5" data-semver="1.5.5" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.js"></script>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<script>
var app = angular.module('myApp', []);
app.controller('MainController', function () {
vm = this;
vm.items = [];
vm.start = 0;
vm.end = 20;
for (var i = 0; i < 10; i++) vm.items.push(i);
// return vm;
vm = this;
vm.show = true;
vm.showItem = function () {
vm.show = !vm.show;
}
});
</script>
</head>
<body>
<div ng-app="myApp">
<div ng-controller='MainController as vm'>
<ul>
<li ng-repeat="item in vm.items" ng-click="vm.showItem()">
{{item}}
<span ng-show="vm.show">test</span>
</li>
</ul>
</div>
</div>
</body>
</html>

angular: calling/triigering directive method from parent scope

in my app I defined a directive which implement a simple slider with a next/prev/goto methods.
The slider then is within an html snipet managed by another controller.
The problem is that the last slide contains a form, so if the submit is ok than I would like to go to the next slide.
In old javascript I would have passed a callback to the submit method in order to apply that callback.
I made the same thing. Is this the best / angular style way to do it?
Javascript (I omitted some detail):
.directive("sfCarousel", function() {
return {
scope: true,
restrict: 'A',
controller: function($scope) {
var slides = $scope.slides = [];
var currentIndex = $scope.currentIndex = -1;
$scope.next = function() {
//mynextfunction...
}
},
link: function(scope, element, attrs) {
console.log("sf-carousel");
}
}
})
.directive("sfCarouselItem", function() {
return {
scope: true,
require: '^sfCarousel',
link: function(scope, element, attrs, sfCarouselController) {
console.log("sf-carousel-item");
sfCarouselController.addSlide(scope);
}
}
})
.controller("mycontroller", ['$scope', function($scope) {
$scope.submit = function (callback) {
//if submit is ok then
callback.apply(null, []);
}
}])
HTML:
<div sf-carousel >
<div sf-carousel-item ng-class="{'active':active}" >
<div>my first slide</div>
<div sf-label="get-started.submit" ng-click="next()" ></div>
</div>
<div sf-carousel-item ng-class="{'active':active}" >
<form>
<!--here my form-->
<button type="submit" ng-click="submit(next)">submit and go</button>
</form>
</div>
<div sf-carousel-item ng-class="{'active':active}" >
<div>my last slide</div>
<!--other things-->
</div>
</div>
You can pass the function that you what to run on the parent controller.
// directive sfCarousel
return {
transclude: true,
template: '<div class="sf-carousel" ng-transclude></div>',
// ...
};
// directive sfCarouselItem
return {
transclude: true,
scope: {
func: '&',
// ...
},
template: '<div class="sf-carousel-item" ng-transclude></div>',
}
// controller html
<div sf-carousel >
<div sf-carousel-item ng-class="{'active':active}" >
<div>my first slide</div>
<div sf-label="get-started.submit" ng-click="next()" ></div>
</div>
<div sf-carousel-item func="next()" ng-class="{'active':active}" >
<form>
<!--here my form-->
<button type="submit" ng-click="func({})">submit and go</button>
</form>
</div>
<div sf-carousel-item ng-class="{'active':active}" >
<div>my last slide</div>
<!--other things-->
</div>
</div>

angular JS and inline editing data from REST

i am very new to angular.
i am reading a JSONP response and showing it in a page. i would like to allow users to edit parts of the text inline. i found angular-xeditable. but i cant figure out
1. how do i use it in the repeater. i have spent several hours trying to use the inline edit. it works if its not in the repeater. otherwise breaks everything.
i have this:
in my app.js:
var app = angular.module('ListApp', ['ui.bootstrap']).config(['$httpProvider', function ($httpProvider)
{
$httpProvider.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];
}]);;
var app11 = angular.module("app11", ["xeditable"]);
app11.run(function (editableOptions) {
editableOptions.theme = 'bs3'; // bootstrap3 theme. Can be also 'bs2', 'default'
});
app11.controller('Ctrl', function ($scope) {
$scope.user = {
name: 'awesome user'
};
});
angular.element(document).ready(function () {
angular.bootstrap(document.getElementById('list-reminders'), ['ListApp']);
angular.bootstrap(document.getElementById('xedittxt'), ['app11']);
});
in my html:
<div id="list-reminders" class="container main-frame" ng-controller="remindersController" ng-init="init()">
<div class="search-box row-fluid form-inline">
<nobr>
<label>Find</label>
<input type="text" ng-model="searchText" value="{{searchText}}" /> <input type="submit" class="submit" ng-click="getRemindersByUserId()" value="Search">
</nobr>
</div>
<div class="results-top"></div>
<div class="results-container">
<div class="row-fluid">
<div class="span1">Title</div>
<div class="span2">My Notes</div>
</div>
<ul class="item-list" style="display:none;">
<li ng-repeat="(key,item) in lists">
<div class=" row-fluid">
<div id="xedittxt" ng-controller="Ctrl" class="span2">
</div>
<div class="span2">{{item.user_notes}} </div>
</div>
</li>
</ul>
in my controller.js
app.controller("remindersController", function ($scope, $modal, $http) {
$scope.items = [];
//api
$scope.getListApi = '....URL REMOVED';
var callback = 'callback=JSON_CALLBACK';
$scope.init = function () {
//items selected from results to add to list
$scope.selectedItems = [];
var $loading = $('.loading');
var $searchOptions = $('.search-options');
var $itemList = $('.item-list');
$scope.getRemindersByUserId = function (userId) {
$itemList.hide();
$loading.show();
var getListUrl = $scope.getListApi + "1&" + callback;
console.log(getListUrl);
$http.jsonp(getListUrl).success(function (data) {
$scope.lists = data;
$loading.hide();
$itemList.show();
}).error(function (err) {
console.log("getRemindersByUserId error: " + err);
});
};
};
});

set a class on a div with ng-class on pageload

As in the title, I'm trying to set a class on a div on page load ( not on a click ) with angular ng-class but I have no clue how to or if it is possible. The div is wrapped in a module and controller where I was previously trying to set the variable with $scope.frontpage but it didn't work.
<div id="main" role="main" class="clearfix" data-ng-module="ValidationBoxModule" ng-controller="ValidationBoxClassController">
<div validation-id="dataValidationSummary" validation-summary ng-class="frontpage" ></div>
<asp:ContentPlaceHolder ID="MainContentPlaceHolder" runat="server"></asp:ContentPlaceHolder>
</div>
angular.module("ValidationBoxModule", []).controller("ValidationBoxClassController", ["$scope", function ($scope) {
$scope.frontpage = "";
($("#main").find(".sliderBox")) ? $scope.frontpage = "frontpage" : $scope.frontpage = "";
}]);
So is there a way to do it ?
What you're trying to do is not the angular way. As in: "don't do DOM manipulation in the controller". Instead use a directive, e.g:
function Ctrl($scope) {
$scope.frontpage = false;
}
angular.module('app', ['app.directives']);
angular.module('app.directives', []).directive('isFrontpage', function () {
return {
restrict: 'A',
link: function (scope, element) {
scope.frontpage = angular.element(element).find('#main .sliderBox').length > 0;
}
};
});
with:
<body ng-controller="Ctrl" is-frontpage>
<div id="main">
<div class="sliderBox"></div>
</div>
<div
validation-id="dataValidationSummary"
validation-summary
ng-class="{frontpage: frontpage}">
</div>
</body>
demo: http://jsbin.com/unihon/4/

Categories