querySelector, get elements whose attribute begins

2019-09-09 13:40发布

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.

2条回答
唯我独甜
2楼-- · 2019-09-09 13:51

@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.

查看更多
Animai°情兽
3楼-- · 2019-09-09 14:11

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.

查看更多
登录 后发表回答