0

I'm looking for a way to find all elements that contain an attribute that begins witha a given string. For example:

document.querySelectorAll('[ng-*]') would return all elements with Angular directives (ng-click, ng-show, ng-hide...).

But the wildcard does not seem to work.

Any ideas? I could implement my own, but it wouldn't be as effective.

Jo Colina
  • 1,870
  • 7
  • 28
  • 46
  • 1
    There is no way to do this. –  Jul 30 '16 at 10:24
  • See http://stackoverflow.com/questions/13222334/wildcards-in-html5-data-attributes or http://stackoverflow.com/questions/27326470/is-it-possible-to-wildcard-for-an-attribute-name-with-a-wildcarded-attribute or http://stackoverflow.com/questions/21222375/css-selector-for-attribute-names-based-on-a-wildcard/36891269#36891269. –  Jul 30 '16 at 10:25
  • @torazaburo: None of those applies as they're all jQuery-specific. – T.J. Crowder Jul 30 '16 at 10:27
  • http://stackoverflow.com/questions/21222375/css-selector-for-attribute-names-based-on-a-wildcard/36891269#36891269 seems like a pure dup. –  Jul 30 '16 at 10:50

2 Answers2

5

There's no CSS selector that lets you do wildcards on the name of an attribute, just on the value, which obviously isn't want you want.

Unfortunately, you'll have to do the work manually.

If you know the list you'll be processing will be small, you can do it with a naive version query-then-filter; if you think it may be larger, a DOM walker solution (see below) would probably be better.

The naive query-then-filter (ES2015 syntax):

// Best if you can use *some* selector in the querySelectorAll rather than just *
// to keep the list to a minimum
let elements =
  [...document.querySelectorAll("*")].filter(
    element => [...element.attributes].some(
      attr => attr.nodeName.startsWith("ng-")
    )
  );
console.log(elements);
<div></div>
<div ng-bar></div>
<div></div>
<div ng-foo></div>
<div></div>

ES5 version:

var elements = Array.prototype.filter.call(
  document.querySelectorAll("*"), // Best if you can use *something* here
                                  // to keep the list to a minimum
  function(element) {
    return Array.prototype.some.call(
      element.attributes,
      function(attr) {
        return attr.nodeName.startsWith("ng-");
      }
    );
  }
);
console.log(elements);
<div></div>
<div ng-bar></div>
<div></div>
<div ng-foo></div>
<div></div>

Walker solution (ES2015+):

function domFind(element, predicate, results = []) {
  if (!element.children) {
    throw new Error("Starting node must be an element or document");
  }
  if (predicate(element)) {
    results.push(element);
  }
  if (element.children && element.children.length) {
    [...element.children].forEach(child => {
      domFind(child, predicate, results);
    });
  }
  return results;
}
let elements = domFind(document, element => {
  return element.attributes && [...element.attributes].some(attr => attr.nodeName.startsWith("ng-"));
});
console.log(elements);
<div></div>
<div ng-bar></div>
<div></div>
<div>
  <div>
    <div ng-foo></div>
  </div>
  <div ng-baz></div>
</div>
<div></div>

ES5:

function domFind(element, predicate, results) {
  if (!results) {
    results = [];
  }
  if (!element.children) {
    throw new Error("Starting node must be an element or document");
  }
  if (predicate(element)) {
    results.push(element);
  }
  if (element.children && element.children.length) {
    Array.prototype.forEach.call(element.children, function(child) {
      domFind(child, predicate, results);
    });
  }
  return results;
}
var elements = domFind(document, function(element) {
  return element.attributes && Array.prototype.some.call(element.attributes, function(attr) {
    return attr.nodeName.startsWith("ng-");
  });
});
console.log(elements);
<div></div>
<div ng-bar></div>
<div></div>
<div>
  <div>
    <div ng-foo></div>
  </div>
  <div ng-baz></div>
</div>
<div></div>

