JavaScript: List Unique Attributes with Count - javascript

I am listing items for sale on a site, and I'd like to have a sidebar to filter the items. So for each attribute, in the filter section I want to list each unique value, and show a count of how many of that value there are.
As a very simple example, in the snippet below, there are four cars listed, with data-attributes for the number of doors. To let people filter by number of doors, I need to show each option in a sidebar and let them check one.
The problem for me is that for some of the attributes, I don't know what values there will be in advance. So I need some way to loop through each item, record each unique value for that attribute, and also count up the instances of each value, and display that in the "Filter By" sidebar.
This is what that sidebar would look like in the example:
Doors:
[] 2 (2)
[] 4 (1)
[] 5 (1)
It looks like I may need to use .each() and .length(), but I'm having trouble putting this together. I have no trouble filtering out the items once the checkboxes are checked, but my problem is just listing out all of the filter options.
<div class="items-list">
<p class="item" data-doors="2">Ford Mustang</p>
<p class="item" data-doors="5">Nissan Versa</p>
<p class="item" data-doors="4">Honda Civic</p>
<p class="item" data-doors="2">Audi A5</p>
</div>

This sets up an object with key/value pairs the key being the number of doors, the value being how many instances of them. Then it runs that object through an iteration which applies the key value pairs to a checkbox input.
const createCheckboxes = (dataName) => {
let refobj = {};
$('.items-list .item').each(function() {
let d = $(this).data(dataName);
refobj[d] = refobj[d] ? refobj[d] + 1 : 1;
});
Object.entries(refobj).forEach(set => $('.inputs[data-group="' + dataName + '"]').append(getCheckbox(set, dataName)));
}
const getCheckbox = (data, keyname) => {
const [key, value] = data;
return `
<label>
<input type='checkbox' name='${keyname}[]' class='${keyname}' value='${key}' onclick='testinput(this)' /> ${key} (${value})
</label>`
}
const testinput = (el) => {
console.log('input with class: ' + $(el).attr('class') + ' and value: ' + $(el).val() + ' and name: ' + $(el).attr('name') + ' clicked')
}
createCheckboxes('doors');
createCheckboxes('wheels');
label {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="items-list">
<p class="item" data-doors="2" data-wheels="4">Ford Mustang</p>
<p class="item" data-doors="5" data-wheels="4">Nissan Versa</p>
<p class="item" data-doors="4" data-wheels="4">Honda Civic</p>
<p class="item" data-doors="2" data-wheels="4">Audi A5</p>
<p class="item" data-doors="1" data-wheels="3">Messerschmitt KR200</p>
</div>
<div class='inputs' data-group="doors">How many doors?</div>
<div class='inputs' data-group="wheels">How many wheels?</div>

The final filterObj (in the below code) holds what you are looking for.
const list = document.getElementsByClassName('item');
//Converting HTMLNode list to Array.
const arrList = Array.from(list);
const filterObj = {};
arrList.map(el => {
filterObj[el.dataset.doors] = filterObj[el.dataset.doors]
? filterObj[el.dataset.doors] + 1
: 1;
});
console.log(filterObj);
<div class="items-list">
<p class="item" data-doors="2">Ford Mustang</p>
<p class="item" data-doors="5">Nissan Versa</p>
<p class="item" data-doors="4">Honda Civic</p>
<p class="item" data-doors="2">Audi A5</p>
</div>

Related

How to pick out span tag that doesn't have a class

Using document.getElementsByClassName("span3 pickItem").outerHTML) I set a variable htmlData to contain:
<div itemscope="" class="span3 pickItem">
<p itemprop="name" class="name">
<a href="/user/view?id=4943">
<span>John Doe</span>
<br />
<span>'Arizona'</span>
<br />
<span>'Student'</span>
</a>
</p>
</div>
How can I pick each value from the span tag and console.log them as such:
console.log(...span[0]) output: John Doe
console.log(...span[1]) output: Arizona
console.log(...span[2]) output: Student
Could do something like this
let namesArr = [];
let name = document.querySelectorAll("span");
name.forEach(function(names) {
namesArr.push(names.innerHTML);//Stores all names in array so you can access later
});
console.log(namesArr[0]);
console.log(namesArr[1]);
console.log(namesArr[2]);
Something like this should work:
// console log all names
const items = document.querySelectorAll('div.pickItem span')
items.forEach(item => {
console.log(item.innerText)
})
// console each from array index
console.log(items[0].innerText)
console.log(items[1].innerText)
console.log(items[2].innerText)
let spans = document.getElementsByTagName('span');
console.log(spans[0].innerHTML); //'Joe Doe'
You don't even need the htmlData variable because the DOM elements already exist. If you want to learn about parsing a string of HTML (this is what your htmlData variable has in it) into DOM elements, you can reivew DOMParser.parseFromString() - Web APIs | MDN.
Select the anchor
Select its child spans and map their textContent properties
function getTextFromSpan (span) {
// Just return the text as-is:
// return span.textContent?.trim() ?? '';
// Or, you can also remove the single quotes from the text value if they exist:
const text = span.textContent?.trim() ?? '';
const singleQuote = `'`;
const hasQuotes = text.startsWith(singleQuote) && text.endsWith(singleQuote);
return hasQuotes ? text.slice(1, -1) : text;
}
const anchor = document.querySelector('div.span3.pickItem > p.name > a');
const spanTexts = [...anchor.querySelectorAll(':scope > span')].map(getTextFromSpan);
for (const text of spanTexts) {
console.log(text);
}
<div itemscope="" class="span3 pickItem">
<p itemprop="name" class="name">
<a href="/user/view?id=4943">
<span>John Doe</span>
<br />
<span>'Arizona'</span>
<br />
<span>'Student'</span>
</a>
</p>
</div>

