Problem Overview
Let's say I have a shipment of candy. The shipment has a number of boxes, and each box has a number of unique candy types. Every box has a unique id, different from every other box; the same is true for candy types. Furthermore, a candy has additional traits, like color, flavor and quantity.
Example Code
Take the following HTML example:
<div class="shipment">
<div class="box" data-boxid="a">
<div class="candy" data-candyid="1" data-color="orange" data-flavor="orange" data-qty="7">
<!-- unimportant content -->
</div>
<div class="candy" data-candyid="2" data-color="red" data-flavor="strawberry" data-qty="4">
<!-- unimportant content -->
</div>
</div>
<div class="box" data-boxid="b">
<div class="candy" data-candyid="3" data-color="green" data-flavor="lime">
<!-- unimportant content -->
</div>
</div>
</div>
Previous Attempts
I've seen similar examples of table parsing with jQuery's .map() function, and I've also seen mention of .each(), but I've been unable to generate any working code.
Desired Output
I want to generate (with jQuery) a JSON object similar to the following:
{
"shipment": {
"a": {
"1": {
"color": "orange",
"flavor": "orange",
"qty": "7"
},
"2": {
"color": "red",
"flavor": "strawberry",
"qty": "4"
}
},
"b": {
"3": {
"color": "green",
"flavor": "lime"
}
}
}
}
Additional Notes
My app already uses jQuery extensively, so it seems like a logical tool for the job. However, if plain 'ol JavaScript is a more appropriate choice, feel free to say so.
The HTML is always going to be well-formed and always going to follow a the format specified. However, in some cases, information may be incomplete. Note that the third candy had no quantity specified, so quantity was simply ignored while building the object.
This generates what you asked for:
var json = {};
$('.shipment').each(function(i,a) {
json.shipment = {};
$(a).find('.box').each(function(j,b) {
var boxid = $(b).data('boxid');
json.shipment[boxid] = {};
$(b).find('.candy').each(function(k,c) {
var $c = $(c),
candyid = $c.data('candyid'),
color = $c.data('color'),
flavor = $c.data('flavor'),
qty = $c.data('qty');
json.shipment[boxid][candyid] = {};
if (color) json.shipment[boxid][candyid].color = color;
if (flavor) json.shipment[boxid][candyid].flavor = flavor;
if (qty) json.shipment[boxid][candyid].qty = qty;
});
});
});
http://jsfiddle.net/mblase75/D22mD/
As you can see, at each level it uses .each() to loop through the elements matching a certain class and then .find().each() to loop through the desired children. In between, .data() is used to grab the desired data- attributes and the json object is built level by level.
Is this close to what you are looking for? – http://jsfiddle.net/4RPHL/
I have used data() and JSON.stringify to create the json.
Be sure to check your console where I have logged the output.
$(".candy").each(function() {
console.log(JSON.stringify($(this).data()))
});
Nice problem! Thanks to Chris' post I was able to get this to work.
var shipments = [],
shipment = {},
boxes = {},
candy = {};
$(".shipment").each(function() {
var shipment = {},
boxes = {};
$(this).children().each(function(){
var boxdata = $(this).data();
candy = {};
$(this).children().each(function(){
var candydata = $(this).data();
candy[candydata["candyid"]] = {
color: candydata["color"],
flavor: candydata["flavor"],
qty: candydata["qty"]
};
boxes[boxdata["boxid"]] = candy;
});
//console.log(JSON.stringify(boxes)); // works
});
shipment = {shipment: boxes};
shipments.push(shipment); // for multiples
});
console.log(JSON.stringify(shipments[0]));
console.log(shipments.length); // 1 for the example html above
Related
I am making a tournament table on HTML and JS. I have a couple of hundred of « match boxes » to which I want to assign a range of different datas (such as a logo/flag, name of the team, score and more).
How could I write a loop that would execute the following potential thousands of lines :
document.getElementById("matchBox1_team1").innerHTML = teams[1].name;
document.getElementById("matchBox1_flag1").innerHTML = teams[1].flag;
document.getElementById("matchBox1_team2").innerHTML = teams[2].name;
document.getElementById("matchBox1_flag2").innerHTML = teams[2].flag;
document.getElementById("matchBox2_team1").innerHTML = teams[3].name;
document.getElementById("matchBox2_flag1").innerHTML = teams[3].flag;
document.getElementById("matchBox2_team2").innerHTML = teams[4].name;
document.getElementById("matchBox2_flag2").innerHTML = teams[4].flag;
etc…
something like this but I havent got the syntax right :
var j=1;
for (i=0;i<1000;i+=2){
document.getElementById("matchBox("j")_team1").innerHTML = teams[i].name;
document.getElementById("matchBox("j")_flag1").innerHTML = teams[i].flag;
document.getElementById("matchBox("j")_team2").innerHTML = teams[i+1].name;
document.getElementById("matchBox("j")_flag2").innerHTML = teams[i+1].flag;
j++}
Thank you!
It really depends on your method of retrieving data. If you are using ajax then create table rows with js and append those rows to the table so you will only have to get only a single element.
var matchesTable = document.getElementById('matches');
function addMatch(name,flag){
var row = document.createElement('tr');
var nameColumn = document.createElement('th');
nameColumn.innerText = name;
var flagColumn = document.createElement('th');
flagColumn.innerText = flag;
row.append(nameColumn);
row.append(flagColumn);
matchesTable.append(row);
}
Wherever you get that data just call addMatch function and pass all the details as parameters and create th elements for all those details and append to that table.
Else you will need thousands of those rows already created for you to get them and then add those details.
Using id attributes with sequential numbers is a code smell: it is bad practice. Instead you should use classes where their sequence follows from their natural order in the DOM.
For instance:
let teams = [
{ name: "New York Knicks", flag: "flagged" },
{ name: "Toronto Raptors", flag: "cleared" },
{ name: "Sacramento Kings", flag: "cleared" },
{ name: "Miami Hear", flag: "pending" }
];
let teamElems = document.querySelectorAll(".matchBox .team");
let flagElems = document.querySelectorAll(".matchBox .flag");
teams.forEach(({name, flag}, i) => {
teamElems[i].textContent = name;
flagElems[i].textContent = flag;
});
.matchBox { border: 1px solid }
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
You should use appendChild method into the for loop you have created.
Also one hint is you have to create the element (createElement) which you want to insert in the for loop and insert it in the table you have created using appendChild.
You can refer to syntax on w3cschool
For example if i had the json dataset here of all languages of books:
$scope.data = [{
"title": "Alice in wonderland",
"author": "Lewis Carroll",
"lang": ["en"]
}, {
"title": "Journey to the West",
"author": "Wu Cheng'en",
"lang": ["ch"]
}]
And I simply wanted to display exclusively english books, would I be able to do this purely using a filter in ng-repeat?
E.g.
<div ng-repeat="d in data | filter:d.lang='en'" style="margin-bottom: 2%">
{{d.title}}
</div>
I do not want to do it via any sort of form control (radio button etc). Would this be possible?
-EDIT- Thanks #GrizzlyMcBear for leading me down the right path! I got it to work with a slightly different filter function (which I'll paste below)
app.filter('MyFilter', function() {
var out = [];
angular.forEach(input, function(i) {
if (i.lang[0] === 'en') {
out.push(i);
}
})
return out;
}
});
and in the HTML
<div ng-repeat="d in data | MyFilter" style="margin-bottom: 2%">
{{d.title}}
</div>
Try like this
<div ng-repeat="d in data | filter: { lang : 'en'} " style="margin-bottom: 2%">
DEMO
You should use angular's filter,
I would also suggest that you use a function in the filter:
<div ng-repeat="item in collection | filter:filteringFunction" style="margin-bottom: 2%">
{{d.title}}
</div>
This way gives you more freeeeeedom (you're more than welcome to shout it Mel Gibson style ;-) )
in filtering your data by introducing more complex filtering logic.
var filteredLang = "en";
function filterByBookLanguage(collectionItem) {
var result = false;
if (collectionItem.lang[0] === filteredLang) {
result = true;
}
return result;
}
$scope.filteringFunction = filterByBookLanguage;
Now If you wish, you can also change the comperator function - filterByBookLanguage
(my terminology).
Say that your boss suddenly wants you to change the filtering logic from filtering books
into filtering by the author's name. Now all you have to do is add this condition:
if (bossWantsToChangeFilter) {
$scope.filteringFunction = filterByAuthorName;
} else {
$scope.filteringFunction = filterByBookLanguage;
}
All you have to remember is to write the comperator function with the current filtered item
as an argument and update the compared value of the language/author name
in the location you've found convenient ($scope, local variable, service etc.).
So the question is how can I keep the HTML elements in sync while I add and delete from an existing array.
If I have an array of javascript objects say element 1 is:
{
"firstName": "John",
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-3100"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "office",
"number": "646 555-4567"
}
....
]
.... ETC ETC COMPLICATED ....
}
Then my initial html might be generated by using the array index for each object of initial size 3 elements [ {}, {}, {} ]:
<div id="arrayPos-0">
<div>John</div>
<input>PROCESS<input>
<input>DELETE<input>
</div>
<div id="arrayPos-1">
<div>Sam</div>
<input>PROCESS<input>
<input>DELETE<input>
</div>
<div id="arrayPos-2">
<div>Timmy</div>
<input>PROCESS<input>
<input>DELETE<input>
</div>
If I add to the Javascript Array, and I expect to increment it and become [ {}, {}, {}, {} ]:
<div id="arrayPos-0">
...
</div>
<div id="arrayPos-1">
...
</div>
<div id="arrayPos-2">
...
</div>
<div id="arrayPos-3">
<div>Simone</div>
<input>PROCESS<input>
<input>DELETE<input>
</div>
However as soon as I delete from the javascript array say index 2 (arrayPos-2) I get the following HTML:
<div id="arrayPos-0">
...
</div>
<div id="arrayPos-1">
...
</div>
<div id="arrayPos-3">
...
</div>
This is all messed up and I'm unable to match "arrayPos-3" to now index 2. In addition when I add a new javascript object which is index 3:
<div id="arrayPos-0">
...
</div>
<div id="arrayPos-1">
...
</div>
<div id="arrayPos-3">
...
</div>
<div id="arrayPos-3">
...
</div>
I am not able to use AngularJS and hence ng-repeat can't be used due to support needed on older browsers. It would be pretty simple to use an observer to bind the javascript objects directly to the html markup.
I can only use jQuery and regular javascript.
But surely this can be solved in a simpler manner? Do I even need to bind by using IDs? Do I need to autogenerate GUIDs for IDs and use a dictionary to match ID with Javascript object index (I hope not)?
The simplest way is to have a generator function which will regenerate the HTML after any changes have been made (adding/deleting/editing).
That way you won't have any confusions with IDs and will always have your current JS object represented in the DOM. That's quite the "Angularish" way to do it - building the DOM from the JS data without really caring what's in the DOM.
Here's a super-simple example to see what I meant:
var data = [
{name: 'John'},
{name: 'Shomz'},
{name: 'Jimmy'}
]
var c = document.getElementById('c');
function build() {
var output = "";
for(var i = 0; i < data.length; i++) {
output += '<div id="arrayPos-' + i + '"><button onclick="del(' + i + ')">Del</button><button onclick="edit(' + i + ')">Edit</button>' + data[i].name + '</div>';
}
c.innerHTML = output;
}
function del(id) {
data.splice(id ,1);
document.getElementById('arrayPos-' + id).className = 'del';
setTimeout(build, 200);
}
function add() {
data.push({name: prompt('Enter name')});
build();
}
function edit(id) {
data[id].name = prompt('Enter name', data[id].name);
build();
}
build();
#arrayPos-0 {color: green}
button {margin: 4px}
#c div {opacity: 1; transition: all 0.2s linear}
#c div:hover {background: #eee}
#c div.del {opacity: 0}
<div id="c"></div>
<br>
<button onclick="add()">Add</button>
You could use JQuery's .attr() method to dynamically set the id property when looping through the objects in the array - if that's the approach you want to take. You could also look at dynamically setting the inner html using JQuery's .html() to set the contents of the divs. If you review traversing the DOM using JQuery it will help with the latter approach. Hope that helps -
If you want to keep it this way and keep them in order then you caan create a function that:
1.deletes the current divs
2. Re generates the divs in the new order with the corresponding new id's
And just have this function fire every time you add or delete something from the array. It's relatively simple and wont take all that long for javascript to do.
Or:
Create a for loop that loops through the array and identifies which position(number of loops) the object with "x" name is in the array and assigns it to the div.
You can use the jQuery .index() method to get the index of the element relative to its siblings. With that, you can match it to the index in the array. No need for adding id's to the elements.
Let's say you already have an index and you want to access the dom element. You can do this:
$('.class-name').get(index).remove();
Or if you're responding to some event and you're not sure which element in the array it corresponds to, you can do this:
$('.class-name').click(function(e){
var index = $('.class-name').index(e.target);
});
In my example I'm using a click event, but it can be any other way.
In-case, if you can modify your existing js object, I suggest to use id for both html element and the js object.
Probably you could go for something:
var id = Math.random().toString(16).slice(-6); // for random id, aplha-numeric
Your html:
<div id="23xc45">
...
</div>
<div id="cd567u">
...
</div>
Your js object:
[{
"id" : "23xc45",
"firstName": "John",
...
},{
"id" : "cd567u",
"firstName": "Sam",
...
}]
EDIT: Would the approach be much easier if the Javascript listed was removed completely, and the dropdown menus restyled as <div>'s within <li>'s, and the final div was generated by a Javascript onclick event? e.g.
<a id="click_link">click me</a>
$("#click_link").click(function(){
$('#div').load('http://www.link.com/');
});
Either way, the problem at hand...
My decision to use an elegant-looking javascript solution is highlighting my massive inexperience when it comes to javascript! The problem is, on the face of it, simple...
Once an option has been chosen on each of the dropdown menus, I need a final div to load so that a specific button can be shown (a link to buy the item with the specified options, e.g. choosing Necklace D, with Stone Option B, and Delivery Option A = loading div with 'Buy' Button #17)
The dropdowns are divs that are filled and styled through the Javascript (as opposed to using the simpler <form> and <input> method), giving the flexibility to add two lines of differently styled text for each option etc. - This is where I step into the realm of the unknown and my inexperience shines through.
The isolated section is viewable in its entirity here
Ok, to the code.
Here's the Javascript:
function createByJson() {
var pearlData = [
{description:'Choose your pearl...', value:'Pearls', text:'Pearls'},
{description:'Beautiful black stone', value:'Black Pearl', text:'Black Pearl'},
{description:'Classic white stone', value:'White Pearl', text:'White Pearl'}
];
$("#DropItPearls").msDropDown({byJson:{data:pearlData, name:'pearls', width: 200}}).data("dd");
var blodeuweddData = [
{description:'Choose your item...', value:'Blodeuwedd', text:'the Blodeuwedd Collection'},
{description:'A striking statement', value:'BlodeuweddCelticStatement', text:'Celtic Statement Piece'},
{description:'Gold laced flower and pearl', value:'BlodeuweddBracelet', text:'Bracelet'},
];
$("#DropItBlodeuwedd").msDropDown({byJson:{data:blodeuweddData, name:'blodeuwedd', width: 250}})
.msDropDown({on:{change:function(data, ui) {
var val = data.value;
if(val!="")
window.location = val;
}}}).data("dd");
var deliveryData = [
{description:'Choose your method...', value:'Delivery', text:'Delivery Options'},
{description:'4-6 weeks delivery', value:'Four Weeks', text:'Made To Order'},
{description:'(unavailable on this item)', value:'Rush', text:'Express Delivery', disabled:true}
];
$("#DropItDelivery").msDropDown({byJson:{data:deliveryData, name:'delivery', width: 200, selectedIndex: 1}}).data("dd");
paymentData = [
{ description:'How would you like to pay?', value:'Payment', text:'Payment Method'},
{image:'images/msdropdown/icons/Visa-56.png', description:'Secure online payment', value:'Visa', text:'Visa'},
{image:'images/msdropdown/icons/Paypal-56.png', description:'Secure online payment', value:'Paypal', text:'Paypal'},
{image:'images/msdropdown/icons/EmailPay-56.png', description:'Order by email', value:'Email Payment', text:'Send Your Details'},
{image:'images/msdropdown/icons/Mastercard-56.png', description:'(coming soon)', value:'Mastercard', text:'Mastercard', disabled:true},
{image:'images/msdropdown/icons/Collect-56.png', description:'(coming soon)', value:'Collection', text:'Order and Collect', disabled:true},
{image:'images/msdropdown/icons/Email-56.png', description:'email Menna', value:'Other Method', text:'Alternatives'}
];
$("#DropItPayments").msDropDown({byJson:{data:paymentData, name:'payments', width: 250}}).data("dd");
}
$(document).ready(function(e) {
//no use
try {
var pages = $("#pages").msDropdown({on:{change:function(data, ui) {
var val = data.value;
if(val!="")
window.location = val;
}}}).data("dd");
var pagename = document.location.pathname.toString();
pagename = pagename.split("/");
pages.setIndexByValue(pagename[pagename.length-1]);
$("#ver").html(msBeautify.version.msDropdown);
} catch(e) {
//console.log(e);
}
$("#ver").html(msBeautify.version.msDropdown);
//convert
$("select").msDropdown();
createByJson();
$("#tech").data("dd");
});
function showValue(h) {
console.log(h.name, h.value);
}
$("#tech").change(function() {
console.log("by jquery: ", this.value);
})
//
And the html:
<div id="dropOptions">
<div id="dropOptionsTitle"><p>Item</p></div>
<div id="DropItBlodeuwedd"></div>
</div>
<div id="dropOptions">
<div id="dropOptionsTitle"><p>Precious Stones</p></div>
<div id="DropItPearls"></div>
</div>
<div id="dropOptions">
<div id="dropOptionsTitle"><p>Payment</p></div>
<div id="DropItPayments"></div>
</div>
<div id="dropOptions">
<div id="dropOptionsTitle"><p>Delivery</p></div>
<div id="DropItDelivery"></div>
</div>
<div id="dropOptions">
<div id="dropOptionsTitle"><p>Buy Now!</p></div>
<div id="DropItBuy"></div>
</div>
Again, working version viewable here
Many thanks in advance!
What I think you want is for your Buy button to dynamically read what the dropdowns currently say and build a link for redirection based on that, rather than trying to update the Buy button every time a dropdown changes.
From your code I can't see what the form of the final URL is supposed to be. For example, to get the current value of the delivery option, you can check $('#DropItDelivery :selected').text() which will be something like "Made To Order".
Your Buy Now! could be a button with a click event that reads these values and constructs the URL with basic string concatenation, e.g.:
window.location = "buynow.html?delivery=" + $('#DropItDelivery :selected').text() +
"&payment=" + $('#DropItPayments :selected').text()
// etc.
Of course you'd have to handle these options on the server.
In case you want to redirect to the payment page of the processor, you can just branch based on the payment method and give them the URL you want based on that.
var pm = $('#DropItPayments :selected').text();
if (pm == "Visa")
{
// Visa payment URL construction
}
else if (pm == "Send Your Details")
{
// Send your details URL construction
}
// etc.
The select lists are not rendering with the correct option selected. I've tried this a number of different ways including a computed selected observable (this.selected = ko.computed(return parseInt(selected(), 10) == this.id; )) and find in array functions.
In production, the dataArea elements would be populated with server side data. Using the divs with "data-" attributes keeps server side and client side scripting separate (I find this helps the designers).
A record would be displayed in non edit mode first with the option to edit by clicking the edit button. In edit mode, the initial values for the record appear in input controls. You would have the option to say, choose another customer and the having the form load new associated projects. Loading a new customer would reset the project list as expected.
So while loading a new customer would work well, its the transition to editing the current values that is causing an issue. The selected project needs to appear in the drop down list. If a new customer is chosen, the list populates with new options and no defaults are required.
http://jsfiddle.net/mathewvance/ZQLRx/
* original sample (please ignore) http://jsfiddle.net/mathewvance/wAGzh/ *
Thanks.
<p>
issue: When the select options are read, the inital value gets reset to the first object in the options. How do I keep the original value selected when transitioning to edit mode?
</p>
<div>
<h2>Edit Quote '1001'</h2>
<div class="editor-row" data-bind="with: selectedCustomer">
<label>Customer</label>
<div data-bind="visible: !$root.isEditMode()">
<span data-bind="text: CompanyName"></span>
</div>
<div data-bind="visible: $root.isEditMode()">
<input type="radio" name="customerGroup" value="1" data-bind="value: id"> Company Name 1
<input type="radio" name="customerGroup" value="2" data-bind="value: id"> Company Name 2
</div>
</div>
<div class="editor-row">
<label>Project</label>
<div data-bind="visible: !isEditMode()">
<span data-bind="text: selectedProject.Name"></span>
</div>
<div data-bind="visible: isEditMode()">
<select data-bind="options: selectedCustomer().projects, optionsText: 'Name', value: selectedProject"></select>
</div>
</div>
<div>
<button data-bind="click: function() { turnOnEditMode() }">Edit</button>
<button data-bind="click: function() { turnOffEditMode() }">Cancel</button>
</div>
</div>
<hr/>
<div data-bind="text: ko.toJSON($root)"></div>
function ajaxCallGetProjectsByCustomer(customerId) {
var database = '[{"CustomerId": 1, "Name":"Company Name 1", "Projects": [ { "ProjectId": "11", "Name": "project 11" }, { "ProjectId": "12", "Name": "project 12" }, { "ProjectId": "13", "Name": "project 13" }] }, {"CustomerId": 2, "Name": "Company Name 2", "Projects": [ { "ProjectId": "21", "Name": "project 21" }, { "ProjectId": "22", "Name": "project 22" }, { "ProjectId": "23", "Name": "project 23" }] }]';
var json = ko.utils.parseJson(database);
//console.log('parseJson(database) - ' + json);
//ko.utils.arrayForEach(json, function(item) {
// console.log('CustomerId: ' + item.CustomerId);
//});
return ko.utils.arrayFirst(json, function(item){
return item.CustomerId == customerId;
});
}
var Customer = function(id, name, projects) {
var self = this;
this.id = ko.observable(id);
this.CompanyName = ko.observable(name);
this.projects = ko.observableArray(ko.utils.arrayMap(projects, function(item) {
return new Project(item.ProjectId, item.Name);
}));
};
Customer.load = function(id) {
var data = ajaxCallGetProjectsByCustomer(id);
var customer = new Customer(
data.CustomerId,
data.Name,
data.Projects);
};
var Project= function(id, name) {
this.id = id;
this.Name = ko.observable(name);
};
var QuoteViewModel = function () {
var self = this;
$customerData = $('#customerData'); // data from html elements
$projectData = $('#projectData');
// intial values to display from html data
var customer = new Customer (
$customerData .attr('data-id'),
$customerData .attr('data-companyName'),
[{"ProjectId": $projectData .attr('data-id'), "Name": $projectData .attr('data-name')}]
)
this.selectedCustomer = ko.observable(customer);
this.selectedProject = ko.observable($projectData.attr('data-id'));
this.isEditMode = ko.observable(false);
this.selectedCustomer.subscribe(function(){
// todo: load customer projects from database api when editing
});
this.turnOnEditMode = function() {
var customerId = self.selectedCustomer().id();
console.log('customerId: ' + customerId);
Customer.load(customerId);
self.isEditMode(true);
};
this.turnOffEditMode = function() {
self.isEditMode(false);
};
};
var viewModel = new QuoteViewModel();
ko.applyBindings(viewModel);
One the initial value you load
this.dongle = ko.observable($dongleData.attr('data-id'));
This would be the string value "3". Where as the dongle html select element is actually saving/expecting to retrieve the object { "Id": "3", "Name": "dongle 3" }.
Here is a working version that gets the correct initial values and allows editing.
http://jsfiddle.net/madcapnmckay/28FVr/5/
If you need to save the a specific value and not the whole dongle/widget object, you can use the optionsValue attribute to store just the id. Here is it working in the same way.
http://jsfiddle.net/madcapnmckay/VnjyT/4/
EDIT
Ok I have a working version for you. I'll try to summarize everything I changed and why.
http://jsfiddle.net/madcapnmckay/jXr8W/
To get the customer info to work
The Customer name was not stored in the ajaxCallGetProjectsByCustomer json so when you loaded a customer there was no way to determine the new name from the data received. I added a Name property to each customer in the json with name "Company Name 1" etc.
To get the projects collection to work
The problem here was as stated originally with the dongles. You initialize the selectedProject observable with $projectData.attr('data-id') which equates to string value of 13. This is incorrect as the select list is configured in such a way that it actually saves/expects to receive the project object itself. Changing this id assignment to an object assignment made the initial value of project work correctly.
var project = ko.utils.arrayFirst(customer.projects(), function(project){
return project.id == Number($projectData.attr('data-id'));
});
this.selectedProject = ko.observable(project);
FYI there was a minor error in the html, the selectedProject.Name needed to be selectedProject().Name. No big deal.
I'm sure you could have figured out those pretty easily. The next bit is where the real issue is. You reload the Customer every time the edit button is clicked. This seems strange and you may want to reconsider that approach.
However what happens is you load a customer object from the server by id. Assign it to the selectedCustomer observable, this actually works fine. But then because the drop down is bound to selectedCustomer().projects and viewModel.selectedProject it expects that selectedProject is a member of selectedCustomer().projects. In the case of objects the equality operator is assessing whether the references match and in your case they do not because the original selectedProject was destroyed with its associated customer when you overwrote the selectedCustomer value. The fact that the ids are the same is irrelevant.
I have put in place a hack to solve this.
var oldProjectId = viewModel.selectedProject().id;
viewModel.selectedCustomer(customer);
var sameProjectDifferentInstance = ko.utils.arrayFirst(customer.projects(), function(project){
return project.id == oldProjectId;
});
viewModel.selectedProject(sameProjectDifferentInstance || customer.projects()[0]);
This saves the old projectId before assigning the new customer, looks up a project object in the new customer object and assigns it or defaults to the first if not found.
I would recommend rethinking when you load objects and how you handle their lifecycle. If you hold the current objects it memory with a full list of projects included you don't need to reload them to edit, simply edit and then send the update back to the server.
You may find it easier to hold json from the server in js variables instead of html dom elements. e.g.
<script>var projectInitialData = '#Model.ProjectInitialData.toJSON()';</script>
Hope this helps.