For all of the above, note that String#startsWith may need a shim prior to ES2015.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • This is a dup I think. –  Jul 30 '16 at 10:26
  • 1
    Yeah, I guessed I had to do it manually. Will make a small funcction a post a gist. Thanks! – Jo Colina Jul 30 '16 at 10:31
  • 1
    Thanks TJ. I added regex support. https://gist.github.com/jsmrcaga/b3c14be5138256dca1d94993d5e6d7b4 – Jo Colina Jul 30 '16 at 10:40
  • These days I am writing `[...element.attributes].some(...`. –  Jul 30 '16 at 11:10
  • @torazaburo: Yeah. Originally I had `Array.from` but the spread operator is **much** more elegant. The reason I didn't create arrays above for the elements and attributes is that there could be a *lot* of elements. Guilty of premature micro-optimizing. :-) – T.J. Crowder Jul 30 '16 at 11:12
  • Instead of using a `querySelectorAll(wildcard)` which will get anything in the page, a TreeWalker with a filter would probably be like a lot much faster, wouldn't it? – Kaiido Jul 30 '16 at 11:21
  • @Kaiido: Probably, yeah. – T.J. Crowder Jul 30 '16 at 11:22
  • @T.J.Crowder, do you want to add it or do you mind if I borrow you your attribute filtering function? – Kaiido Jul 30 '16 at 11:31
  • @Kaiido: LOL, I was in the process of adding it (felt guilty and a bit silly). :-) (You're always welcome to borrow.) – T.J. Crowder Jul 30 '16 at 11:37
  • @Kaiido: **Wow** Chrome is fast. `document.querySelectorAll("*")` on [the ES2016 spec](http://www.ecma-international.org/ecma-262/7.0/index.html) (a fairly large and complex document) takes 5ms. I'd still probably use a walker (because of the memory impact), but...wow. – T.J. Crowder Jul 30 '16 at 11:41
  • @T.J.Crowder did you do some filtering so that I can test against a TreeWalker ? – Kaiido Jul 30 '16 at 11:43
  • @Kaiido: You mean the DOM spec? No, I literally just did `console.time("query"); document.querySelectorAll("*"); console.timeEnd("query");` in the console. – T.J. Crowder Jul 30 '16 at 11:45
  • Hmm IMM TreeWalkers are really good in the filtering process, not sure if getting all the elements is really relevant. But I'm no expert at all ;-) – Kaiido Jul 30 '16 at 11:46
1

@T.J.Crowder's answer is right in everything.

But IMK a faster way to implement a custom querySelector method should definitely be done with a TreeWalker.

Here is a simple (probably not perfect) implementation of what you are after, using the ng- filtering proposed by T.J:

var filterAttr = function(element) {
  return Array.prototype.some.call(
    element.attributes,
    function(attr) {
      return attr.nodeName.indexOf("ng-") === 0;
    }
    // webkit defaults to FILTER_REJECT
  ) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 
};
// IE doesn't like {acceptNode:...} object
filterAttr.acceptNode = filterAttr;

var treeWalker = document.createTreeWalker(
  document.body,
  NodeFilter.SHOW_ELEMENT,
  filterAttr,
  false
);
var nodeList = [];
while (treeWalker.nextNode()) {
  nodeList.push(treeWalker.currentNode);
}

console.log(nodeList);
<div></div>
<div ng-bar></div>
<div></div>
<div>
  <div>
    <div ng-foo></div>
  </div>
  <div ng-baz></div>
</div>
<div></div>

PS: any upvote on this answer should also be rewarded to T.J's one too.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Oddly, when I run that, I only see the `ng-bar` node, not the `ng-foo` or `ng-baz`. – T.J. Crowder Jul 30 '16 at 11:54
  • @T.J.Crowder on Chrome ? only tested on FF which shows `foo` and `baz` too. Indeed that's odd. let me delete it while I investigate. Ok, can repro, I just don't get where this comes from... I will keep deleted for now, and maybe post a question about it tomorrow, and even a bug report to chromium ? – Kaiido Jul 30 '16 at 11:56
  • @T.J.Crowder, I finally got it all, webkit browsers will simply default to `NodeFilter.FILTER_REJECT` when the custom filter function returns false, while FF returns `FILTER_SKIP`. Note sure who is right, but anyway better to write it clearly ;-) Also IE doesn't like the `{acceptNode: function(node){..}}` version and seems to be waiting for a function with a `filterNode` property. Crazy how much I can learn with a single answer, I attempt to write. – Kaiido Jul 31 '16 at 08:41