Reputation: 545
Im trying to create a site that has any number of rows in a table that contain up to 2 inputs. I would like the combination of the inputs to be unique. That is, if we have the following elements
<select><option value='0'>None</option><option value='1'>1</option><option value='2'>2</option><option value='3'>3</option></select>
<select><option value='0'>None</option><option value='4'>4</option><option value='5'>5</option><option value='6'>6</option></select>
Then the any combinations of the two can be used 0-1 times. So, if 1 and 4 are used in one row, if 1 is selected again, 4 should not be allowed to be selected. This should work in the other direction as well. If 4 is selected then 1 should not be allowed to be selected again.
Rows can be added or removed at anytime by the user, so these checks will have to be performed every time a element is changed.
Additional Info
Things Ive tried
Detecting the change event and trying to adjust all the select elements to enforce the unique-ness, but I couldn't quite hack this. I'm pretty sure this is the way to go, but am struggling with the way to implement it.
Trying to remove possible duplicates at the add row time, but you can't decide what is a duplicate until at least one selection is made.
Removing any row(s) that duplicate another row's combination, but this doesn't work as expected and is not user friendly anyway (this is the current implementation in the example below).
Things that don't work
How to select only Unique combinations of select dropdowns in table row (Very similar, but no working solution)
Prevent Multiple Selections of Same Value (the select elements do not contain the same items)
Extra Challenge
The other half of this is that either the projects column and/or the activities column may be hidden if there are no projects/activities in the database. In this case, the default value is 0, but it dramatically changes the expected behavior of the page and adds a bunch of edge cases to the mix. I left it out of the main question since I thought it would add too much confusion, but it's something else I'll need to deal with. This functionality is second to the main problem above, however.
Minimum Reproducible Example:
You should be able to copy-past this directly to a .html file for experimentation. I left some styling in to make it easier to understand what is needed.
jQuery.fn.outerHTML = function() {
return jQuery('<div />').append(this.eq(0).clone()).html();
};
var projects = $("#projectsMaster");
var activities = $("#activitiesMaster");
var numRows = 0;
$(document).ready(function() {
$(".entryTable").on("click", ".deleteButton", function() {
if (numRows > 0) {
var row = $(this).closest("tr");
row.remove();
numRows--;
}
});
$(".entryTable").on('change', 'select', function() {
enforceUniqueProjectsActivities(this)
});
});
function enforceUniqueProjectsActivities(select) {
var usedCombos = new Array();
$(".entryTable").find(".dataRow").each(function() {
var activity = $(this).find(".activity").find("select").val();
if (!activity) {
activity = 0;
}
if (!project) {
project = 0;
}
var project = $(this).find(".project").find("select").val();
usedCombos.push(project + "," + activity);
});
//I was planning on going through each row and removing duplicates, but this isnt very user-friendly.
$(".entryTable").find(".dataRow").each(function() {
var activity = $(this).find(".activity").find("select").val();
if (!activity) {
activity = 0;
}
var project = $(this).find(".project").find("select").val();
if (!project) {
project = 0;
}
for (var i = 0; i < usedCombos.length; i++) {
if (i === $(this).index() - 1) {
continue;
}
if (project === usedCombos[i].split(",")[0] && activity === usedCombos[i].split(",")[1]) {
$(this).remove();
}
}
});
}
function addRow() {
numRows++;
var newRow = "<tr class='dataRow' '" +
"'><td class='project'>" + projects.clone().outerHTML() + "</td><td class='activity'>" + activities.clone().outerHTML() +
"</td><td><button class='deleteButton'>Delete</button></td></tr>";
$(".entryTable").find('tr:last').prev().after(newRow);
}
.entryTable {
margin: auto;
width: 95%;
border: 1px solid black;
border-collapse: collapse;
}
.entryTable input,
select {
width: 100%;
font-size: 18px;
}
.entryTable tr {
border-bottom: 1px solid black;
border-color: black
}
.entryTable td {
text-align: center;
padding: 10px;
border-right: 1px solid black;
}
#addRow {
background-color: darkgray
}
.masters {
display: none;
}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<div class="masters">
<select id="projectsMaster">
<option value='0'>None</option>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
</select>
<select id="activitiesMaster">
<option value='0'>None</option>
<option value='4'>4</option>
<option value='5'>5</option>
<option value='6'>6</option>
</select>
</div>
<br/>
<table class="entryTable" id-">
<tr>
<th>Project</th>
<th>Activity</th>
<th></th>
</tr>
<tr id="addRow">
<td></td>
<td></td>
<td style='text-align: center'><button onclick="addRow()">Add</button></td>
</tr>
</table>
Upvotes: 1
Views: 636
Reputation: 1359
Found the exact what u were looking for... Before user see the options our function is checking the other value and other rows with the same value, and then it disables the options which are not available any more. I Changed the duplicate-checking function, and the "change" listener (i changed it into "focusin" listener).
jQuery.fn.outerHTML = function() {
return jQuery('<div />').append(this.eq(0).clone()).html();
};
var projects = $("#projectsMaster");
var activities = $("#activitiesMaster");
var numRows = 0;
$(document).ready(function() {
$(".entryTable").on("click", ".deleteButton", function() {
if (numRows > 0) {
var row = $(this).closest("tr");
row.remove();
numRows--;
}
});
// run the checking before user sees the options
$(".entryTable").on('focusin', 'select', function() {
// we must run that function giving it the other select because i wrote it backwards ;) sorry
var $this = $(this),
$other = $this.closest('tr').find('select').not($(this));
enforceUniqueProjectsActivities($other);
});
});
function enforceUniqueProjectsActivities(select, isSecondRun) {
var $this = $(select),
thisVal = $this.val(),
thisCol = $this.closest('td').is('.activity') ? 'activity':'project',
pairCol = (thisCol == 'project') ? 'activity':'project',
$table = $this.closest('.entryTable'),
$thisRow = $this.closest('tr'),
$pair = $thisRow.find('td.'+pairCol+' select'),
$otherRows = $table.find('tr').not($thisRow),
$otherLikeThis = $otherRows.find('.'+ thisCol+' option:selected[value="'+thisVal+'"]');
// clear all disabled props of the options of the $pair element
$pair.find('option').prop('disabled',false);
if(thisVal==''||thisVal=='0')return;
// check which pair-values are already used and disable these options
$otherLikeThis.each(function(){
var $currPair = $(this).closest('tr').find('td.'+pairCol+' select'),
currPairVal = $currPair.val();
if(currPairVal && currPairVal!='0'){
$pair.find('option[value="'+ currPairVal +'"]').prop('disabled',true);
}
});
}
function addRow() {
numRows++;
var newRow = "<tr class='dataRow' '" +
"'><td class='project'>" + projects.clone().outerHTML() + "</td><td class='activity'>" + activities.clone().outerHTML() +
"</td><td><button class='deleteButton'>Delete</button></td></tr>";
$(".entryTable").find('tr:last').prev().after(newRow);
}
.entryTable {
margin: auto;
width: 95%;
border: 1px solid black;
border-collapse: collapse;
}
.entryTable input,
select {
width: 100%;
font-size: 18px;
}
.entryTable tr {
border-bottom: 1px solid black;
border-color: black
}
.entryTable td {
text-align: center;
padding: 10px;
border-right: 1px solid black;
}
#addRow {
background-color: darkgray
}
.masters {
display: none;
}
option:disabled {
background-color:#ccc;
color:#888;
}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<div class="masters">
<select id="projectsMaster">
<option value='0'>Select</option>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
</select>
<select id="activitiesMaster">
<option value='0'>Select</option>
<option value='4'>4</option>
<option value='5'>5</option>
<option value='6'>6</option>
</select>
</div>
<br/>
<table class="entryTable" id-">
<tr>
<th>Project</th>
<th>Activity</th>
<th></th>
</tr>
<tr id="addRow">
<td></td>
<td></td>
<td style='text-align: center'><button onclick="addRow()">Add</button></td>
</tr>
</table>
PS. I started with a different idea ...That idea was wrong - but the function was good (just needed the other select as argument - that's why listener function grown a bit bigger)
Upvotes: 2
Reputation: 337560
One way to achieve this would be to build an array of the selected value pairs concatenated together. Then when a new pair of values is selected you can compare that to the existing array to see if it already exists, and handle it appropriately.
Note that I also made some modifications to the logic which builds the HTML to make it more consistent and succinct.
I also removed the 0
value in the default 'Please select' option as this causes needless complication when comparing to other values.
jQuery($ => {
$('button.add').on('click', () => {
$('.entryTable tfoot tr').clone().insertBefore('.entryTable tbody tr:last');
});
$(".entryTable").on("click", ".deleteButton", e => {
$(e.target).closest("tr").remove();
});
let getSelections = () => {
return $('tbody .dataRow').map((i, row) => {
let $row = $(row);
return `${$row.find('.projectsMaster').val()}${$row.find('.activitiesMaster').val()}`;
}).get();
}
$(".entryTable").on('change', 'select', e => {
let $row = $(e.target).closest('tr');
let item = `${$row.find('.projectsMaster').val()}${$row.find('.activitiesMaster').val()}`;
if (item.length == 2 && getSelections().filter(i => i == item).length > 1) {
e.preventDefault();
console.log('Already selected!');
}
});
});
table>tfoot {
display: none;
}
.entryTable {
margin: auto;
width: 95%;
border: 1px solid black;
border-collapse: collapse;
}
.entryTable input,
select {
width: 100%;
font-size: 18px;
}
.entryTable tr {
border-bottom: 1px solid black;
border-color: black
}
.entryTable td {
text-align: center;
padding: 10px;
border-right: 1px solid black;
}
.addRow {
background-color: darkgray
}
.masters {
display: none;
}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<table class="entryTable">
<tbody>
<tr>
<th>Project</th>
<th>Activity</th>
<th></th>
</tr>
<tr class="addRow">
<td></td>
<td></td>
<td style='text-align: center'><button class="add">Add</button></td>
</tr>
</tbody>
<tfoot>
<tr class="dataRow">
<td class="project">
<select class="projectsMaster">
<option value="">Select</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</td>
<td class="activity">
<select class="activitiesMaster">
<option value="">Select</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</td>
<td><button class="deleteButton">Delete</button></td>
</tr>
</tfoot>
</table>
Upvotes: 1