Star Rating Js + html with symfony - javascript

I'm working on a project with symfony 4. I want to implement a star rating system. I display 5 stars in each row in a table. I have two problems, one is more important than the other.
So the first issue is that I want to be able to retrieve the value of the star I selected. If I select 5 stars, I want to get that value back in my entity controller.
The second issue is that, currently, let's say I have 5 items in my table, so 5 rows. Currently, if I select 5 stars in one row it's selected for all rows and I can't select another value anymore. So, it's global or something.
Here is the javascript I'm using:
<script>
const stars = document.querySelectorAll('.star');
let check = false;
stars.forEach(star => {
star.addEventListener('mouseover', selectStars);
star.addEventListener('mouseleave', unselectStars);
star.addEventListener('click', activeSelect);
})
function selectStars(e) {
const data = e.target;
const etoiles = priviousSiblings(data);
if (!check) {
etoiles.forEach(etoile => {
etoile.classList.add('hover');
})
}
}
function unselectStars(e) {
const data = e.target;
const etoiles = priviousSiblings(data);
if (!check) {
etoiles.forEach(etoile => {
etoile.classList.remove('hover');
})
}
}
function activeSelect(e) {
if (!check) {
check = true;
document.querySelector('.note').innerHTML = 'Note ' + e.target.dataset.note;
}
}
function priviousSiblings(data) {
let values = [data];
while (data === data.previousSibling) {
if (data.nodeName === 'I') {
values.push(data);
}
}
return values;
}
</script>
And Here is the twig.html I'm displaying:
<td>
<i class="star" data-note="1">★</i>
<i class="star" data-note="2">★</i>
<i class="star" data-note="3">★</i>
<i class="star" data-note="4">★</i>
<i class="star" data-note="5">★</i>
<i class="note">Note:</i>
</td>
I want to be able to retrieve the value once I made a selection, and to have a different selection for each row I have.

The problem is with the "mouseover" and "mouseleave" event handlers - selectStars and unselectStars. In "selectStars", you are adding the class to only one star. And in "unselectStars", you were not resetting, or applying the "remove" class method to other stars.
Anyway, here is how I have achieved what you are trying to do:
const ratings = document.querySelectorAll('.rating');
ratings.forEach(rating =>
rating.addEventListener('mouseleave', ratingHandler)
);
const stars = document.querySelectorAll('.rating .star');
stars.forEach(star => {
star.addEventListener('mouseover', starSelection);
star.addEventListener('mouseleave', starSelection);
star.addEventListener('click', activeSelect);
});
function ratingHandler(e) {
const childStars = e.target.children;
for(let i = 0; i < childStars.length; i++) {
const star = childStars.item(i)
if (star.dataset.checked === "true") {
star.classList.add('hover');
}
else {
star.classList.remove('hover');
}
}
}
function starSelection(e) {
const parent = e.target.parentElement
const childStars = parent.children;
const dataset = e.target.dataset;
const note = +dataset.note; // Convert note (string) to note (number)
for (let i = 0; i < childStars.length; i++) {
const star = childStars.item(i)
if (+star.dataset.note > note) {
star.classList.remove('hover');
} else {
star.classList.add('hover');
}
}
}
function activeSelect(e) {
const parent = e.target.parentElement
const childStars = parent.children;
const dataset = e.target.dataset;
const note = +dataset.note; // Convert note (string) to note (number)
for (let i = 0; i < childStars.length; i++) {
const star = childStars.item(i)
if (+star.dataset.note > note) {
star.classList.remove('hover');
star.dataset.checked = "false";
} else {
star.classList.add('hover');
star.dataset.checked = "true";
}
}
const noteTextElement = parent.parentElement.lastElementChild.children.item(0)
noteTextElement.innerText = `Note: ${note}`;
}
You might notice I have a .rating class component. This is a div which I have created to hold all these "stars". Here is a link to a codepen I have created. Feel free to play around with it.
And as a note, please provide codepen (or any other) demos so that we can debug a bit better and faster.
I hope the codepen link would help you solve your problem.

Related

Get a result when an option value selection is changed in javascript

