2

I'm currently building an app using the Vue framework and came across a strange issue that I was unable to find a great solution for so far:

What I'm trying to do is add a class to a parent container in case a specific element inside the container (input, select, textarea etc.) gets focus. Here's the example code:

  <div class="form-group placeholder-label">
    <label for="desc"><span>Description</span></label>
    <div class="input">
      <input id="desc" type="text" />
    </div>
  </div>

In Vanilla JS of course, this is easily done:

const parent = document.querySelector('.placeholder-label');
const input = parent.querySelector('input');
input.addEventListener('focus', (e) => {
  parent.classList.add('active');
});

In the same way, you could loop through all .placeholder-label elements and add the event to their child inputs/selects etc. to add this basic functionality. There are two moving parts here:

  1. You don't know the type of the parent element, just that it has .placeholder-label on it.
  2. You don't know the type of the child element, just that it is some sort of HTML form element inside the parent element.

Can I build a Vue component that toggles a class on a given parent element based on focus/blur of a given child element? The best I could come up with is use slots for the child elements, but then I still need to build a component for each parent. Even when using mixins for the reused parts it's still quite a mess compared to the five lines of code I need to write in pure JS.

My template:

<template>
  <div
    class="form-group"
    :class="{ 'active': active }"
  >
    <label :for="inputID"><span>{{ inputLabel }}</span></label>
    <slot
      name="input"
      :focusFunc="makeActive"
      :blurFunc="makeInactive"
      :inputLabel="inputLabel"
      :inputID="inputID"
    />
  </div>
</template>

<script>
export default {
  name: 'TestInput',
  props: {
    inputLabel: {
      type: String,
      default: '',
    },
    inputID: {
      type: String,
      required: true,
    },
  },
  // data could be put into a mixin
  data() {
    return {
      active: false,
    };
  },
  // methods could be put into a mixin
  methods: {
    makeActive() {
      this.active = true;
    },
    makeInactive() {
      this.active = false;
    },
  },
};
</script>

Usage:

<test-input
  :input-i-d="'input-2'"
  :input-label="'Description'"
>
  <template v-slot:input="scopeVars">
    <!-- this is a bootstrap vue input component -->
    <b-form-input
      :id="scopeVars.inputID"
      :state="false"
      :placeholder="scopeVars.inputLabel"
      @blur="scopeVars.blurFunc"
      @focus="scopeVars.focusFunc"
    />
  </template>
</test-input>

I guess I'm simply missing something or is this a problem that Vue just can't solve elegantly?

Edit: In case you're looking for an approach to bubble events, here you go. I don't think this works with slots however, which is necessary to solve my issue with components.

5
  • Have the child component emit an event and parent catch that event? Commented Aug 29, 2019 at 14:38
  • Possible duplicate of Vuejs - bubbling custom events Commented Aug 29, 2019 at 15:20
  • Don't know if could be helpful but there is a css pseudo-selector that selects an element if that element contains any children that have :focus. :focus-whitin Commented Aug 29, 2019 at 15:42
  • @bernie Thanks! The solution seems to work if you know what components you'll be using beforehand. Working with slots and bubbling events seems to be a lot trickier. Commented Sep 6, 2019 at 8:27
  • @DavideCastellini Thanks. That seems to work fine on newer browsers and there's a polyfill available: npmjs.com/package/focus-within-polyfill Commented Sep 6, 2019 at 8:33

2 Answers 2

1

For those wondering here are two solutions. Seems like I did overthink the issue a bit with slots and everything. Initially I felt like building a component for a given element that receives a class based on a given child element's focus was a bit too much. Turns out it indeed is and you can easily solve this within the template or css.

  1. CSS: Thanks to @Davide Castellini for bringing up the :focus-within pseudo-selector. I haven't heard of that one before. It works on newer browsers and has a polyfill available.
  2. TEMPLATE I wrote a small custom directive that can be applied to the child element and handles everything.

Usage:

v-toggle-parent-class="{ selector: '.placeholder-label', className: 'active' }"

Directive:

directives: {
  toggleParentClass: {
    inserted(el, { value }) {
      const parent = el.closest(value.selector);
      if (parent !== null) {
        el.addEventListener('focus', () => {
          parent.classList.add(value.className);
        });
        el.addEventListener('blur', () => {
          parent.classList.remove(value.className);
        });
      }
    },
  },
},
Sign up to request clarification or add additional context in comments.

Comments

0

try using $emit

child:

<input v-on:keyup="emitToParent" />

-------

    methods: {
      emitToParent (event) {
        this.$emit('childToParent', this.childMessage)
      }
    }

Parent:

<child v-on:childToParent="onChildClick">

--------

methods: {
    // Triggered when `childToParent` event is emitted by the child.
    onChildClick (value) {
      this.fromChild = value
    }
  }

use this pattern to set a property that you use to change the class hope this helps. let me know if I misunderstood or need to better explain!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.