Updating $scope values affects it's previous usage points.
After addPhrase call I use sayPhrase to update $scope
function PhrasesCtrl($scope) {
$scope.trail = [0];
$scope.addPhrase = function() {
$scope.phrases.push({
trail: $scope.trail
});
}
$scope.sayPhrase = function(id) {
// id = 1
$scope.trail.push(id);
}
}
Newly created Phrase have it's trail equal to [0], after sayPhrase call it becomes [0, 1]
After $scope.trail.push(id); my new element updates it's trail value.
How to keep used trail value away from changes?
This is because JS objects (and arrays) are passed by reference only. When you push the trail into phrases, you are pushing the reference to the same array that is referenced by $scope.trail.
The easiest solution is to break the reference on $scope.trail, by creating a new array:
$scope.addPhrase = function() {
$scope.phrases.push({
trail: $scope.trail
});
$scope.trail = [0]; // I assume the `0` is on purpose
}
Now $scope.trail will start over every time addPhrase() is called.
Alternatively, if you need to keep the current contents of trail, you should copy the array into a new one. Angular conveniently provides a method just for this:
$scope.addPhrase = function() {
$scope.phrases.push({
trail: angular.copy($scope.trail)
});
}
Related
SAPUI5 - I have an array of objects and one of the properties in those is 'Category'.
For example say I have 2 different types of Category, 'Front Shop' and 'Production Area', what I need to do is to be able to get the first value of each and the last value of each, and then set the enabled property of a button as enabled/disabled.
I'm currently using undercore js (_.each) to loop through to perform some other logic, so can include additional logic here.
Not sure if Underscore has a built in function for this?
Or could someone point me in the right direction on how to do this?
I've got my first pass at what was wanted where I get the very first result and the last result, but now need to set this for each unique category.
Example code below:
// Set view data
oViewData.Questions = oQuestions.results;
oViewData.Questions.TotalNumberOfQuestions = oQuestions.results.length;
// Loop Questions, to get Category Desc and Competency Desc values from relevant Sets
_.each(oViewData.Questions, function (result, index) {
// Read and set Category Desc
this.getView().getModel("Survey").read("/CategorySet", {
filters: [new Filter("CategoryId", FilterOperator.EQ, result.CategoryId)],
success: function (oData) {
oViewData.Questions[index]._CategoryDesc = oData.results[0].CategoryDesc;
this.setViewData(oViewData);
}.bind(this),
error: function (oError) {}.bind(this)
});
// Read and set Competency Desc
this.getView().getModel("Survey").read("/CompetencySet", {
filters: [new Filter("CompetencyId", FilterOperator.EQ, result.CompetencyId)],
success: function (oData) {
oViewData.Questions[index]._CompetencyDesc = oData.results[0].CompetencyDesc;
this.setViewData(oViewData);
}.bind(this),
error: function (oError) {}.bind(this)
});
// Set all move up / down buttons to enabled
oViewData.Questions[index]._MoveUpBtn = true;
oViewData.Questions[index]._MoveDownBtn = true;
// if category id is the first one in the list
}.bind(this));
// Overwrite first move up button and last move down btn to disabled
oViewData.Questions[0]._MoveUpBtn = false;
oViewData.Questions.slice(-1)[0]._MoveDownBtn = false;
// Set view data
this.setViewData(oViewData);
First, you can iterate through arrays with native JavaScript.
_.each(array, function(item) {}) is the same as array.forEach(function(item) {}).
Second, you can use the built-in filter function for your actual question:
const aFrontShopItems = oViewData.Questions.filter(function(oItem) {
return oItem.Category === "Front Shop";
}
If oViewData.Questions is an array then the function passed to filter is applied to every element. If the condition (e.g. oItem.Category === "Front Shop") is true then the element is added to the new array aFrontShopItems. Obviously you need to call filter a second time to get the Production Area items. You can then apply your logic to the first and last items of your new arrays.
Script
var companies=[
{name:'Vicky',category:'Devdas',start:1993,end:2090},
{name:'Vikrant',category:'Devdas',start:1994,end:2019},
{name:'Akriti',category:'mental',start:1991,end:2021},
{name:'Dummy',category:'dummyCategory',start:1995,end:2018},
{name:'Dummy 1',category:'dummyCategory',start:1993,end:2029}
];
var mappingComp=companies.map(company=>{company.start+10;return company});
console.log("mapped company function");
console.log(mappingComp.forEach(company=>console.log(company)));
In the above snippet there is no change in start field of companies array . Why ?
In case I do below I do get modified values for start field from companies array.
var mappingComp=companies.map(company=>company.start+10);
You aren't assigning the result of company.start+10 to anything - it's just an orphaned expression.
var mappingComp = companies.map(company => {
company.start + 10;
return company
});
is just like
var mappingComp = companies.map(company => {
33;
return company
});
The expression is evaluated to a value and then discarded. If you want to add 10 to company.start, use += or =:
var companies=[
{name:'Vicky',category:'Devdas',start:1993,end:2090},
{name:'Vikrant',category:'Devdas',start:1994,end:2019},
{name:'Akriti',category:'mental',start:1991,end:2021},
{name:'Dummy',category:'dummyCategory',start:1995,end:2018},
{name:'Dummy 1',category:'dummyCategory',start:1993,end:2029}
];
var mappingComp = companies.map(company => {
company.start += 10;
return company;
});
console.log(mappingComp);
But this will mutate the original array, which is (often) not a great idea when using map. If you don't want to change the original array, map to a new object:
var companies=[
{name:'Vicky',category:'Devdas',start:1993,end:2090},
{name:'Vikrant',category:'Devdas',start:1994,end:2019},
{name:'Akriti',category:'mental',start:1991,end:2021},
{name:'Dummy',category:'dummyCategory',start:1995,end:2018},
{name:'Dummy 1',category:'dummyCategory',start:1993,end:2029}
];
var mappingComp = companies.map(({ start, ...rest }) => ({
start: start + 10,
...rest
}));
console.log(mappingComp);
company.start + 10 is a simple expression. It's not an assignment statement, that you are expecting it to be. And you are returning the initial array company so it makes sense that it will be returned unaltered.
when you tried the single line fat arrow function with the map. What happens is that you created another entirely different array of mutated values. The array created was populated with values (company.start +10) and returned. Note: This actually didn't change the initial array ie company.
Read up on fat arrow functions, map, filter.
I'm currently making a small app to practice JavaScript and jQuery.
I have an array of fruits.
var fruits = [apples, bananas, grapes, watermelons, tomatoes];
And on a click event I want to remove tomatoes but if I change my mind clicking the same button, it will add tomatoes back to the array fruits. I've been using splice to remove but I don't know what to use to add the splice element back into the array.
Edit for clarification:
The element is not necessary going to be tomatoes, but it could be any random element in the fruits array. I'm using
fruits.splice(i,1);
To insert a value back into an array (at the same position) after you spliced it, can in general be done like this:
// delete:
var deleted = fruits.splice(i, 1);
// restore:
fruits.splice(i, 0, deleted);
Note that deleted is an array with one element here.
It can also done by taking a kind of backup of the original array:
// backup
var backup = fruits.slice(); // copy
// delete:
fruits.splice(i, 1);
// restore:
fruits = backup;
Undo Stack
To support multiple undo actions, you could use an undo stack, which would just keep track of all the versions of your array. When the user performs an undo-action, the previous version is popped from that stack. This way you can undo more than one removal:
var fruits = ['Apples', 'Bananas', 'Grapes', 'Watermelons', 'Tomatoes'];
var undoStack = [];
function showFruits() {
$('#fruits').html('').append(
// translate the array to a list of LI elements with delete buttons
fruits.map(function(fruit) {
return $('<li>').text(fruit).append(
$('<button>').addClass('delete').text('Delete'));
})
);
}
$(document).on('click', 'button.delete', function () {
undoStack.push(fruits.slice()); // save a copy of the current array on the stack
fruits.splice($(this).parent().index(), 1); // remove from array at index
showFruits(); // update display
});
$('#undo').click(function () {
if (!undoStack.length) return; // cannot undo
fruits = undoStack.pop(); // get previous state
showFruits(); // update display
});
showFruits(); // show initial list
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul id="fruits"></ul>
<button id="undo">Undo</button>
More memory efficient alternative
If you are troubled by the memory usage of storing the complete array each time you delete an element, you could use the following alternative functions, which will only store the index and deleted value at every delete action:
$(document).on('click', 'button.delete', function () {
var i = $(this).parent().index(); // get index where to delete
var deleted = fruits.splice(i, 1); // remove from array at that index
undoStack.push([i, deleted]); // save the index and value on the stack
showFruits(); // update display
});
$('#undo').click(function () {
if (!undoStack.length) return; // cannot undo
var restore = undoStack.pop(); // get information for re-inserting
fruits.splice(restore[0], 0, restore[1]); // insert the value
showFruits(); // update display
});
If you would use the undo principle also for other modifications, like undoing an insert, or a modification of the label, then the first solution would not need much modification, while the more memory-efficient one would need a bit more.
For a more generic and elaborated solution on undo/redo operations on any object (not only arrays), see How to version control an object?
If the array position does not matter:
fruits.push(tomatoes);
If you want to insert it at a specific position (index) in the array:
fruits.splice(index, 0, tomatoes);
will insert tomatoes into fruits at the specified index (deleting 0 items first, so it's just an insert).
Array.prototype.splice()
The splice() method changes the content of an array by removing existing elements and/or adding new elements.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
Temp-storing deleted elements and re-adding them
var deletedFruits = fruits.splice(i,1); will contain an array of the removed element(s) because that is the return value of splice(). So
fruits = fruits.concat(deletedFruits);
will re-add the deleted fruits.
Re-adding deleted elements at their original position
Store the position of the deleted element:
var deletedFruit = { fruit: fruits.splice(i,1)[0], index: i }
If need be you can restore deleted fruits at their original array position using the aforementioned
fruits.splice(deletedFruit.index, 0, deletedFruit.fruit);
You can add and remove fruits with these two functions:
function addFruit(fruit) {
fruits.push(fruit);
}
function removeFruit(fruit) {
// indexOf method returns index of fruit in the list, or -1 if fruit is not found.
var index = fruits.indexOf(fruit);
if (index > -1) {
fruits.splice(index, 1);
}
}
This assumes you have already defined an array named fruits. Then you can do something like
<script>
function updateOutput() {
document.getElementById('output').innerHTML = fruits.join(', ');
}
function addSelectedFruit() {
var selectedFruit = document.getElementById('fruit-select').value;
addFruit(selectedFruit);
updateOutput();
}
function removeSelectedFruit() {
var selectedFruit = document.getElementById('fruit-select').value;
removeFruit(selectedFruit);
updateOutput();
}
</script>
<input type="text" id="fruit-select"/>
<button onclick="addSelectedFruit();">Add</button>
<button onclick="removeSelectedFruit();">Remove</button>
List of fruits:
<p id="output"></p>
Example:
<script>
var fruits = ['Apples', 'Pears', 'Pineapples'];
function addFruit(fruit) {
fruits.push(fruit);
}
function removeFruit(fruit) {
var i = fruits.indexOf(fruit);
if (i > -1) {fruits.splice(i, 1);}else{alert(fruit + ' cannot be removed, as it is not present in the array of fruits.');}
}
function selectedFruit() {
return document.getElementById('fruit-select').value;
}
function updateOutput() {
document.getElementById('output').innerHTML = fruits.join(', ');
}
</script>
Fruit:
<input type="text" id="fruit-select" value="Orange"/>
<button onclick="addFruit(selectedFruit());updateOutput();">Add</button>
<button onclick="removeFruit(selectedFruit());updateOutput();">Remove</button>
<p id="output">Apples, Pears, Pineapples</p>
I'm using the latest version of ember-cli, ember-data, ember-localstorage-adapter, and ember.
I have a Node object which has a parent and children. Since I had issues with creating multiple relationships with the same type of object, I decided to store the parentID in a string, and the childIDs in an array of strings. However, when I create a new Node and try to add the new Node's to the parents array of IDs, the ID ends up being added to the correct parent, but also other parents.
level 1 0
/ \
level 2 1 2
| |
level 3 3 4
In a structure like this, 0, 1, and 2 all have correct child and parent IDs. However, after adding 3 and 4, node 1 and node 2's childIDs are [3, 4], instead of [3], [4] respectively.
The Array attribute:
var ArrayTransform = DS.Transform.extend({
serialize: function(value) {
if (!value) {
return [];
}
return value;
},
deserialize: function(value) {
if (!value) {
return [];
}
return value;
}
});
The insertNode code:
insert: function(elem) {
var i,
_store = elem.node.store,
newNodeJSON = elem.node.serialize();
newNodeJSON.childIds = [];
newNodeJSON.level = getNextLevel();
_store.filter('node', function(node) {
return node.get('level') === newnodeJSON.level-1;
}).then(function(prevLevelNodes) {
// if no other nodes yet
if (prevLevelNodes.toArray().length === 0) {
makeNewNode(_store, newNodeJSON, elem.node);
}
// else, generates however many nodes that are in the previous level
else {
prevLevelNodes.toArray().forEach(function(node, idx) {
newNodeJSON.parentId = node.get('id');
makeNewNode(_store, newNodeJSON, elem.node);
});
}
});
}
var makeNewNode = function(_store, newNodeJSON, node) {
console.log(newNodeJSON.parentId); // returns correct value
var newNode = _store.createRecord('node', newNodeJSON);
newNode.save();
var newNodeId = newNode.get('id');
if (newNode.get('parentId')) {
_store.find('node', newNode.get('parentId')).then(function(n) {
var cids = n.get('childIds');
console.log(newNodeId); // returns expected value
console.log(cids); // **DOESN'T RETURN AN EMPTY ARRAY**: returns array with [3,4]
cids.push(newNodeId);
console.log(n.get('childIds')); // returns array with [3,4]
n.save();
});
}
To top this off, this error happens 90% of the time, but 10% of the time it performs as expected. This seems to suggest that there's some sort of race condition, but I'm not sure where that would even be. Some places that I feel like might be causing issues: the ember-cli compilation, passing the entire _store in when making a new node, ember-data being weird, ember-localstorage-adapter being funky... no clue.
For anyone else who may have this problem in the future: the problem lies in two things.
In ArrayTransform, typically I am returning the value sans modification.
In my insert code, I'm passing the same JSON that I defined at the top of the function to makeNewNode.
This JSON contains a reference to a single childIds array; therefore, each new node that gets created uses this same reference for its childIds. Although this doesn't quite explain why the cids array wasn't empty before the push executed (perhaps this is some sort of compiler oddity or console printing lag), it explains why these both Level 3 children were in both Level 2 parents' childIds array.
tl;dr: pass by value vs pass by reference error
My Appmodel consists of an observable array of comments
self.comments=ko.observableArray([]); // Default Value is an empty array
/*
Here comes some code to initially fill the observable array
with items from an JSON Response
*/
Furthermore I have two computeds which should represent the very first comment and the last comment
self.firstComment = ko.computed(function () {
var sorted = self.comments.sort(function (left, right) {
return left.Id() - right.Id();
});
return sorted[0];
});
self.lastComment = ko.computed(function () {
var sorted = self.comments.sort(function (left, right) {
return left.Id() - right.Id();
});
return sorted[sorted.length - 1];
});
This works perfectly on initializing the application (loading the JSON from Server, build up App model...), but when I add a comment to the array, the computeds do not recognize that the number of array items has changed (as I understood it, an observable array is just an observable where the array properties themselves are observed). So when I do:
self.comments.push(aNewCommentObject);
self.lastComment is still bound to the array item, that it was when the app loaded initially.
I have found this blog post how to force computation by introducing a dummy observable, but I don't like the approach. For what purpose is an observableArray used then and how?
Additional Challenge: I would like to keep the observableArray Items sorted under every circumstance (because its a comment feed which should be just sorted chronologically). I tried to do this whith an computed commentsSorted but also have problems that this does not update when the observableArray has new items, so same problem here. Thats the reason, why I am sorting everytime in firstComment and lastComment.
Try unwrapping the comments to trigger Knockout's dependency tracking:
self.firstComment = ko.computed(function () {
var sorted = self.comments().sort(function (left, right) {
// -------------------^^ !
return left.Id() - right.Id();
});
return sorted[0];
});
or (same thing):
self.firstComment = ko.computed(function () {
var sorted = ko.unwrap(self.comments).sort(function (left, right) {
return left.Id() - right.Id();
});
return sorted[0];
});
Of course you can abstract this into a separate computed.
self.commentsSorted = ko.computed(function () {
return self.comments().sort(function (left, right) {
return left.Id() - right.Id();
});
});
self.firstComment = ko.computed(function () {
return ko.unwrap(self.commentsSorted)[0];
});
Since computeds cache their return values (just like every other observable does) you don't need to worry about calling self.commentsSorted multiple times. It will re-calculate only when the underlying observable array chances.
How can you "keep the observableArray items sorted under every circumstance"? My advice is to not try to sort the original array (I'll describe the pitfalls in more detail below) but to use a computed observable that returns a new, sorted array:
self.commentsSorted = ko.computed(function () {
return self.comments.slice(0).sort(function (left, right) {
return left.Id() - right.Id();
});
});
An important thing to understand about the JavaScript Array sort() method is that it changes the original array. Knockout's observableArray wraps many of the array functions so they can be used directly on the observableArray object. That's why you can use myObservableArray.sort() instead of having to do myObservableArray().sort(). But the two aren't equivalent because Knockout wraps array mutator methods differently, approximately like this:
sort: function (sortFunction) {
this.valueWillMutate();
var result = this.peek().sort(sortFunction);
this.valueHasMutated();
return result;
}
So what's wrong with automatically changing the original observableArray?
It's hard to grasp what's going on. It's easy to either not get a dependency on the array and thus never update past the initialization, or to not notify of the change and thus break other elements from getting the correct view of the array.
Even if you do set it up correctly, you've now created a circular dependency because you have something that's updated whenever the array changes, which then updates the array again. Although Knockout currently disables this for synchronous updates of a computed observable (but that might change in the future), it will result in unbounded recursion if using a manual subscription or a computed with the throttle option.
Alternative solution, using subscribe:
self.comments = ko.observableArray([]);
self.comments.subscribe(function(comments) {
var sorted = comments.sort(function (left, right) {
return left.Id() - right.Id();
});
self.firstComment(sorted[0]);
self.lastComment(sorted[sorted.length - 1]);
});