Can I use function generator as event handler? - javascript

I have a list of HTMLCollection:
<div class="demo">Access me by class[1]</div>
<div class="demo">Access me by class[2]</div>
<div class="demo">Access me by class[3]</div>
<div class="demo">Access me by class[4]</div>
And I have a script of JS:
var getElements = document.getElementsByClassName("demo");
const generatorObject = generatorFunction();
function* generatorFunction(e) {
for (i = 0; i < getElements.length; i++) {
yield getElements[i];
}
}
generatorObject.next(); // each time pressed a key down will invoke this line
// after invoking 6th time it will give {value: undefined, done: done}
My goal is to write a keyboardEvent based on .addEventListener("keydown", generatorFunction) method whereby event handler would be presented as function generator i.e. generatorFunction presented above. Is it good or bad practice?

Using a generator function directly as an event callback wouldn't make any sense because calling the function wouldn't execute its body, it would generate and return (to nowhere) an iterator.
Instead you'd need to wrap it in another function which talks to the iterator. I'm not sure exactly what your particular use case is (you talk about keydown events, but don't say on what element). But here's a general setup - in this example I'm yielding a number from an array on each keypress. On the final number, done is set to true.
function* generator() {
let toYield = [1, 2, 3, 4];
for (let i=0; i<toYield.length-1; i++) yield toYield[i];
return toYield.pop(); //return final item in array, which sets iterator to done
}
const iterator = generator();
document.querySelector('input').addEventListener('keydown', evt => {
let yielded = iterator.next();
console.log(yielded);
});
Fiddle

Did a bit of coding with reference which I am very thankful for #Mitya
var toYield;
function* generator() {
toYield = []; // five iterable yieldedObject(s) will be pushed
var getElements = document.getElementsByClassName("demo");
for (let i=0; i<getElements.length; i++)
yield toYield.push(getElements[i]);
}
const iterator = generator();
var yieldedObject;
window.addEventListener('keydown', ()=> {
yieldedObject = iterator.next();
console.log(yieldedObject); // yieldedObject has value:xyz/undefined and flag: false/done
colorizeIt();
});
function colorizeIt() {
for (const items of toYield){
switch(true) {
case yieldedObject.value >= 0:
items.style.backgroundColor = "yellow";
break;
default: items.style.backgroundColor = "white";
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessing Elements in the DOM</title>
<style>
html { font-family: sans-serif; color: #333; }
body { max-width: 500px; margin: 0 auto; padding: 0 15px; }
div { padding: 10px; margin: 5px; border: 1px solid #5e41ff; }
</style>
</head>
<body>
<h1>Simple keyboard event via event generator</h1>
<div class="demo">Access me by class[0]</div>
<div class="demo">Access me by class[1]</div>
<div class="demo">Access me by class[2]</div>
<div class="demo">Access me by class[3]</div>
<div class="demo">Access me by class[4]</div>
</body>
</html>
All worked just fine! Thanks a lot!

Related

Trying to filter 4 different houses from an api

I'm trying to separate characters based on what house they belong to in the API (http://hp-api.herokuapp.com/api/characters)
I have tried using .filter and .map, but have been unable to achieve that goal, I don't know if this is the right place to ask for help understanding how to achieve my goal.
Here is the code:
const studentArray = [];
async function getStudents(url) {
const student = await fetch(url);
const jsondata = await student.json();
jsondata.forEach((student) => {
studentArray.push(student);
});
}
getStudents("http://hp-api.herokuapp.com/api/characters/students").then(() => {
});
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/testing/script.js"></script>
<link rel="stylesheet" href="/testing/styles.css" />
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<main>
<div id="name" class="container"><button onclick="Gryffindor">Test</button></div>
<div id="name" class="container"><button onclick="Slytherin">Test</button></div>
<div id="name" class="container"><button onclick="Ravenclaw">Test</button></div>
<div id="name" class="container"><button onclick="Hufflepuff">Test</button></div>
</main>
</body>
</html>
I've just had a look here and there seems to already be an endpoint set up to get characters from particular houses if that's what you want to do?
E.g. fetch(http://hp-api.herokuapp.com/api/characters/house/gryffindor) will return an array of students in Gryffindor.
You could refactor your getStudents function to take the house as an argument and make a GET request to the http://hp-api.herokuapp.com/api/characters/house/:house endpoint, using a template literal.
Also your onClick isn't invoking any function. I suggest you have a look here for an example of how to use onClick
One possible approach, and especially for the chosen API, is to fetch all student-data exactly once.
From this data one can create an own index of house-specific student-lists. For the chosen API one would realize that there are actually students listed with no relation/link into any house. And the API itself does not provide students which are not associated with a house.
Thus, the task which does render the house-based filter-items can take this additional information (no house) as much into account as the option of displaying any student regardless of the house (all students).
The next step would initialize the filter handling which is the rendering of student-items from the currently chosen (click event) house-specific student-list.
It uses the technique of event delegation. Thus, there is a single listener subscribed to a single element which is the root-node of the students filter (instead of subscribing the listener again and again to each filter-item).
function emptyElementNode(elmNode) {
[...elmNode.childNodes].forEach(node => node.remove());
}
function createFilterItem(houseName) {
houseName = houseName.trim();
const elmItem = document.createElement('li');
const elmButton = document.createElement('button');
elmItem.dataset.filterLabel = houseName;
elmButton.textContent =
(houseName === '') && 'No House' || houseName;
elmItem.appendChild(elmButton);
return elmItem;
}
function createStudentItem(studentData) {
const elmItem = document.createElement('li');
const elmImage = document.createElement('img');
elmImage.src = studentData.image;
elmImage.alt = elmImage.title = studentData.name;
elmItem.appendChild(elmImage);
return elmItem;
}
function renderStudentsFilter(houseBasedIndex) {
const houseNameList = Object.keys(houseBasedIndex);
// console.log({ houseNameList });
const filterRoot = document
.querySelector('[data-students-filter]');
if (filterRoot) {
const filterListRoot = houseNameList
.reduce((rootNode, houseName) => {
rootNode
.appendChild(
createFilterItem(houseName)
);
return rootNode;
}, document.createElement('ul'));
const allStudentsFilterItem =
createFilterItem('All Students');
allStudentsFilterItem.dataset.filterLabel = 'all-students';
filterListRoot.appendChild(allStudentsFilterItem);
emptyElementNode(filterRoot);
filterRoot.appendChild(filterListRoot);
return filterRoot;
}
}
function renderStudentItems(studentList) {
const displayRoot = document
.querySelector('[data-student-list]');
if (displayRoot) {
const listRoot = studentList
.reduce((rootNode, studentData) => {
rootNode
.appendChild(
createStudentItem(studentData)
);
return rootNode;
}, document.createElement('ul'));
emptyElementNode(displayRoot);
displayRoot.appendChild(listRoot);
}
}
function handleStudentsFilterFromBoundIndex({ target }) {
const filterItem = target.closest('li[data-filter-label]')
const { dataset: { filterLabel } } = filterItem;
// console.log({ filterItem, filterLabel });
const studentList = this[filterLabel];
renderStudentItems(studentList);
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}
function initializeStudentsFilterHandling(filterRoot, houseBasedIndex) {
filterRoot
.addEventListener(
'click',
handleStudentsFilterFromBoundIndex.bind(houseBasedIndex)
);
}
function createStudentsFilterByHouse(studentList) {
const houseBasedIndex = studentList
.reduce((index, student) => {
const houseName =
student.house?.trim() ?? '';
(index[houseName] ??= []).push(student);
return index;
}, {});
// console.log({
// studentList,
// houseBasedIndex,
// });
const filterRoot = renderStudentsFilter(houseBasedIndex);
houseBasedIndex['all-students'] = studentList;
initializeStudentsFilterHandling(filterRoot, houseBasedIndex);
}
async function main() {
const url = 'http://hp-api.herokuapp.com/api/characters/students';
const response = await fetch(url);
const studentList = await response.json();
createStudentsFilterByHouse(studentList);
}
main();
body {
margin: 32px 0 0 0!important;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
display: inline-block;
margin: 0 3px 2px 3px;
}
ul li img {
float: left;
height: 120px;
width: auto;
}
ul li img[src=""] {
height: 98px;
width: auto;
padding: 10px 5px;
border: 1px dashed #aaa;
}
[data-students-filter] {
position: fixed;
left: 0;
top: 0;
}
<article data-students-overview>
<navigation data-students-filter>
<em>+++ initialize house based students filter +++</em>
</navigation>
<section data-student-list>
</section>
</article>

How to use scrollTop() inside a loop with delays correctly?

So I am wondering why inside scrollInSteps all the scrolling is basically executed at once (atleast it seems like it since nothing is ever printed, and the scrollbar directly jumps to the last positon)
What I would like is the scrollbar to jump every 0.5 seconds to its next position.
But somehow this does not work at all.
Does anybody have an Idea on why the scroll behaviour is only applied, when the loop basically is finished?
And how can I change this?
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
function scrollInSteps(){
console.log("BUTTON CLICKED");
let counter = 0;
while(counter <= 10){
sleep(500);
counter++;
console.log("SCROLL");
$(".demo").scrollTop($(".demo").prop('scrollTop')+100);
}
}
function scrollOnce(){
console.log("BUTTON CLICKED");
console.log($(".demo").prop('scrollTop'));
$(".demo").scrollTop($(".demo").prop('scrollTop')+100);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>scrollTop demo</title>
<style>
div.demo {
background: #ccc none repeat scroll 0 0;
border: 3px solid #666;
margin: 5px;
padding: 5px;
position: relative;
width: 200px;
height: 200px;
overflow: auto;
}
p {
margin: 10px;
padding: 5px;
border: 2px solid #666;
height: 100px;
}
</style>
</head>
<body>
<div class="demo">
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
<p>Hello</p>
</div>
<button onClick="scrollOnce()">SCROLL ONCE</button>
<button onClick="scrollInSteps()">SCROLL IN STEPS</button>
</body>
</html>
Because you are blocking the main thread, which doesn't give the browser a chance to redraw the screen.
Let's say I have the following code (just for a demo, do not write something like this on a real website):
function main() {
const container = document.getElementById('container');
// Loop 100 times
for (let i = 1; i <= 100; i++) {
container.innerHTML = i;
}
}
main();
<div id="container"></div>
If I run this, you don't see the numbers 1-100 render on the screen. Instead, it just "immediately" shows the number 100. Why is that?
Because JavaScript runs on a single thread, so while each iteration of that code is running, the browser doesn't have an opportunity to redraw the screen.
You essentially are doing the same thing. Your sleep function is all synchronous: it just runs a (potentially) infinite loop as a way to pause execution. This is an extremely bad practice, for the reasons that you are noticing. While the main thread is blocked, nothing else can happen!
Instead, you want to run these "steps" in an asynchronous manner so that the browser can do things in the time between those async things.
A common "sleep" function in modern JavaScript looks like this
function sleep(milliseconds) {
return new Promise(function (resolve) {
setTimeout(function() {
resolve();
}, milliseconds);
});
}
// Alternatively, in modern JS we'd write
const sleep = (milliseconds) => new Promise(resolve => setTimeout(() => resolve(), milliseconds));
Here, we are returning a Promise that will resolve after a millisecond delay via setTimeout.
By using this function and async/await, we can easily run things in an asynchronous manner while still writing like we do with synchronous code.
With my previous example, now if I write:
const sleep = (ms) => new Promise(resolve => setTimeout(() => resolve(), ms));
// Function must be `async` so we can `await` within
async function main() {
const container = document.getElementById('container');
// Loop 100 times
for (let i = 1; i <= 100; i++) {
// Wait 5 milliseconds
await sleep(5);
container.innerHTML = i;
}
}
main();
<div id="container"></div>
Now running the above, we see the numbers increment from 1 to 100.
You'll want to make similar changes to your code.
JavaScript has no sleep function by default, but you can create one using async/await and Promises.
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve,time));
}
Then all you have to do change your scrollInSteps function to be an async function, so that you can 'await' the sleep time.
async function scrollInSteps(){
console.log("BUTTON CLICKED");
for (let counter = 0; counter < 10; counter++) {
await sleep(500); // will pause until the promise is resolved
console.log("SCROLL");
$(".demo").scrollTop($(".demo").prop('scrollTop')+100);
}
}

Javascript loading speed issue as variable increases

I am having an issue with page loading time. Currently right now I am running UBUNTU in Oracle Vm Virtual Box. I am using mozilla firefox as my browser and I am working on an etchasketch project from "The odin project".
My problem is the page loading time. The code takes a prompt at the start and generates a grid for the etch a sketch based on that prompt. I have not given it the minimum and maximum values (16 and 64) respectively, however any number when prompted at the beginning that is beyond 35 doesn't load or takes ages to load.
How do I speed up the process time? / why is it moving so slow? / how can I avoid this ? / is there a fix that I am over looking that can make this work a lot faster? / feel free to tackle any and all of those questions!
This is my HTML CODE:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8"/>
<title>
</title>
</head>
<body>
<div class="etchhead">
<p> Choose your grid size </p>
<input type = "text"></input>
<button id="startOver"> Clear Grid </button>
<p> Change color </p>
</div>
<div id="grid">
</div>
<script src="eas.js"></script>
</body>
</html>
And this is my CSS code:
p {
color: blue;
display: inline;
}
#grid {
display: grid;
width: 800px;
max-width: 800px;
height: 800px;
max-height: 800px;
line-height: 0;
}
.gridBox {
border: 1px solid black;
background-color: lightgrey
}
And this is my JAVASCRIPT code:
gridStart();
function gridStart(){
var boxes = 0
var selectBody = document.querySelector("#grid");
var addBox = document.createElement("div");
var boxCountStart = prompt("enter a number between 16 and 64");
var boxDimensions = (boxCountStart * boxCountStart);
function rowsAndColumns() {
var selectBody = document.querySelector("#grid");
var gridTemplateColumns = 'repeat('+boxCountStart+', 1fr)';
selectBody.style.gridTemplateColumns= gridTemplateColumns;
selectBody.style.gridTemplateRows= gridTemplateColumns;
};
function hoverColor(){
var divSelector = selectBody.querySelectorAll("div");
divSelector.forEach((div) => {
div.addEventListener("mouseover", (event) => {
event.target.style.backgroundColor = "grey";
});
});
};
rowsAndColumns();
for (boxes = 0; boxes < boxDimensions ; boxes++) {
var selectBody = document.querySelector("#grid");
var addBox = document.createElement("div");
addBox.classList.add("gridBox");
addBox.textContent = (" ");
selectBody.appendChild(addBox);
hoverColor();
};
};
There are two components to your issue. One is that you are repeatedly modifying the DOM in a loop. You can fix it by appending all your boxes to a DocumentFragment and then adding that to the DOM after your loop finishes. You are also calling hoverColor(); inside your loop which results in adding tons of event listeners that all do the same thing (since inside hoverColor you are adding a listener to every single div). You can fix both those issues like this:
var fragment = document.createDocumentFragment( );
for (var i = 0; i < boxDimensions ; i++) {
var addBox = document.createElement("div");
addBox.classList.add("gridBox");
addBox.textContent = (" ");
fragment.appendChild(addBox);
}
document.querySelector("#grid").appendChild( fragment );
hoverColor();
Here is a JSFiddle with your original code, and here is one with the modification.
You could also benefit from only having one event listener total. You don't need to loop and add an event listener to every div. Just add one to #grid and use event.target (like you already do, to find the div that the event originated from). Something like this:
function hoverColor(){
document.querySelector("#grid").addEventListener( 'mouseover', function ( event ) {
event.target.style.backgroundColor = "grey";
} );
}

How do I make a JavaScript created list item clickable?

I've been going through all the related questions but none of the solutions have been working for me. I am extremely new to JavaScript and I'm confused as to how to make a list that I created with JavaScript have clickable items. The most recent attempt included attempting to make an alert pop up on click but instead it just pops up the second the page loads. Please help! Here is my current code:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/m-buttons.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
div.links{
margin: auto;
border: 3px solid #003366;
text-align: left;
max-width: 700px;
}
p{
font-size: 40px;
text-align: center;
}
li{
font-size: 1w;
}
body{
font-family: verdana;
}
</style>
</head>
<body>
<div class = "links">
<ul id="blah"></ul>
<script>
var testArray = ["One","Two","Three","Four","Five","Six","Seven"];
function makeLongArray(array){
for(var i = 0; i < 1000; i++){
array.push(i);
}
}
makeLongArray(testArray);
function makeUL(array) {
// Create the list element:
var list = document.createElement("UL");
list.setAttribute("id", "blah");
for(var i = 0; i < array.length; i++) {
// Create the list item:
var item = document.createElement("LI");
// Set its contents:
item.appendChild(document.createTextNode(array[i]));
// Add it to the list:
list.appendChild(item);
list.onclick = alert("Help."); //this is the code causing the issue.
}
// Finally, return the constructed list:
return list;
}
// Add the contents of options[0] to #foo:
document.getElementById("blah").appendChild(makeUL(testArray));
</script>
</div>
</body>
</html>
Your existing code will execute alert('Help.') every time you execute this line of code list.onclick = alert("Help.");
What you need to do is assign a function to onclick. This function then get executed when onclick is executed. As follows:
item.onclick = function() {console.log('hello world');};
Now each list item's onclick event has a function assigned to it that outputs hello world to the console everytime the list item is clicked.
You need to assign a function to the onclick event:
list.onclick = function(){ alert("Help."); }
Here is my solution:
function toggleDone(event) {
var ev = event.target;
ev.classList.toggle("done");
}
ul.addEventListener("click", toggleDone);

How can I get answer on same page instead of an alert?

I want a button that every time you click it it gives you a different answer(Animals, Movies or TV Shows, and so on), the thing is I have a button (that I found online, I'm new at coding) but that button gives me the answer in a little alert box, is there a way to make it give me the answer below the button? Here's the code I found:
var words = ['Animals', 'Movies', 'TV Shows'];
function randomAlert() {
var alertIndex;
var randomValue = Math.random();
if (randomValue < 0.5) {
alertIndex = 0;
}
else if(randomValue < 0.8) {
alertIndex = 1;
}
else {
alertIndex = 2;
}
alert(words[alertIndex]).innerHTML
}
You can just get rid of the alert call and insert it into a div, span, h1, whatever. Here's an example out of the many ways:
var words = ['Animals', 'Movies', 'TV Shows'];
function randomSelect() {
var index;
var randomValue = Math.random();
if (randomValue < 0.5) {
index = 0;
}
else if(randomValue < 0.8) {
index = 1;
}
else {
index= 2;
}
document.getElementById('answer').innerText = words[index]; //Get's an element of id 'answer'
}
randomSelect(); //Executes function
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="answer">
<!-- Answer is inserted into here !-->
</div>
</body>
</html>
The above get's an element of id answer and inserts it into the element's text. This is done by accessing the innerText attribute of the element, which returns the text of the element. I then reassign the value to the index. This is quite a simple way of doing it, and I cleaned up some of the names of functions, variables.

Categories