I'm doing a small project where I show informations about every country in the world via an API
Where I'm struggling here is that whenever I click on a card I want to show a modal with more informations about the country
This part works nicely, BUT whatever the card I click on it's always going to be the last element of the array informations that I'm going to get
For exemple, If I click on "Afghanistan":
I get the last element of the array (the array being every card), which is Zimbabwe
This is my code
Basically the "country" parameter is the API data
What I'm doing is looping through every card and adding an event listener on click and trying to get all the informations about the card in the modal... but it doesn't work, I only get the last card infos
function showModal(country) {
const cards = document.querySelectorAll(".card");
for (card of cards) {
card.addEventListener("click", (e) => {
openModal.classList.toggle("active");
openModal.innerHTML = `
<div id="modal">
<div id="close-modal">
<i class="gg-close"></i></i>
</div>
<div id="modal-img">
<img src="${country.flag}">
</div>
<div id="modal-content">
<div class="country-details">
<h2>${country.name}</h2>
<h4>Nom natif : <span>${country.nativeName}</span></h4>
<h4>Population : <span>${country.population.toLocaleString()}</span></h4>
<h4>Région : <span>${country.region}</span></h4>
<h4>Sous-région : <span>${country.subregion}</span></h4>
<h4>Capitale : <span>${country.capital}</span></h4>
</div>
<div class="country-details">
<h4>Domaine de premier niveau : <span>${
country.topLevelDomain
}</span></h4>
<h4>Monnaie : <span>${country.currencies}</span></h4>
<h4>Langues : <span>${country.languages}</span></h4>
<h4>Fuseau horaire : <span>${country.timezones}</span></h4>
<h4>Indicatif téléphonique : <span>${
country.callingCodes
}</span></h4>
<h4>ISO 3166-1 alpha-3 : <span>${country.alpha3Code}</span></h4>
</div>
<div class="country-details">
<h4>Pays frontaliers</h4>
<span>${country.borders}</span>
</div>
</div>
</div>
`;
/* Close Modal */
const closeModalBtn = document.getElementsByClassName("gg-close");
closeModalBtn[0].addEventListener("click", (e) =>
e.target.parentElement.parentElement.parentElement.classList.remove(
"active"
)
);
});
}
}
I don't know much about arrays yet
Thanks!
The problem is that each time you call showModal, you are adding an event listener to all cards. Instead, you should add an event listener to the card corresponding to the country it received. That's why you should add some kind of id to each card, so you can tell which country goes in which card.
Assuming each card has an id equal to its country's name:
function showModal(country) {
const card = document.querySelectorAll(`#${country.name}`);
card.addEventListener("click", (e) => {
openModal.classList.toggle("active");
openModal.innerHTML = `
<div id="modal">
<div id="close-modal">
<i class="gg-close"></i></i>
</div>
<div id="modal-img">
<img src="${country.flag}">
</div>
<div id="modal-content">
<div class="country-details">
<h2>${country.name}</h2>
<h4>Nom natif : <span>${country.nativeName}</span></h4>
<h4>Population : <span>${country.population.toLocaleString()}</span></h4>
<h4>Région : <span>${country.region}</span></h4>
<h4>Sous-région : <span>${country.subregion}</span></h4>
<h4>Capitale : <span>${country.capital}</span></h4>
</div>
<div class="country-details">
<h4>Domaine de premier niveau : <span>${
country.topLevelDomain
}</span></h4>
<h4>Monnaie : <span>${country.currencies}</span></h4>
<h4>Langues : <span>${country.languages}</span></h4>
<h4>Fuseau horaire : <span>${country.timezones}</span></h4>
<h4>Indicatif téléphonique : <span>${
country.callingCodes
}</span></h4>
<h4>ISO 3166-1 alpha-3 : <span>${country.alpha3Code}</span></h4>
</div>
<div class="country-details">
<h4>Pays frontaliers</h4>
<span>${country.borders}</span>
</div>
</div>
</div>
`;
/* Close Modal */
const closeModalBtn = document.getElementsByClassName("gg-close");
closeModalBtn[0].addEventListener("click", (e) =>
e.target.parentElement.parentElement.parentElement.classList.remove(
"active"
)
);
});
}
By not having const or let in the for-loop card will be set globally and always hold the last the last value, since the "click" event is triggered asynchronously (after the loop is done iterating). Adding const or let to:
for (card of cards) {
Will scope card to the for-loop and let the callbacks refer to their respective value of card, instead of all callbacks referring to the last value of card.
Related
I'm currently on student project where I'm stuck on delete product on cart page I have no problem to delete them on the front page but when it come to remove it too on localStorage honestly I don't know what to do.
I know that using localStorage.setItem allow to update it when necessary but on the code that I wrote I don't know where to put correctly.
I wrote this :
// Targeting arrays
let deleteButton = document.querySelectorAll('.deleteItem');
let localStorageProducts = localStorage.getItem('Produits');
for (let i = 0; i < deleteButton.length; i++) {
// Get all remove buttons
let buttons = deleteButton[i];
// Link to his parent
let myData = deleteButton[i].closest('article');
let getStorageProducts = JSON.parse(localStorageProducts);
buttons.addEventListener("click",() =>
{
getStorageProducts.forEach(localStorageProducts =>{
if(localStorageProducts.id === myData.dataset.id){
// Delete the product
myData.remove();
localStorage.setItem('Produits',(JSON.stringify([localStorageProducts])));
}
})
})
}
<section id="cart__items">
<article class="cart__item" data-id="{product-ID}" data-color="{product-color}">
<div class="cart__item__img">
<img src="../images/product01.jpg" alt="Photographie d'un canapé">
</div>
<div class="cart__item__content">
<div class="cart__item__content__description">
<h2>Nom du produit</h2>
<p>Vert</p>
<p>42,00 €</p>
</div>
<div class="cart__item__content__settings">
<div class="cart__item__content__settings__quantity">
<p>Qté : </p>
<input type="number" class="itemQuantity" name="itemQuantity" min="1" max="100" value="42">
</div>
<div class="cart__item__content__settings__delete">
<p class="deleteItem">Supprimer</p>
</div>
</div>
</div>
</article>
</section>
An example , here I have 4 products : Products in Localstorage
When I click on one of the remove button it's gonna delete 3 of them and one left :
Product left
How could I delete them one by one ?
I refactored your logic in this way (please read comments on commented lines) :
//LOCAL TEST START: i used these lines to test with static local logics you can ignore this
let prods = [{id: 1, prodname: 'prod1'}, {id: 2, prodname: 'prod2'}, {id: 3, prodname: 'prod3'}];
localStorage.setItem('Produits',JSON.stringify(prods));
//LOCAL TEST END.
// i used getElementsByClassName because it's native function which is supported in all browsers even old ones.
let viewArticles = document.getElementsByClassName('cart__item');
let localStorageProducts = localStorage.getItem('Produits');
let products = JSON.parse(localStorageProducts);
// looping on Articles instead of Buttons to be able to get product ID easily
for (let article of viewArticles) {
article.addEventListener("click",function (ev)
{
// by {capturing: true}, i can access to the high clicked element
// (which is the <article> tag in our case) by .currentTarget property
const currentArticle = ev.currentTarget;
// by {capturing: true}, i can access to the exactly clicked child element
//(which is the delete button in this case by performing a check test using .target property class
const isDeleteBtnClicked = ev.target.classList.contains('deleteItem');
// if the child element is the delete button then
// delete the product and update the local storage
if(isDeleteBtnClicked){
// access to product id of the article
const productId = currentArticle.dataset.id;
products = products.filter(prd => prd.id.toString() !== productId.toString());
localStorage.setItem('Produits',JSON.stringify(products));
}
}, {capture: true}); // enable capturing instead of bubbling (top to bottom propagation)
}
for more informations about Capturing and Bubbling you can check this
so im making this web app with api from https://www.football-data.org/
so i make the template for the api content with the dom. but there's one part that are not being shown
function showStanding(data) {
let standings = ``;
const standingElement = document.getElementById("standings");
/* Jika dilihat melalui console.log, seharusnya kakak me looping data.standings[0].table, karena
disitulah object team berada
*/
data.standings[0].table.forEach((standing) => {
console.log(standing);
standings += `
<div class="standing__team">
<div class="favtim">
<img src="${standing.team.crestUrl.replace(
/^http:\/\//i, 'https://')}" alt="Logo team" />
<h3><a class="link" href="#team?id=${standing.team.id}">${standing.team.name}</a></h3>
<h3 class="point">Point: <span>${standing.points}</span></h3>
</div>
</div>
`;
});
// di line ke 23 dan 24 seharusnya competition, bukan competitions
standingElement.innerHTML = `
<div class="standing__header blue lighten-3">
<h1>${data.competition.name}</h1>
<p class="standing__header--place">${data.competition.area.name}</p>
<p class="standing__header--time">${data.season.startDate} - ${data.season.endDate}</p>
</div>
`;
document.querySelectorAll(".link").forEach(function (link) {
link.addEventListener("click", function (e) {
urlTeamParam = e.target.getAttribute("href").substr(9);
loadpage();
});
});
}
so this is the code for the templating the api
the result
so in the result there are a element which is base on the url. but there's one part that are not showing
so i tried to console.log() it and it appear on the console. how can i show it in the browser window?
the console
can you help me how to solve this problem?
Please help a little bit.
I have a list of 7 events displayed already with Angularjs. I'd like when I click on the <h2> (the event name) of some event, to open an ovelay that displays the same data from the database but only for this event which is clicked.
I'm sure that 'filter' will do the work but it seems I'm doing something wrong.
Here is my code. The ng-app and ng-controller are in the <main> tag.
Angularjs version: 1.7.9
My Html:
<main ng-app="eventsApp" ng-controller="eventsCtrl">
<!-- Overlay that holds and displays a single event -->
<div>
<div ng-repeat="x in singlePageEvent | filter:hasName(x.eventName)">
<div>
<img ng-src="{{x.eventImgSrc}}" alt="{{x.eventImgName}}"/>
<h2 class="event-name">{{x.eventName}}</h2>
<p>{{x.eventTime}}</p>
<p>{{x.eventPlace}}</p>
</div>
</div>
</div>
<!-- A list with all the events -->
<div ng-repeat="x in events">
<div>
<img ng-src="{{x.eventImgSrc}}" alt="{{x.eventImgName}}"/>
<h2 ng-click="singleEventOpen(x)" class="event-name">{{x.eventName}}</h2>
<p>{{x.eventTime}}</p>
<p>{{x.eventPlace}}</p>
</div>
</div>
</main>
My script:
let eventsApp = angular.module('eventsApp', []);
this filter below is not working at all. It continues to show all the events.
eventsApp.filter('hasName', function() {
return function(events, evName) {
var filtered = [];
angular.forEach(events, function(ev) {
if (ev.eventName && ev.eventName.indexOf(evName) >-1) {
filtered.push(ev);
}
});
return filtered;
}
});
eventsApp.controller('eventsCtrl', function($scope, $http) {
let x = window.matchMedia("(max-width: 450px)");
let singleEventOverlay = angular.element(document.querySelector('div.single-event.overlay'));
let singleEvent = singleEventOverlay;
function responsiveEventImages(x) { //this displays the list with events
if (x.matches) {
$http.get('./includes/events_res.inc.php').then(function(response) {
$scope.events = response.data.events_data;
});
} else {
$http.get('./includes/events.inc.php').then(function(response) {
$scope.events = response.data.events_data;
});
}
}
...and then by invoking singleEventOpen() the overlay appears, but it displays all the data, not just the clicked event
$scope.singleEventOpen = function(singleEvent) {
let clickedEvent = singleEvent.eventName; //I got the value of each h2 click thanx to #georgeawg but now what?
console.log("Fetching info for ", singleEvent.eventName);
$http.get('./includes/single_event.inc.php').then(function(response) {
$scope.singlePageEvent = response.data.events_data;
});
singleEventOverlay.removeClass('single-event-close').addClass('single-event-open');
}
});
The php file with the database extraction is working fine so I won't display it here.
What should I do to make the overlay display only the event which <h2> is clicked?
Here is a pic of the list with events
Here is a pic of the overlay
Thanx in advance.
EDITED
I got the value of each h2 click thanx to #georgeawg but now what?
UPDATE
Hey, thanx a lot #georgeawg . After many attempts I finally did this:
$scope.singleEventOpen = function(singleEvent) {
$http.get('./includes/single_event.inc.php').then(function(response) {
let allEvents = response.data.events_data;
for (var i = 0; i < allEvents.length; i++) {
singleEvent = allEvents[i];
}
});
console.log('Fetching data for', singleEvent);
$scope.ex = singleEvent;
});
And it works well.
Change the ng-click to pass an argument to the singleEventOpen function:
<div ng-repeat="x in events">
<div>
<img ng-src="{{x.eventImgSrc}}" alt="{{x.eventImgName}}"/>
<h2 ng-click="singleEventOpen(x)" class="event-name">{{x.eventName}}</h2>
<p>{{x.eventTime}}</p>
<p>{{x.eventPlace}}</p>
</div>
</div>
Then use that argument:
$scope.singleEventOpen = function(singleEvent) {
console.log("Fetching info for ", singleEvent.eventName);
//...
//Fetch and filter the data
$scope.ex = "single item data";
}
Adding an argument is the key to knowing which <h2> element was clicked.
Update
Don't use ng-repeat in the overlay, just display the single item:
<!-- Overlay that holds and displays a single event -->
̶<̶d̶i̶v̶ ̶n̶g̶-̶r̶e̶p̶e̶a̶t̶=̶"̶x̶ ̶i̶n̶ ̶s̶i̶n̶g̶l̶e̶P̶a̶g̶e̶E̶v̶e̶n̶t̶ ̶|̶ ̶f̶i̶l̶t̶e̶r̶:̶h̶a̶s̶N̶a̶m̶e̶(̶x̶.̶e̶v̶e̶n̶t̶N̶a̶m̶e̶)̶"̶>̶
<div ng-if="ex"">
<div>
<img ng-src="{{ex.eventImgSrc}}" alt="{{ex.eventImgName}}"/>
<h2 class="event-name">{{ex.eventName}}</h2>
<p>{{ex.eventTime}}</p>
<p>{{ex.eventPlace}}</p>
</div>
</div>
What I am trying to do:
I am trying to have collapsible accordion style items on a page which will expand and collapse on a click event. They will expand when a certain class is added collapsible-panel--expanded.
How I am trying to achieve it:
On each of the items I have set a click event like so:
<div (click)="toggleClass()" [class.collapsible-panel--expanded]="expanded" class="collapsible-panel" *ngFor="let category of categories">
....
</div>
<div (click)="toggleClass()" [class.collapsible-panel--expanded]="expanded" class="collapsible-panel" *ngFor="let category of categories">
....
</div>
and in the function toggleClass() I have the following:
expanded = false;
toggleClass() {
this.expanded = !this.expanded;
console.log(this.expanded)
}
The issue im facing:
When I have multiple of this on the same page and I click one, they all seem to expand.
I cannot seen to get one to expand.
Edit:
The amount of collapsible links will be dynamic and will change as they are generated and pulled from the database. It could be one link today but 30 tomorrow etc... so having set variable names like expanded 1 or expanded 2 will not be viable
Edit 2:
Ok, so the full code for the click handler is like so:
toggleClass(event) {
event.stopPropagation();
const className = 'collapsible-panel--expanded';
if (event.target.classList.contains(className)) {
event.target.classList.remove(className);
console.log("contains class, remove it")
} else {
event.target.classList.add(className);
console.log("Does not contain class, add it")
}
}
and the code in the HTML is like so:
<div (click)="toggleClass($event)" class="collapsible-panel" *ngFor="let category of categories" >
<h3 class="collapsible-panel__title">{{ category }}</h3>
<ul class="button-list button-list--small collapsible-panel__content">
<div *ngFor="let resource of resources | resInCat : category">
<span class="underline display-block margin-bottom">{{ resource.fields.title }}</span><span class="secondary" *ngIf="resource.fields.description display-block">{{ resource.fields.description }}</span>
</div>
</ul>
</div>
you could apply your class through javascript
<div (click)="handleClick($event)">
some content
</div>
then your handler
handleClick(event) {
const className = 'collapsible-panel--expanded';
if (event.target.classList.contains(className)) {
event.target.classList.remove(className);
} else {
event.target.classList.add(className);
}
}
In plain html and js it could be done like this
function handleClick(event) {
const className = 'collapsible-panel--expanded';
if (event.target.classList.contains(className)) {
event.target.classList.remove(className);
} else {
event.target.classList.add(className);
}
console.log(event.target.classList.value);
}
<div onclick="handleClick(event)">
some content
</div>
Try to pass unique Id. (little modification)Ex: -
in component.ts file:
selectedFeature: any;
categories:any[] = [
{
id: "collapseOne",
heading_id: "headingOne",
},
{
id: "collapseTwo",
heading_id: "headingTwo",
},
{
id: "collapseThree",
heading_id: "headingThree",
}
];
toggleClass(category) {
this.selectedFeature = category;
};
ngOnInit() {
this.selectedFeature = categories[0]
}
in html:-
<div class="collapsible-panel" *ngFor="let category of categories">
<!-- here you can check the condition and use it:-
ex:
<h4 class="heading" [ngClass]="{'active': selectedFeature.id==category.id}" (click)="toggleClass(category)">
<p class="your choice" *ngIf="selectedFeature.id==category.id" innerHtml={{category.heading}}></p>
enter code here
-->
.....
</div>
Try maintaining an array of expanded items.
expanded = []; // take array of boolean
toggleClass(id: number) {
this.expanded[i] = !this.expanded[i];
console.log(this.expanded[i]);
}
Your solution will be the usage of template local variables:
see this: https://stackoverflow.com/a/38582320/3634274
You are using the same property expanded to toggle for all the divs, so when you set to true for one div, it sets it true for all the divs.
Try setting different properties like this:
<div (click)="toggleClass("1")" [class.collapsible-panel--expanded]="expanded1" class="collapsible-panel" *ngFor="let category of categories">
....
</div>
<div (click)="toggleClass("2")" [class.collapsible-panel--expanded]="expanded2" class="collapsible-panel" *ngFor="let category of categories">
....
</div>
TS:
expanded1 = false;
expanded2 = false;
toggleClass(number:any) {
this["expanded" + number] = !this["expanded" + number];
console.log(this["expanded" + number])
}
I'm attempting to create a component that consists of rows of data, which when clicked, open a modal with information relating to that table row. For example, when a user clicks on "team 1", a modal would appear showing a new table displaying each of the users assigned to that team.
I've managed to achieve this using manually provided parameters, however I have no idea how to make the modal dynamically display data depending on which table row has been clicked. Here is a link to a jsfiddle that i've made to show my problem.
getInitialState: function () {
return {
teams:[
{
id: '1',
teamName: 'team 1',
users: ['dave', 'steve', 'jim', 'barry', 'tom', 'harry']
},
]
};
render: function () {
var self = this;
var projectsTable = this.state.teams.map(function (obj, index) {
return (
<tr className="table-teamProject" key={index} data-toggle="modal" data-target="#projectUsersModal" data-id='3'>
<div className="mCellsContainer">
<div className="mCellsNames">{obj.teamName}</div>
<div className="mCellsCount">{obj.users.length} Users</div>
</div>
</tr>
);
});
var projectUsersModal = this.state.teams.map(function (obj, index) {
return (
<div className="modal projectUsersModal fade" id="projectUsersModal" tabIndex={-1} role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
</div>
</div>
</div>
);
});
return (
<div>
<div className="projectsColContainer">
<div className="panel panel-default">
<div className="panel-heading">Projects</div>
<table className="scroll-table">
{projectsTable}
{projectUsersModal}
</table>
</div>
</div>
</div>
);
}
The render() method is creating, what I think would be, a hidden modal for every team you have in your teams array, regardless of if the user requested the modal to show up (clicked on the team's link) or not. A better approach would be to create the specific modal on demand, that's when the user clicks on the team's link.
This can be done by creating a click handler and inside that function you would modify the state by setting the id of the team the modal is about, like so:
onClickTeam: function(teamId) {
this.setState({
openModalTeamId: this.state.openModalTeamId == teamId ? null : teamId
});
}
Then in your render() method you will want to check if this openModalTeamId state property has some value in it, if so and since your are storing the team's id in there, you would want to look for this particular team in your state teams array using the Array.prototype.find and then use the returned result to construct your modal's content.
render: function() {
...
var modalBody;
if (this.state.openModalTeamId) {
var team = this.state.teams.find(function(el) {
return el.id == self.state.openModalTeamId
});
modalBody =
...
<div className="modal-body">
Lets assume this is your modal containing the
following info about the selected team:
<br /><br />
{JSON.stringify(team)}
<br /><br />
<div onClick={(this.onClickTeam.bind(this, team.id))}>
Click me to close
</div>
</div>
...
}
...
}
Once you have that you can just append this new modalBody variable to your render's JSX just like you do in your code using the projectUsersModal variable. If no team was clicked on, then this variable would be undefined and no modal will show up.
return (
<div>
<div className="projectsColContainer">
<table className="scroll-table">
{projectsTable}
{modalBody}
</table>
</div>
</div>
);
jsFiddle
You can use https://github.com/fckt/react-layer-stack .
It allows you to both use variables from closure (which will propagate automatically if you'll provide it to "use" property of Layer) and also set event data from your toggle to modal window. Also you can have "stack" of layers with zIndex, one on another.
import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
return (
<Cell {...props}>
// the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
<Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
hideMe, // alias for `hide(modalId)`
index } // useful to know to set zIndex, for example
, e) => // access to the arguments (click event data in this example)
<Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
<ConfirmationDialog
title={ 'Delete' }
message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
confirmButton={ <Button type="primary">DELETE</Button> }
onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
close={ hideMe } />
</Modal> }
</Layer>
// this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
<LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
<div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
<Icon type="trash" />
</div> }
</LayerContext>
</Cell>)
// ...