Dave van Toer
Dave van Toer

Reputation: 21

Create a button with a specific value to use this JS function

I've been searching for this solution for quite some time, although I've been trying to find solutions in anything but JS (since I'm very unfamiliar with it). I've stripped down some code so I can implement it.

Currently it has a search function which can filter based on the input in text you put in, but I'd like to create a link with preformatted text to use the same function.

So for instance, if I'd like to filter on "IS" without typing it, that there is a link/button one can use that uses the same function and give the same result. I can only find other JS examples that don't filter based on the input but only on specific criteria which, if it doesn't match exactly, returns without the other results.

This is the stripped code:

function myFunction() {
  var input, filter, table, tr, td, i, txtValue;
  input = document.getElementById("myInput");
  filter = input.value.toUpperCase();
  table = document.getElementById("myTable");
  tr = table.getElementsByTagName("tr");
  for (i = 0; i < tr.length; i++) {
    td = tr[i].getElementsByTagName("td")[0];
    if (td) {
      txtValue = td.textContent || td.innerText;
      if (txtValue.toUpperCase().indexOf(filter) > -1) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }       
  }
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
</style>
</head>
<body>


<table id="myTable">
  <tr>
    <th>Name</th>
    <th>Country</th>
  </tr>
  <tr>
    <td>Alfreds Futterkiste</td>
    <td>Germany</td>
  </tr>
  <tr>
    <td>Berglunds snabbkop</td>
    <td>Sweden</td>
  </tr>
  <tr>
    <td>Island Trading</td>
    <td>UK</td>
  </tr>
  <tr>
    <td>Koniglich Essen</td>
    <td>Germany</td>
  </tr>
  <tr>
    <td>Laughing Bacchus Winecellars</td>
    <td>Canada</td>
  </tr>
  <tr>
    <td>Magazzini Alimentari Riuniti</td>
    <td>Italy</td>
  </tr>
  <tr>
    <td>North/South</td>
    <td>UK</td>
  </tr>
  <tr>
    <td>Paris specialites</td>
    <td>France</td>
  </tr>
</table>

<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names.." title="Type in a name">
</body>
</html>

Upvotes: 0

Views: 55

Answers (2)

David Thomas
David Thomas

Reputation: 253485

One approach – with explanatory comments in the code – is below:

// to save typing, I cached a reference to the document element:
const D = document,
  // and created utility functions to reduce the amount of typing of 'document.querySelector...',
  // these work as simple aliases for the querySelector(All) functions, though getAll() does return
  // an Array instead of a NodeList, this may break expectations:
  get = (selector, context = D) => context.querySelector(selector),
  getAll = (selector, context = D) => [...context.querySelectorAll(selector)],
  // again, to reduce typing document.createElement(...):
  create = (tag) => D.createElement(tag),
  // named function to create the tag-cloud of <button> elements with which you want to filter
  // the <table>; this function accepts an opts Object to be passed in:
  createTagCloud = (opts = {}) => {
    // the default settings of the function:
    let settings = {
        // the element from which the tags will be formed from its contents:
        tagSource: get('table tbody'),
        // the class-name to add to the element containing the tag-cloud:
        wrapperClass: 'tagCloud',
        // the first required argument:
        // this argument must reference an existing element-node in the document, which will
        // contain the tag-cloud:
        outputNode: null,
        // the final required argument:
        // this argument takes a function that is to be called when clicking on the created
        // tag <button> elements:
        execute: null,
      },
      // creating a <button> element:
      button = create('button'),
      // creating a document-fragment:
      fragment = D.createDocumentFragment(),
      // defining two variables for later use:
      wrapper, clone;

    // using the Object.keys() method to return an Array of the keys of the opts Object (which
    // requires a minimum of two properties), and iterating over that Array using
    // Array.prototype.forEach():
    Object.keys(opts).forEach(
      // using an Arrow function, which passes one argument (a reference to the current key
      // of the Array of keys over which we're iterating) into the function body; in the
      // body of the function we're updating the property of the settings Object to be
      // equal to the property-value of the opts Object:
      (key) => settings[key] = opts[key]
    );

    // if we don't have a 
    if (!settings.outputNode) {
      // here we log a message to the console (I chose not to throw an Error because I didn't want to
      // completely break things):
      console.log(`%c Error: %c "outputNode" must be passed to the function, and specify a parent element into which the tag-cloud is to be inserted.`, "color: black; background-color: red; border-radius: 5px; font-weight: bold;", "all: unset;")
      return false;
    }
    // passing a value to the wrapper variable, so that it becomes a reference to the
    // DOM element-node which will hold the tag-cloud:
    wrapper = settings.outputNode;

    // if there is no settings.execute property-value:
    if (!settings.execute) {
      // we log the following message to the console:
      console.log(`%c Error: %c "execute" must specify a function to call in order to perform a search using the created tags..`, "color: black; background-color: red; border-radius: 5px; font-weight: bold;", "all: unset;")
    }

    // here we retrieve the text-content of the element which will serve as the 'haystack' to be searched, trim
    // leading/trailing white-space from that string of text and then split it on a sequence of one or more
    // white-spaces using String.prototpe.split() with a regular expression:
    let words = settings.tagSource.textContent.trim().split(/\s+/);

    // we iterate over the Array of words, using Array.prototype.forEach():
    words.forEach(
      // with an Arrow function that passes in a reference to the current word of the Array
      // of words over which we're iterating:
      (word) => {
        // we clone the <button> we created earlier, along with any descendants:
        clone = button.cloneNode(true);
        // we bind the named function - the property-value of the 'settings.execute' property that
        // was passed into the function - as the event-handler for the 'click' event:
        clone.addEventListener('click', settings.execute);
        // we set the value-property of the cloned <button>, and its text-content, to
        // the curent word:
        clone.value = word;
        clone.textContent = word;
        // we then append that cloned <button> to the document-fragment:
        fragment.append(clone);
      });

    // if a class-name was passed in to be assigned to the wrapper element:
    if (settings.wrapperClass) {
      // we use the Element.classList API to add that class:
      wrapper.classList.add(settings.wrapperClass);
    }

    // here we append the document-fragment to the wrapper:
    wrapper.append(fragment);
  },
  // the named function to handle the filtering and toggling of the <tr> elements
  // of the <table>; this takes on argument passed automatically from the later
  // use of the EventTarget.addEventListener() function:
  find = (evt) => {
    // we retrieve the value of the <button> that was fired:
    let needle = evt.currentTarget.value,
      // and we retrieve the <tr> elements from the <tbody> of the <table>:
      haystack = getAll('table tbody tr');

    // we use Array.prototype.forEach() to iterate over those <tr> elements:
    haystack.forEach(
      // passing a reference to the current <tr> to the function, in which
      // we again use the Element.classList API to toggle the 'hidden' class
      // on the current <tr> ('row').
      // if the current row does *not* include the current word ('needle'),
      // we invert the false (from String.prototype.includes()) using the
      // NOT operator ('!') which means the assessment becomes truthy which
      // in turn means that the class will be added to those elements which
      // do not contain the current word; this causes them to be hidden.
      // If the current <tr> does contain the 'needle', then the true (from
      // String.prototype.includes()) is inverted to false, so the class-name
      // is *not* added:
      (row) => row.classList.toggle('hidden', !row.textContent.includes(needle))
    )
  };

// calling the function:
createTagCloud({
  // passing in the required arguments, first the outputNode,
  // which is the <div> following the <table>:
  outputNode: get('table + div'),
  // and a reference to the function to be called when the
  // tags are clicked:
  execute: find,
});
*,
 ::before,
 ::after {
  box-sizing: border-box;
  font-family: Roboto, Montserrat, system-ui;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.4;
  margin: 0;
  padding: 0;
}

main {
  border: 1px solid #000;
  display: flex;
  margin-block: 1em;
  margin-inline: auto;
  padding: 0.5em;
  width: 70vw;
}

main>* {
  border: 1px solid currentColor;
  flex-basis: 50%;
  flex-grow: 1;
}

table {
  border-collapse: collapse;
}

thead th {
  border-block-end: 0.1em solid currentColor;
}

tbody tr {
  background-color: #fff;
}

tbody tr:nth-child(odd) {
  background-color: lightblue;
}

td {
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

button {
  padding-block: 0.25em;
  padding-inline: 0.5em;
}

.tagCloud {
  display: flex;
  align-content: start;
  flex-flow: row wrap;
  gap: 0.5em;
  justify-content: space-between;
  padding: 0.5em;
}

.hidden {
  opacity: 0.2;
}
<!-- wrapped all content in a <main> tag to identify the main content of the site: -->
<main>
  <table id="myTable">
  <!-- used a <thead> element to wrap the header rows that contain the <th> elements: -->
    <thead>
      <tr>
        <th>Name</th>
        <th>Country</th>
      </tr>
    </thead>
    <!-- using a <tbody> to wrap the content of the <table> to make it easier to create the
         tag-cloud: -->
    <tbody>
      <tr>
        <td>Alfreds Futterkiste</td>
        <td>Germany</td>
      </tr>
      <tr>
        <td>Berglunds snabbkop</td>
        <td>Sweden</td>
      </tr>
      <tr>
        <td>Island Trading</td>
        <td>UK</td>
      </tr>
      <tr>
        <td>Koniglich Essen</td>
        <td>Germany</td>
      </tr>
      <tr>
        <td>Laughing Bacchus Winecellars</td>
        <td>Canada</td>
      </tr>
      <tr>
        <td>Magazzini Alimentari Riuniti</td>
        <td>Italy</td>
      </tr>
      <tr>
        <td>North/South</td>
        <td>UK</td>
      </tr>
      <tr>
        <td>Paris specialites</td>
        <td>France</td>
      </tr>
    </tbody>
  </table>
  <!-- added a <div> to contain the created tag-cloud: -->
  <div></div>
</main>

JS Fiddle demo.

References:

Upvotes: 1

Diego D
Diego D

Reputation: 8196

You may have buttons next to the input for filtering the list. Each of them will call a common function filterBy(value) that will automatically populate the input text with the passed argument and will trigger its keyup event.

Here's a demo:

function myFunction() {
  var input, filter, table, tr, td, i, txtValue;
  input = document.getElementById("myInput");
  filter = input.value.toUpperCase();
  table = document.getElementById("myTable");
  tr = table.getElementsByTagName("tr");
  for (i = 0; i < tr.length; i++) {
    td = tr[i].getElementsByTagName("td")[0];
    if (td) {
      txtValue = td.textContent || td.innerText;
      if (txtValue.toUpperCase().indexOf(filter) > -1) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }       
  }
}

function filterBy(value){
  const target = document.querySelector('#myInput');
  target.value = value;  
  target.dispatchEvent(new Event('keyup'));
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
</style>
</head>
<body>


<table id="myTable">
  <tr>
    <th>Name</th>
    <th>Country</th>
  </tr>
  <tr>
    <td>Alfreds Futterkiste</td>
    <td>Germany</td>
  </tr>
  <tr>
    <td>Berglunds snabbkop</td>
    <td>Sweden</td>
  </tr>
  <tr>
    <td>Island Trading</td>
    <td>UK</td>
  </tr>
  <tr>
    <td>Koniglich Essen</td>
    <td>Germany</td>
  </tr>
  <tr>
    <td>Laughing Bacchus Winecellars</td>
    <td>Canada</td>
  </tr>
  <tr>
    <td>Magazzini Alimentari Riuniti</td>
    <td>Italy</td>
  </tr>
  <tr>
    <td>North/South</td>
    <td>UK</td>
  </tr>
  <tr>
    <td>Paris specialites</td>
    <td>France</td>
  </tr>
</table>

<div class="filter-container">
  <input
    type="text"
    id="myInput"
    onkeyup="myFunction()"
    placeholder="Search for names.."
    title="Type in a name">
  
  <button type="button" onclick="filterBy('IS');">IS</button>  
</div>

</body>
</html>

Upvotes: 2

Related Questions