Can the built-in "Component" component be used in a render function? - vue.js

Is it possible to use the <component> component in a render function? I am getting the following error trying to use it:
Unknown custom element: <component> - did you register the component
correctly? For recursive components, make sure to provide the "name"
option.
Both <component> and <transition> are listed as Built-In Components and I have successfully used <transition> within a render function, but I am getting the error above when using <component>.
I wanted to use the <component> component since it allows you to work with the <transition> element so painlessly.
This works:
const app = new Vue({
el: "#app",
data() {
return {
cmp: "foo"
}
},
created() {
setInterval(() => this.cmp = (this.cmp === "foo" ? "bar" : "foo"), 2000);
},
components: {
foo: {
template: "<div>foo</div>"
},
bar: {
template: "<div>bar</div>"
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component :is="cmp"></component>
</div>
This does not:
const app = new Vue({
el: "#app",
data() {
return {
cmp: "foo"
}
},
created() {
setInterval(() => this.cmp = (this.cmp === "foo" ? "bar" : "foo"), 2000);
},
components: {
foo: {
template: "<div>foo</div>"
},
bar: {
template: "<div>bar</div>"
}
},
render(h) {
return h("div", [
h("component", {
props: {
is: this.cmp
}
})
]);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Actually, the ease of <transition> use is not predicated on the use of <component>. If you want to set up a transition between components in a render function, you just need to change the element that you return under the transition. See the example below.
I have a feeling that I was overthinking what the <component> actually is. The Vue API likely considers everything defined through Vue.component() or Vue({components: {}) "components". Considering that, there is no reason to outright use <component> within a render function.
const app = new Vue({
el: "#app",
data() {
return {
cmp: "foo"
}
},
created() {
setInterval(() => this.cmp = (this.cmp === "foo" ? "bar" : "foo"), 2000);
},
components: {
foo: {
template: "<div>foo</div>"
},
bar: {
template: "<div>bar</div>"
}
},
render(h) {
return h("div", [
h("transition", {
props: {
appear: true,
name: "fade",
mode: "out-in"
}
}, [h(this.cmp)])
]);
}
});
.fade-enter-active,
.fade-leave-active {
transition: opacity 500ms ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Related

How to pass dynamic props to vue's render function?

I try to render components dynamically based on descriptions.
From
{component: 'customComponent', props: {value: "val1"}, ...}
I'd render
<custom-component :value="val1" #input="v=>val1=v" />`
I aim to do this for arbitrary events and dynamic props.
I have no idea though how to pass dynamic props to render.
Partial solution:
A solution that works but re-renders everytime val1 changes, based on (https://symfonycasts.com/screencast/vue/vue-instance) is
render: function(h){
const template = "...building up the html here..."
return Vue.compile(template).render.call(this, h);
}
My attempt using the VueJS docs
I could not find in the docs on render how I could pass dynamic variables.
In the minimal implementation you can see how far I've got, if you can help me finish it, it would be awesome!
Minimal implementation so far
I expect to see 'hello' instead of 'values.value1' and values.value1 should update once I change the text in the text box.
demo.html:
<!DOCTYPE html>
<html>
<body>
<div id="q-app">
The text input should say 'hello' instead of 'values.value1'
<custom-component :descriptor="mainComponent"></custom-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#^2.0.0/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quasar#1.15.15/dist/quasar.umd.min.js"></script>
<script>
Vue.component('custom-component', {
props: ['descriptor'],
render: function (createElement) {
const attributes = {
on: this.descriptor.events,
props: this.descriptor.props
}
return createElement(
this.descriptor.component,
attributes)
}
})
const app = new Vue({
el: '#q-app',
data: function(){
return {
mainComponent: {
component: 'q-input',
props: {
value: 'values.value1'
},
events: {
input: value => this.values.value1 = value
}
},
values: {
value1: 'hello'
}
}
}
})
</script>
</body>
I guess, I have fixed your example.
Of course, you can watch the value in the main app. But it is better to sent an input event with the value.
Added the reference to the component
ref="mc"
and the input event binding
#input="logEventValue"
Vue.component('custom-component', {
props: ['descriptor'],
render: function (createElement) {
const attributes = {
on: this.descriptor.events,
props: this.descriptor.props
}
return createElement(
this.descriptor.component,
attributes)
}
})
const app = new Vue({
el: '#q-app',
data: function(){
return {
mainComponent: {
component: 'q-input',
props: {
value: 'hello'
},
events: {
input: value => {
this.values.value1 = value;
// with ref="mc"
this.$refs.mc.$emit('input', value);
// or over the list of children
this.$children[0].$emit('input', value);
}
}
},
values: {
value1: 'hello'
}
}
},
watch: {
'values.value1': (newVal) => console.log(`1. watcher: ${newVal}`),
// or deeply
values: {
handler(newVal) {
console.log(`2. watcher: ${newVal.value1}`)
},
deep: true,
}
},
methods: {
logEventValue(value) {
console.log(`logEventValue: ${value}`);
}
}
})
<div id="q-app">
<custom-component ref="mc" #input="logEventValue" :descriptor="mainComponent"></custom-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#^2.0.0/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quasar#1.15.15/dist/quasar.umd.min.js"></script>

I need fresh eyes to fix VUE props not working

This is a silly task in VUE.JS... but I'm missing it.
I have a sub parent component having:
<teamPlayers :teamId="team.id_team"></teamPlayers>
The value teamId is sent to the child component and it works: I can see the value in child template <h2>{{teamId}}</h2> properly.
But in same child component I got undefined inside the methods using this.teamId.
Here the whole child code:
export default {
props: ['teamId'],
methods: {
getJokess: function () {
console.log(this.teamId);
},
},
created() {
this.getJokess();
}
}
The console should return the correct value but it returns undefined instead of the {{teamId}} is render perfectly.
All that I can think of is that teams may not be declared in your data() function. If it isn't it won't be reactive. Consider the example below:
const teamPlayers = {
props: ["teamId"],
methods: {
getJokess() {
console.log(this.teamId);
}
},
created() {
this.getJokess();
},
template: "<h2>{{teamId}}</h2>"
};
const app = new Vue({
el: "#app",
components: {
"team-players": teamPlayers
},
data() {
return {
teams: []
};
},
mounted() {
setTimeout(() => {
this.teams = [{
id_team: "fizz"
},
{
id_team: "buzz"
},
{
id_team: "foo"
},
{
id_team: "bar"
}
]
}, 1000);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="team of teams">
<team-players :team-id="team"></team-players>
</div>
</div>

listen to events from dynamic vue components

How would you listen to an event emitted by a dynamically created component instance?
In the example, the top component is added in the DOM, while the second is dynamically created in javascript.
Vue.component("button-counter", {
data: function() {
return {
count: this.initial_count
}
},
props: ['initial_count'],
methods: {
add: function() {
this.count++
this.$emit('myevent', this.count)
}
},
template: '<button v-on:click="add">You clicked me {{ count }} times.</button>'
})
let app = new Vue({
el: "#app",
data() {
return {
initial_count: 10,
}
},
mounted: function() {
let initial_count = this.initial_count
let ButtonCounterComponentClass = Vue.extend({
data: function() {
return {}
},
render(h) {
return h("button-counter", {
props: {
initial_count: initial_count
}
})
}
})
let button_counter_instance = new ButtonCounterComponentClass()
button_counter_instance.$mount()
button_counter_instance.$on('myevent', function(count) {
console.log('listened!')
this.say(count)
})
this.$refs.container.appendChild(button_counter_instance.$el)
},
methods: {
say: function(message) {
alert(message)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button-counter initial_count=20 v-on:myevent="say"></button-counter>
<div ref='container'></div>
</div>
If I've understood what you want then you just need to listen for the event on the inner component and pass it on.
I've used arrow functions in a couple of places to avoid problems with this bindings. Otherwise I've tried to leave your code unchanged as much as possible. Changes marked with ****.
Vue.component("button-counter", {
data: function() {
return {
count: this.initial_count
}
},
props: ['initial_count'],
methods: {
add: function() {
this.count++
this.$emit('myevent', this.count)
}
},
template: '<button v-on:click="add">You clicked me {{ count }} times.</button>'
})
let app = new Vue({
el: "#app",
data() {
return {
initial_count: 10,
}
},
mounted: function() {
let initial_count = this.initial_count
let ButtonCounterComponentClass = Vue.extend({
data: function() {
return {}
},
render(h) {
return h("button-counter", {
props: {
initial_count: initial_count
},
// **** Added this ****
on: {
myevent: count => {
this.$emit('myevent', count);
}
}
// ****
})
}
})
let button_counter_instance = new ButtonCounterComponentClass()
button_counter_instance.$mount()
// **** Changed the next line ****
button_counter_instance.$on('myevent', count => {
console.log('listened!')
this.say(count)
})
this.$refs.container.appendChild(button_counter_instance.$el)
},
methods: {
say: function(message) {
alert(message)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button-counter initial_count=20 v-on:myevent="say"></button-counter>
<div ref='container'></div>
</div>
It's important to understand that button_counter_instance is not an instance of your button-counter component. You've wrapped it in another component, albeit a component that doesn't add any extra DOM nodes. So listening on the wrapper component is not the same as listening on button-counter.
Docs for what you can pass to h: https://v2.vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth

Passing data-attribute from Vue instance html tag

Is it possible to declare and pass a data-attribute value from a html tag of the Vue instance, and then have it available in the data object?
index.html:
<div id="app" data-title="My app title"></div>
App.vue:
data () {
return {
appTitle: // whatever is declared in data-title
}
}
This code works for me:
index.html:
<div id="app" data-id="123"></div>
index.js:
(function (el) {
new Vue({
el,
render: h => h(Module),
data: () => Object.assign({}, el.dataset) ,
});
})(document.getElementById('app'));
Module.vue:
export default {
name: 'Module',
data() {
return {
id: this.$parent.id,
};
},
};
Yes it is:
data () {
return {
appTitle: document.getElementById('app').dataset.title
}
}
However, it is possible that the DOM is not available on component initialization. So you should probably put that code into the mounted hook of your component:
<script>
export default {
data () {
return {
appTitle: null
}
},
mounted () {
this.appTitle = document.getElementById('app').dataset.title
}
}
</script>
Here's a different approach that doesn't rely on the DOM API, but cannot be used to get data-attributes from the root (#app) element:
{
el: '#app',
template: `
<div ref="mydiv" data-attribute="data attribute">
Hello from template
<div>
Hello from {{attribute}}
</div>
</div>`,
data(){
return {
attribute: ''
}
},
mounted(){
this.$data.attribute = this.$refs.mydiv.dataset.attribute;
}
});
Here's a pen with a working example

VueJs Nested props coming through undefined

I am trying to access an array which is part of a prop (event) passed into a component, but when in created() or mounted() the array part of the event prop (the rest is fine) comes through as undefined.
As can be seen below, when I inspect the props in the vue chrome plugin, the registration_fields are there.
I can add a watcher to the event prop and can access the registration_fields that way, but this seems very awkward to have to do this to access already passed in data.
This is from the Chrome vue inspector:
event:Object
address1_field:"Some Address 1"
address2_field:"Some Address 2"
approved:true
registration_fields:Array[1]
This is what part of my vue file looks like:
export default {
props: ['event'],
data() {
return {
regFields: []
}
},
created() {
this.regFields = this.event.registration_fields // Undefined here!
},
watch: {
event() {
this.regFields = this.event.registration_fields //Can access it here
});
}
}
}
I am using Vue 2.4.4
This is how the component is called:
<template>
<tickets v-if="event" :event="event"></tickets>
</template>
<script>
import tickets from './main_booking/tickets.vue'
export default {
created() {
var self = this;
this.$http.get('events/123').then(response => {
self.event = response.data
}).catch(e => {
alert('Error here!');
})
},
data: function () {
return {event: {}}
},
components: {
tickets: tickets
}
}
</script>
Thank you
It actually works fine without the watcher.
new Vue({
el: '#app',
data: {
event: undefined
},
components: {
subC: {
props: ['event'],
data() {
return {
regFields: []
}
},
created() {
this.regFields = this.event.registration_fields // Undefined here!
}
}
},
mounted() {
setTimeout(() => {
this.event = {
registration_fields: [1, 3]
};
}, 800);
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<sub-c v-if="event" :event="event" inline-template>
<div>
{{regFields}}
</div>
</sub-c>
</div>
If, as Belmin Bedak suggests in the comment below, event is populated asynchronously, it comes in as undefined because it's undefined. In that case, you need a watcher, or, somewhat more elegantly, use a computed:
new Vue({
el: '#app',
data: {
event: {}
},
components: {
subC: {
props: ['event'],
computed: {
regFields() {
return this.event.registration_fields;
}
}
}
},
// delay proper population
mounted() {
setTimeout(() => { this.event = {registration_fields: [1,2,3]}; }, 800);
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<sub-c :event="event" inline-template>
<div>
{{regFields}}
</div>
</sub-c>
</div>