Elegant way to use dynamic components with props in Vue - javascript

Following case: I have an API outputting all the content for my Page, as well as its structure and so on (for better understanding, imaging an CMS which includes kind of a page builder, where an author can place components by drag and drop to generate pages content, which is delivered to the front-end by that api).
The structure of the api output would look something like:
{content: [
{component: hero, content: {...} },
{component: form, content: {...} },
...
]}
So to generate related content I would think of using dynamic components like:
<template v-for="item in content">
<component :is="item.component" />
</template>
However, doing so I would face the problem, that I have to add properties data onto my components somehow, which (as far as I could see) isn't described within the Vue documentation. So now I wonder how to pass props onto dynamic components, which have entirely different props (hero might have an image, form could have input-placeholders, and so on) - any ideas???

Take a look at v-bind https://v2.vuejs.org/v2/guide/components-props.html#Passing-the-Properties-of-an-Object (same as Vue 3).
Assuming your API includes a props property for each component, then you'd do this:
<component v-for="item in content" :is="item.component" v-bind="item.props"></component>

Nowadays it's necessary to state what version of Vue do you work with.
With Vue 2 you could do it like this:
Vue.component('FirstComponent', {
props: ['title', 'prop1_1'],
template: `
<div>
{{ title }}<br />
{{ prop1_1 }}
</div>
`
})
Vue.component('SecondComponent', {
props: ['title', 'prop2_1', 'prop2_2'],
template: `
<div>
{{ title }}<br />
{{ prop2_1 }}<br />
{{ prop2_1 }}
</div>
`
})
new Vue({
el: "#app",
data() {
return {
items: [
{
component: 'FirstComponent',
props: {
title: 'First component title',
prop1_1: 'this is prop 1_1'
},
},
{
component: 'SecondComponent',
props: {
title: 'Second component title',
prop2_1: 'this is prop 2_1',
prop2_2: 'this is prop 2_2',
},
},
]
}
},
template: `
<div>
<component
v-for="(item, idx) in items"
:key="idx"
:is="item.component"
v-bind="item.props"
></component>
</div>
`
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Related

How to import dynamic v-bind:is component with module system in Vue

Problem: I am trying to programmatically register a component to be used in a slot in my Vue/Nuxt site. The component name is included in the data of the parent index.vue file, in this instance the component is named Projects. I am including it in a v-for template as the various objects in the 'modules' data array are iterated over. I had assumed this would be possible/easy from the dynamic component documentation and example however I have not managed to get it working in my case.
What I expect to happen: I expected the component to be registered and 'slotted' into the Module component correctly.
What actually happens: While I can see on the rendered view that the component is 'there', it is not in the correct place (i.e. slotted into the Module component). I also get a vue/no-unused-components error saying: The "Projects" component has been registered but not used.
I have read the documentation about component registration in modular systems but these seem to be for more complex cases than what I am trying to achieve. Any advice would be really helpful as I am totally stuck!
index.vue
<template>
<div>
<template v-for="module in modules">
<Module
:title="module.title"
:to="module.link"
/>
<component v-bind:is="module.slot" />
</Module>
</template>
</div>
</template>
<script>
import Module from '~/components/module/Module.vue'
import Projects from '~/components/module/slots/Projects.vue'
export default {
components: {
Module,
Projects
},
data () {
return {
modules: [
{
title: 'Work',
slot: 'Projects'
},
{
...
}
]
}
}
}
</script>
Edit: As a side note, I get the same error when registering the component with import like so:
components: {
Module,
'Projects': () => import('#/components/module/slots/Projects')
}
Module.vue
<template>
<div>
<h2>
{{ title }}
</h2>
<slot />
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
}
}
}
</script>
Projects.vue
<template>
<div>
<h3>Projects</h3>
</div>
</template>
<script>
export default {
name: 'Projects'
}
</script>
You use the self closure tag in your Module component.
This prevents your Projects component to be rendered within the slot.
Just replace:
<Module
:title="module.title"
:to="module.link"
/>
with:
<Module
:title="module.title"
:to="module.link"
>
and it should work.

How to bind whole component in Vue.js?

I just want to ask how to bind whole component (for example div) in Vue.js. Is there anything like innerHTML? Here is example:
Parent.vue
<template>
<div id="parent">
//some elements
</div>
</template>
Child.vue
<template>
<div id="child">
//some different elements
</div>
</template>
Now how to innerHTML child in parent? I've tried something like v-html:component and then data(){ return{ component: and here I dont know how to pass whole vue Component like Child.vue div. Should I use refs or something?
Now I use visibility attribute from css and I change it but I don't think that is good way to do this.
If you want to switch between components, then check out VueJS dynamic components:
https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components
You can use the component element and the :is prop to send down what component to render.
I have a working demo here: https://codepen.io/bergur/pen/bPEJdB
Imagine the following simple Vue component
Vue.component('menuList', {
data() {
return {
list: ['Menu item A', 'Menu item B']
}
},
template: `
<ul>
<li v-for="item in list">{{ item}}</li>
</ul>
`
})
This is a simple component rendering a unordered list of menu items. Lets create another similiar component that renders ordered list of products. Note that just to make them a little different, the menuList that has ul and the productList has ol
Vue.component('productList', {
data() {
return {
list: ['Product item A', 'Product item B']
}
},
template: `
<ol>
<li v-for="item in list">{{ item}}</li>
</ol>
`
})
Now we can create a main VueJS that renders these components depending on which button I press. You can have what ever trigger/action you want to change the component.
new Vue({
name: 'main',
el: '#main',
data() {
return {
header: 'Component switching',
selectedComponent: 'menuList'
}
},
methods: {
setComponent(name) {
this.selectedComponent = name
}
},
template: `<div>
<button #click="setComponent('menuList')">Menu List</button>
<button #click="setComponent('productList')">Products</button>
<component :is="selectedComponent" />
</div>`
})
So here the magic begins.
We create a app with some data properties. The header property is just a string value, and selectedComponent tells us which component is beeing rendered.
In our template we use the <component :is="selectedComponent /> so initially the menuList component is the active one.
We create a method called setComponent that takes in a string value and sets that as a new value for selectedComponent. By pressing a button a new value for selectedComponent is set and the component is rendered. Voila

