Search code examples
javascriptjqueryjquery-select2

How to create a dynamic chain select with strings using Select2


I am creating a DnD style calculator. There are class trees that are 3 classes deep. How would I be able to force Select 2 to only show level 1, level 2 and level 3 in their respective selects and also base it on the prior selection? I have seen this work with numbers before but I cannot get it to work with specific strings. I created a JSFiddle here: https://jsfiddle.net/3gma61wf/45/

The way the class array is set up:

const classes1 = [
    {
        id: '',
        text: 'Select ICQ...',
    },
    {
        id: 'fighter',
        text: 'Fighter',
        children: [
            {
                id: 'barbarian',
                text: 'Barbarian',
                children: [
                    { id: 'beserker', text: 'Beserker' }
                ],
                id: 'soldier',
                text: 'Soldier',
                children: [
                    { id: 'knight', text: 'Knight' }
                ]
            }
        ]
    },
    {
        id: 'archer',
        text: 'Archer',
        children: [
            {
                id: 'ranger',
                text: 'Ranger',
                children: [
                    { id: 'elite-ranger', text: 'Elite Ranger' }
                ],
                id: 'paladin',
                text: 'Paladin',
                children: [
                    { id: 'bard', text: 'Bard' }
                ]
            }
        ]
    },
    {
        id: 'mage',
        text: 'Mage',
        children: [
            {
                id: 'wizard',
                text: 'Wizard',
                children: [
                    { id: 'warlock', text: 'Warlock' }
                ],
                id: 'druid',
                text: 'Druid',
                children: [
                    { id: 'artificer', text: 'Artificer' }
                ]
            }
        ]
    },
];

On my local project I can get Class 1 to fill in. I want to be able to use Class 1 to find the "children" key and display all of the "text" values there. So on until the third level. I have tried also splitting the array into 3 parts, each for a different select field and then having a "parent" key that would match the prior wording in the array but for obvious reasons that would be more difficult to maintain. Basically, is there a way I can force Select2 to dynamically understand the way I've built the array and automatically update it?

I have all commented out code in the JSFiddle that I have attempted. I was able to get Class2 using the find() function but that only ever returns the first found value and when I attempted to switch it to use filter() it would always come back as undefined.

const classes1 = [
    {
        id: '',
        text: 'Select ICQ...',
    },
    {
        id: 'fighter',
        text: 'Fighter',
        children: [
            {
                id: 'barbarian',
                text: 'Barbarian',
                children: [
                    { id: 'beserker', text: 'Beserker' }
                ],
                id: 'soldier',
                text: 'Soldier',
                children: [
                    { id: 'knight', text: 'Knight' }
                ]
            }
        ]
    },
    {
        id: 'archer',
        text: 'Archer',
        children: [
            {
                id: 'ranger',
                text: 'Ranger',
                children: [
                    { id: 'elite-ranger', text: 'Elite Ranger' }
                ],
                id: 'paladin',
                text: 'Paladin',
                children: [
                    { id: 'bard', text: 'Bard' }
                ]
            }
        ]
    },
    {
        id: 'mage',
        text: 'Mage',
        children: [
            {
                id: 'wizard',
                text: 'Wizard',
                children: [
                    { id: 'warlock', text: 'Warlock' }
                ],
                id: 'druid',
                text: 'Druid',
                children: [
                    { id: 'artificer', text: 'Artificer' }
                ]
            }
        ]
    },
];

//Set up the Class Tree
$("#classtree1").append(classes1);
$('#classtree1').select2({
    multiple: false,
    placeholder: "Select class...",
    data: classes1,
    allowClear: true
});

// Change the values on first select
$("#classtree1").on("change", function () {
    //$("#classtree2 option[value]").remove();
    var selectedClassOne = $(this).val();
    $('#test1').text(selectedClassOne); //TESTING
    console.log(selectedClassOne);

    //var foundClassTwo = classes1.filter(item => item.parent === selectedClassOne).text;
    //$('#test2').text(foundClassTwo); //TESTING
  
    //$("#classtree2").append(newOptions).val("").trigger("change");
    //console.log(foundClassTwo);
    
    //console.log(classes1.find(item => item.id === selectedClassOne));
  });
#classes select {
  width:30%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>

<div id="classes" class="form-int">
  <h5>Classes</h5>
  <select id="classtree1" class="frm-select" name="class-1"></select>
  <select id="classtree2" class="frm-select" name="class-2"></select>
  <select id="classtree3" class="frm-select" name="class-3"></select>
  <div class="row">
    <div class="box">
      <h6>Class1</h6>
      <span id="test1"></span>
    </div>
    <div class="box">
      <h6>Class2</h6>
      <span id="test2"></span>
    </div>
    <div class="box">
      <h6>Class3</h6>
      <span id="test3"></span>
    </div>
  </div>
