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

SFC style CSS variable injection (new edition) #231

Merged
merged 20 commits into from Jul 16, 2021
Merged

SFC style CSS variable injection (new edition) #231

merged 20 commits into from Jul 16, 2021

Conversation

@yyx990803
Copy link
Member

@yyx990803 yyx990803 commented Nov 16, 2020

This is an improved alternative of the previous version (<style vars> as proposed in #226)

Thanks to feedback by @Demivan and @privatenumber, #226 has a few notable issues that can be improved:

  1. Requires manual vars declaration for exposing variables that can be used.
  2. No obvious visual hint that a variable is injected and reactive
  3. Different behavior in scoped/non-scoped mode.
  4. In non-scoped mode, the CSS variables would leak into child components.
  5. In scoped mode, the global: prefix is required to use normal CSS variables declared outside of the component. It would be preferable that normal CSS variable usage stays the same in/out of the component.

This proposal addresses all of the above with the following usage:

<template>
  <div class="text">hello</div>
</template>

<script>
  export default {
    data() {
      return {
        color: 'red',
        font: {
          size: '2em'
        }
      }
    }
</script>

<style>
  .text {
    color: v-bind(color);

    /* expressions (wrap in quotes) */
    font-size: v-bind('font.size');
  }
</style>

Summary:

  • No need to explicit declare what properties are injected as CSS variables (inferred from usage of v-bind() in CSS)
  • Reactive variables are more visibly different
  • Same behavior in scoped/non-scoped mode
  • No leakage into child components
  • Usage of plain CSS variables are unaffected

Rendered

@CyberAP
Copy link
Contributor

@CyberAP CyberAP commented Nov 16, 2020

What do you think about writing those bindings without -- prefix, so we can better distinguish them from the native CSS Custom Properties?

<script>
export default {
  data() {
    return { baz: 'red' }
  }
}
</script>

<style>
.foo {
  color: var(:bar); /* binding */
  font-size: var(--global-font-size); /* native property */
}
</style>

We don't reference those transformed bindings in the code so they don't really require -- prefix.

Also, to avoid Prettier issues the --: prefix could be replaced with a # (also works for CSS parsers), though it might be confusing since url(#foo) can reference SVG path.

<style>
.foo {
  color: var(#bar); /* won't be affected by Prettier */
}
</style>

@yyx990803
Copy link
Member Author

@yyx990803 yyx990803 commented Nov 16, 2020

@CyberAP I think it's better to keep the leading -- because it's a smaller spec mismatch than without it:

  1. --:foo can still be considered a custom property identifier that contains an invalid char
  2. :foo is not a custom property identifier (which is defined as an identifier with leading --) - in fact, CSS identifier tokens cannot start with :.

I think (2) has a higher risk of tooling incompatibility as it's more likely for a parser to fail to tokenize :foo as an identifier due to the invalid leading char.

@privatenumber
Copy link

@privatenumber privatenumber commented Nov 17, 2020

  • Could there be an obfuscate/mangle/minify CSS vars option to enable on production? I'm interested in the following benefits:

    • Minification The CSS distribution size can be reduced if hashed var names are mangled, especially for long & semantic variable names. eg. if var(--:theme.color.secondary) can be compiled to var(--6b537420) instead of var(--6b53742-theme_color_secondary).

    • Obfuscation Like JS minification, mangled CSS property names can deter reverse engineers. I can't remember which CSS-in-JS library Instagram uses, but this is really nice:

      Screen Shot 2020-11-16 at 6 50 25 PM
  • Wondering if there's a scalability issue. If there's a page that renders 100 different themeable components that all reference an injected theme property, I'm thinking that would create 100 different CSS vars that all map to the same state. If all components that read from the same state can share one CSS variable, the performance can improve dramatically. Have a few ideas to address this but would like to validate whether this is a real concern first.

// Button.vue - example of a themable component reading from an injected state
<template>
	<button class="button">
		<slot />
	</button>
</template>

<script>
export default {
	inject: ['theme'],
};
</script>

<style>
.button {
	background-color: var(--:theme.color.primary);
}
</style>
// Input.vue - another example of a themable component reading from an injected state
<template>
	<input
        class="input"
        type="text"
    >
</template>

<script>
export default {
	inject: ['theme'],
};
</script>

<style>
.input {
	color: var(--:theme.color.primary);
}
</style>
// Layout.vue - injects theme state, and also has UI that rapidly updates theme properties
<template>
	<div class="layout">
		<label>
			Primary color:

			<!-- Every change here will update a different CSS var for each component that reads from this  -->
			<input
				type="color"
				v-model="theme.color.primary"
			>
		</label>

		<slot />
	</div>
</template>

<script>
export default {
	data() {
		return {
			theme: {
				color: {
					primary: 'red',
				},
			},
		};
	},

	provide() {
		return {
			theme: this.theme,
		};
	},
};
</script>

@xstxhjh
Copy link

@xstxhjh xstxhjh commented Nov 17, 2020

font-size: var(--:font.size);
: .
vscode problems: ) expectedcss(css-rparentexpected)

Invalid CSS Var

:root {
  --font.size: 22px;
}

.text {
  font-size: var(--font.size);
}

valid CSS Var

:root {
  --fontSize: 22px;
}

.text {
  font-size: var(--fontSize);
}

will go wrong?

.text {
    color: var(--v-bind:color);

    /* shorthand + nested property access */
    font-size: var(--:font.size);
}

@web2033
Copy link

@web2033 web2033 commented Nov 17, 2020

What's the reason of having both long and shorthand notation? Just pick one...

@Kocal
Copy link

@Kocal Kocal commented Nov 17, 2020

@web2033 We have v-bind:attr and :attr in templates, so it makes sense

@CyberAP
Copy link
Contributor

@CyberAP CyberAP commented Nov 17, 2020

From my personal experience v-bind is never used in projects in a long notation and having two types of notation has never been great because people have to choose one when framework should actually solve that problem for them.

@web2033
Copy link

@web2033 web2033 commented Nov 17, 2020

How about

.text {
    color: var(--v-bind-color);
    font-size: var(--v-bind-font-size);
}

since a finger is already on - anyways.
Valid CSS. No tooling tweaking needed.

@web2033
Copy link

@web2033 web2033 commented Nov 17, 2020

... or "vue-branded" vars 😁

<template>
  <div class="text">hello</div>
</template>

<script>
  export default {
    data() {
      return {
        color: 'red',
        font: {
          size: '2em'
        }
      }
    }
</script>

<style>
.text {
    color: var(--vue-color);
    font-size: var(--vue-font-size);
}
</style>

@yyx990803
Copy link
Member Author

@yyx990803 yyx990803 commented Nov 17, 2020

@privatenumber the theme injection is an example, and yes it can potentially lead to inefficiency when used everywhere. Maybe for global themes it is still better to provide it as plain CSS variables at root and let it cascade:

<!-- App.vue -->
<script>
const theme = ref({
  color: 'red',
  // ...
})
</script>

<template>
  <div class="theme-provider>
    <input type="color" v-model="theme.color">
  </div>
</template>

<style>
.theme-provider {
  --color-primary: var(--:theme.color);
}
</style>

@nesteiner
Copy link

@nesteiner nesteiner commented May 13, 2021

在style中绑定变量后,给这个变量设置setInterval实时更新,但是样式没有变化诶
比如这里修改 opacity 属性

<template>
  <div class="shining">
    <h1> Vue </h1>
  </div>
</template>

<script lang="ts">
 import {Vue} from 'vue-property-decorator'
 
 export default class Shining extends Vue {
   private opacity: number = 0

   mounted(): void {
     setInterval(() => {
       this.opacity >= 1 && (this.opacity = 0)
       this.opacity += 0.2
     }, 300)
   }
 }
</script>

<style scoped>
 h1 {
  /* color: rgb(65, 284, 131); */
   color: black;
   opacity: v-bind(opacity);
 }
</style>

但是如果使用js写的话,没有这个问题

 export default {
   data() {
     return {
       opacity: 0
     }
   },

   mounted() {
     setInterval(() => {
       this.opacity >= 1 && (this.opacity = 0)
       this.opacity += 0.2
     }, 300)
   }
 }

@chenxinan
Copy link

@chenxinan chenxinan commented May 17, 2021

how i can use v-bind in background-image: url()

<template>
  <div class="bg" />
</template>

<script setup>
const img = "https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png";
</script>

<style scoped>
 .bg {
   --img: url(v-bind(img));
   --img2: url("https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png");
   width: 100vw;
   height: 100vh;
   background-image: url(v-bind(img)); /* no work */
   background-image: var(--img); /* no work */
   background-image: var(--img2); /* work */
 }
</style>

@daiwanxing
Copy link

@daiwanxing daiwanxing commented May 17, 2021

how i can use v-bind in background-image: url()

<template>
  <div class="bg" />
</template>

<script setup>
const img = "https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png";
</script>

<style scoped>
 .bg {
   --img: url(v-bind(img));
   --img2: url("https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png");
   width: 100vw;
   height: 100vh;
   background-image: url(v-bind(img)); /* no work */
   background-image: var(--img); /* no work */
   background-image: var(--img2); /* work */
 }
</style>

Just write like this, It's wrting similar to the css var function.

<script setup>
const img = "url(https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png)";
</script>


<style scoped>
 .bg {
   background-image: v-bind(img)
 }
</style>

@nesteiner
Copy link

@nesteiner nesteiner commented May 17, 2021

@chenxinan 试试这个

<script>
import img from 'path'
export default {
    data() {
        img,
    }
}

</script>

@0x1af2aec8f957
Copy link

@0x1af2aec8f957 0x1af2aec8f957 commented May 21, 2021

在style中绑定变量后,给这个变量设置setInterval实时更新,但是样式没有变化诶
比如这里修改 opacity 属性

<template>
  <div class="shining">
    <h1> Vue </h1>
  </div>
</template>

<script lang="ts">
 import {Vue} from 'vue-property-decorator'
 
 export default class Shining extends Vue {
   private opacity: number = 0

   mounted(): void {
     setInterval(() => {
       this.opacity >= 1 && (this.opacity = 0)
       this.opacity += 0.2
     }, 300)
   }
 }
</script>

<style scoped>
 h1 {
  /* color: rgb(65, 284, 131); */
   color: black;
   opacity: v-bind(opacity);
 }
</style>

但是如果使用js写的话,没有这个问题

 export default {
   data() {
     return {
       opacity: 0
     }
   },

   mounted() {
     setInterval(() => {
       this.opacity >= 1 && (this.opacity = 0)
       this.opacity += 0.2
     }, 300)
   }
 }

@nesteiner doc: https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-style-variables.md#motivation

@edisdev
Copy link

@edisdev edisdev commented Jun 28, 2021

@privatenumber the theme injection is an example, and yes it can potentially lead to inefficiency when used everywhere. Maybe for global themes it is still better to provide it as plain CSS variables at root and let it cascade:

<!-- App.vue -->
<script>
const theme = ref({
  color: 'red',
  // ...
})
</script>

<template>
  <div class="theme-provider>
    <input type="color" v-model="theme.color">
  </div>
</template>

<style>
.theme-provider {
  --color-primary: var(--:theme.color);
}
</style>

Hi @yyx990803 🖐 i did make a small example for latest version this feature. (with v-bind) 👉 The gist link is also here.

I hope that will be useful. 🙏

<template>
  <div class="Example">
    <div class="area">
      <div class="form">
        <label>Select Color</label>
        <input type="color" v-model="customTheme.bgColor" />
      </div>
      <div class="preview">
        <span>{{ customTheme.bgColor }}</span>
        <div class="customColor"></div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref } from "vue";
export default {
   setup() {
    const customTheme = ref({
      bgColor: "",
    });
    return {
      customTheme
    };
  },
};
</script>

<style>
.Example {
  --custom-color: v-bind("customTheme.bgColor");
}
.customColor {
  background: var(--custom-color);
  width: 100px;
  height: 50px;
}
</style>

@m4heshd
Copy link

@m4heshd m4heshd commented Jul 1, 2021

Amazing. Is SCSS compatible with this feature?

@ITenthusiasm
Copy link

@ITenthusiasm ITenthusiasm commented Jul 1, 2021

Amazing. Is SCSS compatible with this feature?

@m4heshd I think so. When I tried to test out the experimental feature while working on a Vue 3 project, it seemed to work fine while using scss for the style section.

@edisdev
Copy link

@edisdev edisdev commented Jul 1, 2021

Amazing. Is SCSS compatible with this feature?

@m4heshd I tried this. This feature worked with SCSS. But I couldn't get this feature to work in production mode. So when I run vue cli server build it didn't work. What do you think about that? @yyx990803

Screen.Recording.2021-07-01.at.15.25.15.mov
<template>
  <div class="Example">
    <div class="area">
      <div class="form">
        <label>Select Color (SCSS)</label>
        <input type="color" v-model="themeColors.bgColor" />
      </div>
      <div class="preview">
        <span>{{ themeColors.bgColor }}</span>
        <div class="customColor"></div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref } from "vue";

export default {
   setup() {
    const themeColors = ref({
      bgColor: ''
    })

    return {
      themeColors
    };
  },
};
</script>

<style lang="scss">

.Example {
  $customColor: v-bind('themeColors.bgColor');

  .customColor {
    background: $customColor;
    width: 100px;
    height: 50px;
  }
}

</style>

@m4heshd
Copy link

@m4heshd m4heshd commented Jul 1, 2021

@ITenthusiasm good to know that.

@edisdev Ouch. 😬 That's what I was afraid of. I really don't wanna be hit with any "gotchas" when going into production. Did you use dart-sass or node-sass?

@edisdev
Copy link

@edisdev edisdev commented Jul 1, 2021

@m4heshd Yes, project dependencies has node-sass and sass-loader. Actually this feature is not working also with pure css in production mode. While reviewing this, I found that the style variable name that binds to the element is incorrect in production mode.

For Example :
Screen Shot 2021-07-01 at 17 39 56

--7ba5bd90-themeColors_bgColor is not correct. I guess that's the variable name prepared for development mode.

@m4heshd
Copy link

@m4heshd m4heshd commented Jul 1, 2021

@edisdev I don't think that's a problem because there's a bunch of unique identifiers and some extra stuff added in the build process. But you should try sass (dart-sass) instead of node-sass because it's known to have a lot of incompatibilities and it's deprecated. sass-loader works with both of them.

Oops. Just took a look again and saw what's wrong. Way too sleep deprived to be properly conscious. 😩 So it looks like this feature is definitely not production ready and we shouldn't be adding any functionality dependent on it.

@edisdev
Copy link

@edisdev edisdev commented Jul 1, 2021

@m4heshd I hope it will be fixed soon. I opened issue about this.

@GuoChen-thlg
Copy link

@GuoChen-thlg GuoChen-thlg commented Jul 13, 2021

When I tried to use it in a development environment, it worked fine

:root{
     --47536436-theLen: 200px;
    --47536436-angle: 45deg;
    --47536436-offset: 100px;
}
.style{
    position: relative;
    list-style-type: none;
    width: var(--47536436-theLen);
    height: var(--47536436-theLen);
    transform: rotate3d(0.7, 0.5, 0.5, var(--47536436-angle));
    transform-style: preserve-3d;
}

But when I packed it, it didn't work

:root{
   --47536436-theLen: 200px;
   --47536436-angle: 45deg;
   --47536436-offset: 100px;
}
.style{
    position: relative;
    list-style-type: none;
    width: var(--946a2296);
    height: var(--946a2296);
    transform: rotate3d(.7,.5,.5,var(--8afc983c));
    transform-style: preserve-3d;
}

@GulnazKhasanova
Copy link

@GulnazKhasanova GulnazKhasanova commented Jul 13, 2021

Tell me how to dynamically change a property background-image in ::before?

@yyx990803
Copy link
Member Author

@yyx990803 yyx990803 commented Jul 16, 2021

Looks like most of the issues raised after the final comments period were about implementation bugs - most of which have been addressed in the latest version (3.1.5).

  • One of the issues where the feature does not work in production mode for vue-cli is due to an issue of thread-loader and is tracked in vuejs/vue-next#3921

  • If there are other cases where it's not working as specified in the RFC, please open an issue at the vue-next repo with reproduction instead of commenting here.

Since these are all implementation issues, the RFC itself is considered finalized. We will try to resolve all implementation bugs in the upcoming 3.2 and this feature will be out of experimental status when 3.2 stable is released.

@yyx990803 yyx990803 merged commit 0574e70 into master Jul 16, 2021
@yyx990803 yyx990803 deleted the style-vars-2 branch Jul 16, 2021
@bluwy bluwy mentioned this pull request Jul 17, 2021
@LiuQixuan
Copy link

@LiuQixuan LiuQixuan commented Jul 19, 2021

How to prevent the rendered css variable with hash prefixed? The same component will render multiple css files, which is obviously unreasonable. I am not afraid of same css variable name.In my mind,the css variable was written in the HTML element and only works for the css var( ) syntax under the HTML element. Why do you need to add a hash prefix? Just to avoid duplication?

@iDerekLi
Copy link

@iDerekLi iDerekLi commented Jul 26, 2021

BUG: in SCSS

System Info

 OS: Windows 11
   CPU: (8) x64 AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
   Memory: 535.72 MB / 6.94 GB
 Binaries:
   Node: 12.15.0 - C:\Program Files\nodejs\node.EXE
   Yarn: 1.22.5 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
   npm: 6.13.4 - C:\Program Files\nodejs\npm.CMD
 Browsers:
   Edge: Spartan (44.22000.1.0), Chromium (91.0.864.67)
   Internet Explorer: 11.0.22000.1
 npmPackages:
   "sass": "^1.26.5",
   "sass-loader": "^8.0.2"
 vueCLI:
    @vue/cli 4.5.13

CSS

css parsed variables are normal.

<script setup>
const width = 123;
</script>
<style scoped>
.canvas {
  position: relative;
  width: v-bind(width + "px");
  box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2);
  background: #ffffff;
}
</style>

image

SCSS

scss parsing variables are not equal.

<script setup>
const width = 123;
</script>

<style lang="scss" scoped>
.canvas {
  position: relative;
  width: v-bind(width + "px");
  box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2);
  background: #ffffff;
}
</style>

image

@berzi
Copy link

@berzi berzi commented Sep 6, 2021

v-bind() doesn't seem to work inside CSS rules with the ::v-global() selector.

Is this the right place to report this? I got here through a warning in the compiler.

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

Successfully merging this pull request may close these issues.

None yet