Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowing CSS combinators postfixed to the ::slotted() or :host selectors #889

Open
trusktr opened this issue Jul 12, 2020 · 38 comments
Open

Comments

@trusktr
Copy link
Contributor

trusktr commented Jul 12, 2020

Borrowing some ideas from #883, I think it would be great to allow combinators after ::slotted(whatever), for example ::slotted(.foo) .bar > .lorem + ip-sum. Similarly, the previous selector would be effectively the same as :host > .foo .bar > .lorem + ip-sum for styling purposes, but :host also does not allow combinators to be appended to it.

Here's a live example showing that it currently doesn't work:

https://stackoverflow.com/questions/62842418

On the same token, being able to use these selectors in querySelector and querySelectorAll would be very convenient.

Here's a live example that shows that querySelector('::slotted(.foo)') doesn't work despite being a valid selector, and also showing that postfixed combinators throw an error:

https://codepen.io/trusktr/pen/d4a45f1efc9eccc0fdb20164566bada4?editors=1010

Having these features as expansions on the current spec would allow what the OP in #883 asks for: it would make it very easy to style the input tree that a custom element end user provides.

There are currently complicated ways to achieve this (see comments below).

@trusktr trusktr changed the title llow CSS combinators postfized to the ::slotted() selector Allowing CSS combinators postfixed to the ::slotted() selector Jul 12, 2020
@trusktr
Copy link
Contributor Author

trusktr commented Jul 12, 2020

For those reading email, sorry, I submitted the thread early without any content, and edited the original post.

@castastrophe
Copy link

What does the ::slotted syntax add that the querySelector("[slot].foo") would not cover? Slotted content is light DOM so you shouldn't need to run that query inside shadowRoot.

@calebdwilliams
Copy link

Wouldn’t querySelector('::slotted(.foo)') give away that a shadow DOM exists (even in a case where the shadow root is closed)? It seems to me like host.querySelector('.foo') should suffice from a DOM API perspective although I agree the CSS selector from within a shadow root would be a nice addition if the performance worked out.

@trusktr
Copy link
Contributor Author

trusktr commented Aug 2, 2020

@castastrophe Thanks for that question! It's not quite what I'm asking about though.

