Related
I have a series of blog posts that have an array of tags as a data-tags html attribute such as ["foo", "bar", "baz"]. I'm looking to query the DOM elements that have foo included in the data-tags array.
Example markup:
<li data-tags="['foo', 'bar', 'baz']">...</li>
I know it's possible to query if the data attributes were stored as a singular value, ie:
document.querySelectorAll('[data-specifc-tag="foo"]');
Is it even possible to select elements which arrays include a specific value?
(Vanilla JS only, no jQuery please)
To sum up, the query you need is
document.querySelectorAll('li[data-tags*=\'"foo"\']')
But you have to make sure that each element in your html array is enlosed within double quotes. You may change it to a single quote, but make sure to update the query.
You can search for multiple queries by adding more rules as follows
document.querySelectorAll('li[data-tags*=\'"foo"\']'+'[data-tags*=\'"bar"\']')
Below is a snippet that applies those queries based on some value you may be interested in. I made sure to use the same html structure you put in your question.
Edit:
I added one more function, queryAll, that allows for value queries. So, you can search for elements that must have more than one value.
function entry(search){
var string = '"' + search + '"';
return '[data-tags*=\'' + string + '\']';
}
function queryAll() {
var queries = Array.prototype.slice.call(arguments).map(function(a){
return '[data-tags*=\'' + a + '\']';
});
//example: document.querySelectorAll('li[data-tags*=\'"foo"\']');
return document.querySelectorAll('li'+ queries.join(''));
}
function query(search) {
var string = '"' + search + '"';
//example: document.querySelectorAll('li[data-tags*=\'"foo"\']');
return document.querySelectorAll('li[data-tags*=\'' + string + '\']');
}
query("foo1"); // One li
query("foo"); //Two li's
queryAll("foo1", "bar1"); //one li
queryAll("foo1", "bar1Not"); //nothing
<ul>
<li data-tags='["foo", "bar", "baz"]'>...</li>
<li data-tags='["foo1", "bar1", "baz1"]'>...</li>
<li data-tags='["foo2", "bar2", "baz2"]'>...</li>
<li data-tags='["foo", "bar2", "baz2"]'>...</li>
</ul>
Here's a vanilla js solution to your problem:
const posts = document.querySelectorAll('[data-tags]');
const fooPosts = [];
posts.forEach(post => {
if (/foo/.test(post.getAttribute('data-tags'))) {
fooPosts.push(post);
}
});
// output the filtered posts
console.log(fooPosts);
One liner alternative:
document.querySelectorAll('[data-tags]').forEach(post => /foo/.test(post.getAttribute('data-tags')) && console.log(post));
Or splitted:
document.querySelectorAll('[data-tags]').forEach(post =>
/foo/.test(post.getAttribute('data-tags')) && console.log(post));
Please, note that, as mentioned in the comments, your html markup is invalid. Data attributes should not contain complex data structure like arrays or objects. As suggested, consider performing a .join() on your array data and outputting it in the attribute [data-tags] as a string. In it each value can be separated by comma for readability.
Using the valid markup your solution will be slightly different:
const posts = document.querySelectorAll('[data-tags]');
const fooPosts = [];
posts.forEach(post => {
if (post.getAttribute('data-tags').indexOf('foo') > -1) {
fooPosts.push(post);
}
});
console.log(fooPosts);
The above code is also faster as it's using .indexOf() to filter the DOM nodes.
Here's the above code in a reusable function:
const filterNodesByAttr = (attr, tag) => {
let filtered = [];
const items = document.querySelectorAll(`[${attr}]`);
items.forEach(item => {
if (item.getAttribute(attr).indexOf(tag) > -1) {
filtered.push(item);
}
});
return filtered;
};
const fooPosts = filterNodesByAttr('data-tags', 'foo');
console.log(fooPosts);
You can use CSS to get any element on the page.
I.e.
Get elements with href's [href]
Get divs with data-type of price div[data-type=price]
var attributes = ['data-rel','type'];
attributes.forEach((at)=>{
document.querySelectorAll('['+at+']').forEach((element)=>{
element.append('<b>'+at+'</b>');
});
});
<p>Hello</p>
<p data-rel="title">World!</p>
<p type="emoji">:-)</p>
As title , how to JSON.stringify a dom element, and change back the json to a dom element.
Any one know how to do , thanks.
Here is the code :
var container = document.querySelectorAll('.container')
var json=JSON.stringify(container)
{"0":{},"1":{},"2":{},"3":{}}"//result
expected result:
{"tagname":"div","class":"container","value":"test","childelement":[...]}
I think the most reasonable approach would be to whitelist which properties of the DOM element you want to serialize:
JSON.stringify(container, ["id", "className", "tagName"])
The second parameter of the JSON.stringify function allows you to change the behavior of the stringification process. You can specify an array with the list of properties to serialize. More information here: JSON.stringify
If you want to serialize its child nodes too, some extra work is needed. In this case you will have to specify a replacer function as the second parameter of JSON.stringify, instead of an array.
let whitelist = ["id", "tagName", "className", "childNodes"];
function domToObj (domEl) {
var obj = {};
for (let i=0; i<whitelist.length; i++) {
if (domEl[whitelist[i]] instanceof NodeList) {
obj[whitelist[i]] = Array.from(domEl[whitelist[i]]);
}
else {
obj[whitelist[i]] = domEl[whitelist[i]];
}
};
return obj;
}
JSON.stringify(container, function (name, value) {
if (name === "") {
return domToObj(value);
}
if (Array.isArray(this)) {
if (typeof value === "object") {
return domToObj(value);
}
return value;
}
if (whitelist.find(x => (x === name)))
return value;
})
The replacer function transforms the hosted objects in childNodes to native objects, that JSON.stringify knows how to serialize. The whitelist array has the list of properties to serialize. You can add your own properties here.
Some extra work in the replacer function might be needed if you want to serialize other properties that reference hosted objects (for example, firstChild).
I wondered the same thing, and I appreciate the answer from #ncardeli. In my app, I needed something a little different, and I thought I'd share in case anyone is interested. It recursively displays properties of any children too.
Press the button below to run the example. You can add whatever properties you want to obj and therefore to the result.
function showStringifyResult(target) {
let result = document.getElementById("result");
result.select();
result.setRangeText(JSON.stringify(stringify(target), null, ' '));
}
function stringify(element) {
let obj = {};
obj.name = element.localName;
obj.attributes = [];
obj.children = [];
Array.from(element.attributes).forEach(a => {
obj.attributes.push({ name: a.name, value: a.value });
});
Array.from(element.children).forEach(c => {
obj.children.push(stringify(c));
});
return obj;
}
#list {
margin-top: 18px;
}
<h1>
Press the Stringify button to write the stringified object to the textarea below.
</h1>
<button onClick="showStringifyResult(document.body)" class="c1">
Stringify
</button>
<div id="list">
A list for example:
<ul class="first second">
<li id="First Item">Item 1</li>
<li>Item 2</li>
<li class="inactive">Item 3</li>
<li data-tag="tag">Item 4</li>
</ul>
</div>
<textarea id="result" cols="200" rows="20" ></textarea>
Even though this is an old thread here is my addition:
JSON.stringify(target, Object.getOwnPropertyNames(target["__proto__"]), 2)
Give you a quick list for some of the properties of the DOM element.
Old question but anyhow, I was in the same situation. All I needed was to grab the content of the outerHTML property on the DOM object. This gives you a string, which you could put into a JSON as a value or not.
let HTML = container.outerHTML
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 2 years ago.
Improve this question
I'm attempting map HTML into JSON with structure intact. Are there any libraries out there that do this or will I need to write my own? I suppose if there are no html2json libraries out there I could take an xml2json library as a start. After all, html is only a variant of xml anyway right?
UPDATE: Okay, I should probably give an example. What I'm trying to do is the following. Parse a string of html:
<div>
<span>text</span>Text2
</div>
into a json object like so:
{
"type" : "div",
"content" : [
{
"type" : "span",
"content" : [
"Text2"
]
},
"Text2"
]
}
NOTE: In case you didn't notice the tag, I'm looking for a solution in Javascript
I just wrote this function that does what you want; try it out let me know if it doesn't work correctly for you:
// Test with an element.
var initElement = document.getElementsByTagName("html")[0];
var json = mapDOM(initElement, true);
console.log(json);
// Test with a string.
initElement = "<div><span>text</span>Text2</div>";
json = mapDOM(initElement, true);
console.log(json);
function mapDOM(element, json) {
var treeObject = {};
// If string convert to document Node
if (typeof element === "string") {
if (window.DOMParser) {
parser = new DOMParser();
docNode = parser.parseFromString(element,"text/xml");
} else { // Microsoft strikes again
docNode = new ActiveXObject("Microsoft.XMLDOM");
docNode.async = false;
docNode.loadXML(element);
}
element = docNode.firstChild;
}
//Recursively loop through DOM elements and assign properties to object
function treeHTML(element, object) {
object["type"] = element.nodeName;
var nodeList = element.childNodes;
if (nodeList != null) {
if (nodeList.length) {
object["content"] = [];
for (var i = 0; i < nodeList.length; i++) {
if (nodeList[i].nodeType == 3) {
object["content"].push(nodeList[i].nodeValue);
} else {
object["content"].push({});
treeHTML(nodeList[i], object["content"][object["content"].length -1]);
}
}
}
}
if (element.attributes != null) {
if (element.attributes.length) {
object["attributes"] = {};
for (var i = 0; i < element.attributes.length; i++) {
object["attributes"][element.attributes[i].nodeName] = element.attributes[i].nodeValue;
}
}
}
}
treeHTML(element, treeObject);
return (json) ? JSON.stringify(treeObject) : treeObject;
}
Working example: http://jsfiddle.net/JUSsf/ (Tested in Chrome, I can't guarantee full browser support - you will have to test this).
It creates an object that contains the tree structure of the HTML page in the format you requested and then uses JSON.stringify() which is included in most modern browsers (IE8+, Firefox 3+ .etc); If you need to support older browsers you can include json2.js.
It can take either a DOM element or a string containing valid XHTML as an argument (I believe, I'm not sure whether the DOMParser() will choke in certain situations as it is set to "text/xml" or whether it just doesn't provide error handling. Unfortunately "text/html" has poor browser support).
You can easily change the range of this function by passing a different value as element. Whatever value you pass will be the root of your JSON map.
htlm2json
Representing complex HTML documents will be difficult and full of corner cases, but I just wanted to share a couple techniques to show how to get this kind of program started. This answer differs in that it uses data abstraction and the toJSON method to recursively build the result
Below, html2json is a tiny function which takes an HTML node as input and it returns a JSON string as the result. Pay particular attention to how the code is quite flat but it's still plenty capable of building a deeply nested tree structure – all possible with virtually zero complexity
const Elem = e => ({
tagName:
e.tagName,
textContent:
e.textContent,
attributes:
Array.from(e.attributes, ({name, value}) => [name, value]),
children:
Array.from(e.children, Elem)
})
const html2json = e =>
JSON.stringify(Elem(e), null, ' ')
console.log(html2json(document.querySelector('main')))
<main>
<h1 class="mainHeading">Some heading</h1>
<ul id="menu">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
<p>some text</p>
</main>
In the previous example, the textContent gets a little butchered. To remedy this, we introduce another data constructor, TextElem. We'll have to map over the childNodes (instead of children) and choose to return the correct data type based on e.nodeType – this gets us a littler closer to what we might need
const TextElem = e => ({
type:
'TextElem',
textContent:
e.textContent
})
const Elem = e => ({
type:
'Elem',
tagName:
e.tagName,
attributes:
Array.from(e.attributes, ({name, value}) => [name, value]),
children:
Array.from(e.childNodes, fromNode)
})
const fromNode = e => {
switch (e?.nodeType) {
case 1: return Elem(e)
case 3: return TextElem(e)
default: throw Error(`unsupported nodeType: ${e.nodeType}`)
}
}
const html2json = e =>
JSON.stringify(Elem(e), null, ' ')
console.log(html2json(document.querySelector('main')))
<main>
<h1 class="mainHeading">Some heading</h1>
<ul id="menu">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
<p>some text</p>
</main>
Anyway, that's just two iterations on the problem. Of course you'll have to address corner cases where they come up, but what's nice about this approach is that it gives you a lot of flexibility to encode the HTML however you wish in JSON – and without introducing too much complexity
In my experience, you could keep iterating with this technique and achieve really good results. If this answer is interesting to anyone and would like me to expand upon anything, let me know ^_^
Related: Recursive methods using JavaScript: building your own version of JSON.stringify
json2html
Above we go from HTML to JSON and now we can go from JSON to HTML. When we can convert between two data types without losing data, this is called an isomorphism. All we are essentially doing here is writing the inverses of each function above -
const HtmlNode = (tagName, attributes = [], children = []) => {
const e = document.createElement(tagName)
for (const [k, v] of attributes) e.setAttribute(k, v)
for (const child of children) e.appendChild(toNode(child))
return e
}
const TextNode = (text) => {
return document.createTextNode(text)
}
const toNode = t => {
switch (t?.type) {
case "Elem": return HtmlNode(t.tagName, t.attributes, t.children)
case "TextElem": return TextNode(t.textContent)
default: throw Error("unsupported type: " + t.type)
}
}
const json2html = json =>
toNode(JSON.parse(json))
const parsedJson =
{"type":"Elem","tagName":"MAIN","attributes":[],"children":[{"type":"TextElem","textContent":"\n "},{"type":"Elem","tagName":"H1","attributes":[["class","mainHeading"]],"children":[{"type":"TextElem","textContent":"Some heading"}]},{"type":"TextElem","textContent":"\n "},{"type":"Elem","tagName":"UL","attributes":[["id","menu"]],"children":[{"type":"TextElem","textContent":"\n "},{"type":"Elem","tagName":"LI","attributes":[],"children":[{"type":"Elem","tagName":"A","attributes":[["href","/a"]],"children":[{"type":"TextElem","textContent":"a"}]}]},{"type":"TextElem","textContent":"\n "},{"type":"Elem","tagName":"LI","attributes":[],"children":[{"type":"Elem","tagName":"A","attributes":[["href","/b"]],"children":[{"type":"TextElem","textContent":"b"}]}]},{"type":"TextElem","textContent":"\n "},{"type":"Elem","tagName":"LI","attributes":[],"children":[{"type":"Elem","tagName":"A","attributes":[["href","/c"]],"children":[{"type":"TextElem","textContent":"c"}]}]},{"type":"TextElem","textContent":"\n "}]},{"type":"TextElem","textContent":"\n "},{"type":"Elem","tagName":"P","attributes":[],"children":[{"type":"TextElem","textContent":"some text"}]},{"type":"TextElem","textContent":"\n"}]}
document.body.appendChild(toNode(parsedJson))
I got few links sometime back while reading on ExtJS full framework in itself is JSON.
http://www.thomasfrank.se/xml_to_json.html
http://camel.apache.org/xmljson.html
online XML to JSON converter : http://jsontoxml.utilities-online.info/
UPDATE
BTW, To get JSON as added in question, HTML need to have type & content tags in it too like this or you need to use some xslt transformation to add these elements while doing JSON conversion
<?xml version="1.0" encoding="UTF-8" ?>
<type>div</type>
<content>
<type>span</type>
<content>Text2</content>
</content>
<content>Text2</content>
Thank you #Gorge Reith. Working off the solution provided by #George Reith, here is a function that furthers (1) separates out the individual 'hrefs' links (because they might be useful), (2) uses attributes as keys (since attributes are more descriptive), and (3) it's usable within Node.js without needing Chrome by using the 'jsdom' package:
const jsdom = require('jsdom') // npm install jsdom provides in-built Window.js without needing Chrome
// Function to map HTML DOM attributes to inner text and hrefs
function mapDOM(html_string, json) {
treeObject = {}
// IMPT: use jsdom because of in-built Window.js
// DOMParser() does not provide client-side window for element access if coding in Nodejs
dom = new jsdom.JSDOM(html_string)
document = dom.window.document
element = document.firstChild
// Recursively loop through DOM elements and assign attributes to inner text object
// Why attributes instead of elements? 1. attributes more descriptive, 2. usually important and lesser
function treeHTML(element, object) {
var nodeList = element.childNodes;
if (nodeList != null) {
if (nodeList.length) {
object[element.nodeName] = [] // IMPT: empty [] array for non-text recursivable elements (see below)
for (var i = 0; i < nodeList.length; i++) {
// if final text
if (nodeList[i].nodeType == 3) {
if (element.attributes != null) {
for (var j = 0; j < element.attributes.length; j++) {
if (element.attributes[j].nodeValue !== '' &&
nodeList[i].nodeValue !== '') {
if (element.attributes[j].name === 'href') { // separate href
object[element.attributes[j].name] = element.attributes[j].nodeValue;
} else {
object[element.attributes[j].nodeValue] = nodeList[i].nodeValue;
}
}
}
}
// else if non-text then recurse on recursivable elements
} else {
object[element.nodeName].push({}); // if non-text push {} into empty [] array
treeHTML(nodeList[i], object[element.nodeName][object[element.nodeName].length -1]);
}
}
}
}
}
treeHTML(element, treeObject);
return (json) ? JSON.stringify(treeObject) : treeObject;
}
I had a similar issue where I wanted to represent HTML as JSON in the following way:
For HTML text nodes, use a string
For HTML elements, use an array with:
The (tag) name of the element
An object, mapping attribute keys to attribute values
The (inlined) list of children nodes
Example:
<div>
<span>text</span>Text2
</div>
becomes
[
'div',
{},
['span', {}, 'text'],
'Text2'
]
I wrote a function which handles transforming a DOM Element into this kind of JS structure. You can find this function at the end of this answer. The function is written in Typescript. You can use the Typescript playground to convert it to clean JavaScript.
Furthermore, if you need to parse an html string into DOM, assign to .innerHtml:
let element = document.createElement('div')
element.innerHtml = htmlString
Also, this one is common knowledge but if you need a JSON string output, use JSON.stringify.
/**
* A NodeDescriptor stands for either an (HTML) Element, or for a text node
*/
export type NodeDescriptor = ElementDescriptor | string
/**
* Array representing an HTML Element. It consists of:
*
* - The (tag) name of the element
* - An object, mapping attribute keys to attribute values
* - The (inlined) list of children nodes
*/
export type ElementDescriptor = [
string,
Record<string, string>,
...NodeDescriptor[]
]
export let htmlToJs = (element: Element, trim = true): ElementDescriptor => {
let convertElement = (element: Element): ElementDescriptor => {
let attributeObject: Record<string, string> = {}
for (let { name, value } of element.attributes) {
attributeObject[name] = value
}
let childArray: NodeDescriptor[] = []
for (let node of element.childNodes) {
let converter = htmlToJsDispatch[node.nodeType]
if (converter) {
let descriptor = converter(node as any)
let skip = false
if (trim && typeof descriptor === 'string') {
descriptor = descriptor.trim()
if (descriptor === '') skip = true
}
if (!skip) childArray.push(descriptor)
}
}
return [element.tagName.toLowerCase(), attributeObject, ...childArray]
}
let htmlToJsDispatch = {
[element.ELEMENT_NODE]: convertElement,
[element.TEXT_NODE]: (node: Text): string => node.data,
}
return convertElement(element)
}
I am ordering a my data and its working all correcty except some fields are empty or have no value. When ordered these empty field come up first. For example when ordering numbers we would get a huge empty list before getting the "0"-values.
I am doing it like thise:
ng-click="predicate = 'name'; reverse=!reverse"
and
ng-repeat="name in names | orderBy:predicate:reverse"
JSFiddle: http://jsfiddle.net/JZuCX/1/
Is there an easy elegant way to fix this? I want the empty fields to come last, no matter what.
How about this for sorting strings:
item in (items|orderBy:['!name', 'name'])
The advantage (apart from being more concise) is it sorts null & undefined with the blank strings.
In my case I wanted the blanks & nulls & undefineds together at the top (nulls and undefineds by default sort to the bottom), so I used:
item in (items|orderBy:['!!name', 'name'])
I'd write a filter that takes items with empty name from ordered array and places them at the end:
<li ng-repeat="item in (items|orderBy:'name'|emptyToEnd:'name')">{{item.name}}</li>
Code might look like this:
.filter("emptyToEnd", function () {
return function (array, key) {
if(!angular.isArray(array)) return;
var present = array.filter(function (item) {
return item[key];
});
var empty = array.filter(function (item) {
return !item[key]
});
return present.concat(empty);
};
});
Working example.
By the way, your fiddle doesn't contain any relevant code. Did you use the wrong link?
Update 2:
Your fiddle with my filter.
Down here! :D
This solution extends the normal functionality of the angularJs orderBy filter to take a third argument specifying whether or not to invert the normal sorting of null and undefined values. It observes the property names it is passed (not just one), and doesn't iterate over items a second as some of the other solutions do. It's used like this:
<li ng-repeat="item in (items|orderBy:'name':false:true)">{{item.name}}</li>
I found a bunch of threads, some not directly about orderBy, and compiled their techniques plus a couple bits of my own into this:
angular.module('lib')
.config(['$provide', function ($provide) {
$provide.decorator('orderByFilter', ['$delegate', '$parse', function ($delegate, $parse) {
return function () {
var predicates = arguments[1];
var invertEmpties = arguments[3];
if (angular.isDefined(invertEmpties)) {
if (!angular.isArray(predicates)) {
predicates = [predicates];
}
var newPredicates = [];
angular.forEach(predicates, function (predicate) {
if (angular.isString(predicate)) {
var trimmed = predicate;
if (trimmed.charAt(0) == '-') {
trimmed = trimmed.slice(1);
}
var keyFn = $parse(trimmed);
newPredicates.push(function (item) {
var value = keyFn(item);
return (angular.isDefined(value) && value != null) == invertEmpties;
})
}
newPredicates.push(predicate);
});
predicates = newPredicates;
}
return $delegate(arguments[0], predicates, arguments[2]);
}
}])
}]);
To use this code verbatim, be to specify 'lib' as a dependency for your app.
Credits to:
$parse
[nullSorter].concat(originalPredicates)
decorator pattern
I don't believe there's an "out of the box" solution for this. I could easily be wrong.
Here's my attempt at a solution using a function as the predicate:
ng-repeat="name in names | orderBy:predicate"
Inside your controller:
$scope.predicate = function(name) {
return name === '' ? 'zzzzzzz' : !name;
/* The 'zzzzzz' forces the empty names to the end,
I can't think of a simpler way at the moment. */
}
In addition to the solution of Klaster_1, add an extra parameter to make the filter more generic:
http://jsfiddle.net/Zukzuk/JZuCX/27/
Implementation
<tr ng-repeat="name in (names | orderBy:predicate:reverse | orderEmpty:'name':'toBottom')">
Filter
.filter('orderEmpty', function () {
return function (array, key, type) {
var present, empty, result;
if(!angular.isArray(array)) return;
present = array.filter(function (item) {
return item[key];
});
empty = array.filter(function (item) {
return !item[key]
});
switch(type) {
case 'toBottom':
result = present.concat(empty);
break;
case 'toTop':
result = empty.concat(present);
break;
// ... etc, etc ...
default:
result = array;
break;
}
return result;
};
});
Thnx Klaster_1!
Sorting, and reverse sorting, using a variable sort column, and keeping the undefined at the bottom, even below the negative values
I love the elegance of Sean's answer above! I needed to give my users the ability to choose the column to sort on, and choice of sort direction, but still require the undefined's to fall to the bottom, even if there are negative numbers.
The key insight from Sean that fixes negative numbers is !!. Use '!'+predicate if you are doing forward sorting and '!!'+predicate if you are doing reverse sorting.
The snippet below demonstrates this. By the way, I have put the variables that set the predicate (choice of propery to sort on) and reverse inside an object ("d") just so that we don't get weird scope issues. You may not need the "d."s in your environment.
Moreover you would probably want to use something better than my crappy buttons at the bottom of the page to control your sort predicate and direction. However this keeps the key parts of the code easy to read.
function mainController($scope) {
$scope.userArray = [
{ name: "Don", age: 20 },
{ name: "Bob", age: 30, height: 170 },
{ name: "Abe", age: 40, height: 160 },
{ name: "Zoe", age: 70 },
{ age: 70, height: 155 },
{ name: "Shorty",age:45,height: -200},
{ name: "TwinkleInEye", age: -1, height: 152 }
]
$scope.d = {}; // Create an object into which info can be stored and not trashed by Angular's tendency to add scopes
$scope.d.predicate = "name"; // This string is the name of the property on which to sort
$scope.d.reverse = false; // True means reverse the sort order
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="" ng-controller="mainController">
<div ng-repeat="user in (userArray | orderBy: (d.reverse ?['!!'+d.predicate,d.predicate]:['!'+d.predicate,d.predicate]) : d.reverse)">
Name {{ user.name }} : Age {{ user.age }} : Height {{ user.height }}
</div>
<br/>
<button ng-click="d.predicate='name';">Name</button>
<button ng-click="d.predicate='age';">Age</button>
<button ng-click="d.predicate='height';">Height</button> Currently: {{d.predicate}}
<br/> Leave undefined at bottom, but otherwise:
<button ng-click="d.reverse= !d.reverse;">Reverse</button> Currently: {{d.reverse}}
</body>
#Klaster_1 was really on to something but as soon as I needed a nested value the filter stopped working. Also, if I was reverse ordering I still wanted my null values to show up before 0. I added $parse to take care of the nested keys and added a reverse parameter to I knew when to put the null values at the top.
.filter("emptyToEnd", function ($parse) {
return function (array, key, reverse) {
if(!angular.isArray(array)) return;
var keyFn = $parse(key);
var present = [];
var empty = [];
angular.forEach(array, function(item){
var val = keyFn(item);
if(angular.isUndefined(val) || val === null) {
empty.push(item);
} else {
present.push(item);
}
});
if (reverse) {
return present.concat(empty);
} else {
return empty.concat(present);
}
};
});
I don't know why other answer suggest to put the null value records at the bottom, If I want to sort normally, means in ASC order all the null on top and in DESC order all the nulls go to bottom, I tried other answers here but could not helped me so change the code to convert the null to '' in my array and it works now smooth like this:
$scope.convertNullToBlank = function (array) {
for (var i = 0; i < array.length; i++) {
if (array[i].col1 === null)
array[i].col1 = '';
if (array[i].col2 === null)
array[i].col2 = '';
}
return array;
}
I created a gist with an alternative filter based on the previous solutions:
https://gist.github.com/360disrupt/1432ee1cd1685a0baf8967dc70ae14b1
The filter extends the existing angular filter:
angular.module 'tsd.orderByEmptyLast', []
.filter 'orderByEmptyLast', ($filter) ->
return (list, predicate, reverse)->
orderedList = $filter('orderBy')(list, if reverse then ['!' + predicate, '-' + predicate] else ['!' + predicate, predicate] )
return orderedList
On newer angular versions you might need to include orderByFilter instead of using $filter
angular.module 'tsd.orderByEmptyLast', ['orderByFilter']
.filter 'orderByEmptyLast', () ->
return (list, predicate, reverse)->
orderedList = orderByFilter(list, if reverse then ['!' + predicate, '-' + predicate] else ['!' + predicate, predicate] )
return orderedList
Given an arbitrary HTML element with zero or more data-* attributes, how can one retrieve a list of key-value pairs for the data.
E.g. given this:
<div id='prod' data-id='10' data-cat='toy' data-cid='42'>blah</div>
I would like to be able to programmatically retrieve this:
{ "id":10, "cat":"toy", "cid":42 }
Using jQuery (v1.4.3), accessing the individual bits of data using $.data() is simple if the keys are known in advance, but it is not obvious how one can do so with arbitrary sets of data.
I'm looking for a 'simple' jQuery solution if one exists, but would not mind a lower level approach otherwise. I had a go at trying to to parse $('#prod').attributes but my lack of javascript-fu is letting me down.
update
customdata does what I need. However, including a jQuery plugin just for a fraction of its functionality seemed like an overkill.
Eyeballing the source helped me fix my own code (and improved my javascript-fu).
Here's the solution I came up with:
function getDataAttributes(node) {
var d = {},
re_dataAttr = /^data\-(.+)$/;
$.each(node.get(0).attributes, function(index, attr) {
if (re_dataAttr.test(attr.nodeName)) {
var key = attr.nodeName.match(re_dataAttr)[1];
d[key] = attr.nodeValue;
}
});
return d;
}
update 2
As demonstrated in the accepted answer, the solution is trivial with jQuery (>=1.4.4). $('#prod').data() would return the required data dict.
Actually, if you're working with jQuery, as of version 1.4.3 1.4.4 (because of the bug as mentioned in the comments below), data-* attributes are supported through .data():
As of jQuery 1.4.3 HTML 5 data-
attributes will be automatically
pulled in to jQuery's data object.
Note that strings are left intact
while JavaScript values are converted
to their associated value (this
includes booleans, numbers, objects,
arrays, and null). The data-
attributes are pulled in the first
time the data property is accessed and
then are no longer accessed or mutated
(all data values are then stored
internally in jQuery).
The jQuery.fn.data function will return all of the data- attribute inside an object as key-value pairs, with the key being the part of the attribute name after data- and the value being the value of that attribute after being converted following the rules stated above.
I've also created a simple demo if that doesn't convince you: http://jsfiddle.net/yijiang/WVfSg/
A pure JavaScript solution ought to be offered as well, as the solution is not difficult:
var a = [].filter.call(el.attributes, function(at) { return /^data-/.test(at.name); });
This gives an array of attribute objects, which have name and value properties:
if (a.length) {
var firstAttributeName = a[0].name;
var firstAttributeValue = a[0].value;
}
Edit: To take it a step further, you can get a dictionary by iterating the attributes and populating a data object:
var data = {};
[].forEach.call(el.attributes, function(attr) {
if (/^data-/.test(attr.name)) {
var camelCaseName = attr.name.substr(5).replace(/-(.)/g, function ($0, $1) {
return $1.toUpperCase();
});
data[camelCaseName] = attr.value;
}
});
You could then access the value of, for example, data-my-value="2" as data.myValue;
jsfiddle.net/3KFYf/33
Edit: If you wanted to set data attributes on your element programmatically from an object, you could:
Object.keys(data).forEach(function(key) {
var attrName = "data-" + key.replace(/[A-Z]/g, function($0) {
return "-" + $0.toLowerCase();
});
el.setAttribute(attrName, data[key]);
});
jsfiddle.net/3KFYf/34
EDIT: If you are using babel or TypeScript, or coding only for es6 browsers, this is a nice place to use es6 arrow functions, and shorten the code a bit:
var a = [].filter.call(el.attributes, at => /^data-/.test(at.name));
Have a look here:
If the browser also supports the HTML5 JavaScript API, you should be able to get the data with:
var attributes = element.dataset
or
var cat = element.dataset.cat
Oh, but I also read:
Unfortunately, the new dataset property has not yet been implemented in any browser, so in the meantime it’s best to use getAttribute and setAttribute as demonstrated earlier.
It is from May 2010.
If you use jQuery anyway, you might want to have a look at the customdata plugin. I have no experience with it though.
As mentioned above modern browsers have the The HTMLElement.dataset API.
That API gives you a DOMStringMap, and you can retrieve the list of data-* attributes simply doing:
var dataset = el.dataset; // as you asked in the question
you can also retrieve a array with the data- property's key names like
var data = Object.keys(el.dataset);
or map its values by
Object.keys(el.dataset).map(function(key){ return el.dataset[key];});
// or the ES6 way: Object.keys(el.dataset).map(key=>{ return el.dataset[key];});
and like this you can iterate those and use them without the need of filtering between all attributes of the element like we needed to do before.
You should be get the data through the dataset attributes
var data = element.dataset;
dataset is useful tool for get data-attribute
or convert gilly3's excellent answer to a jQuery method:
$.fn.info = function () {
var data = {};
[].forEach.call(this.get(0).attributes, function (attr) {
if (/^data-/.test(attr.name)) {
var camelCaseName = attr.name.substr(5).replace(/-(.)/g, function ($0, $1) {
return $1.toUpperCase();
});
data[camelCaseName] = attr.value;
}
});
return data;
}
Using: $('.foo').info();
You can just iterate over the data attributes like any other object to get keys and values, here's how to do it with $.each:
$.each($('#myEl').data(), function(key, value) {
console.log(key);
console.log(value);
});
I use nested each - for me this is the easiest solution (Easy to control/change "what you do with the values - in my example output data-attributes as ul-list) (Jquery Code)
var model = $(".model");
var ul = $("<ul>").appendTo("body");
$(model).each(function(index, item) {
ul.append($(document.createElement("li")).text($(this).text()));
$.each($(this).data(), function(key, value) {
ul.append($(document.createElement("strong")).text(key + ": " + value));
ul.append($(document.createElement("br")));
}); //inner each
ul.append($(document.createElement("hr")));
}); // outer each
/*print html*/
var htmlString = $("ul").html();
$("code").text(htmlString);
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/prism.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/themes/prism-okaidia.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1 id="demo"></h1>
<ul>
<li class="model" data-price="45$" data-location="Italy" data-id="1234">Model 1</li>
<li class="model" data-price="75$" data-location="Israel" data-id="4321">Model 2</li>
<li class="model" data-price="99$" data-location="France" data-id="1212">Model 3</li>
</ul>
<pre>
<code class="language-html">
</code>
</pre>
<h2>Generate list by code</h2>
<br>
Codepen: https://codepen.io/ezra_siton/pen/GRgRwNw?editors=1111
One way of finding all data attributes is using element.attributes. Using .attributes, you can loop through all of the element attributes, filtering out the items which include the string "data-".
let element = document.getElementById("element");
function getDataAttributes(element){
let elementAttributes = {},
i = 0;
while(i < element.attributes.length){
if(element.attributes[i].name.includes("data-")){
elementAttributes[element.attributes[i].name] = element.attributes[i].value
}
i++;
}
return elementAttributes;
}
If you know the name of keys you can also use object destructuring to get values like this
const {id, cat, cid } = document.getElementById('prod').dataset;
You can also skip keys you don't need and get the ones you need like this
const { cid, id } = document.getElementById('prod').dataset;
100% Javascript no jQuery ;)
DOMStringMap :
console.log(document.getElementById('target-element-id').dataset);
or custom variable :
var data = {};
Object.entries(document.getElementById('target-element-id').dataset).forEach(([key, val]) => {
data[key] = val;
});
console.log(data);