5

I have an ecommerce site that has products with multiple attributes (e.g. size, colour, etc,.)

On each product page there is a dropdown for each attribute with a class of 'attribute_price'.

I have also preloaded hidden inputs onto the page from my database with the pricing for each product with a class of 'hidden_attribute_value'.

So not every combination of size and colour is an option. For example, we might have 'small_red' or 'medium_red' but no 'large_red'

So if they select 'large' from the size dropdown menu 'red' should not be an option for the colour.

What I have so far is:

$("select.attribute_price").on("change", function(){

    var id = event.target.id;
    // determine which dropdown was changed (size or colour)
    var attribute_value = document.getElementById(id).value+'_';
    // get the value of the dropdown that they selected

    var other_attribute_ids = []
    var i;
    var other_attributes = document.getElementsByClassName("attribute_price");
    for(i=0; i<other_attributes.length; i++){
        if(other_attributes[i].id != id){
            var other_attribute_id = document.getElementById(other_attributes[i].id).id;
            other_attribute_ids.push(other_attribute_id);
        }
    }
    // create an array of all of the other dropdown ids excluding the one they changed

    var all_attribute_ids = []
    var i;
    var all_attributes = document.getElementsByClassName("hidden_attribute_value");
    for(i=0; i<all_attributes.length; i++){
        all_attribute_ids.push(all_attributes[i].id);
    }
    // create an array of all of the possible values that it can be

});

So I have a variable 'attribute_value' which would be something like 'red_' or 'blue_'.

I have an array called 'all_attribute_values' which has the ids of hidden inputs for all possible combinations. These would have values like 'small_red_' or 'small_blue'.

And I have an array called 'other_attribute_ids' which has the id of the other dropdown menus that haven't been selected.

So if an item in 'all_attribute_values' does not contain 'attribute_value' remove that option from 'other_attribute_ids'.

halfer
  • 19,824
  • 17
  • 99
  • 186
