Reputation: 527
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
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
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, ref
s 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
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