Home > Software design >  HTML page with multiple selection text changing according to previous choices
HTML page with multiple selection text changing according to previous choices

Time:02-05

I am looking to write an HTML code similar to the one below:

<!DOCTYPE html>
<html>
<body>

<p id="text">This is some text with a few <select id="mySelect1">
  <option value="word1">Word 1</option>
  <option value="word2">Word 2</option>
</select> that can be changed and a <select id="mySelect2">
  <option value="word3">Word 3</option>
  <option value="word4">Word 4</option>
</select> that can be replaced too</p>

<button onclick="displayText()">Display Text</button>

<script>
function displayText() {
  var select1 = document.getElementById("mySelect1");
  var selectedOption1 = select1.options[select1.selectedIndex].value;
  
  var select2 = document.getElementById("mySelect2");
  var selectedOption2 = select2.options[select2.selectedIndex].value;
  
  var text = "This is some text with a few "   selectedOption1   " that can be changed and a "   selectedOption2   " that can be replaced too";
  var newPage = window.open();
  newPage.document.write(text);
}
</script>

</body>
</html>

But the text and option will change based on choices made in previous selection boxes. E.g. if I choose in the first selection box option 2 then the following sentences and choices will be changed accordingly. It should be some kind of flow-chart in the background to "program" the form behavior.

Any idea how can I do that?

CodePudding user response:

Styling

Via CSS

A CSS-only solution should exist by using the relative selector :has() and combinators. This would remove the need for any JS for styling.

Personally, I suggest to use CSS for styling where possible.

Example of using :has():

#p1:has([value="no"]:checked)   #p2 {
  display: none;
}
<p id="p1">
  Show <code>#p2</code>?
  <select id="select1">
    <option value="yes">Yes</option>
    <option value="no">No</option>
  </select>
</p>
<p id="p2">
  This is <code>#p2</code>.
</p>
<p id="p3">
  This is <code>#p3</code>, usually following <code>#p2</code>.
</p>

Via JavaScript

By using event delegation (see below) we can write an input listener to update styles, so that the visible content reflects the content of the new page. Example:

const commonAncestor = document.body;

commonAncestor.addEventListener("input", () => {
  p2.toggleAttribute("hidden", select1.value !== "Yes");
});
<p id="p1">
  Activate <code>#p2</code>?
  <select id="select1">
    <option>Yes</option>
    <option>No</option>
  </select>
</p>
<p id="p2">
  This is <code>#p2</code>.
</p>
<p id="p3">
  This is <code>#p3</code>, usually after <code>#p2</code>.
</p>

You may want to update the styles initially, which has to be done after the (relevant) DOM content has loaded:

const commonAncestor = document.body;
commonAncestor.addEventListener("input", updateStyles);

// Using "DOMContentLoaded" should always work:
addEventListener("DOMContentLoaded", updateStyles);

function updateStyles() {
  p2.toggleAttribute("hidden", select1.value !== "Yes");
}
<p id="p1">
  Activate <code>#p2</code>?
  <select id="select1">
    <!--Notice <select>'s initial value:-->
    <option>Yes</option>
    <option selected>No</option>
  </select>
</p>
<p id="p2">
  This is <code>#p2</code>.
</p>
<p id="p3">
  This is <code>#p3</code>, usually after <code>#p2</code>.
</p>

Event delegation

We can handle the input events of all <select>s in a single listener via event delegation:

const commonAncestor = document.body;

commonAncestor.addEventListener("input", evt => {
  // Handle input events; example: (Log event details)
  const message = `Target ${evt.target.id} changed to value: ${evt.target.value}`;
  console.log(message);
});
<select id="select1">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>
<select id="select2">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

Excursus: Named objects

Elements with IDs are named objects. The DOM API provides a way to access named objects as globals:

console.log(div1);
console.log(div2); // Doesn't exist
<div id="div1"></div>

But notice this note in the specification:

As a general rule, relying on this will lead to brittle code. Which IDs end up mapping to this API can vary over time, as new features are added to the web platform, for example. Instead of this, use document.getElementById() or document.querySelector().

So to have non-brittle code, you should not rely on named objects to be accessible like this. Instead, you can query for them one by one, and assign each to a variable.

For brevity of my code snippets only, I chose to dismiss this warning and use named objects.

Alternatively, instead on relying on this functionality to exist on window, you can mock this functionality on any other object with Proxy:

const elements = new Proxy({}, {
  get(target, prop, receiver) {
    return document.getElementById(prop);
  }
});

console.log('As "named objects":');
console.log("-Text of p1:", elements.p1.textContent);
console.log("-Text of p2:", elements.p2.textContent);
console.log("-Text of p3:", elements.p3?.textContent);
<p id="p1">First paragraph.</p>
<p id="p2">Second paragraph.</p>

Or define a helper function, if the only benefit you are after is short code:

function byId(id) {
  return document.getElementById(id);
}

console.log('As "named objects":');
console.log("-Text of p1:", byId("p1").textContent);
console.log("-Text of p2:", byId("p2").textContent);
console.log("-Text of p3:", byId("p3")?.textContent);
<p id="p1">First paragraph.</p>
<p id="p2">Second paragraph.</p>

Get final text

To get the final text we can use Element.innerText, which is styling-aware. That means you only get the visible content; the content we want:

const commonAncestor = document.getElementById("common-ancestor");
commonAncestor.addEventListener("input", updateStyles);
addEventListener("DOMContentLoaded", updateStyles);

function updateStyles() {
  p2.toggleAttribute("hidden", select1.value !== "Yes");
}

const button = document.querySelector("button");
button.addEventListener("click", () => {
  // Clone text container
  const clone = commonAncestor.cloneNode(true);
  
  // Replace <select>s with their selected option
  const selectElements = clone.querySelectorAll("select");
  selectElements.forEach(clonedSelect => {
    const select = document.getElementById(clonedSelect.id);
    
    // Use either as replacement
    const selectedValue = select.value;
    const selectedLabel = select.selectedOptions[0].textContent;
    
    clonedSelect.replaceWith(selectedValue);
  });
  
  // Get style-aware content
  document.body.append(clone);
  const text = clone.innerText;
  clone.remove();
  
  // Use content
  console.log("Text:", text);
});
<div id="common-ancestor">
  <p id="p1">
    Show <code>#p2</code>?
    <select id="select1">
      <option>Yes</option>
      <option selected>No</option>
    </select>
  </p>
  <p id="p2">
    This is <code>#p2</code>.
  </p>
  <p id="p3">
    This is <code>#p3</code>, usually after <code>#p2</code>.
  </p>
</div>

<button type="button">Log text</button>

Sidenote: When styling via CSS, you would have to stringify the paragraphs and replace the <select>s in reverse tree-order, because you specified that no elements prior to but only following a <select> are affected.

When styling via JS (as in the example above), you shouldn't have any problems, since the elements have their styles inlined.

Explanation

Unfortunately, a stringified <select> includes all its options, so we have to replace the <select> with its selected option manually (e.g. with Element.replaceWith()).

To not destroy our DOM, we can work on a cloned subtree via Node.cloneNode().

Cloning a node clones its "HTML representation", which for <select>s doesn't include the user-selection. Since the cloned and original <select> should select the same option, we can get the expected selected option by the original's selectedIndex (or similar). We can get the corresponding original <select> via its ID, since the clone has the same.

The content getter Element.innerText is only style-aware if the element is part of a DOM tree. This means we have to briefly append it to the DOM to work correctly.

  • Related