How to update an arrays (angularjs) element without splice? - javascript

I am having a situation like in this post here only that I not only need to fetch the element, but change its e.g. name value.
I already found out that one could do it like that:
dataList.splice(index, 1);
dataList.splice(index, 0, newItem);
But there are several problems. I know the id but if I am manipulating the array from time to time I will loose track of the index <=> id correlation because with this method i will take out items, change them and push them as a "new" one, right? But that is kind of not really elegant and could cause problems I think.
Basically I just want to toggle a visible attribute which then should change in the array. Here is the array:
$scope.cLines = [{ id: 1, cColor: 'red', cName: 'Entryline right', visible: true }];
Of course there are usually more elements inside, but I left one for simplicity reasons.
The visible toggler should be working like that (naiv "pseudocode" which would be really awesome if it would work like that simple :) )
$scope.cLines[id === id].visible = !$scope.cLines[id === id].visible;
Second best thing would be if I could access the element directly with the filter, is that possible?
Thank you in advance.

There are several ways to go about it. One is to use filter().
var id = 1;
var visibility = true;
var items = $scope.cLines.filter(function(item) {
return item.id === id;
});
if (items.length >= 1) items[0].visible = visibility;
You can wrap that into a function:
function setVisibility(arr, id, visibility) {
var items = arr.filter(function(item) {
return item.id === id;
});
if (items.length >= 1) items[0].visible = visibility;
}
Then use it like this:
setVisibility($scope.cLines, 1, true);
You could also update $scope.cLines into a more complex object, instead of just an array:
$scope.cLines = {
"item" : function (id) {
var items = this.lines.filter(function(item) {
return item.id === id;
});
if (items.length >= 1)
return items[0];
else
return new Object(); //or throw an error
},
"lines" : [
{ id: 1, cColor: 'red', cName: 'Entryline right', visible: true }
//....and more
]
};
Then use it like this:
$scope.cLines.item(1).visible = true;
With this, make sure to use $scope.cLines.lines if you have to loop through it.

I'm not sure I fully understand the question, so if I'm off here, perhaps you can clarify what you're trying to do.
If you have an ng-repeat and you're trying to toggle some value in the current object, just pass that object to the ng-click function:
<button ng-click="changeVisible(line);" ng-repeat="line in cLines">Visible = {{line.visible}}</button>
and then in your controller, you'd have something like this:
$scope.changeVisible = function(obj) {
obj.visible = !obj.visible;
}
var app = angular.module('demo', []);
app.controller('DemoCtrl', function($scope) {
$scope.cLines = [{
id: 1,
cColor: 'red',
cName: 'Entryline right',
visible: true
}, {
id: 2,
cColor: 'blue',
cName: 'Entryline right',
visible: false
}];
$scope.changeVisible = function(obj) {
obj.visible = !obj.visible;
}
});
<script src="https://code.angularjs.org/1.3.16/angular.js"></script>
<div ng-app="demo">
<div ng-controller="DemoCtrl">
<button ng-click="changeVisible(line);" ng-repeat="line in cLines">Visible = {{line.visible}}</button>
</div>
</div>

Related

JS: Initialize & push to multi-dimensional array in a single line of code? Create a tree structure

