6

I have a bunch of Custom Elements that begin with 'food-cta-'. I am looking for a way in JavaScript/jQuery to be able to select these elements. This is similar to how I can use $('*[class^="food-cta-"]') to select all the classes that start with food-cta-. Is it possible to do a search for elements that start with 'food-cta-'?

Note that I will be injecting this search onto the page, so I won't have access to Angular.

Example of Custom Elements:

  • <food-cta-download>
  • <food-cta-external>
  • <food-cta-internal>

EDIT: The code I am looking at looks like:

<food-cta-download type="primary" description="Download Recipe">
    <img src="">
    <h2></h2>
    <p></p>
</food-cta-download>

The app uses AngularJS to create Custom Elements which I believe is called Directives.

Evan Carroll
  • 78,363
  • 46
  • 261
  • 468
Jon
  • 8,205
  • 25
  • 87
  • 146
  • 2
    @EvanCarroll, [they're custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements), which is a newer standard for HTML. – zzzzBov May 31 '16 at 17:31
  • 1
    @EvanCarroll, or it's a [custom html element](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements) which will inherit from [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) or [HTMLUnknownElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLUnknownElement) if not registered – Patrick Evans May 31 '16 at 17:31
  • 1
    You can add in the link function of each directive `element.addClass('food-cta')` and then all the elements with those directive will have the same class, but AFAIK you can't select element this way as you do with class - You have to list the elements `food-cta-download, food-cta-external, food-cta-internal {background:red;}` – Alon Eitan May 31 '16 at 17:34
  • @AlonEitan Unfortunately I am unable to change any of the Angular code otherwise that would have been a wonderful solution! – Jon May 31 '16 at 17:55
  • Do you already have a list of all the possible tag names it could use? – Patrick Roberts May 31 '16 at 17:56
  • @PatrickRoberts Unfortunately I do not. In addition, the list may grow overtime which is why I am looking for a dynamic solution. – Jon May 31 '16 at 17:57
  • @Jon Can you copy the element after it was altered by the directive and add it to the question? Maybe the directives add some class or change the structure of the elements to something you can then select using a unified selector in css – Alon Eitan May 31 '16 at 18:04
  • See my [answer](http://stackoverflow.com/a/36877416) about wildcard selector for attributes that is also applicable to tag-name wildcard selectors. TLDR: that's currently impossible, but there is a proposal that has some acceptance and will probably specced/implemented in the future. – Marat Tanalin May 31 '16 at 18:22
  • I don't believe that these are [Angular Directives](http://www.w3schools.com/angular/angular_directives.asp). Angular Directives are custom attributes, not Custom Elements. They sit on regular HTML5 elements with the `ng-` prefix/namespace. – Evan Carroll May 31 '16 at 18:24
  • @Jon if you want a direct extension of jquery, see my answer – Patrick Roberts May 31 '16 at 18:49

4 Answers4

3

You can use XPath with the expression

 //*[starts-with(name(),'food-cta-')]

Where

  • //* is wildcard for all nodes
  • starts-with() is a XPath function to test a string starts with some value
  • name() gets the QName (node name)
  • and 'food-cta-' is the search term

Pass it into document.evaluate and you will get a XPathResult that will give you the nodes that were matched.

var result = document.evaluate( "//*[starts-with(name(),'food-cta-')]", document, null, XPathResult.ANY_TYPE, null );

Note you can use any node as the root, you do not need to use document. So you could for instance replace document with the some div:

var container = document.getElementById("#container");
var result = document.evaluate( "//*[starts-with(name(),'food-cta-')]", container, null, XPathResult.ANY_TYPE, null );

Demo

let result = document.evaluate( "//*[starts-with(name(),'food-cta-')]", document, null, XPathResult.ANY_TYPE, null );

let nodes = [];
let anode = null;

while( (anode = result.iterateNext()) ){
   nodes.push( anode.nodeName );
}

console.log(nodes);
<div id="container">
  <br>
  <food-cta-download type="primary" description="Download Recipe">
    <img src="">
    <h2></h2>
    <p></p>
  </food-cta-download>
  <span>Some span</span>
  <food-cta-something>
    <img src="">
    <h2></h2>
    <p></p>
  </food-cta-something>
  <div>In between
      <food-cta-sub>
         <img src="">
         <h2></h2>
         <p></p>
      </food-cta-sub>
  </div>
  <food-cta-hello>
    <img src="">
  </food-cta-hello>
  <food-cta-whattt>
  </food-cta-whattt>
</div>
Patrick Evans
  • 41,991
  • 6
  • 74
  • 87
0

You probably have to just go to the elements in question and check if their tagName begins with that given string...

var myPrefix = "mycustom-thing-";

$("body").children().each(function() {
  if (this.tagName.substr(0, myPrefix.length).toLowerCase() == myPrefix) {
    console.log(this.innerHTML); // or what ever 
  }
})

https://jsfiddle.net/svArtist/duLo2d0z/

EDIT: Included for efficiency's sake:

If you can predict where the elements will be, you can of course specify that circumstance. In my example, the elements in question were direct children of body - so I could use .children() to get them. This would not traverse lower levels.

Reduce the need for traversal by the following:

Start on the lowest needed level ($("#specific-id") rather than $("body"))

If the elements are all to be found as direct children of a container:

  • Use $.children() on the container to obtain just the immediate children

Else

  • Use $.find("*")

If you can tell something about the containing context, filter by that

For example $("#specific-id").find(".certain-container-class .child-class *")

Ben Philipp
  • 1,832
  • 1
  • 14
  • 29
  • This seems like a very expensive call as it will have to traverse pretty much the entire DOM. – Jon May 31 '16 at 17:54
  • So you have to use `.find()` instead? ...Well, in a way, jQ would have to do that anyway if it were a check for a class or something else natively supported. But yes, I get that and of course I too wish it were supported – Ben Philipp May 31 '16 at 17:57
  • Oh! I didn't realize `$.find()` traverses the entire DOM as well. Lol. I thought it did some sneaky algorithm thing that saves performance. – Jon May 31 '16 at 17:59
  • What do you mean? `.find("*")` will check every children in any level, `.children()` will only check the first level. __If__ there was a top-level tagName selector, we could filter the `.find()` call and jQ could take full advantage of its speedy algorithms. Now, as it stands, we have to retrieve an object for every element and then check for its tagName, which is more expensive – Ben Philipp May 31 '16 at 18:10
  • You could improve performance by giving the selector more specificity, for example, if you know the content is going to be inside a specific content area. It would traverse less of the DOM. – heart.cooks.mind May 31 '16 at 18:17
  • @heart.cooks.mind Of course. In my fiddle, I used `children()` and `body` was the direct parent so I didn't think to include that in my answer... will do – Ben Philipp May 31 '16 at 18:19
  • Updated the answer to that regard – Ben Philipp May 31 '16 at 18:44
0

Try this..

let customElements = $('*')
  .filter((index,element) => /FOOD-CTI-/.test(element.tagName));

Note, .tagName should return the result in uppercase. This should get you a jQuery object of the elements you want. It will traverse the entire DOM though. It'll be slow.

This uses the "all selector".

Caution: The all, or universal, selector is extremely slow, except when used by itself.

You can traverse less then entire dom too, by specifying something like $("body *"). Not sure where you have put the Custom Elements, and where they're allowed.

As an aside, I wouldn't use Custom Elements, microformats are a better idea at least now, they're also better supported, and they're less likely to change.

Evan Carroll
  • 78,363
  • 46
  • 261
  • 468
0

Why not extend jquery selectors?

$(':tag(^=food-cta-)')

Would be possible with the following implementation:

$.expr[':'].tag = function tag(element, index, match) {
  // prepare dummy attribute
  // avoid string processing when possible by using element.localName
  // instead of element.tagName.toLowerCase()
  tag.$.attr('data-tag', element.localName || element.tagName.toLowerCase());
  // in :tag(`pattern`), match[3] = `pattern`
  var pattern = tag.re.exec(match[3]);
  // [data-tag`m`="`pattern`"]
  var selector = '[data-tag' + (pattern[1] || '') + '="' + pattern[2] + '"]';
  // test if custom tag selector matches element
  // using dummy attribute polyfill
  return tag.$.is(selector);
};

// dummy element to run attribute selectors on
$.expr[':'].tag.$ = $('<div/>');
// cache RegExp for parsing ":tag(m=pattern)"
$.expr[':'].tag.re = /^(?:([~\|\^\$\*])?=)?(.*)$/;

// some tests
console.log('^=food-cta-', $(':tag(^=food-cta-)').toArray());
console.log('*=cta-s', $(':tag(*=cta-s)').toArray());
console.log('food-cta-hello', $(':tag(food-cta-hello)').toArray());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container">
  <br>
  <food-cta-download type="primary" description="Download Recipe">
    <img src="">
    <h2></h2>
    <p></p>
  </food-cta-download>
  <span>Some span</span>
  <food-cta-something>
    <img src="">
    <h2></h2>
    <p></p>
  </food-cta-something>
  <div>In between
    <food-cta-sub>
      <img src="">
      <h2></h2>
      <p></p>
    </food-cta-sub>
  </div>
  <food-cta-hello>
    <img src="">
  </food-cta-hello>
  <food-cta-whattt>
  </food-cta-whattt>
</div>

This supports a pseudo-CSS-style attribute selector with the syntax:

:tag(m=pattern)

Or just

:tag(pattern)

where m is ~,|,^,$,* and pattern is your tag selector.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153