Paddy Hallihan
  • 1,624
  • 3
  • 27
  • 76
  • So if I'm understanding correctly, you have a drop down that needs to be affected by certain products and it can be multiple drop downs? – rhuntington Oct 01 '18 at 17:01
  • @rhuntington, hi yeah sorry its hard to explain as its all being generated by php, JS isn't my strongest language. So any one product can have any number of attributes size and colour are just examples – Paddy Hallihan Oct 02 '18 at 08:03
  • Maybe looking into AJAX would be a good call of action for you - you can send the option to a script, do the logic, return the available options and append the options to the new select – treyBake Oct 11 '18 at 08:19
  • I guess complexity of configuration will be quite a huge problem when you would have more advanced logic in the future, how complex will the configuration become? – Icepickle Oct 11 '18 at 08:24
  • AJAX is indeed the way to go (especially if you're better in PHP than in JS), but can I tempt you into submitting your code to [Code Review Stack Exchange](https://codereview.stackexchange.com/) afterward ? There is a lot you could improve on ^^ (using jQuery selectors or $.each() for example) – Nomis Oct 11 '18 at 08:29
  • @ThisGuyHasTwoThumbs thanks for your comment. I want both dropdown menus to be available to begin with and remove options from the other when one is selected rather than just having one and then adding the other options. – Paddy Hallihan Oct 11 '18 at 08:52
  • @Icepickle thanks for your comment, like I said colour and size were just examples. I have one product that has 5 attributes which means there is over 100 attribute combinations for that one product – Paddy Hallihan Oct 11 '18 at 08:53
  • @PaddyHallihan then maybe it's best to declare an array of possibilities, select on change, check the array, if not there, remove option – treyBake Oct 11 '18 at 08:54
  • @PaddyHallihan however doing my above solution isn't practical when you have tones and tones of options `(X! * N)` - it's better to do via AJAX - reason being is you don't load unnecessary values - just what the user specifically needs. Is there any reason to go against that idea (e.g. spec) – treyBake Oct 11 '18 at 09:02
  • @ThisGuyHasTwoThumbs, thanks again for your comment, we're actually selling industrial equipment so the attributes are usually very technical so if they need something with a particular spec therefore they would choose that option first and then the relevant other options. I don't want them to have to select the first option and then when the options for the spec they actually want load it isn't available with that combination. Thats why I want them to be able to choose from any option first and then remove options. I hope that makes sense. – Paddy Hallihan Oct 11 '18 at 09:15
  • @PaddyHallihan that's where the logic in the AJAX script comes in, you only return available combinations (e.g. you send off red for color, it looks up attributes in the db that goes with it, e.g. red_medium) and it returns those options - the only way to do this without ajax (that I know) is to do as I specified and make one mega array of available combinations and check in that array on change – treyBake Oct 11 '18 at 09:20
  • 1
    could you post html for the product with 5 attributes? – Ercan Peker Oct 13 '18 at 21:15
  • I think you're better off using `data-` attributes, one for each attribute. Then you can do things like `document.querySelectorAll('[data-color="red"][data-size="medium"]')` and that would get all options that have those attributes. You could then hide or show them as desired. – Heretic Monkey Oct 13 '18 at 23:18
  • In any case, this question is ultimately unanswerable in its current form; we'd need to see how these options are laid out and what the relationship between them looks like. Even a simple [mcve] with three attributes would be sufficient. – Heretic Monkey Oct 13 '18 at 23:19
  • I guess this is what you are looking for: https://stackoverflow.com/questions/30232146/dynamically-populating-drop-down-list-from-selection-of-another-drop-down-value – divy3993 Oct 14 '18 at 09:21

5 Answers5

5

I have created a sample html based on your usecase. Solution is likewise, but you should get inspiration for solving yours.

I have considered independent attributes, so the solution will scale to new attributes with different values. I have also considered that server response is not editable.

I have a quick link in jsfiddle to checkout the solution

https://jsfiddle.net/nfLx6aok/1/

<head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>

<select id="size" class="attribute_price">
  <option value="small">Small</option>
  <option value="large">Large</option>
</select>

<select id="color" class="attribute_price">
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="black">Black</option>
</select>

<select id="pattern" class="attribute_price">
  <option value="solids">Solids</option>
  <option value="checked">Checked</option>
  <option value="dots">Dots</option>

</select>

<input type="hidden" id="small_red_solids" class="hidden_attribute_value">
<input type="hidden" id="small_black_dots" class="hidden_attribute_value">
<input type="hidden" id="large_green_checked" class="hidden_attribute_value">

<script>

  // on page load 
  $( document ).ready(function() {
    renderOptions();
});


  $("select.attribute_price").on("change", function () {
    renderOptions();
  });

  function renderOptions() {
    // create an array of all of the possible values that it can be
    // allowed_attribute_values = ['small_red', 'large_blue']
    var allowed_attribute_values = [];
    var all_attributes = document.getElementsByClassName("hidden_attribute_value");
    for (var i = 0; i < all_attributes.length; i++) {
      allowed_attribute_values.push(all_attributes[i].id);
    }

    function getAllPossibleValues(current_level, all_attributes) {
      var depth_index = all_attributes.length;
      var selected_combination = '';
      for (var i = 0; i < depth_index; i++) {
        if (i <= current_level) {
          selected_combination += all_attributes[i].value;
          if (i != all_attributes.length - 1) {
            selected_combination += '_';
          }
        }
      }

      // hide all lower options
      for (var i = current_level + 1; i < depth_index; i++) {
        var selectedIdOptions = all_attributes[i].options;
        all_attributes[i].value = null
        for (var j = 0; j < selectedIdOptions.length; j++) {
          // hide all lower options
          selectedIdOptions[j].hidden = true;
          var el = allowed_attribute_values.find(a => a.includes(selected_combination + selectedIdOptions[j].value));
          if (el) {
            selectedIdOptions[j].hidden = false;
          }
        }
      }
    }

    if (event) {
      var id = event.target.id;
    } else {
      var id = document.getElementsByClassName("attribute_price")[0].id;
    }

    var other_attributes = document.getElementsByClassName("attribute_price");
    for (var i = 0; i < other_attributes.length; i++) {
      if (other_attributes[i].id == id) {
        allPossibleValues = getAllPossibleValues(i, other_attributes);
        // we dont want to go deeper as of now
        break;
      }
    }
  }
</script>
Faiz Mohamed Haneef
  • 3,418
  • 4
  • 31
  • 41
1

this would work with any number of dropdown.

you can generate random attributes to test.

$(document).ready(function () {
    /* generates random attributes */
    var div_attributes = $('#div_attributes');
    var select_colors = $('#select_colors');
    var select_sizes = $('#select_sizes');
    var select_attr0 = $('#select_attr0');
    var select_attr1 = $('#select_attr1');

    $('#btnGenerate').on('click', function () {
        var result = "";

        //
        var index = getRandomArbitrary(1, select_sizes.find('option').get().length);
        var random_attribute = select_sizes.find('option').eq(index).attr('value');
        result += random_attribute;

        //
        index = getRandomArbitrary(1, select_colors.find('option').get().length);
        random_attribute = select_colors.find('option').eq(index).attr('value');
        result += '_' + random_attribute;

        //
        index = getRandomArbitrary(1, select_attr0.find('option').get().length);
        random_attribute = select_attr0.find('option').eq(index).attr('value');
        result += '_' + random_attribute;

        //
        index = getRandomArbitrary(1, select_attr1.find('option').get().length);
        random_attribute = select_attr1.find('option').eq(index).attr('value');
        result += '_' + random_attribute;

        $('<div>' + result + '</div>').appendTo(div_attributes);

        div_attributes.find('div').each(function () {
            var item = $(this);
            attributes.push(item.text());
        });
        SetFirstSelect();
    });

    var attributes = [];
    //sets first select
    SetFirstSelect();
    function SetFirstSelect() {
        $.each(attributes, function (i, val) {
            var attribute = val.split('_')[0];
            $('.attribute_price').eq(0).find('option[value="' + attribute + '"]').show();
        });
    }
    //control attributes array
    var selected_val = [];
    $('.attribute_price').on('change', function () {
        var item = $(this);
        var index = item.index('.attribute_price');
        selected_val[index] = item.val();
        var select = $('.attribute_price').eq(index + 1);
        var selected_attribute = item.val();
        for (var i = index + 1; i < $('.attribute_price').get().length; i++) {
            $('.attribute_price').eq(i).find('option').hide();
            $('.attribute_price').eq(i).prop('selectedIndex', 0)
        }
        var selected_val_str = selected_val[0];
        for (var i = 1; i <= index; i++) {
            selected_val_str += '_' + selected_val[i];
        }
        $.each(attributes, function (j, val) {
            if (val.indexOf(selected_val_str) >= 0) {
                var attribute1 = val.split('_')[index + 1];

                select.find('option[value="' + attribute1 + '"]').show();
            }
        });
    });

    function getRandomArbitrary(min, max) {
        return Math.floor(Math.random() * (max - min) + min);
    }
});
.attribute_price option {
            display: none;
        }
        
        .container {
        margin:30px;
        }
        
        .row > div {
        padding:10px;
        }
        .btn {
        margin-top:20px;
        }
<script
  src="https://code.jquery.com/jquery-3.3.1.min.js"
  integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
  crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">

<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
   <div class="container">
        <div class="row">
        <div style="width:50%; float:left">
                <input type="button" class="btn btn-primary" id="btnGenerate" value="generate random attributes" />
                <div id="div_attributes"></div>
            </div>
            <div style="width:50%; float:left">
                <div class="form-group">
                    <label>Sizes</label>
                    <select class="form-control attribute_price" id="select_sizes">
                        <option value="">select size</option>
                        <option value="small">small</option>
                        <option value="large">large</option>
                        <option value="medium">medium</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>color</label>
                    <select id="select_colors" class="form-control attribute_price">
                        <option value="">select color</option>
                        <option value="black">black</option>
                        <option value="yellow">yellow</option>
                        <option value="red">red</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>attrType0</label>
                    <select id="select_attr0" class="form-control attribute_price">
                        <option value="">select attr0</option>
                        <option value="attr00">attr00</option>
                        <option value="attr01">attr01</option>
                        <option value="attr02">attr02</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>attrType1</label>
                    <select id="select_attr1" class="form-control attribute_price">
                        <option value="">select attr1</option>
                        <option value="attr10">attr10</option>
                        <option value="attr11">attr11</option>
                        <option value="attr12">attr12</option>
                    </select>
                </div>
            </div>

            
        </div>
    </div>
Ercan Peker
  • 1,662
  • 10
  • 17
1

I don't know if I have understood exactly what you are trying to explain or not but here's what I understood you have a website with suppose dropdowns for say clothes having attributes size, price, color, brand and you have an array of objects containing all of these attributes for each object.

I will be explaining this in reactjs since i am more familiar with it than php

so for each dropdown you can have an onchange handler which invokes a function where you check the values of the other dropdowns. If You select size M then enable and populate the dropdown list after filtering clothes with size M. when that is done invoke a similar function for checking the other attributes.

at this time all of your dropdowns will be active, now if the user makes any changes to the first dropdown i.e. size you can either reset the other dropdowns to be reselected or just pass the new list depending on how you are handling

Here is a similar thing i had done for setting date

const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July','August', 'September', 'October', 'November', 'December']
const Month30 = ['4', '6', '9', '11']
const Month31 = ['1', '3', '5', '7', '8', '10', '12']

class Dropdown extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      birthDay: '',
      birthMonth: '',
      birthYear: ''
    }
  }

  componentDidMount() {
  }

  getMonthTotalDays = (birthMonth, birthYear) => {
    if (birthMonth === '02') {
      if (birthYear !== '' && birthYear % 4 === 0) {
        return 29
      } else {
        return 28
      }
    } else if (Month30.includes(birthMonth)) {
      return 30
    } else {
      return 31
    }
  }

  handleChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    }, () => {
      const { birthMonth, birthYear, birthDay } = this.state
      const days = this.getMonthTotalDays(birthMonth, birthYear)
      if (birthDay > days) {
        this.setState({ birthDay: '' })
      }
    })

  }


  renderMonths = () => {
    return months.map((month, i) => {
      if(i < 9){
        return (<option key={`${month}-${i}`} value={'0' + (i + 1)}>{month}</option>)
      }
      else
        return (<option key={`${month}-${i}`} value={i + 1}>{month}</option>)
    })
  }

  renderDay = () => {
    const { birthMonth, birthDay, birthYear } = this.state
    const daysOptions = []
    let days = this.getMonthTotalDays(birthMonth, birthYear)

    for (let day=1; day<=days; day++) {
      daysOptions.push(<option key={`'date-'${day}`} value={day}> { day } </option>)
    }

    return daysOptions
  }

  renderYears = () => {
    const toYear = (new Date()).getFullYear() - 16
    const yearOptions = []
    for (let year = 1960; year <= toYear; year++) {
      yearOptions.push(<option key={`year-${year}`} value={year}> { year } </option>)
    }

    return yearOptions
  }


  render() {
    const { birthDay, birthMonth, birthYear } = this.state
    return (
      <div>
          <label>Month</label>
          <select
            name="birthMonth"
            value={ birthMonth }
            onChange={this.handleChange}
          >
            <option disabled selected value=''>Month</option>
            { this.renderMonths() }
          </select>
          <label>Day</label>
          <select
            name='birthDay'
            value={ birthDay }
            onChange={this.handleChange}
          >
            <option disabled selected value=''>Day</option>
            { this.renderDay() }
          </select>
          <label>Year</label>
          <select
            name='birthYear'
            value={ birthYear }
            onChange={this.handleChange}
          >
            <option disabled selected value=''>Year</option>
            { this.renderYears() }
          </select>
      </div>
    )
  }
}


