<template> + querySelector using :scope pseudo class works with document but not documentFragment

1.2k Views Asked by At

Depending on the content of a <template>, I want to wrap its contents in a container for easier/consistent traversal. If the contents are <style> and <one-other-element> at the top level, I'll leave it be. Otherwise, whatever's in there will get wrapped in a <div>.

Originally I made my code something like this:

var hasCtnr = template.content.querySelector(':scope > :only-child, :scope > style:first-child + :last-child') != null;

But, I noticed it wasn't working -- that is, hasCtnr was always false. So, I made a reduced test case (jsfiddle). As you can see, :scope works with regular DOM elements. However, it doesn't seem to work with DocumentFragments. I know the technology is new/experimental but is this a bug or am I doing something wrong?

If I use jQuery, it works... but my guess is because jQuery is doing something manually.

var hasCtnr = !!$(template.content).children(':only-child, style:first-child + :last-child').length;

I only care about Chrome/Electron support, by the way.

Here's the jsfiddle inline:

var nonTmplResult = document.querySelector('#non-template-result');
var tmplResult = document.querySelector('#template-result');

var grandparent = document.querySelector('#grandparent');
var parent = document.querySelector('#parent');
var child = document.querySelector('#child');

var who = grandparent.querySelector(':scope > div');
if (who === parent) {
    nonTmplResult.innerHTML = 'parent as expected, :scope worked';
} else if (who === child) {
    nonTmplResult.innerHTML = "child (unexpected), :scope didn't work";
}


var tmpl = document.querySelector('template');
var content = tmpl.content;

var proto = Object.create(HTMLElement.prototype);

var hasCtnr = content.querySelector(':scope > div'); // this and even ':scope div' results in null, 'div' results in DIV
tmplResult.innerHTML += hasCtnr == null ? "null for some reason, :scope didn't work" : hasCtnr.nodeName + ', :scope worked'; // Why is this null..?
tmplResult.innerHTML += '<br/>';

proto.createdCallback = function() {
    var clone = document.importNode(content, true);
    var root = this.createShadowRoot();
    root.appendChild(clone);
    var rootHasCtnr = root.querySelector(':scope > div'); // ':host > div' seems to work but I prefer this check to happen once (above) so createdCallback can be efficient as I'll likely have many custom elements
    tmplResult.innerHTML += rootHasCtnr == null ? "null again, :scope didn't work" : rootHasCtnr.nodeName + ', :scope worked'; // Why is this also null..?
};

document.registerElement('x-foo', { prototype: proto });
#non-template-result {
    background: red;
    color: white;
}
#template-result {
    background: green;
    color: springgreen;
}
* /deep/ * {
    margin: 10px;
    padding: 5px;
}
#grandparent {
    display: none;
}
<div id="grandparent">
    <div id="parent">
        <div id="child"></div>
    </div>
</div>

<div id="non-template-result">????</div>
<div id="template-result"></div>
<x-foo>
    <p>I should be dark golden rod with khaki text.</p>
</x-foo>

<template>
    <style>
        :host {
            background: blue;
            display: block;
        }
        :host > div > p {
            color: white;
        }
        ::content > p {
            background: darkgoldenrod;
            color: khaki;
        }
    </style>
    <div>
        <p>I should be blue with white text</p>
        <content></content>
    </div>
    
</template>

<a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components#Enabling_Web_Components_in_Firefox">Enabling Web Components in Firefox</a>

2

There are 2 best solutions below

0
On

In the statement:

var hasCtnr = template.content.querySelector(':scope > :only-child' ) //...

...the :scope pseudo-class represents the element calling querySelector().

But a DocumentFragment (the type of template.content) is not an element (has no root element, no container) by definition, its localName attribute is undefined.

That's why this call will never select anything.

var df = document.createDocumentFragment()
df.appendChild( document.createElement( 'div' ) )
var res = df.querySelector( ':scope div' )  

console.info( 'df.localName is %s', df.localName )
console.info( 'df.querySelector( :scope div ) returns %s', res )

A workaround could be to put the content in a <div>, perfom the call, then dismiss or use the <div> according to the result.

4
On

This is not a bug:

The :scope CSS pseudo-class matches the elements that are a reference point for selectors to match against. In HTML, a new reference point can be defined using the scoped attribute of the <style> element. If no such attribute is used on an HTML page, the reference point is the <html> element.

In some contexts, selectors can be matched with an explicit set of :scope elements. This is a (potentially empty) set of elements that provide a reference point for selectors to match against, such as that specified by the querySelector() call in [DOM], or the parent element of a scoped <style> element in [HTML5].

Since the scoped attribute is no longer on any standards track, this will only work a document with an <html> tag, which would preclude its use in a document fragment.

Depending on the content of a <template>, I want to wrap its contents in a container for easier/consistent traversal. If the contents are <style> and <one-other-element> at the top level, I'll leave it be. Otherwise, whatever's in there will get wrapped in a <div>.

var bar = document.body.getElementsByTagName("template");
var baz;

var iterator = function(value, index) {
  if(/<style>/.test(value.innerHTML) === false)
    {
    value.innerHTML = "\n<div>" + value.innerHTML + "</div>\n";
    }
  console.log(value.outerHTML);
  return value;
  };

bar.map = Array.prototype.map;
baz = bar.map(iterator);
<template>
   <style>A</style> 
   <one-other-element>B</one-other-element> 
</template>

<template>
  <picture></picture>
</template>

References