I'm trying to get a different value when the value of an option is changed. I have this code, but when I change my selection, the result is not changing.
let cantidad1 = document.getElementById("cantidad");
let moneda1 = document.getElementById("cambio").value;
if (moneda1 === "cup") {
tipodemoneda = 97;
} else if (moneda1 === "mlc") {
tipodemoneda = 0.89;
}
let tasadecambio = tipodemoneda;
let resultado1 = document.getElementById("resultado")
cantidad1.addEventListener("change", () => {
resultado1.value = parseFloat(tasadecambio) * parseFloat(cantidad1.value)
})
Listen for input events instead of change events. Change events only fire for a text input or text area when the the element lose focus (1)
let cantidad1 = document.getElementById("cantidad");
let moneda1 = document.getElementById("cambio").value;
if (moneda1 === "cup") {
tipodemoneda = 97;
} else if (moneda1 === "mlc") {
tipodemoneda = 0.89;
}
let tasadecambio = tipodemoneda;
let resultado1 = document.getElementById("resultado")
cantidad1.addEventListener("input", () => {
resultado1.value = parseFloat(tasadecambio) * parseFloat(cantidad1.value)
})
<input id="cantidad">
<input id="cambio" value="cup">
<input id="resultado">
Also the way the code is right now, moneda1 only gets set once. You should move it into the event handler.
let cantidad1 = document.getElementById("cantidad");
let moneda1 = document.getElementById("cambio");
let resultado1 = document.getElementById("resultado")
let tipodemoneda; // Put this to stop it from being a global variable (window.tipodemoneda)
function updateResultado() { // Handler in its own function
if (moneda1.value === "cup") { // Check mondedal.value since it can change
tipodemoneda = 97;
} else if (moneda1.value === "mlc") {
tipodemoneda = 0.89;
}
let tasadecambio = tipodemoneda;
resultado1.value = parseFloat(tasadecambio) * parseFloat(cantidad1.value)
}
cantidad1.addEventListener("input", updateResultado)
moneda1.addEventListener("change", updateResultado)
<input id="cantidad">
<select id="cambio">
<option value="mlc">mlc</option>
<option value="cup">cup</option>
</select>
<input id="resultado">

How can I solve this circular dependency? (JavaScript)