</div>


Solution

  • I finally figured it out after days of testing! So, I created my big array object to start off.

    Under stage 1 of the class tree I set the data: parameter to a custom function I wrote to only get the top level items and repost it into a dynamic array item.

    That allows me to only ever have to edit the singular array and it will always dynamically grab the top level items on page load. At this point stage 2 and 3 of the selects have their data parameters set to NULL so that they are blank until you select the first stage.

    On the change function of the first stage it removes options from stage 2 and 3 (refreshing), grabs the selected value and uses a find function to find the object that makes that selected value. After that, any options that are found are pushed into a new array for stage 2. On stage 2's change everything else is basically the same as prior. It works wonders and now I'll only ever have to modify the single array. You can check it out below.

    const classes1 = [{
        id: '',
        text: 'Select stage1...',
      },
      {
        id: 'fighter',
        text: 'Fighter',
        children: [{
          id: 'barbarian',
          text: 'Barbarian',
          children: [{
            id: 'beserker',
            text: 'Beserker'
          }],
          id: 'soldier',
          text: 'Soldier',
          children: [{
            id: 'knight',
            text: 'Knight',
            children: [
             {
               id: 'knight2',
               text: 'Knight 2'
             }
            ]
          }]
        }]
      },
      {
        id: 'archer',
        text: 'Archer',
        children: [{
          id: 'ranger',
          text: 'Ranger',
          children: [{
            id: 'elite-ranger',
            text: 'Elite Ranger'
          }],
          id: 'paladin',
          text: 'Paladin',
          children: [{
            id: 'bard',
            text: 'Bard',
            children: [
           {
            id: 'another-test',
            text: 'Another Test'
           }
           ]
          }]
        }]
      },
      {
        id: 'mage',
        text: 'Mage',
        children: [{
          id: 'wizard',
          text: 'Wizard',
          children: [{
            id: 'warlock',
            text: 'Warlock'
          }],
          id: 'druid',
          text: 'Druid',
          children: [{
            id: 'artificer',
            text: 'Artificer'
          }]
        }]
      },
    ];
    
    // Set up the chain select for Classes -----------------
    function getTopLevelClasses(data) {
        objects = [];
        for (i = 0; i < data.length; i++ ) {
            objects.push({id: classes1[i].id, text: classes1[i].text});
        }
        return objects;
    }
    
    //Stage 1 Initialize
    $("#classtree1").append(classes1);
    $('#classtree1').select2({
        multiple: false,
        placeholder: "Select stage1...",
        data: getTopLevelClasses(classes1),
        allowClear: true
    });
    //Stage 2 Initialize
    $('#classtree2').select2({
        multiple: false,
        placeholder: "Select stage1 first...",
        data: null,
        allowClear: true
    });
    // Stage 3 Initialize
    $('#classtree3').select2({
        multiple: false,
        placeholder: "Select stage1 first...",
        data: null,
        allowClear: true
    });
    $("#classtree1").on("change", function () {
        $("#classtree2 option[value]").remove();
        $("#classtree3 option[value]").remove();
        var selectedStageOne = $(this).val();
    
        // SET UP Stage 2
        var findStageTwo = classes1.find(item => item.id === selectedStageOne).children;
        let stageTwoOptions = [""];
        for (let h = 0; h < findStageTwo.length; h++) {
            stageTwoOptions.push(findStageTwo[h].text);
        }
    
        //Create the Options for Class 2
        $('#classtree2').select2({
            placeholder: "Select stage2...",
            data: stageTwoOptions
        });
    
        // SET UP Stage 3
        $("#classtree2").on("change", function () {
            var selectedStage2 = $(this).val();
    
            // Find matched Stages
            var foundStageThree = findStageTwo.find(item => item.text === selectedStage2).children;
            let stageThreeOptions = [""];
            for (let s = 0; s < foundStageThree.length; s++) {
                stageThreeOptions.push(foundStageThree[s].text);
            }
            //Create the Options for Class 3
            $('#classtree3').select2({
                placeholder: "Select stage3...",
                data: stageThreeOptions
            });
            
    
            $("#classtree3").on("change", function() {
                var selectedStageThree = $(this).val();
            });
        });    
      });
    select {
     width:100%;
     }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
    <div id="classes" class="form-int">
      <h5>Classes</h5>
      <select id="classtree1" class="frm-select" name="class-1"></select>
      <select id="classtree2" class="frm-select" name="class-2"></select>
      <select id="classtree3" class="frm-select" name="class-3"></select>
    </div>

    Just note: The array and object combo needs to be the same on every level but additional parameters can be added just fine without it messing anything up. However the id, text and children always need to be on matching levels. I left Mage broken to showcase this.

    I hope this helps anyone who might be looking to do something similar!