javascripthtmldomlit-elementlit-html

LIT-HTML still use the previous dropdown selected index when rendering a dropdown


I am using Lit-HTML ( NOT Lit-Element ) to build a html form. The form is very simple, it has 2 dropdowns: Population dropdown and Animal dropdown.

The animal dropdown options depends on the selection of the Population dropdown. See the getAnimalDS() logic.

Model Definition & Utility function:

const model = {
    Population: null,
    Animal: null,
}

const dataSources = {
    Populations: [null, "p1", "p2"],
    Animals_1:   [null, "a1", "a2"],
    Animals_2:   [null, "a3", "a4", "a5"],
}

function getAnimalDS(population) {
    switch (population) {
        case 'p1':
            return dataSources.Animals_1;
        case 'p2':
            return dataSources.Animals_2;
        default:
            return [];
    }
}

The main function to build LIT-HTML template:

function buildSection1(model) {
    return html`
    
    <div class="mb-3">
        <label for="Population" class="form-label">Population</label>
        <select id="Population" name="Population" class="form-control"
            .value="${model.Population}" @change=${e => {
                            model.Population = e.target.value;
                            model.Animal = null; // Reset Animal
                            renderSection1(); // Re-render the form
                     }}>

            ${dataSources.Populations.map((item) => html`
                    <option value="${item}" ?selected="${model.Population === item}">${item}</option>
            `)}
        </select>
    </div>

    <div class="mb-3">
        <label for="Animal" class="form-label">Animal</label>
        <select id="Animal" name="Animal" class="form-control"
            .value="${model.Animal}" @change=${e => model.Animal = e.target.value}>

            ${getAnimalDS(model.Population).map((item) => html`
                    <option value="${item}" ?selected="${model.Animal === item}">${item}</option>
            `)}
        </select>
    </div>

    `;
}

TESTING:

If I select a population on the Population dropdown, I see the options on the Animal get changed correctly -> Awesome.

STEPS to see issue:

#1: Select option "p1" on the Population dropdown, I see 3 options: "", 'a1', 'a2' on the Animal dropdown -- Good

#2: Next, I select 'a2' on the Animal dropdown ( selected index = 2) -> Good

#3: Now, select option "p2" on the Population dropdown, I see 4 options: "", 'a3', 'a4', 'a5' on the Animal dropdown -> Good

#4: At this point, I expect to see 'EMPTY' line (selected index = -1) on the Animal dropdown , because on @change of the Population, I did reset model.Animal to null.

---- BUT the Animal dropdown shows option 'a4' ( selected index = 2 )

---- It means LIT-HTML uses the previous selected index = 2 at step #2.

Any comments? Thanks!


Solution

  • The reason for this behavior is because lit-html is not aware that the value for the 2nd <select id="Animal"> element has changed, because the @change event handler changes model.Animal to 'a2' but does not call on render() again. Since lit-html thinks the value is still null, it doesn't bother updating the DOM when re-rendering.

    Solution 1: You can fix this by adding renderSection1() call there as well like:

    html`
      ...
      <select id="Animal" name="Animal" class="form-control"
        .value="${model.Animal}"
        @change=${e => {
          model.Animal = e.target.value;
          renderSection1();
        }}>
      ...
      </select>`
    

    which makes lit-html aware that the value has been updated. So when the first dropdown changes the value back to null it knows it's different again and update the DOM.

    Solution 2: Alternatively, you can also make lit-html always check the value against the DOM value by using the live() directive:

    import {live} from 'lit-html/directives/live.js';
    
    html`
      ...
      <select id="Animal" name="Animal" class="form-control"
        .value="${live(model.Animal)}" // always compare against DOM
        @change=${e => model.Animal = e.target.value}>
      ...
      </select>`