SOLUTION
Thanks to Dave's elegeant solution and answer below here is the solution. Side note: fwiw the additional insight or homework like what Dave provided below is very valuable for noobs. Helps us stretch.
This code will walk an existing JSON tree is for example you wanted to parse each value for whatever reason. It doesn't build but walks. In my case I'm walking and parsing each comment to a richer class:
var db = [], instance = {}, commentCounter = [];
function hydrateComments(items, parent) {
_.forEach(items, function(item) {
_.has(item, 'descendants') ? hydrateComments(item.descendants, item) : 0;
instance = new CommentModel(_.omit(item,['descendants']));
// other parsers go here, example the counter for each level
// parseCounter(comment);
(parent['children'] = (parent['children'] || [])) && item.depth > 0 ?
parent.children.push(instance) :
parent.push(instance);
});
}
hydrateComments(comments, storeComments);
ANGULAR DIRECTIVE
For those who use this code for building a tree I'm including a directive that can help you build a tree using the above mentioned tree.
Please note I've remove a lot of my own code and have not tested this, but I know I spent a ton of time trying to find both the tree and template so hopefully this helps you.
buildTree.$inject = [];
function buildTree() {
link.$inject = ["scope", "elem", "attrs"];
function link(scope, elem, attrs) {
}
CommentController.$inject = ["$scope"];
function CommentController($scope) {
$scope.$watchCollection(function () {
return CommentDataService.getComments();
},
function (newComments, oldValue) {
if (newComments) {
$scope.comments.model = newComments;
}
}, true);
}
return {
restrict: "A",
scope: {
parent: "=cmoParent"
},
template: [
"<div>",
"<script type='text/ng-template'",
"id=" + '"' + "{[{ parent.app_id }]}" + '"' + " >",
"<div class='comment-post-column--content'>",
"<div cmo-comment-post",
"post-article=parent",
"post-comment=comment>",
"</div>",
"</div>",
"<ul ng-if='comment.children'>",
"<li class='comment-post-column--content'",
"ng-include=",
"'" + '"' + "{[{ parent.app_id }]}" + '"' + "'",
"ng-repeat='comment in comment.children",
"track by comment.app_id'>",
"</li>",
"</ul>",
"</script>",
"<ul class='conversation__timeline'>",
"<li class='conversation__post-container'",
"ng-include=",
"'" + '"' + "{[{ parent.app_id }]}" + '"' + "'",
"ng-repeat='comment in comments.model[parent.app_id]",
"track by comment.app_id'>",
"</li>",
"<ul>",
"</div>"
].join(' '),
controller: CommentController,
link: link
}
}
BONUS
I also discover a great trick. How to initialize and populate an array with one line of code. In my case I have a counter method that will count each comment at each level where I've used the tip:
parseCounter: function(comment) {
var depth = comment.depth;
(commentCounter[depth] = (commentCounter[depth] || [])) ? commentCounter[depth]++ : 0;
},
ORIGINAL QUESTION
The code below parses a multi-level array of objects with the purpose of parsing all objects to instances of “CommentModel”, which although simple in this example is much richer object class, but for brevity sake I’m simplified the object/class.
EXISTING STACK EXCHANGE CONTENT:
There is a ton of content on setting multi-dimensional arrays and almost all show the examples such as:
var item[‘level1’][‘level2’] = ‘value’;
or
var item = [];
var item['level1'] = [];
or
var item = new Array([]) // and or objects
but, no examples of something like this:
var item[‘level1’].push(object)
QUESTIONS:
Is there way to initialize a 2 level deep multi-dimensional array and at the same time push to it in one line of code?
1.1 i.e. in my example below of parent[‘children’] I’m forced to check if it exists and if not set it. If I attempt parent[‘children’].push(instance) I obviously get a push on undefined exception. Is there a one liner or a better way to check if property exists and if not? I obviously cannot just set an empty array on parent on every iteration i.e. parent[‘children’] = []; and parent[‘children’] = value wont work
Is it possible to move the initialize and validation to the
CommentModel instance? I ask as I attempted to
CommentModel.prototype['children'] = []; but then all child ('descendants')
objects are added to every object in a proto property called
“children”, which makes sense.
side question - I think my tree iteration code function hydrateComments(items, parent) is concise but is there anything I can do to streamline further with lodash and/or angular? Most example I've seen tend to be verbose and don't really walk the branches.
PLUNKER & CODE
https://plnkr.co/edit/iXnezOplN4hNez14r5Tt?p=preview
var comments = [
{
id: 1,
depth: 0,
subject: 'Subject one'
},
{
id: 2,
depth: 0,
subject: 'Subject two',
descendants: [
{
id: 3,
depth: 1,
subject: 'Subject two dot one'
},
{
id: 4,
depth: 1,
subject: 'Subject two dot two'
}
]
},
{
id: 5,
depth: 0,
subject: 'Subject three',
descendants: [
{
id: 6,
depth: 1,
subject: 'Subject three dot one'
},
{
id: 7,
depth: 1,
subject: 'Subject three dot two',
descendants: [
{
id: 8,
depth: 2,
subject: 'Subject three dot two dot one'
},
{
id: 9,
depth: 2,
subject: 'Subject three dot two dot two'
}
]
}
]
}
];
function hydrateComments(items, parent) {
_.forEach(items, function (item) {
// create instance of CommentModel form comment. Simply example
var instance = new CommentModel(item);
// if we have descendants then injec the descendants array along with the
// current comment object as we will use the instance as the "relative parent"
if (_.has(instance, 'descendants')) {
hydrateComments(instance.descendants, instance);
}
// we check is parent has a property of children, if not, we set it
// NOTE : 3 lines of code ? is there a more concise approach
if (!_.has(parent, 'children')) {
parent['children'] = [];
}
// if depth id greater than 0, we push all instances of CommentModel of that depth to the
// parent object property 'children'. If depth is 0, we push to root of array
if (item.depth > 0) {
parent.children.push(instance);
} else {
parent.push(instance);
}
})
}
// simple example, but lets assume much richer class / object
function CommentModel(comment) {
this.id = comment.id;
this.depth = comment.depth;
this.subject = comment.subject;
this.descendants = comment.descendants;
}
var output = [];
// init - pass in data and the root array i.e. output
hydrateComments(comments, output);
// Tada - a hydrated multi-level array
console.log('Iteration output for comments : ', output)
To initialise array in single statement you can do as follows
Method 1: (To initialise parent['children']) ANS to Q#1
Plunker for #1: https://plnkr.co/edit/lmkq8mUWaVrclUY2CoMt?p=preview
function hydrateComments(items, parent) {
_.forEach(items, function(item) {
// create instance of CommentModel form comment. Simply example
var instance = new CommentModel(item);
// if we have descendants then injec the descendants array along with the
// current comment object as we will use the instance as the "relative parent"
_.has(instance, 'descendants') ? hydrateComments(instance.descendants, instance) : 0;
//Less eff. and less readable then method #2
(parent['children'] = (parent['children'] || [])) && item.depth > 0 ?
parent.children.push(instance) :
parent.push(instance);
});
}
Method 2: (To initialise parent['children']) ANS to Q#2 -- I'd prefer this.
Plunker for #2: https://plnkr.co/edit/zBsF5o9JMb6ETHKOv8eE?p=preview
function CommentModel(comment) {
this.id = comment.id;
this.depth = comment.depth;
this.subject = comment.subject;
this.descendants = comment.descendants;
//Initialise children in constructer itself! :)
this.children = [];
}
function hydrateComments(items, parent) {
_.forEach(items, function(item) {
// create instance of CommentModel form comment. Simply example
var instance = new CommentModel(item);
// if we have descendants then injec the descendants array along with the
// current comment object as we will use the instance as the "relative parent"
_.has(instance, 'descendants') ? hydrateComments(instance.descendants, instance) : 0;
item.depth > 0 ? parent.children.push(instance) : parent.push(instance);
});
}
ANS to Q#3
I feel your code is ok. But if depth increases too much, you might encounter stackoverflow. To get rid of this issue with recursion using trampolines. But if you are sure depth is not
I'd like to quote few lines from above article:
What this graph doesn’t show is that after 30,000 recursive
invocations the browser hung; to the point it had to be forcibly shut
down. Meanwhile the trampoline continued bouncing through hundreds of
thousands of invocations. There are no practical limits to the number
of bounces a trampoline can make.
But only use trampoline is you know that the depth is deep enough to cause stack overflow.
Hope this helps !

