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

2

6

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>
Martellato answered 19/8, 2015 at 9:40 Comment(0)
S
0

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

Shields answered 30/8, 2016 at 5:2 Comment(4)
drafts.csswg.org/selectors-4/#scoping "Some host applications may choose to scope selectors to a particular subtree or fragment of the document. The root of the scoping subtree is called the scoping root, and may be either a true element (the scoping element) or a virtual one (such as a DocumentFragment)." The quote from MDN seems to be based on an older version of selectors-4 that does not mention DocumentFragments.Nasopharynx
Besides, I don't think that quote is relevant anyway. The :scope pseudo applies to scoped <style> elements in HTML, which implies it use in CSS, in the context of HTML. The DOM is an entirely different beast altogether (even though the DOM tree is built based on the HTML).Nasopharynx
@Nasopharynx In the MDN doc, it has no alternative than the deprecated scoped attribute to determine context. How is that done currently, via source order or some other API?Shields
In a <style> element, there is indeed no other way than the deprecated scoped attribute to scope a CSS rule to a specific element. But the question isn't about <style> elements or indeed CSS, it's about DocumentFragment.querySelector(), in which the :scope pseudo is expected to refer to that DocumentFragment node, at least according to the current selectors-4 and DOM texts. (My saying "in the context of HTML" was kind of problematic; ignore that bit.)Nasopharynx
S
0

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.

Sascha answered 23/9, 2016 at 20:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.