Knockout: Dynamically controlling number of objects in an array? - javascript

So, I'm grabbing a number from a model, and trying to use that number (the length of an observable array) to dynamically control the number of objects in a "sub-model." In this case, when I add or remove "meta headings" in the parent model, the child model will automatically have the right number of corresponding "meta fields."
So this grabs the length of the array I'm basing things on:
self.metaCount = ko.computed(function() {
return parent.metaHeadings().length;
});
This is the array I want to dynamically push objects into:
self.metaColumns = ko.observableArray([]);
And this is how I'm trying to dynamically push items into the array:
self.columnUpdate = ko.computed(function() {
for (i=0; i<self.metaCount(); i++) {
self.metaColumns.push({heading: ko.observable()});
}
});
Now, I'm doing all of this in the model. The reason is that I have several instances of models and sub-models, and it makes more sense to have each one handle its own updating when a change takes place.
Am I going about this all wrong?

I would say it depends on your requirements. Firstly does your current code work correctly? If not what are the problems?
Is the metaColumns array required to to editable independently? If the answer is no then why maintain them as a separate property when you could simply do:
self.metaColumns = ko.computed(function() {
var result = [];
for (i=0; i<self.metaCount(); i++) {
self.metaColumns.push({heading: ko.observable()});
}
return result;
});
I notice that you currently aren't clearing the metaColumns array when the columnUpdate is recomputed so it will keep adding to the array.
Hope this helps.

Related

Posting model back to controller containing complex collection

I've posted a question similar to this, but I don't believe it clearly stated my question correctly. So here goes a better explanation!
I'm working on a C# MVC 5 application. I have a page that dynamically lists the model's collection property A. Each item in this collection uses a bit of bootstrap to show/hide it's own collection property B. So just to clarify that: This model has a collection of objects each of which has a collection of objects associated with it. These objects each have a checkbox associated with them bound to an isChecked boolean too that can be used to select/deselect items. Checking a box of an item from collection A, automatically selects all checkboxes of its collection B. So, as you can perhaps see, the sole purpose of having a checkbox on collection A items, is to act as a select all for its associated collection B items.
I'm trying to pass the entire model back to the controller on POST and then do stuff with it, e.g. grab all items that have their , but am running into a few problems.
E.g.
#foreach (var category in Model.Categories)
{
var headerId = category.Name;
var childCounter = 0;
#*some divs and headers here*#
#if (category.Items.Any())
{
#*lets give it the ability to check all of its associated items*#
#Html.CheckBoxFor(d => category.IsChecked, new {id = #headerId, onclick = String.Format("CheckUncheckChildren({0}, {1})", #headerId, category.MenuItems.Count)})
}
else
{
#Html.CheckBoxFor(d => category.IsChecked)
}
#*some other divs and headings*#
#if (category.MenuItems.Any())
{
#foreach (var item in dealItemCategory.MenuItems)
{
var childId = childCounter++ + "_" + headerId;
var serverId = item.Id;
#*some other divs and headings*#
#Html.CheckBoxFor(d => item.IsChecked, new {id = #childId})
}
}
}
Lower down on the page I have:
for (var i = 0; i < Model.Categories.Count(); i++)
{
var category = Model.Categories.ElementAt(i);
var headerId = category.Name;
#Html.HiddenFor(d => category, new {id = #headerId})
for (var j = 0; j < Model.Categories.ElementAt(i).Items.Count(); j++)
{
var item = Model.Categories.ElementAt(i).Items.ElementAt(j);
#Html.HiddenFor(d => item, new {id = j + "_" + #headerId})
}
}
In my mind, I'm retaining these properties on the page for this complex collection... (not that my understanding is 100%) I've forced ids for these hidden fields so that they correlate with the ids of the Checkboxes. Maybe not the best idea, but it was just the last thing I tried to get this to work...
Now I have two options here:
1) Believe in the above to mean that whenever a checkbox is selected/deselected, the object in the collection it is associated with, will get its isChecked boolean changed. Then, POST back the entire model and hope I'll be able to cycle through all checked items and do awesome stuff.
or
2) Create a string property in the model to hold a JSON representation of this complex collection of objects. When the page is POSTED, update this JSON string to the state of the complex collection that has been hopefully updated to have some of its items checked/unchecked. Then, POST back the entire model and server-side I would deserialize this single object into a logically equivalent complex collection and work from there.
I.e.
My model change would be adding a property:
public string JsonCategories{ get; set; }
And then in the view I'd have to initiate a post back:
#Html.HiddenFor(d => d.JsonCategories);
function accept() {
var categories= #Html.Raw(Json.Encode(Model.Categories));
var jsonCategories = JSON.stringify(categories);
var a = $('#JsonCategories').val();
$('#JsonCategories').val(jsonCategories );
$("form").submit();
};
Attempting solution A, I either get a null for the Categories property, or if I add #Html.HiddenFor(d => d.Categories); I get an empty object, count of 0.
Attempting solution B, I do get a lovely JSON representation for the complex Categories back in server land, but I see checkboxes I checked aren't changing the isChecked bools leading me to believe that, while the JSON object is able to be set to the Categories object on POST client-side, everything done by the user isn't being kept so ultimately the Categories collection of the model hasn't changed since it was passed to the View initially.
Sorry if this all seems complicated, it is and for a junior like me, I thought it best I ask around as I've never posted back lists and stuff to the controller before.
I'd be happy to provide more information if it will help!
Warmest regards,
If you change your foreach loops to for loops you can make use of the loop index in the lambda expression. This should make the engine output the correct syntax for posting the values. For example:
#for (int i = 0; i < Model.Categories.Count(); i++)
{
...
#Html.CheckBoxFor(d => d.Categories[i].IsChecked)
...
}
This should correctly bind the values to the POST request. The resulting html will be similar to the following snippet (0 being the first item, 1 would be the second etc.):
<input name="Categories[0].IsChecked" ... ></input>
Alternatively you can create editor templates for your child collections, which will result in you writing out this snippet to replace where your for loop is currently.
#Html.EditorFor(m => m.Categories)
More on how to do this here.
This should help you go with option 1.