AngularJS: Merge object by ID, i.e. replace old entry when IDs are identical

I am using Ionic with AngularJS and I am using a localForage database and AJAX via $http. My app has a news stream that contains data like this:
{
"feed":[
{
"id":"3",
"title":"Ein Hund",
"comments:"1"
},
{
"id":"2",
"title":"Eine Katze",
"comments":"2"
}
],
"ts":"20150907171943"
}
ts stands for Timestamp. My app saves the feed locally via localForage.
When the app starts it first loads the locally saved items:
$localForage.getItem("feed").then(function(val) { vm.feed = val; })
Then, it loads the new or updated items (ts < current timestamp) and merges both the old and new data:
angular.extend(vm.feed, response.data.feed);
Updated items look like this:
{
"feed":[
{
"id":"2",
"title":"Eine Katze",
"comments":"4"
}
],
"ts":"20150907171944"
}
That is, the comments count on feed item 2 has changed from 2 to 4. When I merge the old and new data, vm.feed has two items with id = 2.
Does angularjs has a built-in "merge by id" function, i. e. copy from source to destination (if it is a new element), or otherwise replace the old element? In case angularjs does not have such a function, what's the best way to implement this?
Thanks in advance!
angular.merge(vm.feed, response.data.feed);
// EDIT
Probably, it will not merge correctly, so you have to update all properties manually. Update ts property and then find your object with id and replace it.
There is no builtin, I usually write my own merge function:
(function(){
function itemsToArray(items) {
var result = [];
if (items) {
// items can be a Map, so don't use angular.forEach here
items.forEach(function(item) {
result.push(item);
});
}
return result;
}
function idOf(obj) {
return obj.id;
}
function defaultMerge(newItem, oldItem) {
return angular.merge(oldItem, newItem);
}
function mergeById(oldItems, newItems, idSelector, mergeItem) {
if (mergeItem === undefined) mergeItem = defaultMerge;
if (idSelector === undefined) idSelector = idOf;
// Map retains insertion order
var mapping = new Map();
angular.forEach(oldItems, function(oldItem) {
var key = idSelector(oldItem);
mapping.set(key, oldItem);
});
angular.forEach(newItems, function(newItem) {
var key = idSelector(newItem);
if (mapping.has(key)) {
var oldItem = mapping.get(key);
mapping.set(key, mergeItem(newItem, oldItem));
} else {
// new items are simply added, will be at
// the end of the result list, in order
mapping.set(key, newItem);
}
});
return itemsToArray(mapping);
}
var olds = [
{ id: 1, name: 'old1' },
{ id: 2, name: 'old2' }
];
var news = [
{ id: 3, name: 'new3' },
{ id: 2, name: 'new2' }
];
var merged = mergeById(olds, news);
console.log(merged);
/* Prints
[
{ id: 1, name: 'old1' },
{ id: 2, name: 'new2' },
{ id: 3, name: 'new3' }
];
*/
})();
This builds a Map from the old items by id, merges in the new items, and converts the map back to list. Fortunately the Map object will iterate on the entries in insertion order, according to the specification. You can provide your idSelector and mergeItem functions.
Thanks hege_hegedus. Based on your code, I've written my own and tried to use less loops to speed things up a bit:
function updateCollection(localCollection, fetchedCollection) {
angular.forEach(fetchedCollection, function(item) {
var append = true;
for (var i = 0; i < localCollection.length; i++) {
if (localCollection[i].id == item.id) {
// Replace item
localCollection[i] = item;
append = false;
break;
} else if (localCollection[i].id > item.id) {
// Add new element at the right position, if IDs are descending check for "< item.id" instead
localCollection.splice(i, 0, item);
append = false;
break;
}
}
if (append) {
// Add new element with a higher ID at the end
localCollection.push(item);
// When IDs are descending use .unshift(item) instead
}
});
}
There is still room for improvements, i. e. the iteration through all the objects should use binary search since all items are sorted by id.