Pushing objects into an array of objects duplicates the already existing objects in that array

Like the question says, when I push objects into an array of objects it duplicates the already existing objects in that array. Only after page reload the copies are removed. I know it has got to do with reference. I tried copying the object that is pushed into the array, created a new object with props all to no effect. Any help would be appreciated. Thanks
// The persons array is an array of objects
import { persons } from './main.js';
let entriesFound = []
let data = localStorage.getItem('entriesFound') ;
if(data){
entriesFound = JSON.parse(data)
loadList(entriesFound)
}
//Renders the search results to the UI from localStorage
function loadList(array) {
for(let el of array) {
const html =
`<div id="${el.id}" class="item2">
<div class="info2">Name:<div class="name">${el.name}</div></div>
<div class="info2">Date of birth:<div class="born">${el.dob}</div></div>
<div class="info2">Age:<div class="age">${el.age}</div></div>
<div class="info2">Place of birth:<div class="city">${el.city}</div></div>
<div class="info2">ID:<div class="id">${el.id}</div></div>
<div class="info2">Entered:<div class="added">${el.entered}</div></div>
</div>`;
document.querySelector('.searchResult').insertAdjacentHTML('beforeend', html)
}
}
//Search button to search for entry (in the persons array) that matches the condtional
export const searchBtn = document.querySelector('.search').addEventListener('click' , function() {
// Get search string from search bar
const name = document.querySelector('.searchInput')
// persons array
persons.filter( el => {
if(el.name === name.value) {
entriesFound.push(el); // Pushes the object (el) to the entriesFound array
} // I guess this is were it goes wrong
})
addItem(entriesFound)
name.value = ""
localStorage.setItem('entriesFound', JSON.stringify(entriesFound))
})
// Renders the new search result to the UI
function addItem(entries) {
for( let item of entries) {
const html =
`<div id="${item.id}" class="item2">
<div class="info2">Name:<div class="name">${item.name}</div></div>
<div class="info2">Date of birth:<div class="born">${item.dob}</div></div>
<div class="info2">Age:<div class="age">${item.age}</div></div>
<div class="info2">Place of birth:<div class="city">${item.city}</div></div>
<div class="info2">ID:<div class="id">${item.id}</div></div>
<div class="info2">Entered:<div class="added">${item.entered}</div></div>
</div>`;
document.querySelector('.searchResult').insertAdjacentHTML('beforeend', html)
}
}
Found the problem. The addItem function loops over the entire entriesFound array, while it only has to add one entry to the search results.
persons.forEach( el => {
if(el.name === name.value) {
addItem(el) <---- like so, el is an object from
the persons array of objects!
entriesFound.push(el);
}
})
addItem(entriesFound)
name.value = ""
localStorage.setItem('entriesFound', JSON.stringify(entriesFound))
})
// Renders the new search result to the UI
function addItem(entry) {
<--- got rid of the loop!
const html =
`<div id="${entry.id}" class="item2">
<div class="info2">Name:<div class="name">${entry.name}</div></div>
<div class="info2">Date of birth:<div class="born">${entry.dob}</div></div>
<div class="info2">Age:<div class="age">${entry.age}</div></div>
<div class="info2">Place of birth:<div class="city">${entry.city}</div></div>
<div class="info2">ID:<div class="id">${entry.id}</div></div>
<div class="info2">Entered:<div class="added">${entry.entered}</div></div>
</div>`;
document.querySelector('.searchResult').insertAdjacentHTML('beforeend', html)
}

