Delete Row causes issue with Controller Seeing all elements of Table - javascript

Currently I am working on a project in C# MVC5 that will have a list of musicians. This list of musicians in my view has the name of the input in my view as musician[i]. When i go to delete a row in the middle of my table it will remove an integer that causes discontinuity in the array. So when my i submit my table to my controller that has musicians 1 through 5 but musician 4 has been deleted it will also skip 5.
In short I am trying to figure out how to delete a row then change the index numbers of all objects to be flush... so if i have musicians[0], musicians[1], musicians[2] and i delete index 1 then it will shift musician of index to change to index 1 since there is nothing there.
I have attached my Javascript that i am using. Not really anything else than just html at this point and Javascript in my opinion but i could be missing something too?
Thanks in advance for any advice and help!
<script type="text/javascript">
$(document).ready(function () {
$(document).on("click",
".classAddMusician",
function () {
var mrowCount = $('#musicianTableBody').children().length;
var musicianObject = '<tr><td><input class="form-control" type="text" name="track.Musicians[' + mrowCount + '].FirstName" id="track.Musicians[' + mrowCount + '].FirstName" placeholder ="First Name" value=""></td>' +
'<td><input class="form-control" type="text" name="track.Musicians[' + mrowCount + '].LastName" id="track.Musicians[' + mrowCount + '].LastName" placeholder="Last Name" value=""></td>'+
'<td><button type="button" id="btnAddInstrument' + mrowCount + '" class="btn btn-xs btn-primary classAddMusician">Add More</button>' +
'<button type="button" id="btnDelete' + mrowCount + '" class="delete btn btn btn-danger btn-xs">Remove</button></td></tr>';
$('#musicianTableBody').append(musicianObject); // Adding these controls to Main table class
});
$(document).on("click",
".delete",
function () {
$(this).closest("tr")
.remove(); // closest used to remove the respective 'tr' in which I have my controls
});
});
</script>
Controller (POST)
public async Task<ActionResult> Edit(Track track)
{
var instruments = track.Instruments.ToList();
track.Instruments.Clear();
var musicianList = track.Musicians.ToList();
track.Musicians.Clear();
var composerList = track.Composers.ToList();
track.Composers.Clear();
if (!HelperMethods.HelperMethods.IsNullOrEmpty(musicianList))
{
for(int i = 0; i < musicianList.Count(); i++)
{
var tempFirstName = musicianList[i].FirstName;
var tempLastName = musicianList[i].LastName;
if(db.Musicians.Any(x => x.FirstName.Equals(tempFirstName, StringComparison.InvariantCultureIgnoreCase) && x.LastName.Equals(tempLastName, StringComparison.InvariantCultureIgnoreCase)))
{
musicianList[i] = db.Musicians.First(f => f.FirstName.Equals(tempFirstName, StringComparison.InvariantCultureIgnoreCase) && f.LastName.Equals(tempLastName, StringComparison.InvariantCultureIgnoreCase));
}
else
{
musicianList.RemoveAt(i);
}
}
foreach(var x in musicianList)
{
track.Musicians.Add(x);
}
}
if (!HelperMethods.HelperMethods.IsNullOrEmpty(instruments))
{
for(int i = 0; i< instruments.Count(); i++)
{
var tempInstrumentName = instruments[i].Name;
if(db.Instruments.Any(x => x.Name.Equals(tempInstrumentName, StringComparison.InvariantCultureIgnoreCase)))
{
instruments[i] = db.Instruments.First(f => f.Name.Equals(tempInstrumentName, StringComparison.InvariantCultureIgnoreCase));
}
else
{
instruments.RemoveAt(i);
}
}
foreach(var x in instruments)
{
Instrument inst = new Instrument();
inst = x;
track.Instruments.Add(inst);
}
}
if (!HelperMethods.HelperMethods.IsNullOrEmpty(composerList))
{
for (int i = 0; i < musicianList.Count(); i++)
{
var tempFirstName = composerList[i].FirstName;
var tempLastName = composerList[i].LastName;
if (db.Musicians.Any(x => x.FirstName.Equals(tempFirstName, StringComparison.InvariantCultureIgnoreCase) && x.LastName.Equals(tempLastName, StringComparison.InvariantCultureIgnoreCase)))
{
composerList[i] = db.Composers.First(f => f.FirstName.Equals(tempFirstName, StringComparison.InvariantCultureIgnoreCase) && f.LastName.Equals(tempLastName, StringComparison.InvariantCultureIgnoreCase));
}
else
{
composerList.RemoveAt(i);
}
}
foreach (var x in musicianList)
{
track.Musicians.Add(x);
}
}
if (ModelState.IsValid)
{
db.Entry(track).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(track);
}

You appear to be appending musicians one row at a time.
Incrementing the mrowCount variable as the index for each one.
You are then posting back the inputs with their names; track.Musicians[0].FirstName, track.Musicians[1].FirstName, track.Musicians[2].FirstName etc.
You haven't supplied your controller's code, but I'll assume you are iterating over all of the track.Musician[?] items with an index.
If you delete one of these rows on the client, then the inputs you receive at the server will be missing one of these, i.e. track.Musicians[0], track.Musicians[2].
If you are iterating through these items you do not know how many were added or deleted. You may have had 100 musicians added, then 95 of them deleted; but which five were kept and what was the index used on each of them?
There are many ways to handle this, but here are a few of suggestions:
After removing the table row, rename all of the track.Musicians in
the remaining table rows starting from 0 to the last one (length -
1).
Capture the onSubmit() and iterate through all of the remaining
controls who's name starts with 'track.Musicians[' and re-name them
all starting from 0.
Use an extra hidden control that stores the state of the musician
(existing, added, or deleted). Then on a delete, only make the table row hidden - don't remove it. This way, all rows will be submitted
back to the server and you can decide what needs to be done with each
musician.
Use an array to store the current musicians and push to it on an add,
remove (track.Musicians.splice(index, 1);) on a delete. Bind the
table row controls to the values in the array using a framework such
as knockout.js, or redraw the table yourself if it is small. This way
the table will update automatically when you add or remove musicians
from the array.
When you submit your form, you will now have correctly indexed controls according to how many exist.
Of these I would recommend the third option out of personal choice. The HTML code would be smaller and cleaner.
Saying all this though, if you are editing an existing list of musicians, you would have to delete all existing musicians (for the applicable track) in the database and add all those submitted because you cannot match the indexes to those that have been saved.
You could add a hidden input that holds that particular row's musician's id, then compare them on the server for either updating (for existing), adding (if new), and deleting if marked as such.

Related

Append a Nodelist result in a form

I have this function:
function selectCheckedAnswer(){
var checkbox_checked = document.querySelectorAll('input[id="check"]:checked');
var form = document.getElementById('formtosubmit');
for (let i = 0; i < checkbox_checked.length; i++) {
let item = checkbox_checked[i];
form.appendChild(item);
}
}
It is not working as expected. When debugging, there are generated one form to each checkbox checked. (because I inserted the form inside of the For loop, I know)
When I debug the code above, 2 forms are being sent to the controller
image here
...but I just get 1 of the forms and 1 checked checkbox:
image here
What I need is to send only one form with the checkboxes selected to the controller.
I think you are looking for this:
var checkbox_checked = document.querySelectorAll('input[id="check"]:checked');
var form = document.getElementById('formtosubmit');
checkbox_checked.forEach(element => {
form.innerHTML += element.innerHTML;
});

OnClick add a row with data from database laravel/js

I have an invoice and I want to add many items. I have a row with a select and two input fields. Select takes data from DB(it takes all items that are on the database) and input fields are for price and quantity.
I have a button Add Item that I want to add another row with specific inputs. My problem is with select because I do not know how to get data from the database to appear in options.
I am displaying the first line with Laravel while at the click of a button I am trying to implement it with JS. I also want to show the price of the selected item depending on the quantity.
<table id="items_table">...</table>
<button id="add_item">Add Item</button>
<script>
let counter = 1;
const items = {!! json_encode($items->toArray()) !!}
$('#add_item').click(function(event){
event.preventDefault();
counter++;
const select = document.createElement('select')
for(let i = 0; i< items.length; i++){
const opt = items[i]
const el = document.createElement('option');
el.textContent = opt.name;
el.value = opt.id;
select.appendChild(el);
}
const newRow = jQuery(
select +
counter + '"/></td><td><input type="text" name="last_name' +
counter + '"/></td></tr>');
$('#items_table').append(newRow);
});
</script>
It looks like you are getting your data types mixed up. You are creating the select element with document.createElement('select') - that returns a HTMLSelectElement and then you are trying to use that as if it was a String. Here is what your issue really is:
const select = document.createElement('select')
console.log(select + "<br>");
// This prints out "[object HTMLSelectElement]<br>"
To solve the issue, you must convert the select to a string of HTML, try something like this
const select = document.createElement('select')
select.innerText = "Test";
const selectAsHTML = select.outerHTML;
console.log(selectAsHTML + "<br>");

How to sum input numbers and database values in javascript

I have appended data and I print each data price in hidden input. Now I want to sum these prices with input numbers as quantity in order to get the total amount but nothing prints.
Code
javascript
success:function(data) {
//append data to my view
data.forEach(function(row) {
var $meto = '';
$meto += '<div class="col-md-3 mt-2">'+
'<div class="card">'+
'<div class="card-body">'+
'<img src="{{url("images")}}/'+row['photo']+'" class="menuimg img-fluid" alt="image">'+
'<p>'+row['name']+'</p>'+
'<input type="number" name="qty[]" class="qty form-control">'+
'<input type="hidden" class="price" value="'+row['price']+'" />'+
'</div>'+
'</div>'+
'</div>';
$('.here').append($meto);
//sum prices
$('.qty').on('keyup',function(){
var qty = $('.qty').val();
var price = $('.price').val();
$("#total").html(qty*price);
});
});
}
html
<div class="col-md-10">
<div class="row here"></div>
</div>
<p>Total: <span id="total"></span></p>
Explanation
I have filled 2 of my items with quantities 1 & 3 there is hidden input under each of them that holds their prices (as this sample they are 5000 & 16000)
Basically I should have something like this in my total div:
1*5000 = 5000
3*16000 = 48000
Total = 53000
However, it only gets my first input and ignores the rest. Any ideas?
You are not adding all items prices. You need to loop through all items and calculate total.
Note: keep this after your forEach statement.
Try this:
$('.qty').on('keyup',function() {
var quantities = $('.qty');
var prices = $('.price');
var total = 0;
$.each(quantities, (index, qty) => {
total += parseInt($(qty).val() || 0) * parseFloat($(prices[index]).val() || 0)
});
$("#total").html(total);
});
I think the problem is, that you cant get the value from multiple elements with the function ".val()". You only can get multiple values from one element.
You need another function to do want you want to achieve.
You should have a look here: Stackoverflow Question
Anway - you should not save prices into hidden html elements in production environments.
Iterate over the parent element to find the exact price and quantity for each element.
$('.qty').on('keyup',function(){
var total = 0;
$('.card-body').each(function(){
var qty = parseFloat($(this).find('.qty').val());
if (!qty) return;
var price = parseFloat($(this).find('.price').val());
total += (qty * price);
});
$("#total").html(total);
});
Attaching a Fiddle
I don't know jQuery, though I can help you with vanilla JS.
The problem here is:
$('.qty').on('keyup',function(){
var qty = $('.qty').val();
var price = $('.price').val();
$("#total").html(qty*price);
});
This is setting an event listener only for the first .qty, even though there are many of them. So, you should select and iterate over them all, just like you did with the data:
let qtyNodes = document.querySelectorAll('.qty');
let priceNodes = document.querySelectorAll('.price');
let outputNode = document.querySelector('#total');
let sum = 0;
qtyNodes.forEach(function(qtyNode) { // Set event listener to every single qty node
qtyNode.addEventListener('keyup', function() {
qtyNodes.forEach((qtyNode, index) => { // Calculate final price, using each meal price per quantity value.
sum = qtyNode.value * priceNodes[index].value;
})
});
})
outputNode.innerHTML = `${sum}`;
It is important that you keep the arrays-like (qtyNodes and priceNodes) outside the given loop, as it will be accessing the DOM at each iteration and will demand too much performance. Placing it outside, you will access the DOM only one time.
Also, note that: you should be using "input" event, as opposed to "keyup". Input events will fire as soon as the user insert any data, just like a combination of "keyup" and "keydown".

Getting multiple selected checkbox values in a string in javascript and PHP

I have location name and location Id in database table. Using foreach loop i'm printing the values in checkbox in PHP. I have a submit button which triggers a javascript. I want the user selected all checkbox values separated by comma, in a javascript variable. How can I do this?
<!-- Javascript -->
<script>
function getLoc(){
var all_location_id = document.getElementByName("location[]").value;
var str = <!-- Here I want the selected checkbox values, eg: 1, 4, 6 -->
}
<script>
foreach($cityrows as $cityrow){
echo '<input type="checkbox" name="location[]" value="'.$cityrow['location_id'].'" />'.$cityrow['location'];
echo '<br>';
}
echo '<input name="searchDonor" type="button" class="button" value="Search Donor" onclick="getLoc()" />';
var checkboxes = document.getElementsByName('location[]');
var vals = "";
for (var i=0, n=checkboxes.length;i<n;i++)
{
if (checkboxes[i].checked)
{
vals += ","+checkboxes[i].value;
}
}
if (vals) vals = vals.substring(1);
This is a variation to get all checked checkboxes in all_location_id without using an "if" statement
var all_location_id = document.querySelectorAll('input[name="location[]"]:checked');
var aIds = [];
for(var x = 0, l = all_location_id.length; x < l; x++)
{
aIds.push(all_location_id[x].value);
}
var str = aIds.join(', ');
console.log(str);
var fav = [];
$.each($("input[name='name']:checked"), function(){
fav.push($(this).val());
});
It will give you the value separeted by commas
I you are using jQuery you can put the checkboxes in a form and then use something like this:
var formData = jQuery("#" + formId).serialize();
$.ajax({
type: "POST",
url: url,
data: formData,
success: success
});
In some cases it might make more sense to process each selected item one at a time.
In other words, make a separate server call for each selected item passing the value of the selected item. In some cases the list will need to be processed as a whole, but in some not.
I needed to process a list of selected people and then have the results of the query show up on an existing page beneath the existing data for that person. I initially though of passing the whole list to the server, parsing the list, then passing back the data for all of the patients. I would have then needed to parse the returning data and insert it into the page in each of the appropriate places. Sending the request for the data one person at a time turned out to be much easier. Javascript for getting the selected items is described here: check if checkbox is checked javascript and jQuery for the same is described here: How to check whether a checkbox is checked in jQuery?.
This code work fine for me, Here i contvert array to string with ~ sign
<input type="checkbox" value="created" name="today_check"><strong> Created </strong>
<input type="checkbox" value="modified" name="today_check"><strong> Modified </strong>
<a class="get_tody_btn">Submit</a>
<script type="text/javascript">
$('.get_tody_btn').click(function(){
var ck_string = "";
$.each($("input[name='today_check']:checked"), function(){
ck_string += "~"+$(this).val();
});
if (ck_string ){
ck_string = ck_string .substring(1);
}else{
alert('Please choose atleast one value.');
}
});
</script>

add templated div to page multiple times with different ids

I am using ASP.Net MVC along with Jquery to create a page which contains a contact details section which will allow the user to enter different contact details:
<div id='ContactDetails'>
<div class='ContactDetailsEntry'>
<select id="venue_ContactLink_ContactDatas[0]_Type" name="venue.ContactLink.ContactDatas[0].Type">
<option>Email</option>
<option>Phone</option>
<option>Fax</option>
</select>
<input id="venue_ContactLink_ContactDatas[0]_Data" name="venue.ContactLink.ContactDatas[0].Data" type="text" value="" />
</div>
</div>
<p>
<input type="submit" name="SubmitButton" value="AddContact" id='addContact' />
</p>
Pressing the button is supposed to add a templated version of the ContactDetailsEntry classed div to the page. However I also need to ensure that the index of each id is incremented.
I have managed to do this with the following function which is triggered on the click of the button:
function addContactDetails() {
var len = $('#ContactDetails').length;
var content = "<div class='ContactDetailsEntry'>";
content += "<select id='venue_ContactLink_ContactDatas[" + len + "]_Type' name='venue.ContactLink.ContactDatas[" + len + "].Type'><option>Email</option>";
content += "<option>Phone</option>";
content += "<option>Fax</option>";
content += "</select>";
content += "<input id='venue_ContactLink_ContactDatas[" + len + "]_Data' name='venue.ContactLink.ContactDatas[" + len + "].Data' type='text' value='' />";
content += "</div>";
$('#ContactDetails').append(content);
}
This works fine, however if I change the html, I need to change it in two places.
I have considered using clone() to do this but have three problems:
EDIT: I have found answers to questions as shown below:
(is a general problem which I cannot find an answer to) how do I create a selector for the ids which include angled brackets, since jquery uses these for a attribute selector.
EDIT: Answer use \ to escape the brackets i.e. $('#id\\[0\\]')
how do I change the ids within the tree.
EDIT: I have created a function as follows:
function updateAttributes(clone, count) {
var f = clone.find('*').andSelf();
f.each(function (i) {
var s = $(this).attr("id");
if (s != null && s != "") {
s = s.replace(/([^\[]+)\[0\]/, "$1[" + count + "]");
$(this).attr("id", s);
}
});
This appears to work when called with the cloned set and the count of existing versions of that set. It is not ideal as I need to perform the same for name and for attributes. I shall continue to work on this and add an answer when I have one. I'd appreciate any further comments on how I might improve this to be generic for all tags and attributes which asp.net MVC might create.
how do I clone from a template i.e. not from an active fieldset which has data already entered, or return fields to their default values on the cloned set.
You could just name the input field the same for all entries, make the select an input combo and give that a consistent name, so revising your code:
<div id='ContactDetails'>
<div class='ContactDetailsEntry'>
<select id="venue_ContactLink_ContactDatas_Type" name="venue_ContactLink_ContactDatas_Type"><option>Email</option>
<option>Phone</option>
<option>Fax</option>
</select>
<input id="venue_ContactLink_ContactDatas_Data" name="venue_ContactLink_ContactDatas_Data" type="text" value="" />
</div>
</div>
<p>
<input type="submit" name="SubmitButton" value="AddContact" id='addContact'/>
</p>
I'd probably use the Javascript to create the first entry on page ready and then there's only 1 place to revise the HTML.
When you submit, you get two arrays name "venue_ContactLink_ContactDatas_Type" and "venue_ContactLink_ContactDatas_Data" with matching indicies for the contact pairs, i.e.
venue_ContactLink_ContactDatas_Type[0], venue_ContactLink_ContactDatas_Data[0]
venue_ContactLink_ContactDatas_Type[1], venue_ContactLink_ContactDatas_Data[1]
...
venue_ContactLink_ContactDatas_Type[*n*], venue_ContactLink_ContactDatas_Data[*n*]
Hope that's clear.
So, I have a solution which works in my case, but would need some adjustment if other element types are included, or if other attributes are set by with an index included.
I'll answer my questions in turn:
To select an element which includes square brackets in it's attributes escape the square brackets using double back slashes as follows: var clone = $("#contactFields\[0\]").clone();
& 3. Changing the ids in the tree I have implemented with the following function, where clone is the variable clone (in 1) and count is the count of cloned statements.
function updateAttributes(clone, count) {
var attribute = ['id', 'for', 'name'];
var f = clone.find('*').andSelf();
f.each(function(i){
var tag = $(this);
$.each(attribute, function(i, val){
var s = tag.attr(val);
if (s!=null&& s!="")
{
s = s.replace(/([^\[]+)\[0\]/, "$1["+count+"]");
tag.attr(val, s);
}
});
if ($(this)[0].nodeName == 'SELECT')
{ $(this).val(0);}
else
{
$(this).val("");
}
});
}
This may not be the most efficient way or the best, but it does work in my cases I have used it in. The attributes array could be extended if required, and further elements would need to be included in the defaulting action at the end, e.g. for checkboxes.

Categories