根据 props 更新 Vue3 组件中 div 中的 CSS 类不起作用(缺少反应性?)
我在 Github 上发现了一个好看的 vue 底部导航栏(https://github.com/imanmalekian31 /vue-bottom-navigation)。
遗憾的是它不适用于 Vue3、Typescript 和脚本设置语法。
我重写了该组件并让它运行并显示在我的应用程序中。
问题是,按钮目前没有反应。当 click 上的方法运行时,元素上的 css 类不会更新。
我最好的猜测是,localOptions 和按钮需要
<div v-for="(button, index) in localOptions" :key="`grow-button-${index}`"></div>
具有反应性,但由于脚本设置的更改,它们可能不会发生反应。
任何有关如何修复此代码的帮助将不胜感激。
更新: 我确实用 let localOptions = Reactor(props.options.slice())
替换了 let localOptions: any[] = Reactor([])
,这似乎已经完成了诡计。按钮工作和更新都很好。
现在出现的问题是,计算出的Property似乎并没有在每次组件重新渲染时运行。因此按钮不会改变宽度或颜色。
组件文件:
<template>
<div class="gr-btn-container-foreground" :style="cssVariables">
<div v-for="(button, index) in localOptions" :key="`grow-button-${index}`" :class="[
'gr-btn-container',
{ 'gr-btn-container-active': button.selected },
]" @click="handleButtonClick(button, index)">
<div :class="['gr-btn-item', { 'gr-btn-item-active': button.selected }]">
<div :class="['gr-btn-icon', { 'gr-btn-icon-active': button.selected }]">
<slot name="icon" :props="button">
<i :class="`${button.icon}`" />
</slot>
</div>
<div class="gr-btn-title">
<span class="gr-hidden-title">
<slot name="title" :props="button">{{ button.title }}</slot>
</span>
<span :class="[
'gr-animated-title',
{ 'gr-animated-title-active': button.selected },
]">
<slot name="title" :props="button">{{ button.title }}</slot>
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive } from "vue";
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const button = reactive({})
const model = {
prop: "value",
event: "update",
}
const props = defineProps({
value: {
default: null,
},
options: {
type: Array,
default: () => [],
},
color: {
type: String,
default: "#74cbbb",
},
replaceRoute: {
type: Boolean,
default: false,
},
})
const emit = defineEmits<{
(e: 'update', value: string): void
}>()
let prevSelected = null
let currSelected = null
let localOptions: any[] = reactive([])
const cssVariables = computed(() => {
const activeTitle = (localOptions[currSelected] || {}).title;
let activeWidth = 95;
if (activeTitle && activeTitle.length * 15 > 110) {
activeWidth = 95 + (activeTitle.length * 15 - 110) / 2;
}
const mainColor =
(localOptions[currSelected] || {}).color || props.color;
const styles = {
"--color": mainColor,
"--color-background": mainColor + "30",
"--active-width": `${activeWidth}px`,
};
return styles;
})
localOptions = props.options.slice();
let index = localOptions.findIndex(
(item) =>
item.id == props.value ||
(item.path || {}).name == (route || {}).name
);
if (index > -1) {
currSelected = index;
prevSelected = index;
localOptions[index] = { ...localOptions[index], selected: true }
}
function handleButtonClick(button: any, index: number) {
console.log("Button: " + button + " Index: " + index);
currSelected = index;
if (prevSelected !== null) {
localOptions[prevSelected].selected = false;
}
localOptions[index] = { ...localOptions[index], selected: true }
prevSelected = currSelected;
updateValue(button);
}
function updateValue(button) {
console.log("Update value: " + button.id + " with Path: " + button.path)
emit("update", button.id);
if (button.path && Object.keys(button.path).length) {
router[!props.replaceRoute ? "push" : "replace"](
button.path
).catch(() => { console.log("Error updating path") });
}
}
</script>
<style scoped>
.gr-btn-container-foreground {
position: fixed;
direction: ltr;
display: flex;
align-items: center;
bottom: 0;
width: 100%;
z-index: 2147483647;
height: 64px;
background: #fff;
box-shadow: 0 0 5px 0 #eee;
}
.gr-btn-container {
display: flex;
align-items: center;
justify-content: center;
height: 45px;
flex-grow: 1;
transition: all 0.3s;
}
@media (min-width: 576px) {
.gr-btn-container {
cursor: pointer;
}
}
.gr-btn-container-active {
background-color: var(--color-background);
border-radius: 100px;
}
.gr-btn-item {
display: flex;
position: relative;
overflow: hidden;
width: 24px;
transition: all 0.3s ease;
color: #0000008a;
}
.gr-btn-item-active {
width: var(--active-width);
}
.gr-btn-icon {
font-size: 20px;
transition: all 0.3s ease;
margin: 0px !important;
}
.gr-btn-icon-active {
color: var(--color);
}
.gr-btn-title {
position: relative;
color: var(--color);
padding: 0px 5px;
margin-left: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.gr-hidden-title {
visibility: hidden;
}
.gr-animated-title {
color: var(--color);
position: absolute;
left: 5px;
bottom: -15px;
transition: bottom 0.4s ease 0.1s;
}
.gr-animated-title-active {
bottom: 2px;
}
</style>
I found a good looking bottom navigation bar for vue on Github (https://github.com/imanmalekian31/vue-bottom-navigation).
Sadly it did not work with Vue3, Typescript and the script setup syntax.
I rewrote the component and got it to run and show up in my app.
The problem is, the buttons are not reactive at the moment. While the methods on click get run, the css classes on the elements do not get updated.
My best guess here is, that localOptions and button in
<div v-for="(button, index) in localOptions" :key="`grow-button-${index}`"></div>
need to be reactive, which they might not due to the change to script setup.
Any help on how to fix this code would be greatly appreciated.
Update:
I did replace let localOptions: any[] = reactive([])
with let localOptions = reactive(props.options.slice())
and that seems to have done the trick. The buttons are working and updating just fine.
The problem that appeared now is, that the computed Property doesn't seem to be run every time the component re-renders. Therefore the buttons are not changing width or color.
Component file:
<template>
<div class="gr-btn-container-foreground" :style="cssVariables">
<div v-for="(button, index) in localOptions" :key="`grow-button-${index}`" :class="[
'gr-btn-container',
{ 'gr-btn-container-active': button.selected },
]" @click="handleButtonClick(button, index)">
<div :class="['gr-btn-item', { 'gr-btn-item-active': button.selected }]">
<div :class="['gr-btn-icon', { 'gr-btn-icon-active': button.selected }]">
<slot name="icon" :props="button">
<i :class="`${button.icon}`" />
</slot>
</div>
<div class="gr-btn-title">
<span class="gr-hidden-title">
<slot name="title" :props="button">{{ button.title }}</slot>
</span>
<span :class="[
'gr-animated-title',
{ 'gr-animated-title-active': button.selected },
]">
<slot name="title" :props="button">{{ button.title }}</slot>
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive } from "vue";
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const button = reactive({})
const model = {
prop: "value",
event: "update",
}
const props = defineProps({
value: {
default: null,
},
options: {
type: Array,
default: () => [],
},
color: {
type: String,
default: "#74cbbb",
},
replaceRoute: {
type: Boolean,
default: false,
},
})
const emit = defineEmits<{
(e: 'update', value: string): void
}>()
let prevSelected = null
let currSelected = null
let localOptions: any[] = reactive([])
const cssVariables = computed(() => {
const activeTitle = (localOptions[currSelected] || {}).title;
let activeWidth = 95;
if (activeTitle && activeTitle.length * 15 > 110) {
activeWidth = 95 + (activeTitle.length * 15 - 110) / 2;
}
const mainColor =
(localOptions[currSelected] || {}).color || props.color;
const styles = {
"--color": mainColor,
"--color-background": mainColor + "30",
"--active-width": `${activeWidth}px`,
};
return styles;
})
localOptions = props.options.slice();
let index = localOptions.findIndex(
(item) =>
item.id == props.value ||
(item.path || {}).name == (route || {}).name
);
if (index > -1) {
currSelected = index;
prevSelected = index;
localOptions[index] = { ...localOptions[index], selected: true }
}
function handleButtonClick(button: any, index: number) {
console.log("Button: " + button + " Index: " + index);
currSelected = index;
if (prevSelected !== null) {
localOptions[prevSelected].selected = false;
}
localOptions[index] = { ...localOptions[index], selected: true }
prevSelected = currSelected;
updateValue(button);
}
function updateValue(button) {
console.log("Update value: " + button.id + " with Path: " + button.path)
emit("update", button.id);
if (button.path && Object.keys(button.path).length) {
router[!props.replaceRoute ? "push" : "replace"](
button.path
).catch(() => { console.log("Error updating path") });
}
}
</script>
<style scoped>
.gr-btn-container-foreground {
position: fixed;
direction: ltr;
display: flex;
align-items: center;
bottom: 0;
width: 100%;
z-index: 2147483647;
height: 64px;
background: #fff;
box-shadow: 0 0 5px 0 #eee;
}
.gr-btn-container {
display: flex;
align-items: center;
justify-content: center;
height: 45px;
flex-grow: 1;
transition: all 0.3s;
}
@media (min-width: 576px) {
.gr-btn-container {
cursor: pointer;
}
}
.gr-btn-container-active {
background-color: var(--color-background);
border-radius: 100px;
}
.gr-btn-item {
display: flex;
position: relative;
overflow: hidden;
width: 24px;
transition: all 0.3s ease;
color: #0000008a;
}
.gr-btn-item-active {
width: var(--active-width);
}
.gr-btn-icon {
font-size: 20px;
transition: all 0.3s ease;
margin: 0px !important;
}
.gr-btn-icon-active {
color: var(--color);
}
.gr-btn-title {
position: relative;
color: var(--color);
padding: 0px 5px;
margin-left: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.gr-hidden-title {
visibility: hidden;
}
.gr-animated-title {
color: var(--color);
position: absolute;
left: 5px;
bottom: -15px;
transition: bottom 0.4s ease 0.1s;
}
.gr-animated-title-active {
bottom: 2px;
}
</style>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
currSelected
不是反应变量 (ref)。据我很快发现,这就是您的cssVariables
计算不重新计算的原因 - 它错过了currSelected
的反应依赖currSelected
is not a reactive variable (ref). As far as I quickly see this is the reason why yourcssVariables
computed does not recalculate - it misses reactive dependence ofcurrSelected