With CSS v-bind, .value is not automatically used
Vue version
3.3.4
Link to minimal reproduction
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.
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
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>
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.