I am using the following directive for 'add tag' functionality in my application:
directives.addTag = function ($http) {
return {
link: function (scope, element, attrs) {
element.bind('keypress', function (event) {
if (event.keyCode == 13) { /*If enter key pressed*/
if (!scope.$parent.post) { //For KShare
var newTagId = "tagToNote";
}
else { //For KB
var newTagId = "tagToAddFor" + scope.post.meta.id;
}
var tagValue = element[0].value;
if (tagValue == "")
return;
if (!scope.$parent.post) {
scope.$parent.tags.push(tagValue);
scope.addTagButtonClicked = false;
}
else {
scope.post.tags.push(tagValue);
scope.addTagButtonClicked = false;
}
scope.$apply();
element[0].value = "";
}
});
}
}
}
This is the HTML code for rendering the tags:
<div class="tagAdditionSpan" ng-repeat="tag in post.tags" ng-mouseenter="hover = true" ng-mouseleave="hover = false">
<span>{{tag}}</span>
<span class="deleteIconSpan" ng-class="{deleteTagIcon: hover}" ng-click="$parent.deleteTag($index,$parent.$index);"></span>
</div>
I have a textbox to add tags when a user types the name of the tag in it and presses 'Enter' key. On page load, I am statically populating 1 tag into the 'tags' array.
I am even able to add tags using the tags and it is reflected in the view. However after adding 2 or 3 tags, it starts misbehaving and the view is no longer updated with the added tags.
I tried debugging this and found that it is being updated in the 'scope.post.tags' array but is not reflected in the view.
What am I doing wrong?
Based on the comments received, I was able to solve the issue. 'ng-repeat' used to break the loop on addition of duplicate tags and hence the view was not updated accordingly.
This fixed the issue(added 'track by' in ng-repeat):
<div class="tagAdditionSpan" ng-repeat="tag in post.tags track by $index" ng-mouseenter="hover = true" ng-mouseleave="hover = false">
<span>{{tag}}</span>
<span class="deleteIconSpan" ng-class="{deleteTagIcon: hover}" ng-click="$parent.deleteTag($index,$parent.$index);"></span>
</div>
Related
For reasons I won't go into, I'm not using ng-repeat for a list of items.
I have a list like this:
<p>Search: <input type="text" ng-model="search"></p>
<div id="grid" filter-list="search">
<div id="item1" class="[list of properties]">
//item content
</div>
<div id="item2" class="[list of properties]">
//item content
</div>
<div id="item3" class="[list of properties]">
//item content
</div>
<div id="item4" class="[list of properties]">
//item content
</div>
</div>
As you can see I already have a search function working well.
My app script looks like this:
<script>
var app = angular.module('myApp', []).controller('MainCtrl', function ($scope, $timeout) {
});
app.directive('filterList', function ($timeout) {
return {
link: function (scope, element, attrs) {
var div = Array.prototype.slice.call(element[0].children);
function filterBy(value) {
div.forEach(function (el) {
el.className = el.textContent.toLowerCase().indexOf(value.toLowerCase()) !== -1 ? '' : 'ng-hide';
});
}
scope.$watch(attrs.filterList, function (newVal, oldVal) {
if (newVal !== oldVal) {
filterBy(newVal);
}
});
}
};
});
</script>
The problem is I need to be able to reorder the list based on class values or even the ids (At this stage it doesn't matter).
Every tutorial/guide online assumes that the code uses "ng-repeat" ... which I simply can't use here.
Is there any way I can get the items to reorder without using ng-repeat?
Instead of using ng-repeat just sort the data.
$scope.items = $scope.items.sort(yourSortFn);
You can modify this sorting script:
<ul id="id01">
<li>Oslo</li>
<li>Stockholm</li>
<li>Helsinki</li>
<li>Berlin</li>
<li>Rome</li>
<li>Madrid</li>
</ul>
<script>
function sortList() {
var list, i, switching, b, shouldSwitch;
list = document.getElementById("id01");
switching = true;
/* Make a loop that will continue until
no switching has been done: */
while (switching) {
// Start by saying: no switching is done:
switching = false;
b = list.getElementsByTagName("LI");
// Loop through all list items:
for (i = 0; i < (b.length - 1); i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
/* Check if the next item should
switch place with the current item: */
if (b[i].innerHTML.toLowerCase() > b[i + 1].innerHTML.toLowerCase()) {
/* If next item is alphabetically lower than current item,
mark as a switch and break the loop: */
shouldSwitch= true;
break;
}
}
if (shouldSwitch) {
/* If a switch has been marked, make the switch
and mark the switch as done: */
b[i].parentNode.insertBefore(b[i + 1], b[i]);
switching = true;
}
}
}
</script>
Just target the div elements instead of li elements, and compare them based on their .className property (after formating them propelly depending on what data you get) instead of their .innerHTML
Sort or filter or reduce your array of elements any way you need then append the results
Very basic example using descending id:
scope.sortDesc = function() {
div.sort(function(a, b) {
return /\d+/.exec(b.id) - /\d+/.exec(a.id);
});
element.append(div);
}
scope.sortDesc();
Plunker demo
Seems like a simple problem though but finding it hard to fix.
There is a pagination component, that has a button & a dropdown. User can go to a page by either clicking the button or selecting that page number in dropdown.
The problem is, when I select a value in the dropdown, nothing happens. Because the scope variable doesnt change from the previous one.
aspx:
<div data-ng-app="app" data-ng-controller="ReportsCtrl">
<div id="paging-top">
<div>
<ul>
<li>
<select data-ng-model="SelectedPage" data-ng-change="ShowSelectedPage();"
data-ng-options="num for num in PageNumbers track by num">
</select>
</li>
<li data-ng-click="ShowNextPage();">Next</li>
</ul>
</div>
</div>
app.js
var app = angular.module("app", ["ngRoute"]);
ReportsCtrl.js
app.controller("ReportsCtrl", ["$scope","ReportsFactory",function ($scope,ReportsFactory) {
init();
var init = function () {
$scope.ShowReport(1);
}
$scope.ShowReport = function (pageNumber) {
GetUserResponsesReport(pageNumber);
}
function GetUserResponsesReport(pageNumber) {
$scope.UserResponsesReport = [];
var promise = ReportsFactory.GetReport();
promise.then(function (success) {
if (success.data != null && success.data != '') {
$scope.UserResponsesReport = success.data;
BindPageNumbers(50, pageNumber);
}
});
}
function BindPageNumbers(totalRows, selectedPage) {
$scope.PageNumbers = [];
for (var i = 1; i <= 5 ; i++) {
$scope.PageNumbers.push(i);
}
$scope.SelectedPage = selectedPage;
}
$scope.ShowSelectedPage = function () {
alert($scope.SelectedPage);
$scope.ShowReport($scope.SelectedPage);
}
$scope.ShowNextPage = function () {
$scope.SelectedPage = $scope.SelectedPage + 1;
$scope.ShowReport($scope.SelectedPage);
}
}]);
Say, the selected value in dropdown is 1. When I select 2 in the dropdown, the alert shows1. When Next is clicked, the dropdown selection changes to 2 as expected. Now, when I select 1 in the dropdown, the alert shows 2.
Tried to make a fiddle, but do not know how to do with a promise - http://jsfiddle.net/bpq5wxex/2/
With your OP SelectedPage is just primitive variable.
With every angular directive new scope is get created.
So,SelectedPage is not update outside the ng-repeat scope after drop-down is changed i.e. in parent scope which is your controller.
In order to do this,use Object variable instead of primitive data types as it update the value by reference having same memory location.
Try to define SelectedPage object in controller in this way.
$scope.objSelectedPage = {SelectedPage:''};
in HTML
<select data-ng-model="objSelectedPage.SelectedPage" data-ng-change="ShowSelectedPage();"
In ShowSelectedPage
$scope.ShowSelectedPage = function () {
console.log($scope.objSelectedPage.SelectedPage);
$scope.ShowReport($scope.objSelectedPage.SelectedPage);
}
From what I have seen with Angular 2.0, I have a feeling I am going to be using Angular 1.x for a while. It has all the building blocks that I think I need, the only downside is that it does have performance issue with dirty checking so I am trying to think about that more. Now ng-repeat can be an issue because of the number of watchers it adds.
So I have this part of a template (in jade):
li(ng-repeat='topMenuItem in sideMenu.topMenu', ng-class='{"is-active": topMenuItem.display === sideMenu.activeTopMenu}')
a(href='{{ topMenuItem.href }}') {{ topMenuItem.display }}
ul(ng-if='sideMenu.secondMenu.length > 0 && topMenuItem.display === sideMenu.activeTopMenu')
li(ng-repeat='secondMenuItem in sideMenu.secondMenu', ng-class='{"is-active": secondMenuItem.display === sideMenu.activeSecondMenu}')
a(href='{{ secondMenuItem.href }}') {{ secondMenuItem.display }}
When this displays 22 menu items the number of watchers is 90 (using this bookmark snippet).
I decided to play around with trying to use $interpolate to generate that menu. I ended up with a directive with this for the compile function:
compile: function(element, attributes) {
var topLevelExpression = $interpolate('<li{{ cssClass }}>{{ topMenuItem.display }}{{ secondLevelMenuHtml }}</li>');
var secondLevelExpression = $interpolate('<li{{ cssClass }}>{{ secondMenuItem.display }}</li>');
var updateMenu = function() {
var html = '';
sideMenu.topMenu.forEach(function(topMenuItem) {
var cssClass = topMenuItem.display === sideMenu.activeTopMenu ? ' class="is-active"': '';
var secondLevelMenuHtml = '';
if(sideMenu.secondMenu.length > 0 && topMenuItem.display === sideMenu.activeTopMenu) {
secondLevelMenuHtml += '<ul>';
sideMenu.secondMenu.forEach(function(secondMenuItem) {
var cssClass = secondMenuItem.display === sideMenu.activeSecondMenu ? ' class="is-active"': '';
secondLevelMenuHtml += secondLevelExpression({
secondMenuItem: secondMenuItem,
cssClass: cssClass,
});
});
secondLevelMenuHtml += '</ul>';
}
html += topLevelExpression({
topMenuItem: topMenuItem,
cssClass: cssClass,
secondLevelMenuHtml: secondLevelMenuHtml
});
});
element.find('.application-navigation').html(html);
};
return function($scope) {
$scope.$watchCollection('sideMenu', function() {
updateMenu();
});
}
}
From my testing, this code functions exactly the same as the ng-repeat, the output look as it should. This version only has 16 watchers and that number does not increase when more elements are shown where the ng-repeat does.
Since this code is only doing the bare minimum that is needed for this piece of code to work, I imagine the javascript itself is just as efficient (if not more efficient) than the code that executes for ng-repeat.
Is that assumption correct?
Are there any issues with doing looping DOM generation in this way vs using ng-repeat?
Tricky question.
I initially thought that if you one-time-bind the property of an object, then if the object changed, the properties would be re-bound.
<div>{{::menu.menuItem}}</div>
<button ng-click="changeMenu()">switch</div>
That is NOT the case.
What one could do though - albeit with a bit of a flaky/hacky approach (perhaps a better approach could be suggested in comments) - is to force Angular to re-bind. One such candidate is ng-if:
<div ng-if="bound">
<div>{{::menu.menuItem1}}</div>
<div>{{::menu.menuItem2}}</div>
</div>
<button ng-click="changeMenu()">Switch</div>
Then, in the controller:
$scope.bound = true;
$scope.menu = {..}; // menu 1
$scope.changeMenu = function(){
$scope.menu = {..}; // menu 2
$scope.bound = false;
$timeout(function(){$scope.bound = true;}, 0);
}
Plunker
<span ng-click="resetbyTask() || filterTask(task.id, $index)">{{ task.total }}</span>
I am trying to toggle between fucntions in a single ng-click so that the first click will run filterTask() and when clicked again it will run resetbyTask
Try this :
// controller
var called = false;
$scope.toggle = function (taskId, index) {
if (called) { called = false; return $scope.resetbyTask(); }
$scope.filterTask(taskId, index);
called = true;
}
HTML :
<span ng-click="toggle(task.id, $index)">{{ task.total }}</span>
I have a pretty simple textbox filtering an ng-repeat on some unordered lis. When I add a value to the textbox the items with the null values are removed and do not return even when the textbox is cleared. I have an idea of why this is happening (the search object now has an empty property which doesn't match the nulls), but I cannot figure out how to solve the problem. I've tried to pop() the property off of the search object with no luck.
HTML:
<div ng-controller="ListCtrl">
<input type="text" ng-model="search.age" placeholder="Age"></input>
<ul>
<li ng-repeat="item in items | filter:search">
{{item.name}} - {{item.age}}
</li>
</ul>
</div>
JS:
function ListCtrl($scope) {
$scope.items = [
{'name':'Carl', 'age':69},
{'name':'Neil', 'age':54},
{'name':'Richard'},
{'name':'Chris', 'age':58}
];
}
Please checkout the JSfiddle to better illustrate the issue.
I figured it out with the help of this answer. If I just add an ng-change to the textbox I can watch for an empty value and delete the property.
HTML:
<input type="text" ng-model="search.age" ng-change="clear()" placeholder="Age"></input>
JS:
$scope.clear = function(){
if($scope.search.age.length == 0){
delete $scope.search.age;
}
}
Updated fiddle. I am aware the current if prevents a user from filtering on a single space, but so far this does not seem to cause a problem for me.
BONUS: ! will return all null values and !! will return all not null values.
The cleanest solution I have found is writing a custom directive to modify the input field behaviour like this:
app.directive('deleteIfEmpty', function () {
return {
restrict: 'A',
scope: {
ngModel: '='
},
link: function (scope, element, attrs) {
scope.$watch("ngModel", function (newValue, oldValue) {
if (typeof scope.ngModel !== 'undefined' && scope.ngModel.length === 0) {
delete scope.ngModel;
}
});
}
};
});
And use it as follows:
<input type="text" ng-model="filter" delete-if-empty>
Modify the input ng-model:
<input type="text" ng-model="searchObj.age" placeholder="Age"></input>
Add this to your controller:
$scope.searchObj = {
}
And either of these will work in your html repeat:
ng-repeat="item in items | filter: searchObj.age"
Or
ng-repeat="item in items | filter: {age: searchObj.age || undefined}"
jsfiddle
You won't be able to use filter:search. Looking at the Angular code, if your obj with an undefined age gets filtered (even when the input is empty) it will fall through this switch statement and always return false. This switch doesn't get called the first time your ng-repeat is run because $scope.search.age is undefined. After your first entry into the input and clearing it out, now $scope.search.age is an empty string...so the filter will always be run.
switch (typeof obj) { ***<-- obj is undefined when you have a missing age***
case "boolean":
case "number":
case "string":
return comparator(obj, text);
case "object":
switch (typeof text) {
case "object":
return comparator(obj, text);
default:
for ( var objKey in obj) {
if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
return true;
}
}
break;
}
return false;
case "array":
for ( var i = 0; i < obj.length; i++) {
if (search(obj[i], text)) {
return true;
}
}
return false;
default:
return false; ***<--falls through and just returns false***
}
You can try writing your own filter function, something like this.
http://jsfiddle.net/wuqu2/
<div ng-controller="ListCtrl">
<input type="text" ng-model="search.age" placeholder="Age"></input>
<ul>
<li ng-repeat="item in items | filter:checkAge">
{{item.name}} - {{item.age}}
</li>
</ul>
</div>
$scope.checkAge = function(item)
{
if($scope.search && $scope.search.age && $scope.search.age.length > 0)
{
return item.age && item.age.toString().indexOf($scope.search.age) > -1;
}
return true;
}