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

Add keyframes support for scoped style ? #245

Closed
jihchi opened this issue Jan 11, 2017 · 19 comments
Closed

Add keyframes support for scoped style ? #245

jihchi opened this issue Jan 11, 2017 · 19 comments

Comments

@jihchi
Copy link

jihchi commented Jan 11, 2017

Consider following example:
https://svelte.technology/repl/?version=1.6.3&gist=5390c2517520e606bb02ce8168f3b8b0

Currently, Svelte will output results as example:

.foo[svelte-3750005346], [svelte-3750005346] .foo {
    color: red;
    font-size: 2em;
    font-family: 'Comic Sans MS';
    animation: spin-360 infinite 20s linear;
}

@keyframes spin-360 {
    from[svelte-3750005346], [svelte-3750005346] from { transform: rotate(0deg); }
    to[svelte-3750005346], [svelte-3750005346] to { transform: rotate(360deg); }
}

Which animation won't works.

Expect that css should be:

@@ -6,6 +6,6 @@
 }
 
 @keyframes spin-360 {
-    from[svelte-3750005346], [svelte-3750005346] from { transform: rotate(0deg); }
-    to[svelte-3750005346], [svelte-3750005346] to { transform: rotate(360deg); }
+    from { transform: rotate(0deg); }
+    to { transform: rotate(360deg); }
 }
@Conduitry
Copy link
Member

Conduitry commented Jan 11, 2017

I'm looking at the code responsible for this and it's hurting my head https://github.com/sveltejs/svelte/blob/master/src/generators/shared/css/transform.js :( There's no real parsing of the CSS, it's just a bunch of regular expressions.

This is kind of icky because things like @media declarations should have what's inside them transformed, but @keyframes declarations like the one in the example should not - and what's inside the @keyframes declaration looks exactly like CSS selectors that we'd want to transform in other contexts.

I don't know how robust we ought to make this. I believe one workaround now (without any changes to Svelte) would be to just use 0% and 100% instead of from and to - Svelte won't mistake these for selectors. A somewhat hacky solution within Svelte would be to disregard anything that looks like a from or to selector in all cases, and hope no one is creating and attempting to style elements with those names.

Other than that, I don't know what to suggest, because I suspect we don't want to bring in a whole CSS parsing and processing library. The least-impact solution might be to just put a warning in the docs that you need to use 0% and 100% in @keyframes so the CSS processor doesn't get confused.

@Conduitry
Copy link
Member

@jihchi Using 0% and 100% instead of from and to works around this for now - https://svelte.technology/repl/?version=1.6.3&gist=cf921c3813f02e5c2b78d39e1ae448d5

@Rich-Harris
Copy link
Member

My initial thought was that we probably should use a dedicated CSS parser (most likely PostCSS) because that would make it a lot easier to do things like minification/optimisation in a sourcemap-aware way: #8 (comment). Because Svelte only runs at build time, there isn't the same pressure to be lean.

The code in that file is copied straight from Ractive, where there was pressure to keep things compact as in many cases the code runs in the browser. It was only really intended as a stopgap measure.

That said, @keyframes is a tricky one... other CSS is scoped, but keyframes are global. I wonder if we need to manipulate the animation name (and all its references) to achieve encapsulation.

FWIW, in the past when I've encountered this problem (with Ractive projects) I've reluctantly included a separate .css file with my keyframe declarations. Is a cop-out, but it works around the issue until we fix it properly.

@jihchi
Copy link
Author

jihchi commented Jan 18, 2017

Thanks for the help / workaround. @Conduitry @Rich-Harris

@thysultan
Copy link

thysultan commented Jan 24, 2017

@Rich-Harris In case this is useful, i built a css compiler stylis.js that does handle name-spacing keyframes and animations.

@Conduitry
Copy link
Member

I've been playing with this a little bit here. Not sure whether this is going to be a good direction to go in. In my branch, this particular issue is fixed, but there are a few not very nice things still. One is that PostCSS is introduced as an external (non-dev) dependency. It or one of its dependencies was depending on built-in Node modules, which the current build process doesn't seem to be able to handle. It's possible that this could be resolved with rollup-plugin-node-builtins but I didn't want to get bogged down in that for now.

Using PostCSS to process the CSS did remove some of the ugly code from transform.js, but sadly the actual processing of a single selector remains the same, and ugly. I'm not very familiar with PostCSS at all, but I didn't immediately see a way of getting at something more processed than a single selector.

@Conduitry
Copy link
Member

Conduitry commented Mar 1, 2017

Hm. Looks like this is not fixed yet with the new csstree-based parsing and transformation (REPL), but hopefully now this should be actually possible. I'll try to have a look at the new AST walker.

edit: In fact, the workaround I suggested before (which was based on limitations of the old regexes) doesn't work anymore in 1.7+ either. The 0% and 100% are now see as selectors.

@Conduitry
Copy link
Member

