Ixio
Ixio

Reputation: 527

VueJS components ref is undefined at all stages

I'm learning VueJS using it with a rails app and I'm having issues trying to access a component from a parent component. It's possible I'm doing this wrong but I can't figure out what the problem is.

I've tried to boil down my problem to a simple html example:

<html>
  <head>
    <meta charset='utf-8' />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <outer>
        <div>
          <form>
            <inner ref="testref"><input/></inner>
          </form>
        </div>
      </outer>
    </div>
    <script>
      let inner = Vue.component('inner', {
        template: `
          <div>
            <span hidden="true"><slot></slot></span>
            <input v-model="searchText" />
          </div>
        `,
        props: ['placeholder'],
        data: function() {
          return {
            searchText: this.placeholder
          }
        }
      })
      let outer = Vue.component('outer', {
        template: `
          <div>
            <h3 v-on:click="testos">Hello: {{ client_id }}</h3>
            <slot></slot>
          </div>
        `,
        data: function() {
          console.log('data', this.$refs.testref)
          return {
            client_id: ''
          }
        },
        mounted: function() {
          console.log('mounted', this.$refs.testref)
        },
        methods: {
          testos: function() {
            console.log('method', this.$refs.testref)
          }
        }
      })
      new Vue({
        el: '#app',
        components: {
          'inner': inner,
          'outer': outer
        }
      })
    </script>
  </body>
</html>

I get the following console logs:

data undefined
mounted undefined
method undefined

I've tried to follow the syntax in https://v2.vuejs.org/v2/api/#ref, but I've also tried using :ref instead of ref and it doesn't work any better.

I get that there is "ref registration timing" complication even if I don't fully understand it. However shouldn't the timing be good when clicking on the H3 ? I was hoping it'd be even resolved by mounted like in https://stackoverflow.com/a/40884455/2730032.

Bonus question : My question is really about how ref works. However I do have a more general problem that might benefit with advice not using ref.

I have an inner component I'm pleased with that wraps an input field for a client_id with a search field that gives some select-like options from an API and sets the input field client_id to the selected option. I want to re-use that inner component in an outer component that would use the client_id (whenever it is changed by inner) to call an API and fill a bunch of other form fields in the outer component (those fields being given with inner to outer through slots). If that makes any sense.

I figured the best way to do that is put the client_id as a data field of inner and have outer access it through ref. So here I am trying to get ref to work.

EDIT1: There was a copy mistake, sorry about that. The test1 tag was from a previous version. But I get the problem regardless.

EDIT2: I accepted Roy J second answer because I feel it best answers the question and even though it is not good design it's probably a valid solution for some people. However in my actual implementation I used Roy J first answer and anyone reading this question probably should too (I also managed to add inner to outer's template in order to avoid using app).

Upvotes: 0

Views: 7301

Answers (3)

Ohgodwhy
Ohgodwhy

Reputation: 50767

Expounding upon Roy's comment, use the sync modifier if you need 2-way data binding of a property. In your case, you do. The child needs to be able to update the property and let the parent know of the change. There's actually magic under the hood where the value is a callback function which returns the value of the child and the parent updates the variable its self.

As far as why your ref isn't working - the child component hadn't been created even though the parent had been mounted. This is due to how the ._render() method works in VueJS. So if you need to utilize a ref on a custom component that is a child, you need to catch the change in the next tick:

mounted: function() {
  this.$nextTick(() => {
      console.log(this.$refs.testref)
  })
}

Finally, and maybe just a bad copy job, you have a syntax error in your code:

<inner ref="testref"><inner/></test1>

That closing tag </test1> matches nothing and will throw a parser error.

All the above should solve your problem.

Upvotes: 0

Roy J
Roy J

Reputation: 43881

Your ref is defined as part of your root-level template. You are trying to access it from outer, but it is not defined there, so it never shows up.

In general, refs are an exceptional situation. You should treat them as a last resort, when you really need to know something about the state of the DOM, not when you need to share data.

The proper way to share data is to have it owned by a common ancestor of the components that need to use it. That may be a store like Vuex, or the root instance, for example.

I have written up how your example code might work if the root instance owned client_id and shared it down with outer and inner. In passing to inner, I include the .sync modifier for clean handling of updates.

I define searchText as a settable computed that emits the update event that triggers sync, so that you can still v-model="searchText".

const inner = Vue.component('inner', {
  template: `
          <div>
            <span hidden="true"><slot></slot></span>
            <input v-model="searchText" />
          </div>
        `,
  props: ['placeholder'],
  computed: {
    searchText: {
      get() { return this.placeholder; },
      set(newValue) { this.$emit('update:placeholder', newValue); }
    }
  }
});
const outer = Vue.component('outer', {
  template: `
          <div>
            <h3 v-on:click="testos">Hello: {{ client_id }}</h3>
            <slot></slot>
          </div>
        `,
  props: ['client_id'],
  methods: {
    testos() {
      console.log("Yes, client_id is", this.client_id);
    }
  }
})
new Vue({
  el: '#app',
  data: {
    client_id: 'Initial Client ID'
  },
  components: {
    'inner': inner,
    'outer': outer
  }
})
<script src="https://unpkg.com/vue@latest/dist/vue.js"></script>
<div id="app">
  <outer :client_id="client_id">
    <div>
      <form>
        <inner :placeholder.sync="client_id"></inner>
      </form>
    </div>
  </outer>
</div>

Upvotes: 1

Roy J
Roy J

Reputation: 43881

Since the only reason you weren't able to use your ref was that it was defined in the parent, you can go up to $parent and use its refs. Note that this makes outer depend on (entangled with) the structure of inner and the presence of a ref to get to it. It is bad design, but it is the smallest change to the code you have to get it to do what you're trying to do.

const inner = Vue.component('inner', {
  template: `
          <div>
            <span hidden="true"><slot></slot></span>
            <input v-model="searchText" />
          </div>
        `,
  props: ['placeholder'],
  data: function() {
    return {
      searchText: this.placeholder
    }
  }
});
const outer = Vue.component('outer', {
  template: `
          <div>
            <h3 v-on:click="testos">Hello: {{ client_id }}</h3>
            <slot></slot>
          </div>
        `,
  data: function() {
    return {
      client_id: ''
    }
  },
  methods: {
    testos: function() {
      this.client_id = this.$parent.$refs.testref.searchText;    
      console.log('method', this.client_id);
    }
  }

})
new Vue({
  el: '#app',
  components: {
    'inner': inner,
    'outer': outer
  }
})
<script src="https://unpkg.com/vue@latest/dist/vue.js"></script>
<div id="app">
  <outer>
    <div>
      <form>
        <inner ref="testref"></inner>
      </form>
    </div>
  </outer>
</div>

Upvotes: 1

Related Questions