How do I sort items by array? - javascript

I understand that the wording of the question is incorrect (if someone can write it correctly, please). The task is this, I have 30 elements on the page and I need to sort them with the resulting array. That is, I get an array - let order = [2, 5, 3, 6, 12 ...] and sorting should take place in accordance with this order, that is, the first element is the 2nd element from HTML, the second element is the 5th element from HTML (according to the given array). The initial order is equal to the number in data-custom-sort.
There will be many such an array. And I don't understand how to do it universally. Can someone have any ideas?
I have not formulated very well, so if you have questions - ask.
The HTML is something like this:
<a id="sort-best" class="choose-cat">best</a>
<div>
<article data-custom-sort="1">
...
</article>
<article data-custom-sort="2">
...
</article>
<article data-custom-sort="3">
...
</article>
//and etc
</div>
These are product cards in the catalog. I need to sort them
document.querySelector('#sort-best').onclick = sortBest;
function sortBest() {
let nav = document.querySelector('#game-items-cart');
for (let i = 0; i < nav.children.length; i++) {
for (let j = i; j < nav.children.length; j++) {
if (+nav.children[i].getAttribute('data-sort') > +nav.children[j].getAttribute('data-sort')) {
replaceNode = nav.replaceChild(nav.children[j], nav.children[i]);
insertAfter(replaceNode, nav.children[i]);
}
}
}
}
function insertAfter(elem, refElem) {
return refElem.parentNode.insertBefore(elem, refElem.nextSibling);
}
I used this code to sort through the data attributes. That is, the number in the data attribute = the ordinal after sorting.

Like this?
let order = [2, 1, 3];
const container = document.getElementById("container");
document.getElementById("sort-best").addEventListener("click", e => {
e.preventDefault()
order.forEach(idx => container.appendChild(container.querySelector("[data-custom-sort='" + idx + "']")))
})
<a id="sort-best" class="choose-cat">best</a>
<div id="container">
<article data-custom-sort="1">
One
</article>
<article data-custom-sort="2">
Two
</article>
<article data-custom-sort="3">
Three
</article>
</div>
More generic:
const sortArticles = (cont, order) => {
const container = document.getElementById(cont);
order.forEach(idx => container.appendChild(container.querySelector("[data-custom-sort='" + idx + "']")))
};
document.getElementById("sort").addEventListener("click", e => {
const tgt = e.target;
if (tgt.classList.contains("choose-cat")) {
e.preventDefault()
sortArticles("container", tgt.dataset.order.split(","))
}
})
<div id="sort">
<a id="sort-best" class="choose-cat" data-order="3,1,2">best</a> |
<a id="sort-default" class="choose-cat" data-order="1,2,3">default</a>
</div>
<div id="container">
<article data-custom-sort="1">
One
</article>
<article data-custom-sort="2">
Two
</article>
<article data-custom-sort="3">
Three
</article>
</div>

Here is another way of doing this
// Sort reference array
const sortRef = [2, 5, 3, 1, 4];
// Get, sort, update function
const sortFn = () => {
// Apply new order by sorting and replacing sortContainer content
const newArray = [];
for(let i = 0; i < sortRef.length; i++) newArray.push(document.querySelector("[data-custom-sort='" + sortRef[i] + "']").outerHTML);
// Update html
document.getElementById("sortContainer").innerHTML = newArray.join('');
}
// Add click event
document.getElementById("clickMe").addEventListener('click', event => {
sortFn();
});
article {
border: 1px solid #ff0000;
padding: 3px;
width: 100px;
}
<div id="sortContainer">
<article data-custom-sort="1">
1
</article>
<article data-custom-sort="2">
2
</article>
<article data-custom-sort="3">
3
</article>
<article data-custom-sort="4">
4
</article>
<article data-custom-sort="5">
5
</article>
</div>
<p></p>
<button id="clickMe">Sort html data</button>

Related

Not able to use a map function on an array of dictionaries