How to import two VueJS components inside eachother?

I have two components, an item component and a folder component. Each item contains it's own folder component, and each folder component contains a list of items.
So I am trying to use the item component in the folder component, and vice versa, however, I am getting the error: unknown custom element: <item> - did you register the component correctly? For recursive components, make sure to provide the "name" option. Despite the fact that I have the name option set on both components.
Any ideas?
Code is below
item.vue
<template>
<div class="item" style="height: 30px">
. . .
<folder v-if="item.hasChildren" :id="id"></folder>
</div>
</template>
<script scoped>
import Folder from './folder.vue';
export default {
name: 'item',
props: ['id'],
components: {
Folder
}
};
</script>
folder.vue
<template>
<div class="folder">
<template v-for="(child, index) in children">
<item :last-item="index === children.length - 1" :key="child.id" :id="child.id"></item>
</template>
</div>
</template>
<script>
import Item from "./item.vue";
export default {
name: 'folder',
props: ['id', 'hasChildren'],
components: {
Item
}
};
</script>
This is probably happening because of the circular referencing between your components ..
When you look closely, you’ll see that these components will actually
be each other’s descendent and ancestor in the render tree - a
paradox!
To resolve this paradox you can either register your components globally using Vue.component, or you can defer the import of one of your components to a later point (by moving your import to the beforeCreate hook or using async components as demonstrated here) ..
folder.vue
<template>
<div class="folder">
<template v-for="(child, index) in children">
<item :last-item="index === children.length - 1" :key="child.id" :id="child.id"></item>
</template>
</div>
</template>
<script>
export default {
name: 'folder',
props: ['id', 'hasChildren'],
components: {
'item': () => import('./item.vue')
}
};
</script>
You need to provide the key (name) in the components object like so,
item.vue
components: {
folder: Folder
}
folder.vue
components: {
item: Item
}

Modifying the VueJS merge strategy to prioritize templates from a mixin

I have a simple Vue component and a mixin that I am bringing into that component. Both the component and the mixin have a template defined. When the mixin is merged into the component, the template defined at the component level is what is rendered to the document.
Is there any way to prioritize the template from the mixin over the one defined in the component? I would want this to be the setting throughout my application.
Simple code example of what I'm executing here:
HTML
<div id="component-test">
<basic-component></basic-component>
</div>
<script type="text/x-template" id="hello">
<div>
<h2>{{ message }}</h2>
</div>
</script>
<script type="text/x-template" id="welcome">
<div>
<h2>{{ message }}</h2>
<hr />
<p>{{ subMessage }}</h2>
</div>
</script>
JS
const Modify = {
data() {
return {
subMessage: "Welcome to Vue."
}
},
template: '#welcome'
}
const Basic = {
data() {
return {
message: "Hello World!"
}
},
template: '#hello',
mixins: [Modify]
}
new Vue({
el: '#component-test',
components: {
'basic-component': Basic
}
})

Vue: v-model doesn't work with dynamic components

For example: <component v-model='foo' :is='boo' ...>.
foo's value stays the same during input.
I'm trying to solve the issue for quite a long time. I've checked lots of questions and threads but none of those helped me.
HTML doesn't work:
<component
:is="field.component"
:key="key"
:name="field.name"
v-for="(field, key) in integration_data"
v-model="field.value"
>
</component>
HTML works fine:
<input
:key="key"
:name="field.name"
v-for="(field, key) in integration_data"
v-model="field.value"
>
Vue controller:
export default {
init: function (init_data) {
return new Vue({
data: {
integration_data: [
{name: 'field_name0', component: 'input', value: ''},
{name: 'field_name0', component: 'input', value: ''},
]
},
});
}
}
You can't use input as a type of component and expect it to be a native input element. :is must name a component (which can contain an input, if you want).
Then you have to understand how v-model works on components:
So for a component to work with v-model, it should (these can be
configured in 2.2.0+):
accept a value prop
emit an input event with the new value
Putting that all together, you can use v-model with :is.
new Vue({
el: '#app',
data: {
integration_data: [{
name: 'one',
component: 'one',
value: 'ok'
}]
},
components: {
one: {
props: ['name', 'value'],
template: '<div>{{name}} and <input v-model="proxyValue"><slot></slot></div>',
computed: {
proxyValue: {
get() { return this.value; },
set(newValue) { this.$emit('input', newValue); }
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<component
:is="field.component"
v-for="(field, key) in integration_data"
:key="key"
:name="field.name"
v-model="field.value"
>
<div>{{field.value}}</div>
</component>
</div>
v-model has nothing to do with what you're possibly trying to do. It looks like you are trying to insert components dynamically. That's exactly what :is does. Now, to pass data to the component, you should use props.
For example:
Your Vue instance:
const vm = new Vue({
el: '#app',
data: {
exampleValue: 'This value will be passed to the example component'
}
})
Register a component:
Vue.component('example-component', {
// declare the props
props: ['value'],
template: '<span>{{ value}}</span>'
})
Then use it like this:
<example-component :value="exampleValue"></example-component>
Or:
<component :is="'example-component'" :value="exampleValue"></component>

Categories