I watched a tutorial on how to use Javascript with Autocomplete using a JSON file and Fetch. Everything works fine; except for the following:
If I clear the input, it shows all of the results in the file. I have included a comment in the code where I try to clear the results but it doesn't work.
The example on JSFiddle doesn't work because I can't add any assets.
Here is the code that should be clearing the data when no characters are in the input box:
if (matches.length === 0) {
matchList.innerHTML = ''; // Line 31: This doesn't clear the results when no input is entered.
}
But in the CSS field, I have hard coded some of the JSON file for your reference.
Thanks in advance,
Matt
using onChange() you can check final length of the keyword written in input tag, and for NULL you can just ignore the suggestion.
I played around with the code and researched it. I had to separate the code into two events. The one that was missing was when the DOM is loaded, then grab a list of states. Here is the revised code:
const search = document.getElementById('search');
const matchList = document.getElementById('match-list');
let states;
// Get states
const getStates = async () => {
const res = await fetch('states.json');
states = await res.json();
};
// FIlter states
const searchStates = (searchText) => {
// Get matches to current text input
let matches = states.filter((state) => {
const regex = new RegExp(`^${searchText}`, 'gi');
return state.name.match(regex) || state.abbr.match(regex);
});
// Clear when input or matches are empty
if (searchText.length === 0) {
matches = [];
matchList.innerHTML = '';
}
outputHtml(matches);
};
// Show results in HTML
const outputHtml = (matches) => {
if (matches.length > 0) {
const html = matches
.map(
(matt) => `<div class="card card-body mb-1">
<h4>${matt.name} (${matt.abbr})
<span class="text-primary">${matt.capital}</span></h4>
<small>Lat: ${matt.lat} / Long: ${matt.long}</small>
</div>`
)
.join('');
matchList.innerHTML = html;
}
};
window.addEventListener('DOMContentLoaded', getStates);
search.addEventListener('input', () => searchStates(search.value));
Related
I've been learning JavaScript for only one week so please be gentle! Tried searching for an answer but no luck.
I am writing a names generator and I want the user to click on a button "girlButton" to see a random girl name, same with boys. The problem is that it is generating one name for each button and to see another name, I need to refresh the page. I want to be able to smash the buttons and see a different name every time. I am pretty sure it's something basic, but I am stuck so please could you help? :) I am not pasting Arrays because they are pretty long. Thanks!
const girlsNamesArray = girlsNames.split(' ');
console.log(girlsNamesArray);
const boysNamesArray = boysNames.split(' ');
console.log(boysNamesArray);
let randomGirl = girlsNamesArray[Math.floor(Math.random() * girlsNamesArray.length)];
const girlButton = document.querySelector('.names__button--girl');
let randomBoy = boysNamesArray[Math.floor(Math.random() * boysNamesArray.length)];
const boyButton = document.querySelector('.names__button--boy');
let proposedName = document.querySelector('.names__idea');
girlButton.addEventListener('click', e => proposedName.innerText = `Nazwij córkę ${randomGirl.slice(0, -1)}`);
boyButton.addEventListener('click', e => proposedName.innerText = `Nazwij syna ${randomBoy.slice(0, -1)}`);
I want to be able to hit buttons multiple times and see different values from the array.
Please try with following code block
const girlsNamesArray = girlsNames.split(' ');
console.log(girlsNamesArray);
const boysNamesArray = boysNames.split(' ');
console.log(boysNamesArray);
const girlButton = document.querySelector('.names__button--girl');
const boyButton = document.querySelector('.names__button--boy');
let proposedName = document.querySelector('.names__idea');
girlButton.addEventListener('click', e => {
const string = girlsNamesArray[Math.floor(Math.random() * girlsNamesArray.length)];
proposedName.innerText = `Nazwij córkę ${string}`
});
boyButton.addEventListener('click', e => {
const string = boysNamesArray[Math.floor(Math.random() * boysNamesArray.length)];
proposedName.innerText = `Nazwij syna ${string}`
});
Here you have to write the logic of random inside the event binding.
When a button is clicked, all it does right now is set the innerText of the proposedName element. It doesn't run the random number generator again. So you need to group the code to re-run it:
let girlsNames = "gloriax deborahx fayex"
let boysNames = "jackx petex stefanx"
const girlsNamesArray = girlsNames.split(' ');
console.log(girlsNamesArray);
const boysNamesArray = boysNames.split(' ');
console.log(boysNamesArray);
let randomGirl;
const girlButton = document.querySelector('.names__button--girl');
let randomBoy;
const boyButton = document.querySelector('.names__button--boy');
let proposedName = document.querySelector('.names__idea');
function selectNewNames() {
randomBoy = boysNamesArray[Math.floor(Math.random() * boysNamesArray.length)];
randomGirl = girlsNamesArray[Math.floor(Math.random() * girlsNamesArray.length)];
}
girlButton.addEventListener('click', e => {selectNewNames(); proposedName.innerText = `Nazwij córkę ${randomGirl.slice(0, -1)}`});
boyButton.addEventListener('click', e => {selectNewNames(); proposedName.innerText = `Nazwij syna ${randomBoy.slice(0, -1)}`});
<button type="button" class="names__button--girl">New girl</button>
<button type="button" class="names__button--boy">New boy</button>
<h2 class="names__idea"></h2>
Firstly all the best for your journey.To achieve this, you can move the logic for generating random names inside the event listeners so that every time the button is clicked, a new random name is generated .The code for the same will be something like this.
const girlButton = document.querySelector('.names__button--girl');
const boyButton = document.querySelector('.names__button--boy');
const randomIndex = (arr) = >{
return Math.floor(Math.random() * arr.length);
}
girlButton.addEventListener('click', e = >{
let randomGirl = girlsNamesArray[randomIndex(girlsNamesArray)];
proposedName.innerText = `Nazwij córkę$ {
randomGirl.slice(0, -1)
}`;
});
boyButton.addEventListener('click', e = >{
let randomBoy = boysNamesArray[randomIndex(boysNamesArray)];
proposedName.innerText = `Nazwij syna $ {
randomBoy.slice(0, -1)
}`;
});
`
I got this these values.
And I want to have this result.
So I made the following test code and tried it to the first cell.
function test2() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName("richText3");
const range1 = sheet.getRange("A1");
const text1 = range1.getValue();
Logger.log(text1);
const re = new RegExp(/\([ a-zA-Z\/']*\)\?/dg);
const redBold = SpreadsheetApp.newTextStyle().setBold(true).setForegroundColor('red').build();
let array;
while ((array = re.exec(text1)) !== null) {
const [start, end] = array.indices[0];
const richTxtValBlder = SpreadsheetApp.newRichTextValue()
.setText(text1)
.setTextStyle(start, end, redBold)
.build();
range1.setRichTextValue(richTxtValBlder);
}
}
After first try, I got this result.
I checked the Reference Document again and I found this comment.
setText(text) : Sets the text for this value and clears any existing text style.
When creating a new Rich Text value, this should be called before setTextStyle()
I found that I should call .setText() once and call .setTextStyle() multiple times.
But the problem is .setTextStyle() should be called programmatically according to the number of patterns in each cell and I cannot find how to do it programmatically.
Each cell may have 0 to 10 patterns and I don't want to make 10 different richTExtValueBuilder which only differ in the number of .setTextStyle() calls.
Do you have any different ideas ?
Modification points:
In your script, only cell "A1" is used, and also the 1st match is used. I thought that this might be the reason for your issue.
In order to achieve your goal, I retrieve the values from column "A". And also, I use matchAll instead of exec.
When these points are reflected in your script, how about the following modification?
Modified script:
function test2() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName("richText3");
const range1 = sheet.getRange("A1:A" + sheet.getLastRow());
const re = new RegExp(/\([ a-zA-Z\/']*\)\?/g);
const redBold = SpreadsheetApp.newTextStyle().setBold(true).setForegroundColor('red').build();
const richTextValues = range1.getRichTextValues();
const res = richTextValues.map(([a]) => {
const array = [...a.getText().matchAll(re)];
if (array) {
const temp = a.copy();
array.forEach(a => temp.setTextStyle(a.index, a.index + a[0].length, redBold));
return [temp.build()];
}
return [a];
});
range1.setRichTextValues(res);
}
Testing:
When this script is run, the following result is obtained.
From:
To:
References:
map()
setRichTextValues(values)
It looks like you need to call .setText() once, .setTextStyle() multiple times, and .build() once, e.g. change your while loop. Untested code:
let richTxtValBlder = SpreadsheetApp.newRichTextValue().setText(text1);
while ((array = re.exec(text1)) !== null) {
const [start, end] = array.indices[0];
richTxtValBlder = richTxtValBlder.setTextStyle(start, end, redBold);
}
richTxtValBlder = richTxtValBlder.build();
range1.setRichTextValue(richTxtValBlder);
I came across the following topic, it just has 1 line instead of 2 columns.
How do I return the second value here (see topic below)
Compare my variable with a csv file and get the matching value in javascript
This is my CSV file values:
csv screenshot of columns
This is what I have currently
IT just checks the file for the serial number from the user and marks the div with text "Valid".
This Valid should have the second Columns value.
<script>
const checkm = document.getElementById('check');
checkm.addEventListener('click', serialChecker)
async function serialChecker(){
const url = 'http://localhost/validator/serials.csv';
const response = await fetch(url);
// wait for the request to be completed
const serialdata = await response.text();
console.log(serialdata);
const inputserialnumber = document.getElementById('serialnumber').value.toString();
console.log(inputserialnumber);
// serialdata.match(/inputserialnumber/)
// serialdata.includes(inputserialnumber)
if(serialdata.includes(inputserialnumber) == true && inputserialnumber.length == 7 ){
document.getElementById('validity').innerHTML = "Valid";
startConfetti(); // from confetti.js
}else {
document.getElementById('validity').innerHTML = "Invalid";
stopConfetti(); // from confetti.js
}
//document.getElementById('test').innerHTML = "Valid";
}
</script>
This is my console output
It shows the full csv(currently), & the users input
changed the csv data into to different arrays if that helps:
array
& Thanks all in advance for taking the time to reply to my silly question!
EXTRA Clarification:
What I'm trying to do is a validate website checker.
So the user inputs their serial through an simple input field. & I have the serials in a csv file with an extra column that has the name matching to the serial.
So if the user inputs 1234567 it is present in the CSV file, my current code returns value = true for that. as it is present in the CSV file.
But I want it to return the value next to 1234567 (so in the second Column) instead, in this case "test1". So I can use that value instead of just a standard "Valid" text to be pushed back onto the website.
You can match values of two arrays by their index. In your case, I think it's easiest to use Array.map() to return a transformed array based on the one you loop trough. So for example, if you have two arrays called namesArray and valuesArray, do the following:
const validationResults = valuesArray.map((value, index) => {
return {
valid: checkValidity(value), // or whatever your validation function is called
name: namesArray[index] // match the index of the namesArray with the index of this one (valuesArray)
};
// or `return namesArray[index] + ', valid: ' + checkValidity(value)`
});
This loops through the valuesArray, and validationResults will then be an array of what you return per each item in the map function above.
One important note is that this assumes the arrays are both in the same order . If you want to sort them, for instance, do this after this.
Looking up and registering the values in a Map seems like the best answer.
// ...
const serialdata = await response.text();
const seriallookup = new Map();
// Set all Serial values to Names
for (let s in serialdata.split("\n")) {
let data = s.split(',');
seriallookup.set(data[0], data[1]);
}
Using this, checking for a serial's existance could be done with .has()
if (inputserialnumber.length == 7 && seriallookup.has(inputserialnumber)) {
And set to the elements text using
document.getElementById('validity').innerHTML = serialdata.get(inputserialnumber);
If the .csv file most likely wouldn't change between multiple requests (or if you only send just one request), you should probably initialize and request the data outside of the function.
Thank you all for the feedback.
I have not been able to use your suggestions exactly as intended.
But I managed to combine the idea's and create a new piece that does the trick for me!
const checkm = document.getElementById('check');
checkm.addEventListener('click', serialChecker)
async function serialChecker(){
const url = 'http://localhost/validator2/naamloos.csv';
const response = await fetch(url);
// wait for the request to be completed
const serialdata = await response.text();
const table = serialdata.split('\r\n');
const serialsArray = [];
const nameArray = [];
table.forEach(row =>{
const column = row.split(',');
const sArray = column[0];
const nArray = column[1];
serialsArray.push(sArray);
nameArray.push(nArray);
})
var array1 = serialsArray,
array2 = nameArray,
result = [],
i, l = Math.min(array1.length, array2.length);
for (i = 0; i < l; i++) {
result.push(array1[i], array2[i]);
}
result.push(...array1.slice(l), ...array2.slice(l));
function testfunction(array, variable){
var varindex = array.indexOf(variable)
return array[varindex+1]
}
//calling the function + userinput for serial
const inputserialnumber = document.getElementById('serialnumber').value.toString();
console.log(testfunction(result, inputserialnumber))
if(serialsArray.includes(inputserialnumber) == true && inputserialnumber.length == 7 ){
document.getElementById('validity').innerHTML = "Valid " + testfunction(result, inputserialnumber);
startConfetti();
}else {
document.getElementById('validity').innerHTML = "Invalid";
stopConfetti();
}
}
Hope this can help someone out in having an input field on their website with a .csv file in the backend (possible to have multiple for the user to select with a dropdown box with the async function).
This will check the file & will return the value from the csv that matches the serial!(based on serial number & length of the serial number(7characters))
I am unable to turn off auto suggestions if the input field value doesn't match the pulled 'suggestions' anymore. I am getting data from a json file.
Example, 'Piranha' is a valid entry in the json file, so if i start typing P.I.R... it correctly shows 'Piranha' as a suggestion. But it keeps showing it even if I go beyond the letters and type something like 'Piranhat' (image attached for reference).
Code:
const specieInput = document.querySelector('#specie_input');
const suggestions = document.querySelector('.suggestions');
const searchSpecies = async searchText => {
let my_data = networkJSON.full_data;
let matches = my_data.filter(my_data => {
const regex = new RegExp(`^${searchText}`, 'gi');
return my_data.specie.match(regex);
});
if (searchText.length === 0){
matches = [];
suggestions.innerHTML = '';
}
outputHTML(matches);
};
//Show results in HTML
const outputHTML = matches => {
if(matches.length > 0) {
const html = matches.map(match => `
<div class="card card-body">
<h4>${match.specie} - ${match.rate}</h4>
</div>
`
).join('');
suggestions.innerHTML = html;
}
}
specieInput.addEventListener('input', () => searchSpecies(specieInput.value));
you are not resetting the html when 0 matches found.
if (matches.length === 0) {
suggestions.innerHTML = '';
}
or you can also try to reset it at the first line of searchSpecies method
The autocomplete script:
const search = document.getElementById('search');
const matchList = document.getElementById('match-list');
let states;
// Get states
const getStates = async () => {
const res = await fetch('../complete/data/pictures.json');
states = await res.json();
};
// FIlter states
const searchStates = searchText => {
// Get matches to current text input
let matches = states.filter(state => {
const regex = new RegExp(`^${searchText}`, 'gi');
return state.title.match(regex);
});
// Clear when input or matches are empty
if (searchText.length === 0) {
matches = [];
matchList.innerHTML = '';
}
outputHtml(matches);
};
// Show results in HTML
const outputHtml = matches => {
if (matches.length > 0) {
const html = matches
.map(
match => `<div class="card card-body mb-1">
<h4>${match.title}</h4>
</div>`
)
.join('');
matchList.innerHTML = html;
}
};
window.addEventListener('DOMContentLoaded', getStates);
search.addEventListener('input', () => searchStates(search.value));
The code above generates autocomplete HTML suggestions. I want to add clickable functionality which would insert a clicked html element into the search bar.
The closest I've gotten is this:
matchList.addEventListener('click', () => {search.value = matchList.textContent.trim()})
While this works if there's only one suggestion, if there's more, all the suggestions are inserted together.
The problem seems to be the fact that matchList is an object which returns a single HTML element.
How do I return an object which I can iterate over and place an onclick on every HTML suggestion within that object?
Or, how do I return multiple objects, each containing one HTML suggestion on which I can then place an onclick?
Or some third option?
matchList.addEventListener('click', e => search.value = e.target.textContent.trim())