Updating CSS classes in on div in Vue3 component, depending on props, does not work (Missing reactivity?)

1k Views Asked by At

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>

1

There are 1 best solutions below

1
On

currSelected is not a reactive variable (ref). As far as I quickly see this is the reason why your cssVariables computed does not recalculate - it misses reactive dependence of currSelected