Hope someone can help me out.
I am trying to dynamically create some cards on my webpage out of a dictionary.
I tried to create the function but the code inside the first <div>
cards.map((character)=>(
is not recognizing the array of dictionaries.
Any ideas on how to fix it?
function MemoryCards() {
const images = [
"./img/Abadango.jpeg",
"./img/abradolf.jpeg",
"./img/Adjudicator.jpeg",
"./img/AgencyDirector.jpeg",
"./img/Alan.jpeg",
"./img/Albert.jpeg",
"./img/Alexander.jpeg",
"./img/AlienMorty.jpeg",
"./img/AlienRick.jpeg",
"./img/Annie.jpeg",
"./img/AntsJonson.jpeg",
"./img/Beth.jpeg",
"./img/Jerry.jpeg",
"./img/morty.jpeg",
"./img/ricky.jpeg",
"./img/summer.jpeg"
]
const cards = [];
let len = images.length;
for (let i = 0; i < len; i++) {
let end = images[i].indexOf('.', 3);
let name = images[i].substring(6, end);
let card = { 'name': name, 'img': images[i], 'id': i };
cards.push(card);
}
return (
<div>
cards.map((character)=>(
<div class="card">
<div className="card_header">
<img src={cards.img}></img>
</div>
<div className="card_body">
<h3>{cards.name}</h3>
</div>
</div>
))
</div>
)
}
export default MemoryCards;
Inside your loop you have {cards.img} and {cards.name} but what you want is {character.img} and {character.name}
Also you are missing curly brackets {} before initializing cards loop
Note, you have a typo, instead of className you have just class here: <div class="card">
function MemoryCards() {
const images = [
"./img/Abadango.jpeg",
"./img/abradolf.jpeg",
"./img/Adjudicator.jpeg",
"./img/AgencyDirector.jpeg",
"./img/Alan.jpeg",
"./img/Albert.jpeg",
"./img/Alexander.jpeg",
"./img/AlienMorty.jpeg",
"./img/AlienRick.jpeg",
"./img/Annie.jpeg",
"./img/AntsJonson.jpeg",
"./img/Beth.jpeg",
"./img/Jerry.jpeg",
"./img/morty.jpeg",
"./img/ricky.jpeg",
"./img/summer.jpeg"
];
const cards = [];
let len = images.length;
for (let i = 0; i < len; i++) {
let end = images[i].indexOf(".", 3);
let name = images[i].substring(6, end);
let card = { name: name, img: images[i], id: i };
cards.push(card);
}
return (
<div>
{cards.map((character, idx) => (
<div key={idx} className="card">
<div className="card_header">
<img src={character.img} alt="" />
</div>
<div className="card_body">
<h3>{character.name}</h3>
</div>
</div>
))}
</div>
);
}
export default MemoryCards;
You need to wrap your variables in curly braces {} for it to work inside JSX:
return (
<div>
{cards.map((card)=>(
<div key={card.name} class="card">
<div className="card_header">
<img src={card.img}></img>
</div>
<div className="card_body">
<h3>{card.name}</h3>
</div>
</div>
))}
</div>
)
In the example above, you can see that the entire map block should be inside the curly braces, also don't forget to add an ID to the element inside the map and to use the actual variable defined inside the map function

Group 'for loop' output into groups of four

I have a JS object array that I am using a for loop to print to the page:
// For loop through druid specs
for (let i = 0; i < druidSpecs.length; i++) {
// Get spec
let spec = druidSpecs[i];
// Log Spec name, background, talents
console.log(spec.name, spec.background);
// Create spec containers
html += `
<div id="${spec.name.replace(/\s+/g, '-').toLowerCase()}" class="container">
`;
// Loop through spec talent objects
for (let i = 0; i < spec.talents.length; i++) {
let talent = spec.talents[i];
// If empty string, add empty talent box, else, add real talent
if (talent.name == "") {
html += `
<div class="blank talent"></div>
`;
} else {
html += `
<div id="${talent.name.replace(/\s+/g, '-').toLowerCase()}" class="talent" style="background: url('${talent.icon}')">
<h2 class="talent-name">${talent.name}</h2>
<h3 class="description">${talent.description}</h3>
</div>
`;
}
}
// Close spec containers
html += `
</div>
`;
}
If you're familiar with World of Warcraft talents then this may make more sense. The first loop creates a container div for each class 'spec'. Then each spec has 16 talent spaces that are grouped in rows of four.
As you can see I can get the talents to print in one large list within the spec container, but I have no clue how to wrap every four talent spaces within a container div.
Ultimately, the final html output would be something like:
<div class="container" id="balance">
<div class="tier">
<div class="talent" id="improved-wrath" style="background: url('src/assets/druid/icons/0.png')">
<h2 class="talent-name">Improved Wrath</h2>
<h3 class="description">Reduces the cast time of your Wrath spell by ${value1} seconds.</h3>
</div>
<div class="talent" id="nature's-grasp" style="background: url('src/assets/druid/icons/1.png')">
<h2 class="talent-name">Nature's Grasp</h2>
<h3 class="description">While active, any time an enemy strikes the caster they have a ${value1}% chance to become afflicted by Entangling Roots (Rank 1). Only useable outdoors. 1 charge. Lasts 45 sec.</h3>
</div>
<div class="talent" id="improved-nature's-grasp" style="background: url('src/assets/druid/icons/1.png')">
<h2 class="talent-name">Improved Nature's Grasp</h2>
<h3 class="description">Increases the chance for your Nature's Grasp to entangle an enemy by ${value1}%.</h3>
</div>
<div class="blank talent"></div>
</div><!-- End tier 1 -->
<div class="tier">
<div class="talent" id="improved-entangling-roots" style="background: url('src/assets/druid/icons/2.png')">
<h2 class="talent-name">Improved Entangling Roots</h2>
<h3 class="description">Gives you a ${value1}% chance to avoid interruption caused by damage while casting Entangling Roots.</h3>
</div>
<div class="talent" id="improved-moonfire" style="background: url('src/assets/druid/icons/3.png')">
<h2 class="talent-name">Improved Moonfire</h2>
<h3 class="description">Increases the damage and critical strike chance of your Moonfire spell by ${value1}%.</h3>
</div>
<div class="talent" id="natural-weapons" style="background: url('src/assets/druid/icons/4.png')">
<h2 class="talent-name">Natural Weapons</h2>
<h3 class="description">Increases the damage you deal with physical attacks in all forms by ${value1}%.</h3>
</div>
<div class="talent" id="natural-shapeshifter" style="background: url('src/assets/druid/icons/5.png')">
<h2 class="talent-name">Natural Shapeshifter</h2>
<h3 class="description">Reduces the mana cost of all shapeshifting by ${value1}%.</h3>
</div>
</div><!-- End tier 2 -->
etc..
</div>
Thanks for any help!
One approach is use a for loop that increments by 4 and use Array#slice() within that loop to get each group of 4 items.
This allows you to create a wrapping element within each iteration of the for loop and iterate the sub group of items to put in that wrapping element
Basic example:
const data = Array(10).fill(0).map((_, i) => `Item #${i+1}`);
const container = document.getElementById('cont')
for (let i = 0; i < data.length; i = i + 4) {
const group = document.createElement('div')
group.className = 'group';
const items = data.slice(i, i + 4);
items.forEach(item => {
const itemDiv = document.createElement('div')
itemDiv.textContent = item
group.append(itemDiv)
})
container.append(group)
}
.group {
border: 2px solid #ccc;
margin: .5em
}
<div id="cont"></div>
You can create a counter with the value of iterator variable (i) from the for loop which is going through Talents. I've modified your code to depict what I mean. You might have to slightly modify it as per your requirements. Hope it helps. Also, I moved the code appending the HTML to a function because we will now need to call it at 2 places.
EDIT: I've modified the code, instead of using i for the comparision, I've added a separate counter. And this time instead of checking for modulus, I'm straight up checking if the value of counter has been incremented to 4, when it is 4, I'm running the else condition and setting the counter back to 0. I think this will do the trick.
import _ from 'lodash';
import classData from '.\\assets\\classData.js';
import './style.css';
var druidSpecs = classData.druid.specs;
let html = '';
// For loop through druid specs
for (let i = 0; i < druidSpecs.length; i++) {
// Get spec
let spec = druidSpecs[i];
// Create spec containers
html += `
<div id="${spec.name.replace(/\s+/g, '-').toLowerCase()}" class="container">
`;
const buildTalentHTML = talent => {
// If empty string, add empty talent box, else, add real talent
if (talent.name) {
//empty string is a falsy value, you can skip the equality operator
html += `
<div class="blank talent"></div>
`;
} else {
// If empty string, add empty talent box, else, add real talent
html += `
<div id="${talent.name
.replace(/\s+/g, '-')
.toLowerCase()}" class="talent" style="background: url('${
talent.icon
}')">
<h2 class="talent-name">${talent.name}</h2>
<h3 class="description">${talent.description}</h3>
</div>
`;
}
};
let talentCounter = 0;
// Loop through spec talent objects
for (let i = 0; i < spec.talents.length; i++) {
let talent = spec.talents[i];
if (talentCounter !== 4) {
buildTalentHTML(talent);
talentCounter++;
} else {
html += '<div class="tier">';
buildTalentHTML(talent);
if (talentCounter === 4) {
html += '</div>';
talentCounter = 0;
}
}
}
// Close spec containers
html += `
</div>
`;
}
document.body.insertAdjacentHTML('beforeend', html);

How to create a nested list elements of heading tags in the same order from DOM

So I have an HTML document containing any number of h1, h2, h3, h4 etc. tags, with nesting.
Example.
<h2>About cats</h2>
<p>Some Info about cats</p>
<h2>About dogs</h2>
<p>Some Info about cats</p>
<h3>Breed 1</h3>
<p>About breed 1</p>
<h3>Breed 2</h3>
<p>About breed 2</p>
<h2>About birds</h2>
<p>Info about birds</p>
Now, what I want is, do some DOM traversing, get the all of the headings tags, add id attribute to them, by making their content snake case
<h2 id="about-dogs" >About Dogs</h2>
And then, create a list element with the following content.
Nesting will be done according to the position and of the heading tag. Means each heading will be nested inside the first higher-level heading and so on.
So if there's only single h1, so it will form a tree with h1 as the root and lowest level headings as the leaves.
<ul>
<li><a href="#about-cats" >About cats</a></li>
<li>About dogs</li>
<ul>
<li><a href='#breed-1' >Breed 1</a></li>
<li><a href='#breed-2' >Breed 1</a></li>
</ul>
<li><a href='#about-birds' >About birds</a></li>
</ul>
DEMO
function getHeaders() {
const hTags = ["h1", "h2", "h3", "h4", "h5", "h6"];
const elements = document.querySelectorAll(hTags.join());
const headers = [];
elements.forEach(el => {
const text = el.innerText;
const id = text
.toLowerCase()
.split(" ")
.join("-");
el.setAttribute("id", id);
headers.push({
id,
text,
level: hTags.indexOf(el.tagName.toLowerCase())
});
});
return headers;
}
function buildTree(headers) {
const list = [];
let nextLevelHeaders = [];
let lastLevel = -1;
if (headers.length === 0) {
return "";
}
const buildSubTree = () => {
if (nextLevelHeaders.length > 0) {
list[list.length - 1] += buildTree(nextLevelHeaders);
}
};
headers.forEach(h => {
if (lastLevel !== -1 && lastLevel < h.level) {
nextLevelHeaders.push(h);
return;
}
buildSubTree();
lastLevel = h.level;
list.push(`${h.text}`);
nextLevelHeaders = [];
});
buildSubTree();
const listHTML = list.map(i => `<li>${i}</li>`).join("");
return `<ul>${listHTML}</ul>`;
}
const headers = getHeaders();
document.querySelector("#root").innerHTML = buildTree(headers);
<div id="root"></div>
<h3>About horses (h3)</h3> <!-- corner case -->
<p>Some Info about horses</p>
<h2>About cats (h2)</h2>
<p>Some Info about cats</p>
<h2>About dogs (h2)</h2>
<p>Some Info about cats</p>
<h3>Breed 1 (h3)</h3>
<p>About breed 1</p>
<h3>Breed 2 (h3)</h3>
<p>About breed 2</p>
<h4>Breed 2.1 (h4)</h4>
<p>About breed 2.1</p>
<h4>Breed 2.2 (h4)</h4>
<p>About breed 2.2</p>
<h3>Breed 3 (h3)</h3>
<p>About breed 3</p>
<h3>Breed 4 (h3)</h3>
<p>About breed 4</p>
<h2>About birds (h2)</h2>
<p>Info about birds</p>
<h4>Bird <b>one</b> (h4)</h4><!-- corner case -->
<p>Info about birds</p>
OK, I think we can do better than that, but for now, this code does what you expect,
provided that you have an element in your html with the id="list" to put your list into
see it in action on codepen
<nav id="list">
</nav>
let headers = document.querySelectorAll('h1, h2, h3')
let list = document.createElement('ul')
document.querySelector('#list')
.appendChild(list)
// list is now a ul in the nav section
let currentListLevel = 0
let currentListUl = list
let lastListItem = list
headers.forEach(h => {
let text = h.innerText
let level = h.tagName.slice(-1)
console.log(level + ':' + text)
let snakeCase = text.toLowerCase()
.trim()
.replace(' ', '-')
h.id = snakeCase // now title has id
let link = document.createElement('a')
// create the link
link.appendChild(document.createTextNode(text))
// give it the text of the header
link.href = '#' + snakeCase
// give it the reference to the header
let li = document.createElement('li')
li.appendChild(link)
if (level === currentListLevel) {
currentListUl.appendChild(li)
lastListItem = li
} else if (level > currentListLevel) {
currentListLevel = level
let ul = document.createElement('ul')
ul.level = level // store the level in a property
ul.appendChild(li)
lastListItem.appendChild(ul)
currentListUl = ul
lastListItem = li
} else if (level < currentListLevel) {
while (level < currentListLevel) {
currentListUl = currentListUl.parentNode
level = currentListUl.level
}
currentListUl.appendChild(li)
lastListItem = li
currentListLevel = level
}
})

Why can I only fire this function once?

I have this function:
const sliderTextChange = document.getElementsByClassName('slider') // text change
const changeSliderText = change => {
const sliderLeft = document.getElementsByClassName('switch-left')
const sliderRight = document.getElementsByClassName('switch-right')
for (let i = 0; i < change.length; i++) {
change[i].addEventListener('click', () => {
sliderRight[i].style.display = 'flex';
sliderLeft[i].style.display = 'none';
});
}
}
changeSliderText(sliderTextChange);
This is one of the many sliders on the website:
<div class="flex-column">
<h3>Text Colour</h3>
<div class="slider">
<div class="slider-back"></div>
<div class="slider-circle"></div>
</div>
<h3 class="switch-left">White</h3>
<h3 class="switch-right">Black</h3>
</div>
This function is quite a lot like many other functions in my code but they're only firing once. AKA I fire the event listener and but then I can't fire it again.
What's the issue here?
I have tried to simplify your code and keep the scope to be modular and reusable view.
function bindEvent() {
const sliderList = document.querySelectorAll('.slider');
[...sliderList].forEach((slider) => slider.addEventListener('click', () => {
const left = slider.parentElement.querySelector('.switch-left');
const right = slider.parentElement.querySelector('.switch-right');
const leftDisplay = left.style.display || 'flex';
const rightDisplay = right.style.display || 'none';
left.style.display = rightDisplay;
right.style.display = leftDisplay;
}, false));
}
window.onload = bindEvent;
<div>
<button class="slider"> - SLIDER 1 - </button>
<div class="switch-left">L</div><div class="switch-right">R</div>
</div>
<div>
<button class="slider"> - SLIDER 2 - </button>
<div class="switch-left">L</div><div class="switch-right">R</div>
</div>
<div>
<button class="slider"> - SLIDER 3 - </button>
<div class="switch-left">L</div><div class="switch-right">R</div>
</div>
<div>
<button class="slider"> - SLIDER 4 - </button>
<div class="switch-left">L</div><div class="switch-right">R</div>
</div>
Parameters you have chosen for your function are not really intuitive and make your example more complex.
We use querySelector, it's nicer to read but if you prefer speed, just go for getElementsByClassName, it also works on any DOM element.

react.js every nth item add opening tag or closing tag

I'm having trouble with this logic since react/jsx does not allow for non closing tags to be added to an array/child component. For example with bootstrap css I want to add a row for every 4 columns.
So the logic is as follows:
Add a opening row ex: <div className="row">, then loop inside this row and every loop append a column ex: <div className="column>{this.data}</div> when the loop reaches 4 check with if(i % 4 == 0) and add a closing </div> tag while adding new row tag <div className="row">;
The code below would work in another language but in react this is not doable since we push a closing tag and a opening tag (which is invalid jsx):
generateColumns(columns) {
let newColumns = [];
columns.forEach(function(column, idx) {
newColumns.push( <div className="column"> some data </div> );
if (idx % 4 == 0) {
// Here we end the row and start a new row, works in any other language.
newColumns.push( </div> <div className="row"> );
}
});
// This array now has the proper tags for opening a row every 4th item and closing it.
return newColumns;
},
render() {
return (
<div className="row">
{this.generateColumns(this.props.columns)}
</div>
)
}
The expected output would be:
<div class="row">
<div class="column">
Some data
</div>
<div class="column">
Some more data
</div>
<div class="column">
Other data
</div>
<div class="column">
Something else
</div>
</div>
<div class="row">
<div class="column">
Some data
</div>
<div class="column">
Some more data
</div>
<div class="column">
Other data
</div>
<div class="column">
Something else
</div>
</div>
//the above would be repeated and new rows would appear every 4 columns.
render() {
const rows = array_chunk(this.props.columns, 4)
return (
{
rows.map((row) => (
<div className="row">
{
row.map((col) => (
<div className="col">{ col }</div>
))
}
</div>
))
}
)
}
An example array_chunk (I recommend that you use lodash)
module.exports = function chunks(arr, size) {
if (!Array.isArray(arr)) {
throw new TypeError('Input should be Array');
}
if (typeof size !== 'number') {
throw new TypeError('Size should be a Number');
}
var result = [];
for (var i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, size + i));
}
return result;
};
I actually just used arrays and react handled fine the rendering.
render() {
let rows = [],
cols = []
let index = 0
const totalCols = 20;
for (index; index < totalCols; index++) {
cols.push(<div class="col" key={index}/>)
if ((index + 1) % 4 == 0) {
rows.push(
<div class="row" key={index}>
{cols}
</div>
)
cols = []
}
}
return (
<div class="container">
{rows}
</div>
)
}

Categories

Resources