nuxt / ui

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
https://ui.nuxt.com
MIT License
4.06k stars 518 forks source link

How to create Steps or Wizard with UTabs #1928

Closed nosizejosh closed 4 months ago

nosizejosh commented 4 months ago

Description

I have tried to follow as suggested here but I am still unable to get next and previous buttons to work. Please can you check my code to se what I am missing?

I have current version of my code attempts below and also here

<template>
  <div>
    <UTabs :items="items" v-model="selected" ref="tabs" class="w-full">
      <template #default="{ item, index }">
        <div class="relative flex items-center gap-2 truncate">
          <span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
        </div>
      </template>
      <template #item="{ item, index }">
        <UCard>
          <template #header />

          <!-- step 1 -->
          <UFormGroup v-if="index === 0" size="xl" class="mx-auto max-w-xl">
            Tab 1 Content
          </UFormGroup>

          <!-- step 2 -->
          <UFormGroup v-if="index === 1" size="xl" class="mx-auto max-w-xl">
            Tab 2 Content
          </UFormGroup>

          <!-- step 3 -->
          <UFormGroup v-if="index === 2" size="xl" class="mx-auto max-w-xl">
            Tab 3 Content
          </UFormGroup>

          <template #footer>
            <div class="flex justify-center space-x-6">
              <UButton
                v-if="index > 0"
                @click="previousTab(index)"
                label="Previous"
              />
              <UButton v-if="index < 2" @click="nextTab(index)" label="Next" />
            </div>
          </template>
        </UCard>
      </template>
    </UTabs>
  </div>
</template>

<script setup>
const items = [
  {
    stage: 1,
    label: 'Tab1',
    description:
      "Make changes to your account here. Click save when you're done.",
  },
  {
    stage: 2,
    label: 'Tab2',
    description:
      "Change your password here. After saving, you'll be logged out.",
  },
  {
    stage: 3,
    label: 'Tab3',
    description:
      "Make changes to your account here. Click save when you're done.",
  },
];

const currentStep = ref(0);

function updateStep(index) {
  const stage = items[index].stage;
  router.replace({ query: { stage } });
}

function nextTab(index) {
  // const nextStage = items[index + 1].stage;
  // router.replace({ query: { stage: nextStage } });
  // selected.value = index + 1; // update selected value
  selected.value = selected.value + 1;
  if (selected.value >= items.length) {
    selected.value = 0;
  }
}

function previousTab(index) {
  // const previousStage = items[index - 1].stage;
  // router.replace({ query: { stage: previousStage } });
  // selected.value = index - 1; // update selected value
  selected.value = selected.value - 1;
  if (selected.value < 0) {
    selected.value = items.length - 1;
  }
}

function updateActiveTab() {
  const tabs = this.$refs.tabs;
  const activeTab = tabs.querySelector(`[data-index="${this.selected}"]`);
  tabs.querySelector('.active').classList.remove('active');
  activeTab.classList.add('active');
}

const route = useRoute();
const router = useRouter();

const selected = computed({
  get() {
    const index = items.findIndex((item) => item.stage === route.query.stage);
    if (index === -1) {
      return 0;
    }
    return index;
  },
  set(value) {
    router.replace({ query: { stage: items[value].stage } });
    // router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
  },
});

// watch(selected, (selected, previous) => {
//   console.log("🚀 ~ watch ~ selected:", selected)
//   // router.push({
//   //   path: '/test',
//   //   query: { streamer: twitchStreamer },
//   // })
// })

// watch(selected, () => {
//   const tabs = ref.tabs;
//   const activeTab = tabs.querySelector(`[data-index="${selected.value}"]`);
//   tabs.querySelector('.active').classList.remove('active');
//   activeTab.classList.add('active');
// });

// onUpdated(() => {
//   const tabs = ref.tabs;
//   const activeTab = tabs.querySelector(`[data-index="${selected.value}"]`);
//   tabs.querySelector('.active').classList.remove('active');
//   activeTab.classList.add('active');
// });

// watch(selected, () => {
//   nextTick(() => {
//     const tabs = ref.tabs;
//     const activeTab = tabs.querySelector(`[data-index="${selected.value}"]`);
//     tabs.querySelector('.active').classList.remove('active');
//     activeTab.classList.add('active');
//   });
// });
</script>

<style>
body {
  @apply antialiased font-sans text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-900;
}
</style>
noook commented 4 months ago

I am not sure what's happening, but if the reproduction is exactly like your project, you would need to use Nuxt pages instead of the app.vue file to be able to use the router.

noook commented 4 months ago

I found the culprit:

const selected = computed({
  get() {
    const index = items.findIndex((item) => item.stage === route.query.stage); // Here
    if (index === -1) {
      return 0;
    }
    return index;
  },
  set(value) {
    router.replace({ query: { stage: items[value].stage } });
    // router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
  },
});

On the line I highlighted, you are comparing a number (item.stage) to undefined | string | string[] (route.query.stage).

You should compare it like this:

const selected = computed({
  get() {
    const index = items.findIndex((item) => item.stage.toString() === route.query.stage?.toString());
    if (index === -1) {
      return 0;
    }
    return index;
  },
  set(value) {
    router.replace({ query: { stage: items[value].stage } });
    // router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
  },
});
nosizejosh commented 4 months ago

Whoa! it works Thank you @noook.

how did you catch that?

noook commented 4 months ago

Experience haha

Also, Typescript does a great job at spotting this, it warns you this condition can never happen because there is no overlap between the types number and undefined | string | string[]