The `::slotted` selector runs within a shadow root context (f.e. in a shadow root `querySelector`, or in a shadow root's `<style>` element), but this is already expected behavior. More details (click here to expand)...
const root = this.attachShadow(...) // 'this' is a custom element

root.innerHTML = html`
  <style>::slotted(.foo) { ... }</style>
  <slot></slot>
`

The ::slotted(.foo) selector in this example is in fact still an implementation detail encapsulated inside the (custom element's) shadow root.

The [slot].foo selector does a similar thing, but does it from the outside of the custom element (i.e. outside of the shadow root), so it is a tool used on the outside of a custom element's internal shadow tree by an end user of the custom element.

The custom element itself could in fact run this code internally: this.querySelector('[slot].foo'), but the style of the shadow root actually can't contain such a selector as it would not select anything. For example, this won't work on the inside of a custom element:

this.attachShadow(...).innerHTML = `
  <style>[slot].foo { ... }</style>
  <slot></slot>
`

because it will not select nodes from the light tree that have been "slotted" into the shadow root; instead it will select nodes that are within the shadow root's own DOM that have a slot="" attribute (and in that example there aren't any).

In other words, calling this.querySelector('[slot].foo .bar') on a custom element selects any elements in the light tree with a slot attribute even if they are not actually slotted.

So inside of a custom element's implementation (in its shadow root), the ::slotted(.foo) selector is useful for selecting actually-slotted elements from the light tree, which is something that only the custom element (its shadow root) would care about (otherwise, as @calebdwilliams mentioned, the user would be aware of the shadow root's existence if they could run that selector on the custom element instead of on the custom element's (possibly closed and invisible) shadow root).

END DETAILS

Anywho, all of the aforementioned is already an existing concept.

The main ask of the OP is to be able to take a selector like ::slotted(.foo) (which already works, within a shadow root) and to be able to append combinators to it. For example ::slotted(.foo) .bar or ::slotted(.foo) > .bar.

As an example, try running the following in your console (tested in Chrome).

What you'll notice is that the square will be colored deeppink, but the expected result is for the square to be cyan.

The reason is because the ::slotted(.foo) selector is working fine, but the ::slotted(.foo) .bar selector doesn't do anything (unlike what we may expect).

class MyEl extends HTMLElement {
	connectedCallback() {
		const root = this.attachShadow({mode: 'open'})
		root.innerHTML = /*html*/ `
			<style>
				:host { display: block; }
				
				/* This works. */
				::slotted(.foo) {
					background: deeppink;
				}

				/* This does not work. */
				::slotted(.foo) .bar {
					background: cyan;
				}
			</style>
			<slot></slot>
		`
	}
}

customElements.define('my-el', MyEl)

document.body.insertAdjacentHTML(
	'beforeend',
	/*html*/ `
	<my-el>
		<div class="foo">
			<div>
				<div class="bar"></div>
			</div>
		</div>
	</my-el>

	<style>
		my-el {}
		.foo {
			width: 50px;
			height: 50px;
		}
		.foo div {
			width: 100%;
			height: 100%;
		}
	</style>
`,
)

Expected result:

expected

Actual result:

actual

Live demo

@castastrophe
Copy link

castastrophe commented Aug 2, 2020

Ah, thank you for the code example. This is not really about the ::slotted selectors then. This is about the fact that a web component cannot style or access any descendent greater than a direct child, top-level nodes inside slotted content. Your .bar class is nested, which is why ::slotted cannot influence it. https://developers.google.com/web/fundamentals/web-components/shadowdom#slots

@castastrophe
Copy link

That is not to say that is not a valuable conversation to have - whether or not a component should be able to style more than just top-level nodes inside a slot - but I do think it's a different topic than the title and description here imply.

@castastrophe
Copy link

I wanted to share this codepen that plays around with a few different ways you can use slotted selectors and pseudo elements: https://codepen.io/castastrophee/pen/OJMKeKa

@Danny-Engelman
Copy link

Danny-Engelman commented Aug 2, 2020

That is not to say that is not a valuable conversation to have - whether or not a component should be able to style more than just top-level nodes inside a slot

Continue the conversation with its full history, that started 5 years ago, and resulted in V1 ::slotted only taking a simple selector,
because ::content and <content> in V0 (allowed for complex selectors, never implemented in FireFox & Safari) did not perform.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/content (deprecated)

( 2015 #331 ) "::slotted" pseudo elements
( 2018 #745 ) ::slotted() should full support complex selector!!

The main take away from 5 years of W3C standards discussions is:

I am not the one to disagree with a Components Lead Developer

@castastrophe
Copy link

Thank you for the links. I am aware of the current spec. Five years is a long time in tech and specs are not immutable. As efficiency is improved, new options may be available. I also was not implying the conversation should be_started_, but pointing out on this thread that it's a different conversation than this one.

@bathos
Copy link

bathos commented Aug 2, 2020

FWIW, if complex slotted light dom selectors were permitted, I'd expect the selector in the examples above to be ::slotted(.foo .bar), not ::slotted(.foo) .bar. The latter seems to describe a slotted light dom element ".foo" with a descendent shadow element ".bar". Since that scenario can't seemingly occur, the problem might not be obvious ... but switch to just about any other combinator and you get a scenario that can occur (e.g. ::slotted(.foo) + .bar - "style the shadow .bar when preceded by a slot with an assignee matching .foo").

AFAIK, selectors like this are also not currently permitted, but I'm unsure if that's a matter of deliberate design or not; they still seem to adhere to query-direct-assignees-only.

Edit:

Thinking about this further, I guess + ... also would never match, given the slot itself is "in the way" - the assignee is not a sibling. So perhaps this distinction doesn't matter, at least so long as CSS continues to have no "backwards" selectors.

@Danny-Engelman
Copy link

If you want to track selectors (in this case :host-context) about to be removed from the spec also read:

w3c/csswg-drafts#1914

@trusktr
Copy link
Contributor Author

trusktr commented Aug 5, 2020

but I do think it's a different topic than the title and description here imply.

@castastrophe Did you mean the title I chose doesn't match what I proposed in the OP? If so, in my mind I think it matches because it says "combinators postfixed to the ::slotted() selector", and then I am describing what I believe would be intuitive for that to do.

@trusktr
Copy link
Contributor Author

trusktr commented Aug 5, 2020

FWIW, if complex slotted light dom selectors were permitted, I'd expect the selector in the examples above to be ::slotted(.foo .bar), not ::slotted(.foo) .bar. The latter seems to describe a slotted light dom element ".foo" with a descendent shadow element ".bar".

@bathos That is not intuitive because it is impossible. Why would someone be thinking that, when it doesn't exist?

That's like if I said .foo > .bar should select any .foo elements that have greater amount of text content than the .bar element with largest amount of text. But I'd be making things up at that point.

I think a better interpretation is this:

  • ::slotted(.foo .bar) represents an element in the light tree that matches .foo .bar in the light tree (it is a descendant of a .foo element anywhere in that light tree). I think that's intuitive.
  • ::slotted(.foo) .bar represents the .bar element that happens to be a descendant of a slotted .foo element. This is also intuitive.

I guess + ... also would never match,

If we think intuitively about this, then: a selector like ::slotted(.foo) + .bar would style a .bar element that happens to be the "adjacent sibling" of a .foo element where the .foo element is a slotted element. This is intuitive.

Note that the .bar element could very well be distributed to an entirely different slot (but still have the styling specified for that selector). That makes intuitive sense and could be totally useful.

@trusktr
Copy link
Contributor Author

trusktr commented Aug 5, 2020

@Danny-Engelman, @hayatoito's comment you screenshotted shows no performance metrics. I am not a browser developer, but I very much doubt (I could be wrong) that ::slotted(.foo) .bar could really be so slow that it matters for the vast majority of use cases.

I wrote a comment about that at #745 (comment).

What I mean is, there's plenty of ways to make really slow selectors in regular DOM, without any shadow DOM even existing. We should not throw out an idea based on a single thought that said it would be slow without any viable data.

What if I said "multi-pass WebGL rendering is slower than single-pass rendering, so we should throw out multi-pass APIs".

But in fact, multi-pass rendering can be very useful when the performance implications fit within given constraints.

It would be great to give web developers useful selectors, and then explain to them that they should avoid re-running these selectors repeatedly; but that it is fine if the cost fits within performance requirements for the application.

I feel that we're prematurely optimizing here. However I know that the web APIs can't be reversed (though I've been imagining how to do that without breaking the web).

Every single DOM API that exists can technically be slooow if we examine it within the context of a particular use case that happens to be the worst use case where we'd never want to perform the given action in the way it is performed.

The following would not be ideal (if selectors like ::slotted(.foo) .bar were supported) unless the use case still fits withing some given performance requirements:

connectedCallback() {
  requestAnimationFrame(function loop(t) {
    this.shadowRoot.querySelector('::slotted(.foo) .bar + .baz').style.transform = `rotateY(${10 * Math.sin(t * 0.001)}deg)`
    requestAnimationFrame(loop)
  })
}

The following may be much better (if the selector were supported) for a use case in which the functionality works as intended:

connectedCallback() {
  const el = this.shadowRoot.querySelector('::slotted(.foo) .bar + .baz')

  requestAnimationFrame(function loop(t) {
    // does not re-run the selector every time:
    el.style.transform = `rotateY(${10 * Math.sin(t * 0.001)}deg)`
    requestAnimationFrame(loop)
  })
}

@hayatoito (and @emilio from the other thread) Can you please expand on the performance issues, and provide useful metrics that we can reference here?

@emilio
Copy link

emilio commented Aug 5, 2020

The performance issue is that it increments the amount of subtrees in which every node needs to go look for rules that affect to them.

Right now the logic goes like: if you're slotted, traverse your slots and collect rules in their shadow trees as needed. This is the code fwiw. This is nice because the complexity of styling the element depends directly on the complexity of the shadow trees that you're building, and it only affects slotted nodes.

If you want to allow combinators past slotted then every node would need to look at its ancestor and prev-sibling chain and look at which ones of them are slotted, then do that process for all their slots. Then, on top, you also need to change the general selector-matching code so that selectors that do not contain slotted selectors don't match if you're not in the right shadow tree.

That's a cost that you pay for all elements, regardless of whether you use Shadow DOM or ::slotted, and is probably just not going to fly.

@calebdwilliams
Copy link

@emilio wpuld something like I described in #883 work better, which is essentially inverting part?

@castastrophe
Copy link

The following may be much better (if the selector were supported) for a use case in which the functionality works as intended:
connectedCallback() {
const el = this.shadowRoot.querySelector('::slotted(.foo) .bar + .baz')

requestAnimationFrame(function loop(t) {
// does not re-run the selector every time:
el.style.transform = rotateY(${10 * Math.sin(t * 0.001)}deg)
requestAnimationFrame(loop)
})
}

I'm just not sure that referencing slotted content inside the ShadowRoot is possible or makes logical sense if we consider the main benefit of web components: scope. The ShadowRoot does not know what it's slotted content looks like, only that it has slots. The slots point to the light DOM. If you want to capture the light DOM on a component, you use: this.querySelector(".foo"). If you want to make sure .foo is assigned to a slot, you query for: this.querySelector("[slot].foo") or even this.querySelector("[slot=bar].foo") if you want to make sure it's in a specific slot. There is no need for the ::slotted selector inside the querySelector. ::slotted in CSS was a means for the component to be able to influence the light DOM styles directly nested inside the slot (and only directly, top-level items) and it does it very weakly. Most light DOM styles will overwrite anything the component tries to apply to a ::slotted style unless that component uses !important to beat it.

tldr; Scope is the primary discussion here imo. A component is tightly scoped to see it's own template and it's top-level nodes assigned to a slot, no deeper and no higher. That scope is a powerful tool that can be leveraged.

@emilio
Copy link

emilio commented Aug 5, 2020

@calebdwilliams I think for the dialog usecase named slots are a reasonable solution. You then could do slot[name="cancel"]::slotted(button) { whatever } or what not.

Introducing a particular part-like attribute definitely mitigates the "now all elements need to look at all their ancestors for slots", for sure (at the cost of adding one more part-like attribute, which is also a bit of an annoyance because it involves one extra branch in all attribute mutations, but probably not a huge issue).

That being said, allowing arbitrary access to the slotted DOM is a bit fishy. That way you start depending on the shape of your slotted DOM tree and that reintroduces the same issue that you're trying to solve with shadow DOM in the first place, which is making an isolated, reusable component. I think that's the point that @catastrophe is making, which I agree with.

@calebdwilliams
Copy link

@emilio, yeah, I wrestled with that but I keep coming back to the idea that the slotted content is not (or should not be) necessarily required. A dialog without a cancel button is potentially fine.

Granted that might not be how people use the feature.

@emilio
Copy link

emilio commented Aug 5, 2020

That's fine? You can have an empty slot. Anyway that's a bit off-topic anyhow.

@trusktr
Copy link
Contributor Author

trusktr commented Aug 11, 2020

That being said, allowing arbitrary access to the slotted DOM is a bit fishy. That way you start depending on the shape of your slotted DOM tree and that reintroduces the same issue that you're trying to solve with shadow DOM in the first place, which is making an isolated, reusable component. I think that's the point that @catastrophe is making, which I agree with.

@emilio In some ways, I feel you both on that sentiment.

But there are other possibilities too. The idea is that we allow custom element authors to be more inventive by giving them flexibility.

For example, a custom element author may describe certain usage requirements in the component documentation, and it could require a user to nest elements like follows, where the foo- prefix denotes the elements from the component author's foo- lib:

<foo-interesting-layout>
  <div slot="left">
    ... any other stuff ...
    <foo-close></foo-close>
    ... any other stuff ...
  </div>
  <div slot="center">
    ... any other stuff ...
    <foo-open-left></foo-open-left>
    ... any other stuff ...
    <foo-open-right></foo-open-right>
    ... any other stuff ...
  </div>
  <div slot="right">
    ... any other stuff ...
    <foo-close></foo-close>
    ... any other stuff ...
  </div>
</foo-interesting-layout>

Now, the foo- lib author needs to style the slotted elements, as well as the nested foo- elements in a certain way for this layout (f.e. positioning, or depending on which slot they are slotted into). The component author could use ::slotted(), ::slotted() foo-close, ::slotted() foo-open-left, and ::slotted() foo-open-right within the <foo-interesting-layout> element's shadow root in order to perform the necessary styling.

In my mind, this sort of nesting is a totally valid thing that a library author could document as a requirement, and therefore should have some easy way to perform the styling.

The most important thing to note is that performing the styling is entirely possible today, the feature I ask for only makes it easier with less code.

The way we can do it today is the library author places a <style> element in the nearest root (be that the Document, or nearest ShadowRoot). That of course is less ideal, but completely doable, and more error prone.

If the author was able to use ::slotted() foo-open-left, it would keep the styling co-located with the components it is meant to accompany without extra complication and maintenance burden.

As with many features of a language or API, there's wrong ways to do just about anything, but I do believe this feature would give authors easier inventiveness without (for example) having to track root nodes and ensure that they don't have duplicate <style> tags placed in the root nodes.

If the foo-interesting-layout author has selectors like ::slotted() foo-whatever, they can write simpler code, and rely on web APIs like adoptedStyleSheets to handle de-duplication of stylesheets.

@emilio @catastrophe If the custom element author relies on certain slotted DOM structure without documenting that, of course that's bad. It isn't to say a custom element author can't make good documentation to describe what an end user should do. Secondly, without any documentation, the end user will have a hard time guessing what the structure should be anyway, so they probably wouldn't even bother to use that custom element. People generally don't like to guess how an API works.

So that point, though fully valid, doesn't have as high of a significance (in my humble opinion) as the proposed feature does, in that the proposed feature would allow CE authors to achieve things more easily (and usage of those things should be documented for end users).

@trusktr
Copy link
Contributor Author

trusktr commented Aug 11, 2020

@calebdwilliams I think for the dialog usecase named slots are a reasonable solution. You then could do slot[name="cancel"]::slotted(button) { whatever } or what not.

@emilio For that particular use case, where the slotted thing is a simple <button> and nothing more, that would work. But it is easy to imagine use cases where a CE author asks end users to supply DOM trees with certain structures, or even just certain elements anywhere inside the tree; it's a valid use case. For example, even with builtins we have <table>, <thead>, <tr>, <td>, etc, and the OP idea makes it easier for CE authors to write those sorts of structure requirements.

A CE author could be fairly strict with the requirements, f.e. the author could document the strict requirements and be using selectors like ::slotted(foo) > bar. Or, the well-documented requirements could be less strict while the author would use selectors like ::slotted() bar to allow lose tree structure.

One feature in particular that relies on lose structure is CSS transforms.

CSS transform causes absolutely-positioned transformed elements to escape from their DOM hierarchy layout, and they enter into their own 3D layout hierarchy within the nearest position:relative element. In order to perform the proper styling with CSS transforms in a slotted tree, a CE author would document less-strict requirements (f.e. ::slotted() bar instead of ::slotted() > bar) in order to move those things around in 3D space, for example.

Imagine the 3D possibilities: imagine how a custom element wrapping a DOM tree, relying on selectors post-fixed to ::slotted could make certain elements break out into 3D space, and the only thing the end user has to do is wrap the tree with the custom element, then apply names (classes or attributes, or something) to elements that should break out into a 3D layout. The wrapper custom element would enforce a scope where the 3D effects are applied (thanks to ShadowDOM), and would do other things under the hood like use a canvas for WebGL effects added to the elements.

(I'm working on this at http://lume.io, but I have a lot left to do... F.e. here's WebGL-enhanced <button> elements, but it is not using the approach I just described, which is something I've been thinking about adding to make it easier to add 3D to existing DOM whereas currently 3D scenes need to be defined from scratch.)

There are many possibilities. What we've just imagined is doable today, but ::slotted() foo could make it simply easier.

@OnurGumus
Copy link

I think, at least slotted parts should be supported by minimum:

w3c/csswg-drafts#3896

@trusktr
Copy link
Contributor Author

trusktr commented Aug 29, 2020

@OnurGumus From reading that, I'm not sure what is being proposed there or how that's an alternative to the OP. Could you provide an example?

Is it to say that the light DOM author could specify parts, then the Custom Element author (or ShadowDOM author) could style those ::parts even if they are nested any level deep inside of a slotted node?

If that's what is meant (the light DOM author specifies stylable ::parts), I see how that can satisfy some of the scenerios discussed above, but with some limitations. Namely, the above would allow a custom element author to style any elements in the light DOM without the light DOM author having to explicitly label all of them with part.

For example, ::slotted() * can style all of the elements inside a slotted node, but to do this with ::part the light DOM author would need to go and apply the part to all of the elements (or worse, the custom element author would need to traverse the light DOM and add the parts and hence mess with the light DOM author's interface in a possibly unexpected way).

@OnurGumus
Copy link

@trusktr What we know is slotted is limited to the "public surface" of the component for performance reasons. OP argues that we shouldn't have that limitation whereas probably some people would reject that.
What I argue is as a user I should be at least use ::slotted(::part Foo ...) since parts are exposed public parts of a component. But even that is not allowed.

@trusktr
Copy link
Contributor Author

trusktr commented Aug 31, 2020

parts are exposed public parts of a component

The to make sure we're on the same page, the OP is not talking about exposing parts of a component to the user, the OP is talking about the component being able to more fully work with the tree that the user passed into the component as a distributed/slotted tree; to be able to more easily access that tree in style context.

Note: I know we can already access the light tree, and style all of its elements. This is simply proposing something more ergonomic.

In React, for example, this is easy to do. The component author simply iterates over the array this.props.children and can work with anything as needed: read data, change styling, replace props, etc, before finally passing it into the component's internal ("shadow") tree, all without affecting the component user's outside interface. It's like a map function: it takes a set of children as input (with their descendants) and can map it to whatever the component author desires.

This easy flexibility is what the OP is asking for, but for Web Components, and in this case the ::slotted() .foo selector with combinators would add that flexibility in the context of CSS styling.

The main thing to note, is that Web Component authors can already access all light DOM and style the elements any way they wish. The OP only aims to make it easy to implement for WC authors.

implementation example using current available APIs:

  • The CE author defines a custom element named <foo-bar>.
  • In the foo-bar element's connectedCallback, for the first instance of <foo-bar> in a document or shadow root, the CE author attaches a <style> element to the document or shadow root where the <foo-bar> element lives.
  • The <style> element contains foo-bar .foo {...} to style any .foo elements that are descendants of <foo-bar> elements
  • Any of the styled .foo elements may be descendants of elements that will be slotted into <foo-bar> (effectively this achieves the equivalent ::slotted() .foo that the OP asks for).
  • In the element's disconnectedCallback, when there is only one instance of <foo-bar> left (achieved with ref counting) in the document or shadow root where the aforementioned <style> element is placed, the <style> element is removed.

So you see, what the OP asks for is totally possible today. The OP merely asks for a concise and simple syntax option.

@Lonniebiz
Copy link

Lonniebiz commented Oct 7, 2020

What a disappointing arbitrary limitation. Let the browser-makers solve performance issues and write the spec to be ideal. WEAK!

I want complete control over the styling of slots and all their children. I wish there was a flag we could flip so that slots would ignore non-shadow styling. A slot is really just a really ugly argument to a function. If you pass something into my function (custom element) I should be able to do what ever I want to that input and all its children too.

The whole goal, for me, is to have every aspect of my web component contained in one file and I don't like having to fool with document level CSS to style children of shadow slotted element. I can still do what I'm wanting with hacks and tricks, but allowing shadow level CSS to style children of slotted elements would make the whole experience much nicer. What @trusktr has been saying is ideal to me.

Make the spec ideal and let the browsers-makers earn their money. They're smart enough to overcome the performance issues, so don't be so easy on them in the spec. This stuff is for the whole world. Let's impress the aliens when they arrive. Their web standards already do this!

@Danny-Engelman
Copy link

Danny-Engelman commented Oct 7, 2020

There is no fool with document level CSS issue, the only limitation is:

You have to wrap your component A (with shadowDOM and slots) in another component B with shadowDOM.
You can then do all the styling you want in A lightDOM.

Then the browser-makers can focus on creating something that really impresses aliens...
Maybe Apple can do Customized Built-In Elements

This is becoming an "I think Array index should start at 1" topic 🖖

@Danny-Engelman
Copy link

Danny-Engelman commented Oct 10, 2020

Since Lonnie Best challenged me, here is my workaround to style all slotted content without using ::slotted

Code at: https://jsfiddle.net/CustomElementsExamples/Lhcsd2m5/

Code at: https://jsfiddle.net/CustomElementsExamples/Lhcsd2m5/

  customElements.define('to-do', class extends HTMLElement {
    connectedCallback() {
      setTimeout(() => { // make sure we can access (light)DOM here
        let template = document.getElementById(this.nodeName).content.cloneNode(true);

        let div = this.attachShadow({ // create TO-DO with shadowDOM and DIV in it
          mode: 'open'
        }).appendChild(document.createElement("div"));

        div.attachShadow({ // attach shadowDOM to DIV container
          mode: 'open'
        }).append(template); /* add Template content to DIV shadowRoot */

        div.append( // move to DIV lightDOM:
          div.shadowRoot.querySelector('#styleslots'), // style originally from Template
          ...this.children // all original to-do lightDOM (this required the setTimeout!)
        );

      })
    }
  });

@trusktr
Copy link
Contributor Author

trusktr commented Mar 22, 2021

Cool. So basically what you are saying is slot > div span (inside a shadow root style) achieves the same as the hypothetical concept ::slotted(div) span (in a shadow root style) of the OP!

(You could've simply just said that.)

A hypothetical ::slotted(div[slot="foo"]) span is the same as slot[name="foo"] > div span.

Works great! For some reason I hadn't thought of it.

Side note: performance of the hypothetical ::slotted(div) span can and should in theory be the same as slot > div span. I still don't see how performance can be an argument against it.

@trusktr trusktr closed this as completed Mar 22, 2021
@trusktr
Copy link
Contributor Author

trusktr commented May 13, 2021

@Danny-Engelman Can you make a really simple example, not a super complex one? I am not having any luck with it:

https://codepen.io/trusktr/pen/e80e9d5d09de7facf22aeee85f6a7549

As you can see there, only the Shadow DOM's nodes are styled, but not the content that is "slotted".

Works great! For some reason I hadn't thought of it.

Not sure why I thought it "worked great" before, but clearly in that example it doesn't work.

Update: I thought I could make it work with a combination of slot > * and :host > * but apparently that doesn't work either:

slot > p > span,
:host > p > span { ... }

https://codepen.io/trusktr/pen/cf1ee41ddf779a7fb60eff121b65bc1c

It seems CSS combinators are not allowed after :host either.

@trusktr trusktr reopened this May 13, 2021
@trusktr
Copy link
Contributor Author

trusktr commented May 13, 2021

This is becoming an "I think Array index should start at 1" topic

From what I can tell, this is a "There's no way to index into an array with simple notation" problem. 😄

Here's a solution that works:

// within the custom element code, inside a MutationObserver handler:
this.children.forEach(child => {
  if (child instanceof HTMLParagraphElement && child.children[0] instanceof HTMLSpanElement) {
    child.children[0].style = 'background: cyan'
  }
})

but that's like a

we can't index an array with array[index] so just write

function getItemAtIndex(array, idx) {
  for (const [i, o] of entries(array)) {
    if (i === idx) return o
  }
}
getItemAtIndex(index, array)

sort of topic, except the CSS version is uglier because it modifies the end user's public style API contract.

@Danny-Engelman
Copy link

Danny-Engelman commented May 17, 2021

Cool. ... you say slot > div span (inside a shadow root) achieves the same as the hypothetical concept ::slotted(div) span

No, I am not. It is not possible.

Slotted content is styled by its host container (global CSS if you only have 1 one element with shadowDOM)

Can you make a really simple example, not a super complex one? I am not having any luck with it:
https://codepen.io/trusktr/pen/e80e9d5d09de7facf22aeee85f6a7549

There is no simple example. Your CodePen has one shadowRoot.

ps: your CodePen is private so can't be forked


I keep the ::slotted StackOverflow answer, I posted a year ago, updated with every fact and rumor I read.

https://stackoverflow.com/questions/61626493/slotted-css-selector-for-nested-children-in-shadowdom-slot/61631668#61631668

no updates there for months.

@Danny-Engelman
Copy link

Joe Pea aka trusktr, got me working again.

It is still a Work-Around!! To style <slot> content from a <style> tag IN the TEMPLATE

I rewrote the code.. JS is still the same, except I changed ...this.children to ...this.childNodes to include #textnodes in the move from one lightDOM to a nested lightDOM.

I deleted most styling examples and comments.

Is this simple enough?

TWO shadowRoots to style <slot>s and not bleed CSS: https://jsfiddle.net/WebComponents/rhodv143/

@trusktr
Copy link
Contributor Author

trusktr commented May 19, 2021

Ah, ok. I see what you did, which I missed before.

It is a very bad solution because it totally obliterates the end user's tree (removes it from their DOM, and places it into an internal shadow root). This has critical issues:

  • The end user's light-tree styling no longer applies
    • for example, when the end user writes the custom element in the top level Document, and adds top-level styling in the document, this styling will not style the elements as they were expecting, because the elements have been moved into a shadow root.
  • The end user will have a very false sense of their tree structure. They will experience errors like this:
     // `el` will be `undefined` instead of an `<h1>`
    const el = document.querySelector('component-with-slots').children[0]
    
    el.style.foo = 'bar' // runtime error!!!
  • The code is cryptic and difficult to understand. The end user will be forced to read the the source code to find out what happened to their tree (or the CE author would have to document the awkward behavior).
  • This leads to harder-to-maintain code, both for the end user, and for custom element authors.
  • More resource usage: two shadow roots per custom element, and extra CPU usage moving nodes every time one of the custom elements is created, etc.

I would suggest for anyone reading this, never ever write code that way. That is simply not an acceptable solution because the downsides (especially for the end user) are bigger than the benefit (namely for the CE author only).

@Danny-Engelman Unless there is some solution we've missed, you shown why clearly we need a solution, so as to avoid complicated and highly undesirable workarounds.

The most acceptable solution that I can think of right now is, to implement scoped styling:

  • Instead of creating an extra layer of shadow root,
  • write a special style handler in the custom element that attaches certain styles to the custom element's nearest root node (either a Document, or a ShadowRoot).
  • In those styles, style the desired elements using scoped styles (for example with UUIDs or similar) so that the styles affect only the particular elements of the custom element's light tree.
  • This would require setting a unique attribute (or similar) on the custom element, for example <component-with-slots library-name-style-id="12345">
  • The style processing mechanism inside the custom element would convert selectors like :outside span to component-with-slots[library-name-style-id="12345"] span
  • Or better yet, to semi-polyfill the concept of this issue's OP, it would convert ::slotted() span or :host > * span to component-with-slots[library-name-style-id="12345"] > * span.
    • Note, I was hoping :host > * foo would essentially be a working alternative for ::slotted() foo, but :host also does not accept combinators after it. I updated the OP to mention this too.

This approach would be much less invasive to the end user's API contract (the end user's input (light tree nodes and attributes) should ideally be left in-tact and undisturbed, because that is theirs to manipulate). The end user will most likely be unaffected by the library-name-style-id attributes added to their light tree, but at least this is a much smaller invasion of their input space.

There is no simple solution to this, as far as I can tell so far (apart from having a feature like in the OP).

@trusktr trusktr changed the title Allowing CSS combinators postfixed to the ::slotted() selector Allowing CSS combinators postfixed to the ::slotted() or :host selectors May 19, 2021
@bennypowers
Copy link

See also w3c/csswg-drafts#3896

@leonheess
Copy link

This is needed

@trusktr
Copy link
Contributor Author

trusktr commented Jul 29, 2022

Related: #936

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests