#############################
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()``.


.. code-block:: js

	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.

.. code-block:: js

	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.

.. code-block:: js

	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.

.. code-block:: js

	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.

.. video:: ../../_assets/videos/sliderFilterAction.mp4
	

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.

.. code-block:: js

	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.

.. code-block:: js

	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();
		};
	}

.. video:: ../../_assets/videos/companyFilterAction.mp4
	

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.

.. code-block:: js

	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.

.. code-block:: js

	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.

.. video:: ../../_assets/videos/gradientSelectionAction.mp4

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. 