I have the next problem: I had been creating a webpage for search GIFs using GIPHY API, everything was going OK until I had to implement a functionality to add GIFs to favorites, the favorites are stored in local storage and they are shown in a separate HTML file using CSS grid. I can add and delete favorites without problems outside /favorites.html, and I can also add favorites from trending GIFs inside this url, the problem is that when I need to remove a favorite and I am on /favorites.html I want the grid to be created from zero and therefore the user will see the gif disappear.
For that function I've created a GIFCard class which allow me to create all the HTML elements for a card and add all the classes they need to look fine using css styles, also this class handles the events so it automatically adds event listeners to all the buttons. I have a file called favoritesPage.js which defines a function called showAllUntilOffset() this function is used whenever the user press the favorite button for a GIF Card and is seeing favorites its work is to create again the GIF grid so it will be updated but I also need to import the GIFCard class in favoritesPage.js to create the Card object, add the html classes and add the event listener.
Here is an image of my webpage working correctly, it just stops working when I import "showAllUntilOffset" from favoritesPage.js because of the circular dependency (I guess).
Next is the code for my favoritesPage.js file and the GIFCard class, constructor and other methods that are not relevant to the problem are not shown here.
favoritesPage.js:
import { GIFCard as Card } from "../entities/GIFCard.js";
import { favoriteGIFs } from "../favorites.js";
import {
favoritesGrid,
favoritesEmpty,
favoritesShow,
seeMoreFavorites,
} from "../dom/favoritesSelectors.js";
import { displayComponent, hideComponent } from "../utils/utils.js";
function createAndAddFavGIFCards(start, end) {
for (let i = start; i < end; i += 1) {
const newCard = new Card(
favoriteGIFs[i].id,
favoriteGIFs[i].title,
favoriteGIFs[i].username,
favoriteGIFs[i].url,
favoriteGIFs[i].fixedHeightURL,
i,
"Favorite GIF"
);
if (window.innerWidth >= 1280) {
favoritesGrid.append(newCard.createCardDesktop());
} else {
favoritesGrid.append(newCard.createCardMobile());
}
}
}
function loadAndShowFavorites() {
favoritesGrid.innerHTML = "";
if (favoriteGIFs.length === 0) {
displayComponent(favoritesEmpty);
} else {
displayComponent(favoritesShow);
let limit = favoriteGIFs.length >= 12 ? 12 : favoriteGIFs.length;
if (favoriteGIFs.length > 12) {
displayComponent(seeMoreFavorites);
}
createAndAddFavGIFCards(0, limit);
}
}
export function showAllUntilOffset() {
favoritesGrid.innerHTML = "";
if (favoriteGIFs.length === 0) {
displayComponent(favoritesEmpty);
hideComponent(favoritesShow);
}
if (favoriteGIFs.length <= 12) {
hideComponent(seeMoreFavorites);
offset = favoriteGIFs.length;
} else if (favoriteGIFs.length < offset) {
offset -= 1;
}
createAndAddFavGIFCards(0, offset);
}
function getOffsetAndUpdateBtn() {
start = offset;
if (offset + 12 <= favoriteGIFs.length) {
offset += 12;
} else {
const excess = favoriteGIFs.length - offset;
offset += excess;
}
if (offset === favoriteGIFs.length) {
hideComponent(seeMoreFavorites);
}
}
let start;
let offset = 12;
loadAndShowFavorites();
seeMoreFavorites.addEventListener("click", () => {
getOffsetAndUpdateBtn();
createAndAddFavGIFCards(start, offset);
});
GIFCard Class:
import { showAllUntilOffset } from "../favorites/favoritesPage.js";
export class GIFCard {
createCardDesktop() {
const card = document.createElement("div");
card.classList.add("card");
const overlay = document.createElement("div");
overlay.classList.add("overlay");
const text = this.createCardInfo();
const favoriteBtn = this.createCardButton("favorite", "card-btn");
this.favoriteBtn = favoriteBtn;
const downloadBtn = this.createCardButton("download", "card-btn");
this.downloadBtn = downloadBtn;
const expandBtn = this.createCardButton("expand", "card-btn");
this.expandBtn = expandBtn;
const gifImg = this.createCardImg();
gifImg.classList.add("card-img");
card.append(overlay, text, favoriteBtn, downloadBtn, expandBtn, gifImg);
this.htmlDesktopCard = card;
this.checkAndUpdateFavBtn();
this.expandGIFDesktopEventListener();
this.addFavoriteEventListener();
return card;
}
favEventAction() {
if (!seeingFavorites()) {
if (!this.isInFavorites()) {
this.pushToFavorites();
} else {
this.removeFromFavorites();
}
} else {
if (!this.isInFavorites()) {
this.pushToFavorites();
if (this.type === "Trending GIF") {
hideComponent(favoritesEmpty);
displayComponent(favoritesShow);
if (window.innerWidth >= 1280) {
favoritesGrid.append(this.createCardDesktop());
} else {
favoritesGrid.append(this.createCardMobile());
}
}
} else {
this.removeFromFavorites();
if (this.type === "Favorite GIF") {
showAllUntilOffset();
}
}
}
}
addFavoriteEventListener() {
this.favoriteBtn.addEventListener("click", () => {
this.favEventAction();
});
}
}
Hope you can help me, I've tried everything and I just can't figure out a solution. This is my first JavaScript big project so if you can recommend me good practices I'd really appreciate it. Thank you so much.

Splicing only one element when creating a new element