How to order html elements in the same order as array using jQuery?

I'd like to order html elements in the same order as arrayData.
JavaScript
var arrayData = [59, 56, 57];
HTML
<div class="selectize-input">
<div class="item" data-value="56">Dog</div>
<div class="item" data-value="57">Rabbit</div>
<div class="item" data-value="59">Cat</div>
</div>
I want the result like this.
<div class="selectize-input">
<div class="item" data-value="59">Cat</div>
<div class="item" data-value="56">Dog</div>
<div class="item" data-value="57">Rabbit</div>
</div>
Is there any good way to achive this?
Thank you in advance.
[Additional]
I want to get result something like this, but don't now how to apply it to html elements and render on browser.
const orderRule = ['Cat', 'Rabbit', 'Dog', 'Pig', 'Mouse'],
array = ['Mouse','Rabbit', 'Pig', 'Dog', 'Cat'];
const sortArray = [...array].sort((a, b) => orderRule.indexOf(a) - orderRule.indexOf(b));
console.log(sortArray);
You can loop over your array using $.each and then use append to add div at particular position.
Here is demo code :
var arrayData = [59, 56, 57];
//looping through array
$.each(arrayData, function() {
//append div at particular positon using data-value
$(".selectize-input").append($("div [data-value=" + this + "]"));
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="selectize-input">
<div class="item" data-value="56">Dog</div>
<div class="item" data-value="57">Rabbit</div>
<div class="item" data-value="59">Cat</div>
</div>
So, I'm not quite sure if I fully understand the question. But if you're trying to dynamically render a div for each piece of data, you could try something like...
JS:
const arrayData = [
{value:59, title:"Cat"},
{value:56, title:"Dog"},
{...}
]
arrayData.forEach((dataObject) => {
$('.selectize-input').append(
document.createElement('div')
.innerHTML(dataObj.title)
.attr('data-value', dataObj.value)
.addClass('item')
)
}
I'm not really a jQuery person, so I'm not entirely sure that this will work, but I believe it will send you in the right direction.
First, I am making your array into an array of objects which include both the value of the item, but also the title of the item.
Then, I am using a forEach loop to run through each item and create a new div based on it. It appends these divs to the .selectize-input.
I hope it helps you.
I should say, the stuff that you're doing here is something that ReactJS is really good at. It's like a souped-up javascript that allows you to create and manipulate HTML elements really easily.
Check it out at https://reactjs.org/!
If you only have the parent element in html and want to add the child div in sorted order then :
const divList = [
{value:59, title:"Cat"},
{value:56, title:"Dog"}
]
divList.forEach((obj) => {
$("<div />")
.html(obj.title)
.attr('data-value', obj.value)
.addClass('item')
.appendTo( $('.selectize-input'))
})
Codepen : https://codepen.io/AdityaDwivedi/pen/pojMYRr
If you already have the div element added in your html & you want to sort them up :
<div class="selectize-input">
<div class="item" data-value="56" id="56">Dog</div>
<div class="item" data-value="57" id="57">Rabbit</div>
<div class="item" data-value="59" id="59">Cat</div>
</div>
`
const divOrder = ["59", "56", "57"]
const orderedDiv = $('.selectize-input > div').sort((a, b) =>
divOrder.indexOf(a.id) - divOrder.indexOf(b.id)
);
$('.selectize-input').empty()
orderedDiv.map((obj) =>{
$("<div />")
.html(orderedDiv[obj].innerHTML)
.attr('data-value', orderedDiv[obj].value)
.appendTo( $('.selectize-input'))})
`
Codepen : https://codepen.io/AdityaDwivedi/pen/QWjeopv?editors=1111

Looping through all divs inside a container div using jQuery

I'm trying to do a sort of invoicing system, and the html looks like this:
<invoice>
<headers>
<div date contenteditable>15-Jan-2020</div>
<div buyer contenteditable>McDonalds</div>
<div order contenteditable>145632</div>
</headers>
<item>
<div name contenteditable>Big Mac</div>
<div quantity contenteditable>5</div>
<div rate contenteditable>20.00</div>
</item>
<item>
<div name contenteditable>Small Mac</div>
<div quantity contenteditable>10</div>
<div rate contenteditable>10.00</div>
</item>
</invoice>
<button>Loop</button>
I need to loop through each <invoice> and get details from <headers> and <item>, so the end results look like this.
date : 15-Jan-2020 buyer : McDonalds order:145632
item : Big Mac quantity : 5 rate : 20.00
item : Small Mac quantity : 10 rate : 10.00
I plan on sending this data as json to a PHP script for processing.
The problem is, <headers>,<items> wont be the only containers in each invoice. There could be <address>,<transporter> etc. but they'll all be inside each <invoice>.
With that being the case, how can I loop through each container and get it's data?
Here's the jQuery I was attempting:
var button = $("button")
button.on("click", function() {
$('invoice').each(function() {
alert('It works');
});
});
Fiddle here
You can loop through div and use data-attribute for name label as below
$('invoice>headers>div, invoice>item>div').each(function(index,item) {
console.log($(this).attr('data-name'), $(this).text());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<invoice>
<headers>
<div date contenteditable data-name="date">15-Jan-2020</div>
<div buyer contenteditable data-name="buyer">McDonalds</div>
<div order contenteditable data-name="order">145632</div>
</headers>
<item>
<div name contenteditable data-name="name">Big Mac</div>
<div quantity contenteditable data-name="quantity">5</div>
<div rate contenteditable data-name="rate">20.00</div>
</item>
<item>
<div name contenteditable data-name="name">Small Mac</div>
<div quantity contenteditable data-name="quantity">10</div>
<div rate contenteditable data-name="rate">10.00</div>
</item>
</invoice>
$('headers > div, item > div').each(function(item) {
console.log('item');
});
It seems your HTML isn't valid HTML. The spec doesn't define elements like <invoice>, <headers> and <item>. Besides that, attributes on elements almost always resemble key-value pairs, meaning you should declare your name, buyer, order, quantity and rate attributes as values of existing attributes. The contenteditable attribute is a boolean attribute which is OK to be left as it currently is.
Here is a fixed and working example:
var button = $('#read-invoice');
// readLine :: [String] -> (HTMLElement -> String)
function readLine(fields) {
return function (el) {
return fields.reduce(function (txt, field) {
var data = $('.' + field, el).text();
return txt === ''
? field + ': ' + data
: txt + '; ' + field + ': ' + data
}, '');
}
}
// readBlock :: { (HTMLElement -> String) } -> (HTMLElement -> String)
function readBlock(readers) {
return function (el) {
var rtype = el.className;
if (typeof readers[rtype] === 'function') {
return readers[rtype](el);
}
return '';
}
}
// autoRead :: HTMLElement -> String
var autoRead = readBlock({
headers: readLine(['date', 'buyer', 'order']),
item: readLine(['name', 'quantity', 'rate'])
// ... address, etc.
});
button.on('click', function () {
var result = $('.invoice').
children().
toArray().
reduce(function (txt, el) {
var line = autoRead(el);
return line === ''
? txt
: txt + line + '\n';
}, '');
console.log(result);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="invoice">
<div class="headers">
<div class="date" contenteditable>15-Jan-2020</div>
<div class="buyer" contenteditable>McDonalds</div>
<div class="order" contenteditable>145632</div>
</div>
<div class="item">
<div class="name" contenteditable>Big Mac</div>
<div class="quantity" contenteditable>5</div>
<div class="rate" contenteditable>20.00</div>
</div>
<div class="item">
<div class="name" contenteditable>Small Mac</div>
<div class="quantity" contenteditable>10</div>
<div class="rate" contenteditable>10.00</div>
</div>
</div>
<button id="read-invoice">Loop</button>
JS explanation
The function readLine takes an Array of Strings, where each String resembles the class name of one of the inner <div> elements. It returns a function that's waiting for a "block" element (like <div class="headers">) and reads the contents of it's contained <div>'s into a single String. Let's call the returned function a reader.
The readBlock function takes an Object of reader functions and returns a function taking a "block" element. The returned function determines which type of "block" it received and calls the matching reader function with the element as argument. If no reader matches the block type, it returns the empty String.
In the end, autoRead becomes a single function taking in a whole "block" element and returning all of it's contents as a line of text.
The button click handler looks up the <div class="invoice"> element, traverses it's DOM tree down to it's child elements (our "block" elements) and passes each "block" to autoRead, building up a result String. The final result is logged to the console.
Extending
To add new types of "block"s, simply define a new reader for it and add it to the Object passed to readBlock. For example, to add an <div class="address"> reader that reads "name", "street", "zip" and "city" infos:
var autoRead = readBlock({
headers: readLine(['date', 'buyer', 'order']),
item: readLine(['name', 'quantity', 'rate']),
address: readLine(['name', 'street', 'zip', 'city']) // <<< new
});
Extending the fields a certain reader reads is also simple, just add the name of the field to read:
var autoRead = readBlock({
headers: readLine(['date', 'buyer', 'order']),
item: readLine(['name', 'quantity', 'rate', 'currency']) // <<< added "currency"
});

I'm ordering data. How to put first elements with a fixed class?

I'm ordering some div with jQuery/Javascript: first for an attribute, than for another.
This is my code:
<div id="parent">
<div class="item" data-title="Marco" data-count="1">Marco (1)</div>
<div class="item selected" data-title="Fabio" data-count="5">Fabio (5)</div>
<div class="item selected" data-title="Edoardo" data-count="4">Edoardo (4)</div>
<div class="item " data-title="Paolo" data-count="8">Paolo (8)</div>
<div class="item selected" data-title="Luca" data-count="0">Luca (0)</div>
<div class="item" data-title="Andrea" data-count="4">Andrea (4)</div>
</div>
Order('#parent');
function Order(type) {
var items = $(type + ' > .item');
items.sort(function (a, b) {
return +b.getAttribute('data-count') - +a.getAttribute('data-count') || a.getAttribute('data-title') > b.getAttribute('data-title');
}).detach().appendTo($(type));
}
How you can see in the example, first is ordered by data-count, than by the data-title of the content.
What I'd like to do now is to put first the elements with the selected class on top; the other later.
But each "block" must be ordered as well by data-count and data-title. So in the example the result should be:
Fabio (5)
Edoardo (4)
Luca (0)
Paolo (8)
Andrea (4)
Marco (1)
how would you add a "order by class" here?
Start by comparing the results of hasClass():
(+$(b).hasClass('selected') - +$(a).hasClass('selected'))
(+ boolean is evaluated as 1 or 0 for true or false)
Order('#parent');
function Order(type) {
var items = $(type + ' > .item');
items.sort(function(a, b) {
return (+$(b).hasClass('selected') - +$(a).hasClass('selected')) ||
(+b.getAttribute('data-count') - +a.getAttribute('data-count')) ||
(a.getAttribute('data-title').localeCompare(b.getAttribute('data-title')));
}).appendTo($(type));
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="parent">
<div class="item" data-title="Marco" data-count="1">Marco (1)</div>
<div class="item selected" data-title="Fabio" data-count="5">Fabio (5)</div>
<div class="item selected" data-title="Edoardo" data-count="4">Edoardo (4)</div>
<div class="item " data-title="Paolo" data-count="8">Paolo (8)</div>
<div class="item selected" data-title="Luca" data-count="0">Luca (0)</div>
<div class="item" data-title="Andrea" data-count="4">Andrea (4)</div>
</div>
Note the corrected string comparison (via localeCompare()), and that detach() isn't needed.
To sort on a primary and a secondary key, the general approach is to return a -1 or +1 when you detect that the primary keys are different, and only move on to the secondary key when the primary keys are the same. edit In your case you have three keys: the presence/absence of "selected" in the "class", the count, and the title:
items.sort(function (a, b) {
// test for the "selected" class
var aSel = /\bselected\b/.test(a.className), bSel = /\bselected\b/.test(b.className);
if (aSel && !bSel) return -1;
if (bSel && !aSel) return 1;
// test the counts
var prim = +b.getAttribute('data-count') - +a.getAttribute('data-count');
if (prim) return prim;
// compare titles
var aTitle = a.getAttribute('data-title'), bTitle = b.getAttribute('data-title');
return aTitle < bTitle ? -1 :
aTitle > bTitle ? 1 :
0;
}).detach().appendTo($(type));
That way, if the count on one row is larger than the count on the next, there's no need to bother with the title so we don't even compare them. But if the counts are equal, then the title comparison determines the ordering.
That can be generalized to as many keys as you wanted of course.
Order('#parent');
function Order(type) {
var items = $(type + ' > .item');
items.sort(function (a, b) {
return $(b).hasClass('selected') || a.getAttribute('data-count').toLowerCase().localeCompare(b.getAttribute('data-count').toLowerCase())
|| a.getAttribute('data-title').toLowerCase().localeCompare(b.getAttribute('data-title').toLowerCase());
}).detach().appendTo($(type));
}

Categories