JSFiddle - React, Tailwind, and code Playground

by jlspake

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="AttrValidationDiv">
  <span data-bind="text: validations"></span>
</div>
<table>    
    <tbody>
    <!--ko foreach: AttrsViewModels-->
     <tr>
       <td>
         <span data-bind="text: 'Level-' + Level()"></span>
       </td>
        <td>
          <select data-bind="options:$root.optionsViewModel, optionsText:'ProductName', optionsValue:'ProductId',value:ServiceGroup, optionsCaption:'Select'"></select>
       </td>
    </tr>
    <!--/ko-->
    </tbody>
</table>

JavaScript

function viewModel(){
  var self = this;
  
  this.optionsViewModel = [
    { ProductId: 1, ProductName: 'product 1' },
    { ProductId: 2, ProductName: 'product 2' },
    { ProductId: 3, ProductName: 'product 3' }
  ];
  
  this.AttrsViewModels = ko.observableArray([
    { ServiceGroup: ko.observable(), Level: ko.observable(1) },
    { ServiceGroup: ko.observable(), Level: ko.observable(1) },
    { ServiceGroup: ko.observable(), Level: ko.observable(1) },
    { ServiceGroup: ko.observable(), Level: ko.observable(2) },
    { ServiceGroup: ko.observable(), Level: ko.observable(2) },
    { ServiceGroup: ko.observable(), Level: ko.observable(2) },
  ]);
  
  this.LevelGroups = ko.computed(function(){
    var levelMap = {};
    //group viewmodels with a dictionary object by level
    self.AttrsViewModels().map(function(item){
      levelMap[item.Level()] = levelMap[item.Level()] || [];
      levelMap[item.Level()].push(item);
    });
    return levelMap;
  });
  
  this.validations = ko.computed(function(){
    var groups = self.LevelGroups();
    for(var i=0; i<self.optionsViewModel.length; i++){
      var option = self.optionsViewModel[i];
      for(var x in groups){
        if(groups.hasOwnProperty(x)){
      	  var matches = groups[x].filter(function(item){
            return item.ServiceGroup() === option.ProductId;
          });
          if(matches.length >= 2){
            return option.ProductName + ' is selected more than once in level ' + x.toString();
          }
        }
      }      
    }
    return '';
  });
}
    
ko.applyBindings(new viewModel());