I am working on a custom directive in angular (1.4.8) to display an array of items in a vertical list with gradient background colors. I have put together a plunker to demonstrate. The directive takes an initial RGB value (separate values for r,g, and b), converts to HSL, then decreases the saturation value of the HSL for each additional index in the array.
The Good
The directive works fine for a static list of items...
The Bad
...but in my use case items will be added to the array by the user, and the directive should account for changes to the array and re-render the list with the color gradient adjusted accordingly. At the moment I am having a mental block about how to get this part working.
The directive right now sits inside of an <li> that ng-repeats through the array, and I have given it an isolate scope that has knowledge of the array item and the index of the item within that <li> element. I am also passing in the full array as a scope item so that I can $watch for changes to it, which feels hacky and isn't working, to boot:
return {
...
scope: {
origArray: '=',
item: '=',
idx: '#'
},...
}
scope.$watchCollection(function() { return scope.origArray; }, function(newVal) {
if (newVal) {
console.log('there was a new item added');
removeGradient(); // function to set element background to grey
renderGradient(); // function to set element background to index-appropriate gradient color
}
});
If you have any thoughts about how I might achieve the dynamic re-rendering, I'd really appreciate it. Thank you!
The problem is in your listLength variable. It's true that you were tracking the collection but you're not updating listLength that renderGradient is depending on.
So the fix is simple but first of all, there is no need to compute the list's length by querying the dom. You can simply do listLength = scope.origArray.length.
The fix:
// In function decrementHsl, the first line: update listLength
listLength = scope.origArray.length;
...
The updated plunker here.
Related
Using angular 1.2.4
I'm trying to figure out how to trigger ng-animate's move when a repeated item is reordered. I know the ng-animate is working because the animation for enter, leave, and move are all triggered when a filter is applied. However, when I use some array methods to reorder the array, no animations are triggered.
I suspect part of the problem is that I am actually removing and adding elements to the array with this method, not really 'moving' them:
$scope.moveDown = function(order){
var temp = $scope.names[order];
$scope.names.splice(order, 1);
$scope.names.splice(order+1, 0, temp);
}
Here is a plunker that shows what I'm up to: http://plnkr.co/edit/SuahT6XXkmRJJnIfeIO1?p=preview
Click on any of the names to have it move down the list.
Is there a way to reorder the array without splicing? Or else to manually trigger a move animation when the $index of an item changes?
Try giving a gap (In digestion) between removal and insertion, that will get the ng-enter and ng-leave animations to kick in.
var temp = $scope.names.splice(order, 1).pop();
$timeout(function(){
$scope.names.splice(order+1, 0, temp);
});
Plnkr
If you want to avoid using timeout, restructure your data a bit, make it array of objects (which is always desirable anyways) and do:-
ViewModel:-
$scope.names = [{'name':'Igor Minar'}, {'name':'Brad Green'}, {'name':'Dave Geddes'}, {'name':'Naomi Black'}, {'name':'Greg Weber'}, {'name':'Dean Sofer'}, {'name':'Wes Alvaro'}, {'name':'John Scott'}, {'name':'Daniel Nadasi'}];
In the handler:-
$scope.moveDown = function(order){
var itm = $scope.names.splice(order+1, 1).pop(); //Get the item to be removed
$scope.names.splice(order, 0, angular.copy(itm)); //use angular.clone to get the copy of item with hashkey
}
2 things are important here, you would need to use angular.clone so that default tracker property ($$hashkey) will be removed from the shifting item,it seems like only when the item is removed and new item is inserted (based on tracker property) angular adds the animation classes to it. You cannot do it with primitive as you originally had.
Plnkr2
The cursor gets moved to the beginning instead of being set to last type position when the change event is fired. I am using content editable div using angular js.
plunker
http://plnkr.co/edit/FFaWNZmgk00Etubtjgg8?p=preview
Why does the cursor position move to beginning?
I know this is an old question but, as I've been searching my whole working day to find a solution to this issue, I'd like to share this solution to people who might still looking for a working answer :
var el, el2, range, sel;
el = element[0];
range = document.createRange();
sel = window.getSelection();
if (el.childNodes.length > 0) {
el2 = el.childNodes[el.childNodes.length - 1];
range.setStartAfter(el2);
} else {
range.setStartAfter(el);
}
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
I got this code originally from akatov/angular-contenteditable directive, "moveCaretToEndOnChange" method.
and this worked for me.
I would assume Angular is rewriting the innerHTML property of the editable element, thus destroying and recreating all of the DOM tree within it, which results in the caret resetting to the start. See these answers for potential solutions:
https://stackoverflow.com/a/13950376/96100
https://stackoverflow.com/a/8240055/96100
In my case I was using the same reference of array for changing the innerHTML as was used on the DOM (two way binding). Hence the dom re-rendered and thus the cursor moved to the beginning.
The Solution was to make a deep copy of the array using JSON.parse(JSON.stringify(a)); where a is the array that stores the words that are contenteditable and then do the change
let filetexts =JSON.parse(JSON.stringify(this.filetexts));
// updating the current text
filetexts[alternativesIndex].Alternatives[0].Words[wordIndex].Word = html;
let data = { Text: JSON.stringify(filetexts) };
this.fileService.changeFileText(data, this.file.fileId).subscribe(res => {
});
I am trying to bind directive's attribute to a scope variable. I would like my UI Bootstrap control to dynamically change appearance when this scope variable changes value.
Here is the plunker:
http://plnkr.co/edit/gEjerpXhy3IuT5qRNeL8?p=preview
Every time you press on some of the checkmarks, $scope.max is increased by 1.
But, since that same $scope.max is passed to the 'rating' element as the 'max' attribute, I would like this 'rating' element to put additional checkmark on the right every time I click on some of the existing checkmarks.
I guess I am trying to re-draw this 'rating' element with new parameters. Is this possible and how?
I was able to simulate the behavior you want by adding an ng-if to the element, and modifying a boolean value when the rating changes.
<rating ng-if="render" value="rating.x" max="rating.max"...
$scope.rating.max += 1;
$scope.render = false;
$timeout(function() {
$scope.render = true;
}, 0, true);
Here is a demo, forked from your original plunkr.
For what it is worth, I think you would be better off customizing the directive's source or just writing your own. This workaround isn't going to scale, and it wouldn't be too hard to sprinkle in the functionality you need.
I have an array of items bound to <li> elements in a <ul> with AngularJS. I want to be able to click "remove item" next to each of them and have the item removed.
This answer on StackOverflow allows us to do exactly that, but because the name of the array which the elements are being deleted from is hardcoded it is not usable across lists.
You can see an example here on JSfiddle set up, if you try clicking "remove" next to a Game, then the student is removed, not the game.
Passing this back from the button gives me access to the Angular $scope at that point, but I don't know how to cleanly remove that item from the parent array.
I could have the button defined with ng-click="remove('games',this)" and have the function look like this:
$scope.remove = function (arrayName, scope) {
scope.$parent[arrayName].splice(scope.$index,1);
}
(Like this JSFiddle) but naming the parent array while I'm inside it seems like a very good way to break functionality when I edit my code in a year.
Any ideas?
I did not get why you were trying to pass this .. You almost never need to deal with this in angular. ( And I think that is one of its strengths! ).
Here is a fiddle that solves the problem in a slightly different way.
http://jsfiddle.net/WJ226/5/
The controller is now simplified to
function VariousThingsCtrl($scope) {
$scope.students = students;
$scope.games = games;
$scope.remove = function (arrayName,$index) {
$scope[arrayName].splice($index,1);
}
}
Instead of passing the whole scope, why not just pass the $index ? Since you are already in the scope where the arrays are located, it should be pretty easy from then.
I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).