Using this walker in processCSS.js seems to work. It adds a special case for @keyframes and doesn't process those at all.

	function walk ( node ) {
		if ( node.type === 'Rule' ) {
			transform( node );
		} else if ( node.type === 'Atrule' && node.name === 'keyframes' ) {
			// do not process keyframes declarations
		} else if ( node.children ) {
			node.children.forEach( walk );
		} else if ( node.block ) {
			walk( node.block );
		}

This doesn't take care of scoping the keyframes rules. Prefixing the @keyframes name (and the animation-name references to it) shouldn't be too hard, I'm looking into that now.

@Rich-Harris
Copy link
Member

Nice! Re scoping – one thing that occurs to me is that while it should be possible to transform keyframe declarations and animation-name in the same component, it would behave slightly unexpectedly here:

<!-- Main.html -->
<Widget/>

<style>
  div {
    color: red;
  }

  @keyframes foo {...}
</style>

<!-- Widget.html -->
<div>this text is red, because child components inherit parent styles</div>

<style>
  div {
    /* foo would not be transformed correctly, so the animation would not
       be inherited */
    animation: foo 1s;
  }
</style>

So some CSS (color: red) would be inherited by child components, but some (i.e. keyframes) wouldn't. How do we feel about that? Maybe it's not a big deal. Or maybe there's some way that keyframes could be inherited that I haven't thought of? Or maybe no styles should ever be inherited by child components (though I often find it quite useful to be able to declare styles at a certain point in the tree, rather than repeating them for child components).

@Conduitry
Copy link
Member

Something else I realized is that the keyframes names won't always show up in animation-name of course, they'll also show up in animation shorthand properties. We might have to do two passes, one where we collect keyframes names, and the next to replace instances of them in animation-name and animation declarations.

Doing two passes is probably necessary anyway - we don't want to be transforming keyframes names that aren't defined in that component, because that would break using keyframes that were defined globally.

As for inheriting keyframes ... I don't see, currently, how to do that in a nice way. It seems like it would necessarily involve some runtime manipulation of the css, because at compile time a component has no idea what context it will be used in, or what the keyframes it's using have been renamed to.

@Rich-Harris
Copy link
Member

Yeah, I think you're right. Perhaps a workaround would be to have a way of marking keyframes as global, like this...

@keyframes !foo {...}

...though it feels a bit hacky, and I suppose it can be worked around by just adding a regular stylesheet or <style> tag somewhere on the page. Maybe it's just a case of documenting that particular (fairly minor) limitation

@Conduitry
Copy link
Member

Just pushed what I have now to https://github.com/Conduitry/svelte/tree/gh-245-keyframes-scoping

It uses two passes - one where it collects the names of the keyframes definitions and transforms their definitions - and then a second where it does the normal [svelte-xxx] transformations and then also changes any identifiers in animation or animation-name properties that were previously found to be the names of keyframes.

This does allow you to use globally-defined keyframes within components, but will not allow you to globally define keyframes within components. I think that's reasonable behavior.

@thysultan
Copy link

@Conduitry You could also change the keyframe and animation names to ${name}+xxx where xxx is from [svelte-xxx] that way you can do it in one pass and allow the children of a component to inherit keyframes.

@Conduitry
Copy link
Member

@thysultan Hm I'm not sure quite what you're suggesting. What would the output be for the example CSS in the original post on this issue?

@thysultan
Copy link

thysultan commented Mar 1, 2017

@Conduitry

.foo[svelte-3750005346], [svelte-3750005346] .foo {
    animation: spin-360-3750005346 infinite 20s linear;
}

@keyframes spin-360-3750005346 {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

@Conduitry
Copy link
Member

@thysultan As far as I can tell that's what I'm outputting now (apart from a svelte- prefix on the keyframes identifier names). I don't see how to do that in only one pass. The first time through, for each identifier in an animation or animation-name property, we don't know whether that's going to be something that's defined in our styles (and so should be transformed) or whether it's a global keyframes definition (and so should be left alone).

Additionally, in the css AST, things like spin-360 and infinite and linear all simply have type 'Identifier' and so it will be difficult to tell which of those should be transformed unless we already know what the names of the keyframes are.

@thysultan
Copy link

The svelte- prefix is what should allow children to inherit parent keyframes.

we don't know whether that's going to be something that's defined in our styles (and so should be transformed) or whether it's a global keyframes definition (and so should be left alone).

If the syntax for promoting a keyframe into the global scope is explicit it should be possible to identify i.e the ! in !foo otherwise yes that will be tricky with this approach.

Additionally, in the css AST, things like spin-360 and infinite and linear all simply have type 'Identifier'

Come across this problem with stylis as well, since any variable number of identifiers can come with the short hand animation property, but you don't need to know the names of the keyframes defined in the css just the names of the possible css identifiers that the animation short hand supports.

@Rich-Harris
Copy link
Member

@Conduitry your branch looks good — could you open a PR please, and I'll merge it in? One small thing, it turns out keyframes are case-sensitive, so they should be left as-is and not lowercased.

@thysultan afraid I can't quite understand what you're saying about allowing child components to inherit keyframes — how would that work, given that the hash is unique to each component?

@thysultan
Copy link

@Rich-Harris probably be a bit more tricky than i thought of it the first time round, maybe a parent reference escape hatch like !foo i.e &foo though i imagine it would be easier to just have one for global.

Rich-Harris added a commit that referenced this issue Mar 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants