We're creating an app where the interface is dynamically mounted based on a JSON returned by an API. It looks like this:
{
"path": "Container",
"children": [
{
"slot": "default",
"path": "Banner",
"props": {
"items": [ "image1.jpg", "image2.jpg", "image3.jpg" ]
}
},
{
"slot": "header",
"path": "Flex",
"props": {
"flow": "row"
},
"children": [
{
"slot": "default",
"path": "Icon",
"props": {
"name": "mdi-forum"
}
},
{
"slot": "default",
"text": "Example of title"
}
]
}
]
}
So I created a dynamic ComponentLoader with a computed doing a dynamic import, then I also inject more dynamic component loaders recursively as needed, using a v-for through the children list:
<template>
<component v-if="component" :is="component" v-bind="$attrs">
<template v-for="(child, i) of children">
<ComponentLoader
v-if="child.path"
v-bind="child.props"
:key="`${child.path}-${i}`"
:path="child.path"
:children="child.children"
/>
<template v-else>{{ child.text || '' }}</template>
</template>
</component>
</template>
<script>
import Error from '~/components/ComponentLoaderError.vue'
export default {
name: 'ComponentLoader',
components: { Error },
props: {
path: { type: String, required: true },
children: { type: Array, default: () => [] },
},
computed: {
component() {
if (!this.path) return null
return () => import(`~/components/${this.path}`).then((m) => m || m.default).catch(() => Error)
},
},
}
</script>
It's almost working, but all children gone injected to the default slot of each loaded component, which makes all sense since I'm not informing the desired slot during the loop through the children.
Inspired in this answer, I added a v-slot bind on the <template> with the v-for, using Dynamic Slot Names to inject on the right slot based in the child.slot property already received from the JSON:
<template v-for="(child, i) of children" #[child.slot]>
For nodes with only one child to be distributed on each slot, it's working as expected. But when I have more children to be distributed in the same slot (like the last children array in that JSON), only the last child is injected, overriding others before.
So, how to inject many children to dynamic named slots inside a loop?
Related
<el-popover
ref="popover1">
<div>
content
</div>
</el-popover>
<el-button v-popover:popover1>Button</el-button>
I am not sure how to call v-popover:popover1 property but I want to ask how can I create such a property via createElement method dynamically?
return createElement('span', {}, [
createElement('el-popover', {
props: {
ref: component.props.ref,
}
}, 'content'),
createElement('el-button', {
props: {
'v-popover': component.props.ref
}
}, 'trigger')
])
Above way didn't work.
If you want to generate v-popover:popover1 in your template, you can do it via the directives property in createElement, i.e.:
createElement('el-button', {
directives: [
{
name: 'popover',
arg: component.props.ref
}
]
}, 'trigger')
The nomenclature of a VueJS binding:
v-popover:popover1
├─────┘ ├──────┘
└name └argument
I'm trying to use conditional (ternary) operator to decide upon two template strings inside component's template property.
I am passing down list of jobs to my component:
<jobs :title="this.data.titles['experience']" :data="this.data.jobs"></jobs>
and here is my job and jobs component:
Vue.component('jobs', {
props: ['title', 'data'],
template: `<section id='jobs'>
<header>{{ title }}</header>
<job v-for="(job, index) in data" :job="job" :key="index"></job>
</section>`
})
Vue.component('job', {
props: ['job'],
template: job.hasOwnProperty('phases') && job.phases.length > 0
? `<p>A</p>`
: `<p>B</p>`
})
For some weird reason loop prints paragraph A even when list of jobs are:
[
{
"phases": [
{
"title": "Title 2"
},
{
"title": "Title 1"
}
],
"workPlace": "Company B"
},
{
"title": "Title 1",
"workPlace": "Company A"
}
]
I haven't conditionally rendered templates like that before, so not sure if it's good practice.
If you do it like this, does it work?
Vue.component('job', {
props: ['job'],
template: `
<p v-if="job.hasOwnProperty('phases') && job.phases.length > 0">A</p>
<p v-else>B</p>
`
})
I connected to a URL using Axios using this:
getUsers: function() {
axios.get(urlUsers).then(response => {
this.lists = response.data
});
and get this data:
"lists": [
{
"name": "Destacados",
"tags": [
"Aguila"
],
"isRoot": true,
"products": [
{
"name": "Coors",
"code": "139017",
And tryng to list products.
How?
You can use v-for to render lists. In your case you have a nested array so you would need to do it twice. However I think you should change your data element 'lists' and remove all the excess quotes ("") so it looks like this:
lists: [
{
name: "Destacados",
tags: [
"Aguila",
],
isRoot: true,
products: [
{
name: "Coors",
code: 139017,
},
{
name: "Bud",
code: 139019,
}
],
}
]
and then run your v-for loops:
<template>
<div v-for="list in lists" :key="list.name">
<ul v-for="product in list.products :key="product.name">
<li>{{product.name}}</li>
<li>{{product.code}}</li>
</ul>
</div>
</template>
I have form and select components.
In fact things are simple: I need two binding model.
The parent component:
Vue.component('some-form', {
template: '#some-form',
data: function() {
return {
countryNameParent: ''
}
}
});
The child component with items:
Vue.component('countries', {
template: '#countries',
data: function () {
return {
items: {
"0": {
"id": 3,
"name": "Afghanistan"
},
"1": {
"id": 4,
"name": "Afghanistan2"
},
"2": {
"id": 5,
"name": "Afghanistan3"
}
},
countryName: ''
}
},
props: ['countryNameParent'],
created: function() {
var that = this;
this.countryName = this.countryNameParent;
},
methods: {
onChange: function (e) {
this.countryNameParent = this.countryName;
}
}
});
I'm using v-model to incorporate components above.
Templates like this:
<template id="some-form">
{{ countryNameParent }}
<countries v-model="countryNameParent"></countries>
</template>
<template id="countries">
<label for="">
<select name="name" #change="onChange" v-model="countryName" id="">
<option value="0">Select the country!</option>
<option v-for="item in items" v-bind:value="item.name">{{ item.name }}</option>
</select>
</label>
</template>
My target is getting data in parent component to send it to server (real form is much bigger), however I can't get the value of the countryName in countryNameParent. Moreover, Parent should setting data in successor if not empty.
Here you go link where I've been attempting to do it several ways (see commented part of it).
I know that I need to use $emit to set data correctly, I've even implemented model where I get image as base64 to send it by dint of the same form, hence I think solution is approaching!
Also: reference where I've built sample with image.
Here is your countries component updated to support v-model.
Vue.component('countries', {
template: `
<label for="">
<select v-model="countryName">
<option value="0">Select the country!</option>
<option v-for="item in items" v-bind:value="item.name">{{ item.name }}</option>
</select>
</label>
`,
data: function () {
return {
items: {
"0": {
"id": 3,
"name": "Afghanistan"
},
"1": {
"id": 4,
"name": "Afghanistan2"
},
"2": {
"id": 5,
"name": "Afghanistan3"
}
},
}
},
props: ['value'],
computed:{
countryName: {
get() { return this.value },
set(v) { this.$emit("input", v) }
}
},
});
v-model is just sugar for setting a value property and listening to the input event. So to support it in any component, the component needs to accept a value property, and emit an input event. Which property and event are used is configurable (documented here).
I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent