-
Notifications
You must be signed in to change notification settings - Fork 168
Primitive shadow parts support #329
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
Conversation
This supports basic patterns of styling parts whenever a node is inserted into a shadow root. It also adds and tests a disableShadowParts ShadyCSS setting. Support for more kinds of mutations, exportparts, CSS custom properties are coming in follow up PRs.
bc6557e
to
4e29618
Compare
@@ -23,6 +23,8 @@ import templateMap from './template-map.js'; | |||
import * as ApplyShimUtils from './apply-shim-utils.js'; | |||
import {updateNativeProperties, detectMixin} from './common-utils.js'; | |||
import {CustomStyleInterfaceInterface, CustomStyleProvider} from './custom-style-interface.js'; // eslint-disable-line no-unused-vars | |||
import * as shadowParts from './shadow-parts.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider whether shadow parts should be a part of the scoping shim or given separate opt-in support in ShadyCSS, like @apply or custom style interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good thought, hadn't considered that. What are the main advantages you are thinking of?
The main one I can think of is that there would be more options for shipping smaller bundles to users who don't need shadow parts. We could create more permutations of the bundles, and update the loader to check the disableShadowParts
flag, and only load shadow parts support when not true.
Does this suggestion imply you think shadow parts should be opt-in rather than opt-out? I've been thinking we should do opt-out, because 1) shadow parts are so widely supported now that it feels like they are part of the core set of web components APIs, and 2) I think the performance cost to applications that don't end up using shadow parts is very small (since we will do almost nothing until a ::part
style is first detected in at least one template).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you've evaluated it correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #339, will think about this a little more and refactor in a separate PR.
export function onInsertBefore(parentNode, newNode, referenceNode) { | ||
/* eslint-enable no-unused-vars */ | ||
if (!parentNode.getRootNode) { | ||
// TODO(aomarks) Why is it in noPatch mode on Chrome 41 and other older |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In noPatch
mode, the DOM is not patched so getRootNode
is not brought into existence because of the polyfill. On browsers that don't require the polyfill, getRootNode
already exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To include support for noPatch
mode, ShadyDOM.wrap
must be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having trouble getting tests to work with noPatch enabled -- it looks like there are no tests under shadycss with noPatch enabled yet. I'll need to spend some more time on it, so I've updated the TODO and filed #343 and will do noPatch support as a separate PR in next sprint, if that's ok.
// we don't waste time querying it. | ||
return; | ||
} | ||
const parts = parentNode.querySelectorAll('[part]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, to support noPatch
mode any DOM API must use ShadyDOM.wrap
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this do a querySelectorAll
on every call to insertBefore
? If so, have you measured the performance impact of this? I'd expect it to be noticeable. ShadyDOM already conditionally does a walk during insertBefore (for example, to find slots); maybe consider a tighter integration to be able to participate in that walk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also consider doing this in prepareTemplateDom
so that only dynamic parts need to be changed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be searching newNode
and newNode.querySelectorAll
, not parentNode.querySelectorAll
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also consider doing this in
prepareTemplateDom
so that only dynamic parts need to be changed here.
Good idea. Probably missing something obvious here and I can look into it myself, but how can I avoid not querying the template again to look for parts when the template is stamped? Would I need to add some new tracing to understand when an insertBefore
is happening as a result of the element's shadycss-registered template stamping, or is there already a way I could figure that out?
Also, it seems like this could cut down on the cost of the querySelectorAll
since I guess I could store some kind of index of the part locations (or at the very least, a bit to tell me if it is even worth querying for parts), but I couldn't actually fully pre-process the template to add the shady-part
attribute, since the value of that attribute depends on knowing the super-scope, which we don't know until we're connected. Does that sound right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'd need to add support for knowing when to not support the querySelector, but there's precedent for this: https://github.com./webcomponents/polyfills/blob/master/packages/shadydom/src/patches/Node.js#L309.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be searching
newNode
andnewNode.querySelectorAll
, notparentNode.querySelectorAll
.
Done. I also needed some other changes to make this happen (since newNode
can sometimes be a DocumentFragment
or Text
), and added tests for various kinds of inserts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, to support
noPatch
mode any DOM API must useShadyDOM.wrap
.
As mentioned in other comment, will do in follow up (#343).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this do a
querySelectorAll
on every call toinsertBefore
? If so, have you measured the performance impact of this? I'd expect it to be noticeable. ShadyDOM already conditionally does a walk during insertBefore (for example, to find slots); maybe consider a tighter integration to be able to participate in that walk.
No benchmarks yet. I will add them in an upcoming task: #344
Will look into replacing the querySelectorAll
with an integration into the ShadyDOM walk as an upcoming task: #345
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it seems like this could cut down on the cost of the
querySelectorAll
since I guess I could store some kind of index of the part locations (or at the very least, a bit to tell me if it is even worth querying for parts), but I couldn't actually fully pre-process the template to add theshady-part
attribute, since the value of that attribute depends on knowing the super-scope, which we don't know until we're connected. Does that sound right?
@sorvell any thoughts on this? I've filed #346 for future investigation.
|
||
/* eslint-disable no-unused-vars */ | ||
/** | ||
* Perform any needed Shadow Parts updates after a ShadyDOM insertBefore call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you describe in more detail what this does and why it's necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be much less stress if we didn't have to alter the part attribute. Can you describe how this would limit the fidelity of the shim?
In particular, naively, a rule like x-super x-host [part~=foo]
is at least somewhat similar to what you have. This doesn't do lower bound encapsulation at all, but ShadyCSS already addresses this, so could it be x-super .x-host[part~=foo]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, interesting. I thought of an example that seemed sort of realistic where your suggestion would have caused an error, but then I thought of a modification that resolves it:
(1)
<style>
/* error, the <div> grand-child of <x-other> shouldn't match */
x-super .x-host[part~=foo] { color: red; }
/* but is OK with this modification */
x-super x-host.x-super .x-host[part~=foo] { color: red; }
</style>
<x-super>
<x-host class="x-super">
<div class="x-host" part="foo">
<x-other class="x-super">
<x-host class="x-other">
<div class="x-host" part="foo">
However, you can still construct some patterns that fail:
(2)
<style>
x-super x-host.x-super .x-host[part~=foo] { color: red; }
</style>
<x-super>
<x-host class="x-super">
<div class="x-host" part="foo">
<x-other class="x-super">
<x-host class="x-other">
<!-- error: should not be red -->
<div class="x-host" part="foo">
Whereas with the shady-parts
attribute, the selectors are much tighter:
(3)
<style>
x-super x-host [shady-part~="x-super:x-host:foo"] { color: red; }
</style>
<x-super>
<x-host>
<div class="x-host" part="foo" shady-part="x-super:x-host:foo">
<x-other>
<x-host>
<!-- OK -->
<div class="x-host" part="foo" shady-part="x-other:x-host:foo">
This is not to say that the shady-parts
approach is perfect -- once you start repeating the same elements in a nesting hierarchy then you get similar problems (as described at https://github.com./webcomponents/polyfills/blob/shady-parts-basic/packages/shadycss/shadow-parts.md#3-recursion-is-crude). But it is at least a more limited set of patterns.
I guess the next question is, are examples like (2) above acceptable gaps in fidelity? It seems a little tricky to workaround the (2) example unless you can control the part names. WDYT? Can you think of any other selector tricks that would work even better?
The other consideration here is exported parts. In the implementation in #331, the way we support export parts is by adding additional shady-parts
attributes to each part node based on the exportparts
attributes found when we walk up the tree of shadow hosts. This allows one transformed style rule to match parts in all possible export paths.
I guess the way to do this if we used this other approach would be: each time we discover that an exported part needs to receive some style, we clone that style rule (or update its selector) with the new scope. We'd need to do per-instance styling here, though, in cases where two instances of the same element have different exportparts attributes. I already interact with per-instance styling in #333 to support custom properties within part rules, so it wouldn't be that wild. The difference here, though, would be that we'd be doing per-instance styling for more cases, even in Edge when native custom property support is available.
WDYT, am I thinking about this right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for considering this so carefully. To summarize, nesting is a problem with any of our approaches, but with the current approach nesting of (A->B) inside an (A->B) is problematic whereas with the proposed approach nesting an A inside an A is problematic.
I was considering this without exportparts. I think considering that argues for how you've done it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged.
if (!disableShadowParts && PART.test(selector)) { | ||
// Hacky transform "::part(foo bar)" to "::part(foo,bar)" so that | ||
// SIMPLE_SELECTOR_SEP isn't confused by the spaces. | ||
// TODO(aomarks) Can we make SIMPLE_SELECTOR_SEP smarter instead? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to work this out, but acknowledge that it may be non-trivial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged. Leaving for now with TODO.
@@ -396,6 +405,20 @@ class StyleTransformer { | |||
return output.join(''); | |||
} | |||
|
|||
_transformPartSelector(selector, scope) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc with an example transformation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -375,6 +375,12 @@ export const NodePatches = utils.getOwnPropertyDescriptors({ | |||
} else if (node.ownerDocument !== this.ownerDocument) { | |||
this.ownerDocument.adoptNode(node); | |||
} | |||
if (!utils.disableShadowParts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See previous comment about performance concerns and consider instead a tighter integration at https://github.com./webcomponents/polyfills/blob/master/packages/shadydom/src/patches/Node.js#L311-L333.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged. Added TODO and filed #345 for upcoming task.
@@ -12,6 +12,10 @@ import {shadyDataForNode} from './shady-data.js'; | |||
/** @type {!Object} */ | |||
export const settings = window['ShadyDOM'] || {}; | |||
|
|||
/** @type {boolean} */ | |||
export const disableShadowParts = | |||
Boolean(window['ShadyCSS'] && window['ShadyCSS']['disableShadowParts']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be true
if ShadyCSS
does not exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, should it? If there's no ShadyCSS
, then the user hasn't set any initialization flags, so the default should hold, which would be disableShadowParts
set to false
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for review. Addressed some comments, but the others I've filed as follow up tasks, in the interest of incremental progress and getting the 3 PRs landed (note I'm merging all these PRs into the shady-parts
dev branch). Hope that's ok!
- Use ShadyDOM walk instead of
querySelectorAll
: Shady Parts: integrate with ShadyDOM walk instead of querySelectorAll #345 - noPatch support: ShadyParts: noPatch mode support #343
- Add benchmarks: Benchmarks for Shady Parts #344
- Move work into
prepareTemplateDom
where possible: Shady Parts: move some work to prepareTemplateDom if possible #346
@@ -23,6 +23,8 @@ import templateMap from './template-map.js'; | |||
import * as ApplyShimUtils from './apply-shim-utils.js'; | |||
import {updateNativeProperties, detectMixin} from './common-utils.js'; | |||
import {CustomStyleInterfaceInterface, CustomStyleProvider} from './custom-style-interface.js'; // eslint-disable-line no-unused-vars | |||
import * as shadowParts from './shadow-parts.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #339, will think about this a little more and refactor in a separate PR.
|
||
/* eslint-disable no-unused-vars */ | ||
/** | ||
* Perform any needed Shadow Parts updates after a ShadyDOM insertBefore call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged.
export function onInsertBefore(parentNode, newNode, referenceNode) { | ||
/* eslint-enable no-unused-vars */ | ||
if (!parentNode.getRootNode) { | ||
// TODO(aomarks) Why is it in noPatch mode on Chrome 41 and other older |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having trouble getting tests to work with noPatch enabled -- it looks like there are no tests under shadycss with noPatch enabled yet. I'll need to spend some more time on it, so I've updated the TODO and filed #343 and will do noPatch support as a separate PR in next sprint, if that's ok.
// we don't waste time querying it. | ||
return; | ||
} | ||
const parts = parentNode.querySelectorAll('[part]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be searching
newNode
andnewNode.querySelectorAll
, notparentNode.querySelectorAll
.
Done. I also needed some other changes to make this happen (since newNode
can sometimes be a DocumentFragment
or Text
), and added tests for various kinds of inserts.
@@ -12,6 +12,10 @@ import {shadyDataForNode} from './shady-data.js'; | |||
/** @type {!Object} */ | |||
export const settings = window['ShadyDOM'] || {}; | |||
|
|||
/** @type {boolean} */ | |||
export const disableShadowParts = | |||
Boolean(window['ShadyCSS'] && window['ShadyCSS']['disableShadowParts']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, should it? If there's no ShadyCSS
, then the user hasn't set any initialization flags, so the default should hold, which would be disableShadowParts
set to false
.
// we don't waste time querying it. | ||
return; | ||
} | ||
const parts = parentNode.querySelectorAll('[part]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this do a
querySelectorAll
on every call toinsertBefore
? If so, have you measured the performance impact of this? I'd expect it to be noticeable. ShadyDOM already conditionally does a walk during insertBefore (for example, to find slots); maybe consider a tighter integration to be able to participate in that walk.
No benchmarks yet. I will add them in an upcoming task: #344
Will look into replacing the querySelectorAll
with an integration into the ShadyDOM walk as an upcoming task: #345
// we don't waste time querying it. | ||
return; | ||
} | ||
const parts = parentNode.querySelectorAll('[part]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it seems like this could cut down on the cost of the
querySelectorAll
since I guess I could store some kind of index of the part locations (or at the very least, a bit to tell me if it is even worth querying for parts), but I couldn't actually fully pre-process the template to add theshady-part
attribute, since the value of that attribute depends on knowing the super-scope, which we don't know until we're connected. Does that sound right?
@sorvell any thoughts on this? I've filed #346 for future investigation.
if (!disableShadowParts && PART.test(selector)) { | ||
// Hacky transform "::part(foo bar)" to "::part(foo,bar)" so that | ||
// SIMPLE_SELECTOR_SEP isn't confused by the spaces. | ||
// TODO(aomarks) Can we make SIMPLE_SELECTOR_SEP smarter instead? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged. Leaving for now with TODO.
@@ -396,6 +405,20 @@ class StyleTransformer { | |||
return output.join(''); | |||
} | |||
|
|||
_transformPartSelector(selector, scope) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -375,6 +375,12 @@ export const NodePatches = utils.getOwnPropertyDescriptors({ | |||
} else if (node.ownerDocument !== this.ownerDocument) { | |||
this.ownerDocument.adoptNode(node); | |||
} | |||
if (!utils.disableShadowParts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged. Added TODO and filed #345 for upcoming task.
parts = newNode.querySelectorAll('[part]'); | ||
} else if (newNode instanceof Text) { | ||
// E.g. an innerHTML assignment. | ||
parts = parentNode.querySelectorAll('[part]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question, I got muddled in some debugging and was thinking the innerHTML
call would show up as an insert with text, but obviously that's wrong. Removed.
document.body.removeChild(xa); | ||
}); | ||
|
||
test('appendChild: part', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add test for appending/inserting a Text node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, added test and an early return so that we don't crash in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One little nit but otherwise looks good.
Co-authored-by: Steve Orvell <[email protected]>
Note: suggest reviewing the two commits separately, since the first one contains some reformatting.
This supports basic patterns of styling parts whenever a node is inserted into a shadow root. It also adds and tests a
disableShadowParts
ShadyCSS setting.Support for more kinds of mutations, exportparts, CSS custom properties are coming in follow up PRs.
Part of #311 and #252. Fixes #337.