searchvue-componentvuejs3piniav-model

I'm having trouble getting v-model searchTerm to work with Vue and Pinia


So I have edited this. What I'm trying to do is search through an array of music tracks using v-model. Here is the code in the vue component.

<script setup>

import SongRow from '../components/SongRow.vue';
import Magnify from 'vue-material-design-icons/Magnify.vue';
import tracks from '../tracks.json'

import { useSongStore } from '../stores/song'
import { storeToRefs } from 'pinia';

const useSong = useSongStore()
const {isPlaying, currentTrack,} = storeToRefs(useSong)

const playFunc = () => {
    if (currentTrack.value) {
        useSong.playOrPauseThisSong(currentTrack.value)
        return
    }  
    useSong.playFromFirst()
}

  const searchTerm = store.state.searchTerm

  const filteredTracks = computed(() =>{
    const term = searchTerm.toLowercase()
    return store.state.tracks.filter(track => 
tracks.description.toLowercase().includes(term))
  })

  return {searchTerm, filteredTracks}

</script>

<template>
    <div
    id="TopNav2"
    class="fixed position-absolute top-0 right-0 flex items-center 
justify-between w-[calc(100%-240px)] h-[56px] border-b border-b- 
[#32323D]"
  >
    <div class="flex items-center w-full">
      <Magnify class="pl-6 mt-1 pr-2" fillColor="#7E7E88" 
:size="22"/>
      <input

        class="
          p-1 
          bg-transparent 
          outline-none 
          font-[300] 
          placeholder-[#BEBEC7] 
          text-[#FFFFFF] 
          w-full 
          max-w-xl
        "
        placeholder="Double click to search all tracks..."
        type="text"
        v-model="searchTerm"
      >
    </div>
    
  </div>

    <div class="border-b border-b-[#302d2d]"></div>
    <div class="mb-10"></div>

    <div id="SongsSection" class="w-[calc(100%-1px)]">
        
        <div class="mb-4"></div>

        <div class="flex items-center justify-between min-w-[590px] 
mx-8 border-b border-b-[#302d2d] py-2.5 px-1.5">
            <div class="text-xs font-light text- 
[#aeaeae]">TRACK</div>
        </div>

        <ul class="px-5 w-[calc(100%-22px)]" v-for="track in 
 filteredTracks.tracks" :key="track.id">
            <SongRow v-if="track" :track="track"/>
        </ul>
    </div>
    <div class="mb-40"></div>
</template>

<style scoped>
    .circle {
        width: 4px;
        height: 4px;
        background-color: rgb(189, 189, 189);
        border-radius: 100%;
    }
</style>

And here is the SongRow Component code:

   <template>
<li 
    @mouseenter="isHover = true" 
    @mouseleave="isHover = false" 
    class="display:inline-block p-2 ml-4 hover:bg-[#979797] hover:bg-opacity-5"
>
    <div class="display:inline-block">

            <div> 
                <img width="38" height="38" class="absolute p-0 mt-2 border border-[#494949]" :src="track.cover_art_path">
            </div>
        
            <div 
                v-if="isHover" 
                class="p-1 mt-[11.5px] ml-[3px] absolute rounded-full cursor-pointer text-white drop-shadow-[0_1.2px_1.2px_rgba(0,0,0,0.9)]"
            >
                <Play
                    
                    v-if="!isPlaying"
                    @click="useSong.playOrPauseThisSong(track)" 
                />
                <Play
                    v-else-if="isPlaying && currentTrack.name !== track.name"
                    @click="useSong.loadSong(track)"
                    
                   
                />
            </div>
            
            <div 
                @mouseenter="isHoverGif = true"
                @mouseleave="isHoverGif = false"
                v-if="isPlaying && track && currentTrack && currentTrack.name === track.name" 
                class="p-1 mt-[11.5px] ml-[3px] absolute rounded-full cursor-pointer text-white drop-shadow-[0_1.2px_1.2px_rgba(0,0,0,0.9)]"
            >
                <img
                    v-if="!isHoverGif"
                    src="/images/sound-wave.gif"
                >
                <Pause v-if="isHoverGif" :size="25" @click="useSong.playOrPauseSong()"/>
            </div>
            
        
        <div
            v-if="track"
            :class="track && currentTrack && currentTrack.name === track.name ? 'text-[#4ea1ff]' : 'text-[#d4d4d4]'"
            class="flex items-center text-[13px] ml-12 p-0 pt-1 font-bold"
        >
             {{ track.name }}
        </div>
        
    </div>
    <div class="flex items-center justify-between">

        <div 
            v-if="track"
            :class="track && currentTrack && currentTrack.description === track.description ? 'text-[#4ea1ff]' : 'text-[#d4d4d4]'"
            class="align-left text-[13px] ml-12 p-0 font-[200] text-[#d4d4d4]"
        >
             {{ track.description }}
        </div>
        <div class="flex">
        <div
            v-if="isTrackTime"
            :class="track && currentTrack && currentTrack.name === track.name ? 'text-[#EF5464]' : 'text-[#d4d4d4]'"
            class=" text-[13px] p-2 font-[200] text-[#d4d4d4]"
        >
            {{ isTrackTime }}
        </div> 
        
        
        <a :href=(track.license_path) target=”_blank” type="button" class=" rounded-full p-2 hover:bg-[#2b2b30]">
        <Basket fillColor="#EAEAEA" :size="20"/>
        </a>
        
        <div class=" rounded-full p-2 hover:bg-[#979797] hover:bg-opacity-20 cursor-pointer">
            <DotsHorizontal fillColor="#CCCCCC" :size="21"/>
        </div>
    </div>

        

    </div>
</li>
</template>

    <script setup>
 import { ref, toRefs, onMounted } from 'vue'
 import music from '../tracks.json'
 import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue';
 import Play from 'vue-material-design-icons/Play.vue';
 import Pause from 'vue-material-design-icons/Pause.vue';
 import Basket from 'vue-material-design-icons/basket.vue';
 import { useSongStore } from '../stores/song'
 import { storeToRefs } from 'pinia';
 const useSong = useSongStore()
 const { audio, isPlaying, currentTrack, } = storeToRefs(useSong)

 let isHover = ref(false)
 let isHoverGif = ref(false)
 let isTrackTime = ref('00:00')

 const props = defineProps({ track: Object, String })
 const { track } = toRefs(props)
 onMounted(() => {
     const audioMeta = new Audio(track.value.path);
     audioMeta.addEventListener('loadedmetadata', () => {
         const duration = audioMeta.duration;
          const minutes = Math.floor(duration / 60);
          const seconds = Math.floor(duration % 60);
          isTrackTime.value = minutes+':'+seconds.toString().padStart(2, '0')
        }); 
  })

 </script>

And here is what is in Pinia:

import { defineStore } from 'pinia'
import tracks from '../tracks'

export const useSongStore = defineStore('song', {
  state: () => ({
    isPlaying: false,
    audio: null,
    currentTrack: null,
    trackTime: null,
    currentVolume: 80,
    id:'',
    name:'',
    description:'',
    cover_art_path:'',
    license_path:'',
    keywords:'',
    genre:'',
    moods:'',
    tempo:'',
    theme:'',
  }),

  actions: {

    loadSong(track, id, description, cover_art_path, license_path, keywords, genre, moods, tempo, theme,) {
        this.currentTrack = track
        this.currentId = id
        this.currentDescription = description
        this.currentCover_Art_Path = cover_art_path
        this.currentLicense_Path = license_path
        this.currentKeywords = keywords
        this.currentGenre = genre
        this.currentMoods = moods
        this.currentTempo = tempo
        this.currentTheme = theme


        if (this.audio && this.audio.src) {
            this.audio.pause()
            this.isPlaying = false
            this.audio.src = ''
        }

        this.audio = new Audio()
        this.audio.src = track.path

        setTimeout(() => {
            this.isPlaying = true
            this.audio.play()
        }, 200)
    },

    playOrPauseSong() {
        if (this.audio.paused) {
            this.isPlaying = true
            this.audio.play()
        } else {
            this.isPlaying = false
            this.audio.pause()
        }
    },

    playOrPauseThisSong(track) {
        if (!this.audio || !this.audio.src || (this.currentTrack.id !== track.id)) {
            this.loadSong(track)
            return
        }

        this.playOrPauseSong()
    },

    prevSong(currentTrack) {
        let track = tracks.tracks[currentTrack.id - 2]
        this.loadSong(track)
    },

    nextSong(currentTrack) {
        if (currentTrack.id === tracks.tracks.length) {
            let track = tracks.tracks[0]
            this.loadSong(track)
        } else {
            let track = tracks.tracks[currentTrack.id]
            this.loadSong(track)
        }
    },

    playFromFirst() {
        this.resetState()
        let track = tracks.tracks[0]
        this.loadSong(track)
    },

    resetState() {
        this.isPlaying = false
        this.audio = null
        this.currentTrack = null
    }
  },
  persist: true
})

If I remove all the search filter code it works perfectly as a full array but as soon as I add the filter it breaks so I'm clearly doing it wrong.

I'm quite new to this and am just a musician trying to create my own website so something that might be obvious to you guys is not so obvious to me. Any help would be great and hugely appreciated.


Solution

  • Here are the issues that I noticed. It might not be comprehensive as it's a lot of code to parse through (I didn't really look much through the Pinia store or SongRow component, I don't think most of their code matters as much in relation to your issue):

    1. Usage of store:

    e.g. const searchTerm = store.state.searchTerm

    I'm not sure what store is in reference to. It's not being imported from anywhere and your Pinia store is already imported as useSongStore (and assigned to useSong). I might be missing some context regarding store, but regardless based on the usage of searchTerm it looks like it should work just fine as a normal ref variable

    const searchTerm = ref('');
    

    2. missing imports?

    Unless you have auto importing that you didn't mention you'll need to import computed (and the ref above) from vue:

    <script setup>
    import { ref, computed } from 'vue';
    ...
    

    3. return statement inside script setup

    return {searchTerm, filteredTracks}

    This line can be removed. It looks like you've mixed up the syntax of Composition API using setup() function which does use a return statement, and the syntax of Composition API using script setup which does not use a return statement.

    4. The filteredTracks computed function

    With my advice that searchTerm be made a ref, some code here will need to be changed. Also, another reference to store here when I think you mean to use the useSong store object... However I noticed tracks is not a piece of state on your Pinia store (again, I'm probably missing some context on what store is). I actually think instead of importing tracks.json into your store and your component that tracks.json should be added to your store's state.

    stores/song.js

    import tracks from './tracks';
    export const useSongStore = defineStore('song', {
      state: () => ({
      tracks: tracks,
      ...
    

    Now the computed function can search tracks just using the store and you no longer need to separately import ../tracks.json

    const filteredTracks = computed(() => {
      // added if statement to return all songs while searchTerm is empty string
      if (searchTerm.value === '') return useSong.tracks;
      // additional fix for typo "toLowercase" should be "toLowerCase" (uppercase C)
      const term = searchTerm.value.toLowerCase();
      return useSong.tracks.filter(track =>
        track.description.toLowerCase().includes(term)
      );
    });
    

    5. v-for looping on filteredTracks.tracks

    This is an error because filteredTracks is itself the array of tracks already, filtered from the useSong.tracks array, so the .tracks part here can be dropped.

    <ul class="px-5 w-[calc(100%-22px)]" v-for="track in filteredTracks" :key="track.id">
        <SongRow v-if="track" :track="track" />
    </ul>