Comparing and interlacing variables for editing purposes with AngularJS

I have a list of names (staff in stafflist) from which I select some names and add them as an object to an array (paxlist). This operation is repeated, so several objects with different names are added into the array.
What I am attempting to do is to be able to edit each one of this objects to add or remove names.
For UX reasons, when I first select names from stafflist, they turn blue, and they reset to white when the object is added.
Basically, the effect/functionality I'm looking for is:
The object is added -> The main list resets
The edit button from one of the objects is clicked
The list of names of the object is compared with the main list, and the relevant names are highlighted (in blue) as existing/already selected names.
The user selects or deselects names.
The edition is completed, the resulting object saved and the main list reset.
I have a Plunkr depicting the addition functionality, but I don't see clear how could I compare and make the 2 variables (stafflist and pax in recordlist) work together as to edit the result.
I'm not specially looking for somebody to do it and solve this for me, but more to understand the logic behind a possible solution, as so far I can't think of anything...
Any comments will be highly appreciated!
I created a new Plunk with what I think you were trying to accomplish. Basically I just added a new state (editMode) which captured the pax being edited.
var editMode;
$scope.editRecord = function(record) {
editMode = record.pax;
$scope.stafflist.forEach(function (s) {
s.chosen = false;
});
record.pax.forEach(function(p) {
$scope.stafflist.forEach(function (s) {
if(p.name === s.name) {
s.chosen = true;
}
});
});
};
I then used this new state to figure out whether I was creating a new record or editing an existing one.
$scope.pushStaff = function (staff) {
staff.chosen = true;
var arr = editMode || $scope.paxlist;
arr.push(staff);
};
$scope.unpushStaff = function (staff) {
staff.chosen = false;
var arr = editMode || $scope.paxlist;
var index=arr.indexOf(staff);
arr.splice(index,1);
};
I'm sure there are cleaner approaches, but this is one way to do it.

Filter/Search JavaScript array of objects based on other array in Node JS

i have one array of ids and one JavaScript objects array. I need to filter/search the JavaScript objects array with the values in the array in Node JS.
For example
var id = [1,2,3];
var fullData = [
{id:1, name: "test1"}
,{id:2, name: "test2"}
,{id:3, name: "test3"}
,{id:4, name: "test4"}
,{id:5, name: "test5"}
];
Using the above data, as a result i need to have :
var result = [
{id:1, name: "test1"}
,{id:2, name: "test2"}
,{id:3, name: "test3"}
];
I know i can loop through both and check for matching ids. But is this the only way to do it or there is more simple and resource friendly solution.
The amount of data which will be compared is about 30-40k rows.
This will do the trick, using Array.prototype.filter:
var result = fullData.filter(function(item){ // Filter fulldata on...
return id.indexOf(item.id) !== -1; // Whether or not the current item's `id`
}); // is found in the `id` array.
Please note that this filter function is not available on IE 8 or lower, but the MDN has a polyfill available.
As long as you're starting with an unsorted Array of all possible Objects, there's no way around iterating through it. #Cerbrus' answer is one good way of doing this, with Array.prototype.filter, but you could also use loops.
But do you really need to start with an unsorted Array of all possible Objects?
For example, is it possible to filter these objects out before they ever get into the Array? Maybe you could apply your test when you're first building the Array, so that objects which fail the test never even become part of it. That would be more resource-friendly, and if it makes sense for your particular app, then it might even be simpler.
function insertItemIfPass(theArray, theItem, theTest) {
if (theTest(theItem)) {
theArray.push(theItem);
}
}
// Insert your items by using insertItemIfPass
var i;
for (i = 0; i < theArray.length; i += 1) {
doSomething(theArray[i]);
}
Alternatively, could you use a data structure that keeps track of whether an object passes the test? The simplest way to do this, if you absolutely must use an Array, would be to also keep an index to it. When you add your objects to the Array, you apply the test: if an object passes, then its position in the Array gets put into the index. Then, when you need to get objects out of the Array, you can consult the index: that way, you don't waste time going through the Array when you don't need to touch most of the objects in the first place. If you have several different tests, then you could keep several different indexes, one for each test. This takes a little more memory, but it can save a lot of time.
function insertItem(theArray, theItem, theTest, theIndex) {
theArray.push(theItem);
if (theTest(theItem)) {
theIndex.push(theArray.length - 1);
}
}
// Insert your items using insertItem, which also builds the index
var i;
for (i = 0; i < theIndex.length; i += 1) {
doSomething(theArray[theIndex[i]]);
}
Could you sort the Array so that the test can short-circuit? Imagine a setup where you've got your array set up so that everything which passes the test comes first. That way, as soon as you hit your first item that fails, you know that all of the remaining items will fail. Then you can stop your loop right away, since you know there aren't any more "good" items.
// Insert your items, keeping items which pass theTest before items which don't
var i = 0;
while (i < theArray.length) {
if (!theTest(theArray[i])) {
break;
}
doSomething(theArray[i]);
i += 1;
}
The bottom line is that this isn't so much a language question as an algorithms question. It doesn't sound like your current data structure -an unsorted Array of all possible items- is well-suited for your particular problem. Depending on what else the application needs to do, it might make more sense to use another data structure entirely, or to augment the existing structure with indexes. Either way, if it's planned carefully, will save you some time.

