Subscribe on changes!

With CSS v-bind, .value is not automatically used

avatar
Jul 27th 2023

Vue version

3.3.4

Link to minimal reproduction

https://play.vuejs.org/#eNqNVW1v20YM/iucMLTS4CgpsmKDq3gvhTFsxpYgNfLFMlBFOiWXSHfC3clzavi/l+SdZQtoi35ILJHHh3we8qhd9EfXpZteRNMoc6LtmsKJWa4AsvveOa2gbAprr/JIbAv0ijNvziNwL51A+/D+e9nI8hkt5WOhHsRNUVVSPeQRowG8Zyt0wUwZzn0sH2CD7QoF1r00BBxOnjndTeEifWtE+w5qrdyZlZ8EmX79hY1Dirui6dGx20Gp21arNEDAfu/zET6ezc5PmOKrLY3sHDRYIOZ1FslY4foOvbLttHGwY7bwXrdd70R1K+qJt/ATZmPzBIyoYQ+10S28RlFfvyP8UeiN0Z0wTgqbLWdwBTuqbLUAqeBZvOgaluvpaZ5suVqsZ4izZyypnDB1UQr4SyhhZHl9/yRKl92B2DqhKguFekFY/D8L2Ag7RVENCoHQdwRFSKVW1oHtGukwjcWYjI4vB6BRggn5FoMvlIpB4Yn98y/4ldgIk6tZTAdQnilJhtQ5QHdOYhnYMt/ADgfotyksVmuskgxiWzZ9JdA2D7Y9aYYEkqNKJ5Jet9JlN4iSLSewmE1gPkORgxKesRG2bxyjQGG/F4TFP0AgO9LLK5PSW4zM0g2NX0KgceCfcNHHwB0z9KrZyYEdvq/WFIYcmV+QZRRaywY7j07Cnh4ERhXwCSTGeqxsQeUi5aOgqVTsshSZwKtX8ENIPPJwAUnIWWsD8UAWMBVVnPoiYv+TJIe2eUlp0NZY4OE2xHFC0g/KsJ8SYBN9FoOXzKgQPoy4TxvWDeIhQMyJwm2eQh5dpJdvRZtHPEXB/qEtmoaduBp+PrrvtamEuS0q2eOkUewb3hvBTX+bwshCORpEfgc4P4c0TbEAvL1EX3OvbSh+nxwLHW076g+z5oIDB09/WEZX8PHH3b+Fe0zrRmsT86MpVKVbjP0J3lwk+277cXxP/ULD2OHCYntGOo+SJZNDBdzgKazy6EAyj9ZInCjgQuTNF7Yg7V2wJd6DCi3peOF7vKEFm7N7qaoYl/1o0+YRNRjrRmiCm0UT3KdIoZYP6ZPVCj8zDMSBnWyEufbDnkfDFsgjbKT+/x+2OdML7hPHPIry+Qv2J7slWx7d4CwJsxGht+RzhXkQzrvnH/7DDXXibHXVN3j6G85bYXXTU43+2J+9qrDsk3Nc7d/8oUAJlnZOS9AeSFGhPDZ+2iL8LNDO+Rr1Y7mX6eVh3KL9Z92jkYs=

Steps to reproduce

I sent a playground with the code below that shows the variable used in v-bind in CSS does not automatically infer the .value, so it does not update, and the value in the CSS ends up as [object Object].

Due to the kind of clunky way these computedRefs are created, I've confirmed they are reactive by using the same variable inside <template>, which works fine.

If you edit v-bind("common.padding") to v-bind("common.padding.value") it works properly.

<template>
  <button class="example-button" type="button" @click="changePadding">
    Change padding
  </button>
  
  <span style="padding-top: 0.5rem; font-size: 0.875rem;">
    Value: {{ common.padding }}
  </span>
</template>

<script lang="ts" setup>
import { type ComputedRef, type Ref, computed, ref } from 'vue';

