2

I have been working on an SPA with Vue 3, TypeScript and The Movie Database (TMDB).

I am currently working on a search form.

In src\components\TopBar.vue I have:

<template>
  <!-- More code -->
  <form ref="searchForm" class="search_form w-100 mx-auto mt-2 mt-md-0">
    <div class="input-group">
      <input v-on:keyup="debounceMovieSearch" v-model="searchTerm" class="form-control search-box" type="text" placeholder="Search movies...">
      <div class="input-group-append">
        <button class="btn" type="button">
          <font-awesome-icon :icon="['fas', 'search']" />
        </button>
      </div>
    </div>

    <div v-if="isSearch" @click="isSearch = false" class="search-results shadow-sm">
      <div v-if="this.movies.length">
        <router-link v-for="movie in movies.slice(0, 10)" :key="movie.id" :to="`/movie/${movie.id}`">
          <SearchItem :movie="movie" />
        </router-link>
      </div>

      <div v-else>
        <p class="m-0 p-2 text-center">No movies found for this search</p>
      </div>
    </div>
  </form>
</template>

<script lang="ts">
  import { defineComponent, ref } from 'vue';
  import axios from 'axios';
  import env from '../env';
  import SearchItem from './SearchItem.vue';

  export default defineComponent({
        name: 'TopBar',

    components: {SearchItem},

    data() {
      return {
        searchForm: ref(null),
        isSearch: false,
        searchTerm: '',
        timeOutInterval: 1000,
        movies: []
      }
    },

    mounted() {
      this.windowEvents();
    },

    methods: {
      windowEvents() {
        window.addEventListener('click', (event) => {
          if (!(this.$refs.searchForm as HTMLElement).value.contains(event.target)){
            console.log('click outside');
          }
        });
      },
      
      debounceMovieSearch() {
        setTimeout(this.doMovieSearch, this.timeOutInterval)
      },

      doMovieSearch() {
        if (this.searchTerm.length > 2) {
            this.isSearch = true;
            axios.get(`${env.api_url}/search/movie?api_key=${env.api_key}&query=${this.searchTerm}`).then(response => {
            this.movies = response.data.results;
          })
          .catch(err => console.log(err));
        }
      },
    }
  });
</script>

enter image description here

The goal is to hide the search results lists if the user clicks outside the form. To achiwve this, I have:

windowEvents() {
 window.addEventListener('click', (event) => {
  if (!(this.$refs.searchForm as HTMLElement).value.contains(event.target)){
    console.log('click outside');
  }
 });
}

The problem

The method above throes the error:

Property 'value' does not exist on type 'HTMLElement'.

Questions

  1. What am I doing wrong?
  2. What is the most reliable way to fix this issue?
Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252

1 Answers1

2

You are mixing up two ways to declare template refs.

In composition API, you declare a template ref by declaring a ref and setting the variable name to a ref attribute on a tag. Then Vue will link the node to the variable, and you can access it through the .value property of the ref.

In options API, you just set the ref attribute on the tag and use this.$refs to access the node. this.$refs is a Proxy that gives you the nodes directly (there is no .value property).

You are using options API, so the line in should be:

 if (!(this.$refs.searchForm as HTMLFormElement).contains(event.target as Node|null)) { // no ".value"

You can also remove the searchForm ref from data.

Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252
Moritz Ringler
  • 9,772
  • 9
  • 21
  • 34