Why do I get different results using withMutations?

Am I misunderstanding its purpose or how it works?
var menuItems = Immutable.List.of(
{ parent_id: 0, id: 1 },
{ parent_id: 1, id: 2 },
{ parent_id: 1, id: 3 }
);
var results1 = menuItems
.filter(function(menuItem) { return menuItem.parent_id === 1; }) // Filter out items with parent_id = 1
.sort(function(childA, childB) { return childA.sort_order - childB.sort_order; }); // Sort them by sort_order
var results2 = menuItems.withMutations(function(list) {
list
.filter(function(menuItem) { return menuItem.parent_id === 1; }) // Filter out items with parent_id = 1
.sort(function(childA, childB) { return childA.sort_order - childB.sort_order; }); // Sort them by sort_order
});
console.log(results1.size); // 2
console.log(results2.size); // 3
My understanding is that they would yield the same results, but that withMutations would be faster due to the chaining of operations.
You have misunderstood withMutations. The point of it is to give you a temporary playground where you can actually change the list instead of creating copies.
An example would be:
var results2 = menuItems.withMutations(function(list) {
list.shift()
});
In your code, you use filter inside withMutations. Filter creates a new array and does not modify the original array, so your withMutations does nothing.
I think you would be better off just not using withMutations at all. If at some point you think "this would be so much easier if I could just modify the array instead of making copies", you can turn to withMutations.

