javascripttypescriptvue.jsvuejs3flickity

Flickity API with Vue 3 and Typescript


I've been working with Flickity in my Vue 3 app, and it works fine when I use a hard-coded HTML carousel with static cells. I need to add cells programmatically at runtime, however, and I can't get the Flickity API to work correctly.

I'm trying to follow the append() example, but I get the error:

flickity.js?1385:72 Bad element for Flickity: .carousel

in my inspector during runtime. I tried to follow the solution here and here, but neither have been able to run successfully. It looks like it's due to TypeScript errors on the Flickity lib. I also installed @types/flickity, fyi.

What can I do to fix my append logic below?

<template>
        <div class="row">
          <div class="col d-block m-auto payment-option">
            <flickity ref="carousel" :options="flickityOptions">
            </flickity>
          </div>
      </div>
</template>

<script lang="ts">
import {defineComponent} from "vue";
//import Flickity from 'vue-flickity/src/flickity.vue';
import Flickity from 'flickity'
export default defineComponent({
  name: "PageName",
  components: {
    Flickity
  },
  data() {
    return {
      flickityOptions: {
        initialIndex: 3,
        prevNextButtons: false,
        pageDots: true,
        wrapAround: true
      }
    };
  },
  methods: {
    createBankAccountCarousel(flkty: Flickity) {
      flkty.append(this.makeFlickityCell())
    },
    makeFlickityCell() {
      const cell = document.createElement('div');
      cell.className = 'carousel-cell'
      cell.textContent = "Hi"
      return cell
    }
  },
  mounted() {
    let flkty = new Flickity(this.$refs.carousel)
    this.createBankAccountCarousel(flkty)
  }
});
</script>

Solution

  • It looks like you were possibly trying vue-flickity in Vue 3, but that component was built for Vue 2.

    You can create your own Flickity component in Vue 3:

    1. Create Flickity.vue with the following template and script, which applies a template ref on the root element, and contains a slot to receive .carousel-cell elements:

      <template>
        <!-- 👇 template ref -->
        <div ref="root" class="flickity">
          <slot />
        </div>
      </template>
      
      <script lang="ts">
      import { defineComponent, ref } from 'vue'
      
      export default defineComponent({
        setup() {
          const root = ref<HTMLElement | null>(null) // reference to template ref named "root"
          return {
            root,
          }
        }
      })
      </script>
      
    2. Declare an options prop, which we'll pass to the Flickity constructor later.

      <script lang="ts">
      import { defineComponent } from 'vue'
      
      export default defineComponent({
        props: {
          options: Object,
        }
      })
      </script>
      
    3. In the component's mounted hook, instantiate Flickity with the "root" template ref and the options prop; and in unmounted, destroy the Flickity instance:

      <script lang="ts">
      import { defineComponent, onMounted, onUnmounted } from 'vue'
      import Flickity from 'flickity'
      
      export default defineComponent({
        setup(props) {
          let flickity: typeof Flickity | null = null
          onMounted(() => flickity = new Flickity(root.value as HTMLElement, props.options))
          onUnmounted(() => flickity?.destroy())
        }
      })
      </script>
      
    4. Add a method named "append" to allow appending new elements to the carousel:

      <script lang="ts">
      import { defineComponent } from 'vue'
      import Flickity from 'flickity'
      
      export default defineComponent({
        setup() {
          let flickity: typeof Flickity | null = null
          return {
            append(element: HTMLElement) {
              flickity?.append(element)
              flickity?.select(-1)
            }
          }
        }
      })
      </script>
      
    5. Create src/flickity.d.ts with the following type declarations (if using VS Code, you'll have to restart the IDE for these typings to be indexed):

      declare module 'flickity' {
        const Flickity: {
          new (el: string | HTMLElement, options?: Record<string, unknown>): this
          append(element: HTMLElement)
          destroy()
          select(id: string | number)
        }
        export = Flickity
      }
      
    6. Add the following <style> block, which pulls in the flickity default styles, and styles the .carousel-cell elements that would be received in the slot:

      <style scoped>
      @import '~flickity/dist/flickity.css';
      
      .flickity .carousel {
        background: #EEE;
        margin-bottom: 40px;
      }
      /* use ::v-deep to get at slot elements */
      .flickity::v-deep .carousel-cell {
        height: 200px;
        width: 25%;
        margin: 0 10px;
        background: #6C6;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 8px;
      }
      </style>
      

    Example Usage:

    <template>
      <div class="app">
        <flickity ref="flickity" :options="flickityOptions">
          <div class="carousel-cell">1</div>
          <div class="carousel-cell">2</div>
          <div class="carousel-cell">3</div>
        </flickity>
        <div class="actions">
          <button @click="addElement">Append element</button>
        </div>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from 'vue'
    import Flickity from './components/Flickity.vue'
    
    export default defineComponent({
      name: 'App',
      components: {
        Flickity
      },
      data() {
        return {
          flickityOptions: {
            pageDots: true,
            wrapAround: true,
          }
        }
      },
      methods: {
        addElement() {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          (this.$refs.flickity as any).append(this.makeFlickityCell())
        },
        makeFlickityCell() {
          const cell = document.createElement('div')
          cell.className = 'carousel-cell'
          cell.textContent = 'Hi'
          return cell
        }
      }
    })
    </script>
    
    <style scoped>
    .app {
      display: flex;
      flex-direction: column;
      justify-content: space-around;
      height: 50vh;
    }
    </style>
    

    demo