Adding the UI event listeners

Summary

In this chapter, we will hook the UI event listeners up to our written functionality using the DisplayHelper.

Concepts

  • Using browser event listeners to call HC code


Use the ‘oninput event listener

Here, we’ll use the oninput event listener to link the DisplayFilter functionality to the slider UI elements and update the DOM to reflect user input.

If we take a look at the provided HTML template, we can determine the UI elements to be the four sliders (two sliders on each main category slider), the company buttons, the load model button, and the rendering options buttons. We have already implemented the load model button, so let’s focus on the other UI elements within the DisplayFilter.

When the user moves one of the sliders, we want the DisplayFilter class to update its filter values, gather the nodes that meet the newly set criterion, and render those nodes. We also want to protect for any UI quirks (such as setting the min slider higher than the max slider). For each slider, we will add an event listener, giving us four total event listeners for the sliders. However, each slider will enact the same series of calls, so we can abstract this out into its own main class member function, sliderOnInput().

sliderOnInput(slider) {
        this._displayFilter.updateSliderRange(slider);
        this._displayFilter.updateSliderLabels(slider);
        this._displayFilter.gatherFilteredNodes(this._modelData);
        this._displayFilter.setRenderingSelection();
}

The sliderOnInput function takes a filter label, indicating which slider it is working with. The first thing we do is update the slider range that is tracked by the DisplayFilter. We have not written this function yet, so let’s do so now.

updateSliderRange(id) {
        let sliderElement = document.getElementById(`${id}Slider`);
        let key = id + "Val";
        this._sliderVals.set(key, sliderElement.value);
}

Notice that the slider naming is done consistently, so we can identify everything we need to do only given the “stock” or “price” identifier.

Next, we need to update the UI to display the new value of the slider and handle any odd behavior that may occur if passing one slider over another.

updateSliderLabels(filter) {
        let id1 = filter.substring(0, 2) + "MinVal";
        let id2 = filter.substring(0, 2) + "MaxVal";
        let minVal = parseInt(this._sliderVals.get(id1));
        let maxVal = parseInt(this._sliderVals.get(id2));
        if (minVal >= maxVal) {
                minVal = maxVal - 1;
                this._sliders.get(id1).value = minVal.toString();
                this._sliderVals.set(id1, minVal.toString());
                return;
        }
        if (maxVal <= minVal) {
                maxVal = minVal + 1;
                this._sliders.get(id2).value = maxVal.toString();
                this._sliderVals.set(id2, maxVal.toString());
                return;
        }
        let valueLabels = document.querySelectorAll(`#${id1} , #${id2}`);
        valueLabels[0].innerHTML = minVal.toString();
        valueLabels[1].innerHTML = maxVal.toString();
}

First, we gather the stored values of the sliders from our object’s Map. We then compare the values to ensure minimums and maximums have not passed over one another. If they have, we adjust the other slider accordingly. We then take these values and update the DOM elements displaying their values.

The next two functions should look familiar, as we wrote them earlier when creating the DisplayFilter class. We will pass the application model data to the gatherFilteredNodes call to gather the nodes that meet the criterion, and then call setRenderingSelection to render our filtered nodes.

Finally, we have to pass the correct identifying filter label to the sliderOnInput function. Set up an oninput event listener for each slider handle, and pass the prefix of the handles ID to sliderOnInput. (For example, id="psMinSlider" would call sliderOnInput("psMin")). Be sure to include these event listeners in the setEventListeners() function, so they are called by the application constructor.

document.getElementById("psMinSlider").oninput = () => {
        this.sliderOnInput("psMin");
};
document.getElementById("psMaxSlider").oninput = () => {
        this.sliderOnInput("psMax");
};
document.getElementById("ssMinSlider").oninput = () => {
        this.sliderOnInput("ssMin");
};
document.getElementById("ssMaxSlider").oninput = () => {
        this.sliderOnInput("ssMax");
};

If we reload our application and load the moto model, moving the slider values will update the nodes meeting the rendering criterion, and the change will be displayed in the viewer.

Hookup company filter buttons

Here, we aill wire the company filter toggle buttons to give the user the ability to add and remove companies from the DisplayFilter.

Now that we have the slider UI elements working, let’s focus on the push buttons for the companies filter. By default, all the companies are selected. However, in between loading models, the state may not always be this way. So, we should gather selected companies whenever there is a change to them and keep them tracked with the DisplayFilter class. This is not unlike what we did earlier when we tracked the slider values – by tracking the companies in the DisplayFilter class, we minimize calls to the DOM.

We have already incorporated the initial gathering of companies selected in our DisplayFilter constructor, and we have incorporated this into our gatherFilteredNodes function. Therefore, we just need a way to add or remove companies tracked by our DisplayFilter. Let’s add these public methods to the DisplayFilter class.

addCompany(company) {
        this._companyFilter.push(company);
}
removeCompany(company) {
        let index = this._companyFilter.indexOf(company);
        this._companyFilter.splice(index, 1);
}

We can hook these public functions up to our UI elements in the main class. Let’s visit that now.

Since each company button is a toggle-like button, clicking on the button has one of two actions to perform. If it is selected, we want to remove the element when clicked again (making it unselected). If it is currently unselected, clicking the element should add it to the selected list. Once the companies list is updated in the DisplayFilter class, we want to make sure our filtered node list is up to date, and the rendering reflects any changes, so we must make calls to these functions too.

let compButtons = document.getElementById("companyFilter").getElementsByClassName("companyFilterButton");
for (let element of compButtons) {
        let htmlelement = element;
        htmlelement.onclick = () => {
                if (htmlelement.classList.contains("selected")) {
                        htmlelement.classList.remove("selected");
                        this._displayFilter.removeCompany(htmlelement.alt);
                }
                else {
                        htmlelement.classList.add("selected");
                        this._displayFilter.addCompany(htmlelement.alt);
                }
                this._displayFilter.gatherFilteredNodes(this._modelData);
                this._displayFilter.setRenderingSelection();
        };
}

Setup the event listeners

Finally, we will hook up the rendering selection buttons. When the user clicks on one of the radio button groups (whether it be the rendering mode or gradient mode), the DisplayFilter will need to update its rendering mode and gradient selections. We can do this by making a public setter in the DisplayFilter class for these two properties.

setFilterSelection(filter) {
        this._filterSelection = filter;
}
setGradientSelection(choice) {
        this._gradientSelection = choice;
}

And again, let’s set up the event listeners for these radio groups. Remember that we need to gather nodes and update the rendering on each of these actions too.

document.getElementsByName("displaymode").forEach((element) => {
        let inputElement = element;
        inputElement.onclick = () => {
                this._displayFilter.setFilterSelection(inputElement.id);
                this._displayFilter.gatherFilteredNodes(this._modelData);
                this._displayFilter.setRenderingSelection();
        };
});
document.getElementsByName("gradientmode").forEach((element) => {
        let inputElement = element;
        inputElement.onclick = () => {
                this._displayFilter.setGradientSelection(inputElement.id);
                this._displayFilter.setRenderingSelection();
        };
});

Now as you click through each of these modes, you can see the rendering update in the viewer.

With this, all pieces of your tutorial should be complete. Feel free to build upon this example or experiment with additional APIs to further your learning.