Check Checkboxes from an array of objects on load

i have a model like this
function ViewModel(){
var self = this
self.Choices = ko.observableArray([])
self.AcceptedChoices = ko.observableArray([])
self.LoadData = function(){
self.ViewAnswered()
}
self.ViewAnswered = function(){
var url = 'QuestionsApi/ViewAnswered'
var type = 'GET'
ajax(url , null , self.OnViewAnsweredComplete, type )
}
self.OnViewAnsweredComplete = function(data){
var currentAnswer = data.Answer
self.Choices(currentAnswer.Choices)
self.AcceptedChoices(currentAnswer.AcceptedChoices)
}
self.LoadData()
}
Here is my object. I have removed extra things
{
"AcceptedChoices": [94, 95],
"Choices": [{
"ChoiceId": 93,
"ChoiceText": "Never"
}, {
"ChoiceId": 94,
"ChoiceText": "Sometimes"
}, {
"ChoiceId": 95,
"ChoiceText": "Always"
}]
}
And here is binding
<u data-bind="foreach:Choices">
<li>
<input type="checkbox" name="choice[]" data-bind="value:ChoiceId,checked:$root.AcceptedChoices">
<span data-bind="text:ChoiceText">Never</span>
</li>
</u>
Now the problem is that checkboxes are not being checked due to the choices being array of objects. How can i resolve this issue? Although the same thing works for radio where there is only one selection.
Never mind i have found a solution here
checked binding does not properly compare primatives
Also it tells two ways for this. The Solution provided in fiddle is creepy so i will use the one using knockout version 3.0.0.
All i need to do is attach knockout-3.0.0.js instead of any other and then use checkedValue instead of value.
<input type="checkbox" name="choice[]"
data-bind="
checkedValue:ChoiceId,
checked:$root.AcceptedChoices"
>
And that's done. Hope it helps someone.
EDITS :
I noticed it is not working on the Chrome. So i found an alternative. I created these two functions.
self.ConvertToString = function(accepted){
var AcceptedChoices = []
ko.utils.arrayForEach(accepted, function(item) {
AcceptedChoices.push(item.toString())
})
return AcceptedChoices
}
self.ConvertToInteger = function(accepted){
var AcceptedChoices = []
ko.utils.arrayForEach(accepted, function(item) {
AcceptedChoices.push(parseInt(item))
})
return AcceptedChoices
}
And use them
self.AcceptedChoices(self.ConvertToString(currentAnswer.AcceptedChoices))
To get the value
AcceptedChoices: self.ConvertToInteger(self.AcceptedChoices()),
You need to be checking to see if the Id of a choice is in the AcceptedChoices array. Use the ko.utils array function to help do that:
checked: function() { return ko.utils.arrayFirst($root.acceptedChoices(), function(item){
return item == ChoiceId();
} !== null }
You could put this into a function on your root object:
self.isChoiceAccepted = function(choiceId){
return ko.utils.arrayFirst($root.acceptedChoices(), function(item){
return item == choiceId;
} !== null
};
then call it in your data-bind as:
checked: function() { return $root.isChoiceAccepted(ChoiceId()); }
This isn't tested, I'm not 100% sure that the arrayFirst method returns null if it doesn't find a matching item in the array, so chack that.

JavaScript: How do I dynamically "filter" my objects

How do I user the JavaScript "filter" attribute as filter my JavaScript object?
I've been reading the following StackOverflow post, and am in a similar situation.
I have the following JavaScript object:
{
'cars' :
[{
"car_id" : "1",
"price" : "42999",
"make_id" : "050",
"year_built" : "2007",
"color_id" : "832"
},
..........
]}
I'm using JQuery to display controls to allow people to filter based on: Price, Make, Year Built, Color
Per that other post, I can use the following code:
// if using an old browser, define the 'filter' attribute
if (!Array.prototype.filter)
{
Array.prototype.filter = function(fun /*, thisp*/)
{
var len = this.length >>> 0;
if (typeof fun != "function")
throw new TypeError();
var res = new Array();
var thisp = arguments[1];
for (var i = 0; i < len; i++)
{
if (i in this)
{
var val = this[i]; // in case fun mutates this
if (fun.call(thisp, val, i, this))
res.push(val);
}
}
return res;
};
}
then to perform the actual filter, I can do:
result = cars.
filter(function(p) { return p.price >= 15000 }).
filter(function(p) { return p.price <= 40000 }).
filter(function(p) { return p.year_built >= 2000 }) etc
What I don't understand is, how do I use my JQuery controls to dynamically change the filter once the filter has already been set? Meaning, let's say I have the filter applied from above, then the user changes there mind and wants to increase the maximum they are willing to pay for a car from $40,000 to $50,000.
How would I problematically modify my filter from :
filter(function(p) { return p.price <= 40000 }).
to:
filter(function(p) { return p.price <= 50000 }).
how do I use my JQuery controls to dynamically change the filter once the filter has already been set?
You don't set a filter. You call filter() with a filter function and get a filtered array back; you can't change the filter that was applied to the array afterwards. Instead you must call filter() again, and pass a different filter function.
Or the same filter function with a closure over a variable that has changed:
var minprice= 10000;
var minpricefilter= function(p) { return p.price>=minprice };
result= cars.filter(minpricefilter);
minprice= 20000;
result= cars.filter(minpricefilter);
You could use a function generator.
function createPriceFilter(price)
{
filter = function(){ return p.price >= price };
return filter;
}
Then, when you filter, always use a function generator.
cars
.filter( createPriceFilter( mySelectedPrice ) )
.filter( createSomethingFilter(...) )
. (...)
Instead of filter, how about a plain old loop:
var min_year = 2000;
var min_price = 15000;
var max_price = 40000;
function fillTable() {
clearTheTable();
for (i = 0; i < cars.length; i++) {
var car = cars[i];
if (p.price >= min_price && p.price <= max_price && p.year_built >= min_year)
addCarToTable(car);
}
}
Each time your parameters change, just call fillTable() again to regenerate the whole table.
(There are much cleverer things you can do but this is the simplest thing I could think of.)
Forget callback based filtering. Enter jOrder: http://github.com/danstocker/jorder.
Filtering by iterating over your entire table is tedious and slow. With jOrder, you search by index:
var table = jOrder(json.cars)
.index('id', ['car_id'])
.index('price', ['price'], { grouped: true, ordered: true, type: jOrder.number })
.index('maker', ['maker_id'], { grouped: true })
.index('year', ['year_built'], { grouped: true, ordered: true, type: jOrder.number })
.index('color', ['color_id'], { grouped: true });
Then you get the records you want by:
var filtered = table.where([{ price: { lower: 15000, upper: 40000 } }], { mode: jOrder.range });
Note that you can only apply one inequality filter at a time. To do more, use filtered as an input for a different jOrder table, put only the necessary index on it, and perform the second inequality filter on that one. And so on. Even if you stack up a couple of filters like this, it will be still faster than iteration by a factor of about 10 to 100 depending on the size of your table.

Categories