I'm trying to make it where when a user creates a widget, and then reloads the page (it'll appear because it's saved in localStorage) and then once you create another widget, I want to be able to delete the old widget before the page refreshes but it deletes the widget that the user clicked and the new widget.
Each time a new widget it created, it gets assigned a property name 'id' and the value is determined based on what is already in localStorage and it finds the next available (or not in use) id number. The widgets array also gets sorted from smallest id to largest id before setting it back to localStorage.
I've tried attaching a click listener for the delete button on the widget both when it's created and when the document is loaded. But that wasn't working.
Now i'm thinking I have to call a function with the id as its param to add a click listener to all the widgets that are appended to the document and when a new widget is created.
app.js:
function addRemoveListener(id) {
let storageUi = localStorage.getItem('ui');
let localUi = JSON.parse(storageUi);
$(`#widget-${id} > span > .widget-clear`).click(() => {
for (let i = 0; i < localUi.widgets.length; i++) {
let thisWidget = `#widget-${id}`;
if (localUi.widgets[i].id == id) {
localUi.widgets.splice(i, 1)
}
$(thisWidget).remove();
console.log(localUi)
}
let newUi = JSON.stringify(localUi);
localStorage.setItem('ui', newUi);
})
}
widget.js:
static appendToDom(ui) {
let storageUi = localStorage.getItem('ui');
let localUi = JSON.parse(storageUi);
for (let i = 0; i < localUi.widgets.length; i++) {
let widget = localUi.widgets[i];
let query = () => {
if (widget.type == 'humidity') {
return `${Math.floor(ui.weather.currently.humidity * 100)}`
} else if (widget.type == 'eye') {
return `${Math.floor(ui.weather.currently.visibility)}`
} else if (widget.type == 'windsock') {
return `${Math.floor(ui.weather.currently.windSpeed)}`
} else if (widget.type == 'pressure') {
return `${Math.floor(ui.weather.currently.pressure)}`
} else if (widget.type == 'uv-index') {
return `${ui.weather.currently.uvIndex}`
}
}
$('nav').after(`<div class="widget widget-${widget.size}" id="widget-${widget.id}">
<span>
<i class="material-icons widget-clear">clear</i>
<i class="material-icons widget-lock-open">lock_open</i>
<i class="material-icons widget-lock">lock_outline</i>
</span>
<div class="data-container">
<img src=${widget.image}>
<h1> ${widget.type}: ${query()} ${widget.unit} </h1>
</div>
</div>`)
$(`#widget-${widget.id}`).delay(1000 * i).animate({ opacity: 1 }, 1000);
$(`#widget-${widget.id}`).css({ left: `${widget.left}`, top: `${widget.top}`, 'font-size': `${widget.dimensions[2]}` })
$(`.widget`).draggable();
$(`#widget-${widget.id}`).css({ width: `${widget.dimensions[0]}`, height: `${widget.dimensions[1]}` })
addRemoveListener(i);
}
// this function is called earlier in the script when the user has selected
// which kind of widget they want
let makeWidget = () => {
let newWidget = new Widget(this.size, this.id, this.image, this.type, this.unit, this.dimensions);
saveWidget(newWidget);
addRemoveListener(this.id)
}
I have no problems with this until I delete an existing widget after I create a new one, and before refreshing.
You might have a problem with the id that is passed to your addRemoveListener function. It could be passing the same id for any widget so the loop will delete the UI because thisWidget is in the for loop. Try adding some console logging.
function addRemoveListener(id) {
let storageUi = localStorage.getItem('ui');
let localUi = JSON.parse(storageUi);
$(`#widget-${id} > span > .widget-clear`).click(() => {
for (let i = 0; i < localUi.widgets.length; i++) {
let thisWidget = `#widget-${id}`;
if (localUi.widgets[i].id == id) {
localUi.widgets.splice(i, 1)
}
// Move this inside the if statement above.
$(thisWidget).remove();
console.log(localUi)
}
let newUi = JSON.stringify(localUi);
localStorage.setItem('ui', newUi);
})
}
or better yet, re-write it to continue if the id doesn't match
function addRemoveListener(id) {
let storageUi = localStorage.getItem('ui');
let localUi = JSON.parse(storageUi);
$(`#widget-${id} > span > .widget-clear`).click(() => {
for (let i = 0; i < localUi.widgets.length; i++) {
let thisWidget = `#widget-${id}`;
if (localUi.widgets[i].id !== id) {
continue;
}
localUi.widgets.splice(i, 1)
$(thisWidget).remove();
console.log(localUi)
}
let newUi = JSON.stringify(localUi);
localStorage.setItem('ui', newUi);
})
}

Compare array items with HTML dataset with pure Javascript?

Using Jquery you can do:
array[0].data('id') == array[1].data('id')
and compare 2 items in an same array by their HTML dataset (in this case it's data-id="1"). Is there a way to do it with pure Javascript???
This is HTML. It's a list of images.
<li class="card" data-id="1"><img src="images/labrys.svg" alt=""></li>
<li class="card" data-id="2"><img src="images/laurel.svg" alt=""></li>
....and so on
This is JS:
let cardArray = []; //Empty Array for selected cards
cards.addEventListener("click", function (e) {
e.preventDefault();
if (e.target.nodeName==="LI"){ // If a list element is hit...
const data = e.target.dataset.id; //checking
console.log(data);
cardArray.push(e.target); //Place current card into array...
var hit = cardArray[0].dataset.id;
var hot = cardArray[1].dataset.id;// throws error
console.log (hit);
console.log (hot);
}
I am trying to do this:
var match = cardArray[0].dataset.id=== cardArray[1].dataset.id;
This project is a memory game:
https://github.com/StamatisDeli/memory-game.github.io
You have to check how many items are in the array before accessing their index.
I believe that you are trying to avoid duplicating items when the user selects a particular card multiple times through click events. if that is what you are trying to achieve, You will be facing two scenerio.
first is to make sure that the listener function does not misbehave on multiple clicks.
second is to implement a binary search algorithm to help locate items easily rather than iterating through the items one after the other during searching. the id is a great stuff to use in sorting the list items.
var processing = false, //used to handle radical click event triggers
cardArray = [];
//note, it is better to add your event listener, to the card container `ul`
//since click events on the li items will will bubble up to it.
cardContainer.addEventListener('click', function(e){
e.preventDefault();
if (processing) {
return;
}
if (e.target.tagName.tolowerCase() !== 'li') {
return;
}
processing = true;
var cardlocation = searchCard(e.target.dataset.id);
if (cardlocation > -1) {
//card is already in array. do what you wish here
}
else {
//this is a new selection. do whatever you wish here
cardArray.push(e.target);
}
processing = false; //done processing.
});
Implement a search algorithm
/**
*This one will be slow if there loads of cards in the array
*#returns int returns the card index or -1 if not found
*/
var searchCard = function(id) {
var len = cardArray.length;
while (--len >= 0) {
if (id == cardArray[len].dataset.id) {
return len;
}
}
return -1;
}
Binary Search
/**
*used when sorting the cards
*/
var sortCard = function(card1, card2) {
var id1 = parseInt(card1.dataset.id),
id2 = parseInt(card2.dataset.id);
return id1 - id2;
};
/**
* used for checking the cards search
*/
var checkCard = function(id, card) {
return parseInt(id) - parseInt(card.dataset.id);
};
/**
*performs binary search
*/
var searchCard = function(id) {
if (cardArray.length === 0) {
return -1;
}
var low = 0,
high = cardArray.length - 1,
middle = parseInt((high + low + 1)/2, 10),
locationindex = -1,
searchindex = 0;
cardArray.sort(sortCard); //sort the card array
do {
searchindex = checkCard(id, cardArray[middle]);
if (searchindex === 0) {
locationindex = middle;
}
else {
//split and recalculate the middle
if (searchindex < 0) {
high = middle - 1;
}
else {
low = middle + 1;
}
middle = parseInt((high + low + 1) / 2, 10);
}
}
while (low <= high && locationindex === -1);
return locationindex;
}

Add different event listener to a set of elements in a loop [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 6 years ago.
Hello people of the internet,
I have a set of buttons and if a button is clicked it's content should be appended to a text field's content.
Lets say I have three buttons: [first] [second] [third]
My addEventListener-implementation results in "third" appended to the in text field's content, regardless which button I press. I don't know hot to fix this.
function setupListeners() {
var targetInputField = d.querySelector("#expression");
var t = d.querySelectorAll(".expression-button").length;
for (var i = 1; i <= t; i++) {
var btnElem = d.querySelector("#expression-button-"+i);
btnElem.addEventListener('click', function() {
if (targetInputField.value == "") {
targetInputField.value = btnElemLocal.innerText;
}
else {
targetInputField.value += ";"+btnElemLocal.innerText;
}
});
}
}
What I want:
If I click all of the three buttons in a row, the text field's content should be :
"first;second;third"
And not :
"third;third;third"
Put the click event code inside another function (btnClick in my example) and just call it when you attach the event in .addEventListener() inside the loop, then use this to refer to the current clicked element :
function btnClick() {
var targetInputField = d.querySelector("#expression");
if (targetInputField.value == "") {
targetInputField.value = this.innerText;
}
else {
targetInputField.value += ";"+this.innerText;
}
}
Hope this helps.
var d = document;
function setupListeners() {
var t = d.querySelectorAll(".expression-button").length;
for (var i = 1; i <= t; i++) {
var btnElem = d.querySelector("#expression-button-"+i);
btnElem.addEventListener('click', btnClick);
}
}
function btnClick() {
var targetInputField = d.querySelector("#expression");
if (targetInputField.value == "") {
targetInputField.value = this.innerText;
}
else {
targetInputField.value += ";"+this.innerText;
}
}
setupListeners();
<button class='expression-button' id='expression-button-1'>First</button>
<button class='expression-button' id='expression-button-2'>Second</button>
<button class='expression-button' id='expression-button-3'>Third</button>
<input id='expression' />

Categories