Search code examples
javascripthtmldomselectors-api

querySelector, get elements whose attribute begins with given string


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.


Solution

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