How to work with javascript object methods in Angularfire

I have an object that represents a restaurant order:
function order () {
this.customer_name = ''
this.menu = // menu object
}
extended with some object methods for business logic, like:
order.prototype.value = function() {
var total = 0;
for (var i = 0; i < this.menu.length; i++) {
// calculate the order value
}
return total;
}
In the angular controller orders get pushed onto an array when submitted (via ng-click from a button in the view):
var ref = new Firebase('https://myfirebase.firebaseio.com');
$scope.orders = [];
angularFire(ref, $scope, 'orders');
$scope.currentOrder = orderService;
$scope.submitOrder = function() {
$scope.orders.push($scope.currentOrder);
};
Once orders are pushed into the array, properties like orders[0].customer_name work, but methods like orders[0].value() don't.
It seems reasonable that Firebase/Angularfire would only be syncing JSON, but is there an approach that would allow me to keep order-related logic included with the order object, i.e without having to write $scope.getOrderValue(orders[0])?
There isn't a great way to do exactly what you want, since according to the Firebase FAQ:
At a low-level, we support basically the same data types as JSON: Strings, Numbers, Booleans, and Objects (which in turn contain Strings, Numbers, Booleans, and more Objects).
Which means you can store data but not functions. It seems like a clean way to accomplish the same thing would be to store the latest order value as a property of your order object, and have a method as part of your orderService that updates it whenever menu items are added or removed. Alternatively, do what you suggested and have a getOrderValue somewhere, but it probably still makes sense to put that in a service.
I actually had the same issue.
I wanted to add a method to my firebase object.
After looking in the latest angularfire docs I found that $extend can do just that
I didn't test it yet, but I think this is the way to go about it.

Duplicate an array in Knockout.js

I'm trying to make a little damage calculator for the game Diablo 3 (I know, I know).
Basically the idea is that it has a "before" and "after" array of values that represent items for your character. The "after" array should duplicate the "before" array when that's updated. However, changes to the "after" array should not update the "before" array.
Each array then displays a DPS (more of this is better) total, and it shows you the difference between the two. The idea is then it makes for easy comparison of two items when using the in-game auction house.
I have the first bit set up - the "before" array is working great. However I'm at a loss as to how to create the "after" array, and I'm wondering if I've made this a different magnitude of complexity. Should I be using two view models, replicating it in jQuery, or using the mapping plugin? I can't quite find anything that's exactly what I'm after, the UI requirements especially seem a bit of a sticking point
Fiddle of where I'm up to: http://jsfiddle.net/kimadactyl/GuMuY/8/
Here's a solution that should get you started. I refactored your HeroItem to take a config object and an optional linked Hero.
I am assuming for the moment the array is fixed length. I create the after array from the items array by mapping it to a new HeroItem, using jquery extend to do a deep copy.
When a link is passed in the HeroItem will subscribe to changes on it's observables and update one-way only as specified.
function HeroItem(config, link) {
var self = this, prop;
self.item = config.item;
self.int = ko.observable(config.int);
self.ias = ko.observable(config.ias);
self.critdmg = ko.observable(config.critdmg);
self.critpc = ko.observable(config.critpc);
self.min = ko.observable(config.min);
self.max = ko.observable(config.max);
if (link) {
for (prop in link) {
if (link.hasOwnProperty(prop) && ko.isObservable(link[prop])) {
console.log("subscribing " + prop);
link[prop].subscribe((function(p) {
return function (newValue) {
console.log("updating " + p+ " to " + newValue);
self[p](newValue);
}
})(prop));
}
}
}
}
self.after = ko.observableArray(ko.utils.arrayMap(self.items(), function(i) {
return new HeroItem($.extend({}, ko.toJS(i)), i);
}));
http://jsfiddle.net/madcapnmckay/2MNFn/1/
No custom bindings needed, it just uses the subscription capabilities all KO observables have. If you want to extend this to cope with dynamic length arrays simple subscribe to the items array and cleanup the after array accordingly.
Hope this helps.

Categories

Resources