type ComputedProperties<T> = {
  [K in keyof T]: ComputedRef<T[K]>;
};

interface GenericObject<V extends any = any> {
  [key: string]: V;
}

const splitRefs = <
  T extends GenericObject,
  K extends keyof T = keyof T,
  E extends keyof T = never
>(
  ref: Ref<T>,
  options: {
    pick?: K[];
    exclude?: E[];
  } = {}
): ComputedProperties<Omit<Pick<T, K>, E>> => {
  const result = {} as ComputedProperties<Omit<Pick<T, K>, E>>;

  const keys = Object.keys(ref.value) as (keyof T)[];

  const { pick = keys, exclude = [] as E[] } = options;

  const filter = (key: keyof T): key is Exclude<K, E> =>
    pick.includes(key) && !exclude.includes(key as E);

  for (const key of keys.filter(filter)) {
    result[key] = computed(() => ref.value[key]);
  }

  return result;
};

const example = ref({
  padding: "0.35em",
  paddingSmall: "0 0.45em",
  borderRadius: "0.175rem",
  
  variants: { 
    // ...plenty of objects
  }
})

const changePadding = () => {
  example.value.padding = `${Math.floor(Math.random() * 10)}px`;
}

const common = splitRefs(computed(() => example.value), {
  exclude: ["variants"],
})
</script>

<style scoped>
.example-button {
  padding: v-bind("common.padding");
}
</style>

What is expected?

Expected behavior is for Vue to automatically kind of use .value when reactive variables are referenced in CSS with v-bind. The below should create a CSS variable with the value of the reactive property common.padding.

.example-button {
  padding: v-bind("common.padding");
}

What is actually happening?

I manually have to specify .value on the reactive variable for it to work as intended, otherwise the CSS variable returns [object Object].

.example-button {
  padding: v-bind("common.padding.value");
}

System Info

System:
  OS: macOS 13.2.1
  CPU: (12) arm64 Apple M2 Max
  Memory: 38.68 GB / 64.00 GB
  Shell: 5.8.1 - /bin/zsh
Binaries:
  Node: 18.16.0 - /usr/local/bin/node
  Yarn: 1.22.19 - /usr/local/bin/yarn
  npm: 9.5.1 - /usr/local/bin/npm
  pnpm: 8.6.1 - ~/Library/pnpm/pnpm
Browsers:
  Safari: 16.3

Any additional comments?

I realize the minimal reproduction seems kind of odd, it is extracted and minimized from a bigger useTheme hook.

avatar
Jul 28th 2023

I'd call this behavior expected as its the same as in templates:

  • Top-level refs are unwrapped
  • refs nested in reactive() objects are unwrapped
  • refs nested in plain objects are not unwrapped (your case is this)

Changing this now would also ve a breaking change

avatar
Jul 28th 2023

Oh, okay, thanks @LinusBorg. It's the non-top level properties of an object that do not work, but from what I understand that means the use of style.padding below in the template shouldn't work either (even though it looks like it does when I test it), or am I misunderstanding something?

<script lang="ts" setup>
import { ref } from 'vue'

const useHook = () => {
  const style = {
    padding: ref('10px'),
  }
  
  return {
    style,
  }
}

const { style } = useHook();

const increasePadding = () => {
  const currentValue = parseInt(style.padding.value, 10);
  style.padding.value = `${currentValue + 1}PX`
}
</script>

<template>
  <h2>{{ style.padding }}</h2>

  <button @click="increasePadding" class="example" type="button">Increase padding</button>
</template>

<style>
.example {
  padding: v-bind("style.padding");
}
</style>
avatar
Jul 28th 2023

Priting a ref out to the template in an interpolation ({{}}) is a special case as it needs to be converted to a string by Vue, and that process unwraps it.

We are aware that this is a bit inconsistent, but that's rhe way it was designed for v3 and will stay until the next major at least.

avatar
Jul 28th 2023

@LinusBorg alright, makes sense, thanks for the quick response!