Search code examples
cssvue.jsclickdropdown

Vue component - detect outside click


So, I've implemented this custom dropdown list in Vue JS (2.x) and pretty much have what I need, except once the list is open, I would like for a click outside the component (anywhere on the parent page or component) to close the list. I tried catching the blur event of the root div of my component, but it understandably didn't work, because divs don't get focus and hence cannot be blurred. So at this moment, the solution seems to be - to be able to listen for a click event outside the component. Is that possible? Can a child listen for events on its parent in Vue? If or even if not, what is the best and/or the easiest way to achieve this behavior?

Here's my code in a CodeSandbox, and I am also reproducing it below for convenience - https://codesandbox.io/s/romantic-night-ot7i8

Dropdown.vue

<template>
  <div class="main-container" @blur="listOpen = false">
    <button class="sel-btn" @click="listOpen = !listOpen">
      {{ getText() }}
    </button>
    <br />
    <div class="list-items" v-show="listOpen">
      <button
        class="item"
        v-for="(l, i) in list"
        :key="i"
        @click="btnClicked(i)"
      >
        {{ l }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    defaultText: {
      type: String,
      required: false,
    },
    list: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
      selectedIndex: -1,
      listOpen: false,
    };
  },

  methods: {
    getText() {
      if (this.selectedIndex !== -1) return this.list[this.selectedIndex];
      if (this.defaultText) return this.defaultText;
      return "Please Select";
    },

    btnClicked(index) {
      this.selectedIndex = index;
      this.listOpen = false;
      this.$emit("input", this.list[index]);
    },
  },
};
</script>

<style scoped>
.main-container {
  position: relative;
  display: inline-block;
  min-width: 125px;
  max-width: 125px;
  text-align: left;
  border: 0;
  background: #0000;
}

.sel-btn {
  display: inline-block;
  width: 100%;
  text-align: left;
  border: 0;
  background: linear-gradient(0deg, #efefef, #fff);
  padding: 8px;
  border: 1px solid #1111;
  box-shadow: 1px 1px 1px #1115;
  cursor: pointer;
  overflow: hidden;
}

.sel-btn:hover {
  background: linear-gradient(0deg, #cef, #fff);
  box-shadow: 1px 1px 1px #6390bd;
  color: #58a;
}

.sel-btn::after {
  content: "\2bc6";
  position: absolute;
  right: 4%;
}

.list-items {
  position: absolute;
  width: 100%;
  background: #fff;
  box-shadow: 1px 1px 3px #3333;
}

.item {
  display: block;
  width: 100%;
  text-align: left;
  background: #fff;
  border: 0;
  cursor: pointer;
  padding: 8px;
  border-bottom: 1px solid #2221;
  font-size: 0.8rem;
}

.item:hover {
  background: #4b84c5;
  color: #fff;
}
</style>

App.vue

<template>
  <div id="app">
    <br />
    <div style="margin-top: 30px; margin-bottom: 10px">
      <Dropdown
        :list="regions"
        v-model="selectedRegion"
        style="margin-right: 10px"
        defaultText="--Select Region--"
      ></Dropdown>
      <Dropdown
        :list="cities"
        v-model="selectedCity"
        defaultText="--Select City--"
      ></Dropdown>
    </div>
    <div style="padding: 30px; background: #27e2">
      (This poem excerpt is here to act as a filler)
      <h2>The Daffodils</h2>
      <h4><i>William Wordsworth</i></h4>
      I wondered lonely<br />
      As a cloud<br />
      That floats on high<br />
      O'er vales and hills<br />
      When all at once<br />
      I saw a crowd<br />
      A host<br />
      Of golden daffodils.<br />
    </div>
    <div style="padding: 30px">
      <b>Region Selected:</b>&nbsp; {{ selectedRegion }}<br />
      <b>City Selected:</b>&nbsp; {{ selectedCity }}
    </div>
  </div>
</template>

<script>
import Dropdown from "./components/Dropdown";

export default {
  name: "App",
  components: {
    Dropdown,
  },

  data() {
    return {
      set: false,
      regions: ["California", "Nova Scotia", "Kerala", "Bavaria", "Queensland"],
      cities: [
        "Munich",
        "San Diego",
        "Paris",
        "Prague",
        "Copenhagen",
        "Kolkata",
        "Dhaka",
        "Colombo",
        "Little City",
      ],
      selectedRegion: "",
      selectedCity: "",
    };
  },
};
</script>

<style>
* {
  font-family: "Segoe UI", Helvetica, Arial, sans-serif;
}

h1,
h2,
h3,
h4,
b {
  font-weight: 400;
  color: #27c;
}
</style>


Solution

  • ok... this is by far not the best solution but it is a working solution. i used all my MacGyver powers and found a way.

    please check this CodeSandbox

    all i did was to use your listOpen and added a eventListner. i figured out that your custom dropdown has no build in @blur because its not a input ofc. so i added a event for it inside the mounted hook.

    the key is i also added a setTimeout on 100ms because otherwise you where not able to select any item inside your dropdown, the dropdown would close with blur faster then you are able to select anything.