ReactDOM.render(
  <Dropdown />,
  document.getElementById('drop')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="drop"></div>
Nishith
  • 928
  • 9
  • 13
0

Here is a solution that takes a skus array as input and create a dropdown for each attribute. When any dropdown value changes, the options in all other dropdowns are updated to show only options consistent with picked options.

https://codepen.io/rockysims/pen/PyJbbv

<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script type="text/javascript">
    skus = [{
        id: 1,
        attributes: {
            color: "red",
            size: "small",
            shape: "circle"
        }
    }, {
        id: 2,
        attributes: {
            color: "red",
            size: "medium",
            shape: "square"
        }
    }, {
        id: 3,
        attributes: {
            color: "yellow",
            size: "small",
            shape: "circle"
        }
    }, {
        id: 4,
        attributes: {
            color: "yellow",
            size: "medium",
            shape: "square"
        }
    }, {
        id: 5,
        attributes: {
            color: "yellow",
            size: "large",
            shape: "square"
        }
    }, {
        id: 6,
        attributes: {
            color: "green",
            size: "medium",
            shape: "square"
        }
    }, {
        id: 7,
        attributes: {
            color: "green",
            size: "large",
            shape: "square"
        }
    }];

    $(function() {
        const allOptionsByAttrName = buildAllOptionsByAttrName();

        //create dropdowns
        for (let attrName in allOptionsByAttrName) {
            const dropdownId = attrName + "Dropdown";
            const options = allOptionsByAttrName[attrName];
            let html = "";
            html += attrName + ": ";
            html += buildDropdownHtml(dropdownId, options);
            html += "<br/>";
            $("#dropdowns").append(html);
        }

        //on dropdown changes, update options of all dropdowns
        for (let changedAttrName in allOptionsByAttrName) {
            $("#" + changedAttrName + "Dropdown").on('change', function() {
                //build pickedOptionByAttrName
                const pickedOptionByAttrName = {};
                for (let attrName in allOptionsByAttrName) {
                    const dropdown = $("#" + attrName + "Dropdown");
                    pickedOptionByAttrName[attrName] = dropdown.val();
                }

                refreshAvailableOptions(pickedOptionByAttrName);
            });
        }
    });

    function buildAllOptionsByAttrName() {
        const allOptionsByAttrName = {};
        for (let sku of skus) {
            for (let attrName in sku.attributes) {
                allOptionsByAttrName[attrName] = allOptionsByAttrName[attrName] || [];
                if (allOptionsByAttrName[attrName].indexOf(sku.attributes[attrName]) == -1) {
                    allOptionsByAttrName[attrName].push(sku.attributes[attrName]);
                }
            }
        }

        return allOptionsByAttrName;
    }

    function buildDropdownHtml(dropdownId, options) {
        let html = "";
        html += "<select id='" + dropdownId + "'>";
        html += "<option value=''>";
        html += "";
        html += "</option>";
        for (let option of options) {
            html += "<option value='" + option + "'>";
            html += option;
            html += "</option>";
        }
        html += "</select>";

        return html;
    }

    function refreshAvailableOptions(pickedOptionByAttrName) {
        for (let attrName in pickedOptionByAttrName) {
            //build availableOptions
            const dropdown = $("#" + attrName + "Dropdown");
            const options = $("#" + attrName + "Dropdown option");
            const availableOptions = buildAvailableOptions(pickedOptionByAttrName, attrName);
            availableOptions.push(""); //nothing picked option

            //show available options and hide others
            options.each(function() {
                const option = $(this);
                const optionIsAvailable = availableOptions.indexOf(option.val()) != -1;
                if (optionIsAvailable) {
                    option.show();
                } else {
                    option.hide();
                }
            });
        }
    }

    function buildAvailableOptions(pickedOptionByAttrName, attrNameToBuildFor) {
        //build availableSkus
        const availableSkus = skus.filter(function(sku) {
            let available = true;
            for (let attrName in pickedOptionByAttrName) {
                if (attrName != attrNameToBuildFor) {
                    const pickedOption = pickedOptionByAttrName[attrName];
                    if (pickedOption) {
                        available = available && sku.attributes[attrName] == pickedOption;
                    }
                }
            }

            return available;
        });

        //build availableOptions
        const availableOptions = [];
        for (let sku of availableSkus) {
            if (availableOptions.indexOf(sku.attributes[attrNameToBuildFor]) == -1) {
                availableOptions.push(sku.attributes[attrNameToBuildFor]);
            }
        }

        return availableOptions;
    }
</script>

<div id="dropdowns">
</div>

If you prefer not to dynamically create the dropdowns, comment out the for loop under //create dropdowns and replace <div id="dropdowns"></div> with the following:

<div id="dropdowns">
    color: 
    <select id="colorDropdown">
        <option value=""></option>
        <option value="red">red</option>
        <option value="yellow">yellow</option>
        <option value="green">green</option>
    </select><br/>
    size: 
    <select id="sizeDropdown">
        <option value=""></option>
        <option value="small">small</option>
        <option value="medium">medium</option>
        <option value="large">large</option>
    </select><br/>
    shape:
    <select id="shapeDropdown">
        <option value=""></option>
        <option value="circle">circle</option>
        <option value="square">square</option>
    </select><br>
</div>
Rocky Sims
  • 3,523
  • 1
  • 14
  • 19
0

If you can identify in PHP which options for other categories are available for certain drop-down category-value selection. You can incrementally flag each option as excluded for each selected value to keep it disabled:

JS:

$(document).ready(function () {
    $(".dropdown").change(function(){
        var val = $(this).val();
        var id = $(this).attr('id');

        $.get('get_options.php', {category: id, value:val}, function(data) {

            $(".dropdown:not(#"+id+")").each(function() {

                var cat = $(this).attr('id');
                $(this).find('option').each(function() {

                    var cat_val = $(this).val();
                    var options = data[cat];
                    var exluded = $(this).attr('exlude');

                    if ($.inArray(cat_val, options) !== -1) {

                        $(this).attr('exlude', exluded-1);
                        if(exluded == 1) {
                            $(this).prop('disabled', false);
                        }
                    } else {

                        $(this).attr('exlude', exluded+1);
                        $(this).prop('disabled', true);
                    }
                });
            });
        }, 'json');
    });
});

HTML:

<div id="dropdowns">
color: 
<select id="color" class="dropdown">
    <option value=""></option>
    <option value="red">red</option>
    <option value="yellow">yellow</option>
    <option value="green">green</option>
</select><br/>
size: 
<select id="size" class="dropdown">
    <option value=""></option>
    <option value="small">small</option>
    <option value="medium">medium</option>
    <option value="large">large</option>
</select><br/>
shape:
<select id="shape" class="dropdown">
    <option value=""></option>
    <option value="circle">circle</option>
    <option value="square">square</option>
    <option value="triangle">triangle</option>
</select><br>

**SAMPLE DATA: ** example if user selects color first, return all available options for other drop-down categories.

{"size": ["small","large"],"shape":["circle"]}

or in php:

$data = array(
    'size' => array(
        'small', 'large'
    ),
    'shape' => array(
        'circle'
    ),
);

echo json_encode($data);
ACD
  • 1,431
  • 1
  • 8
  • 24