VueJs: Pass Dynamic Components as props to another Component and render them - vuejs2

I am trying to build a table component.
I want to define and pass the column metadata for the grid as an array prop and also pass the actual data as another prop to the grid.
I was able to achieve that without many issues.
But, now I would like to pass a dynamic component as a part of each column definition so that the user can define/control the way the cell gets rendered (content with edit delete buttons in same cell etc.)
Is there a way to pass a dynamic component as a prop and then have this component rendered?
<parent-comp>
<tr class="" v-for="result in dataSource">
<template v-for="column in columns">
<td>
<template v-if="column.customComponent">
######## How do I render this customComponent ########
</template>
</td>
</template>
</tr>
</parent-comp>
where the dataSource data can be something like
[
columns: [{
name: "something",
customComponent: SomeCustomComponent
}, {
name: "another thing",
customComponent: AnotherOtherCustomComponent
}]
]
Will be happy to elaborate/clarify on this if the ask above is not clear.

As suggested in the comments above, you can use a dynamic component in your template and pass the definition of the component in your property.
console.clear()
const ColumnOne = {
template: `<h1>I am ColumnOne</h1>`
}
const ColumnTwo = {
template: `<h1>I am ColumnTwo</h1>`
}
Vue.component("parent-comp",{
props:["columns"],
template:`
<div>
<component v-for="column in columns"
:is="column.customComponent"
:key="column">
</component>
</div>
`
})
new Vue({
el:"#app",
data:{
columns:[{
name: "something",
customComponent: ColumnOne
}, {
name: "another thing",
customComponent: ColumnTwo
}]
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<parent-comp :columns="columns"></parent-comp>
</div>

Related

How can i pass in more data values inside a child component from my route parameter in vue?

So I am bad, really bad at asking questions so I decide to make a video so I can explain myself what I want to ask, in little words I can say I want to have more data available from a component to a child component, just like we do on a Ruby on Rails CRUD app in the view post page.
this is the video please look at the video and discard my words --> https://youtu.be/026x-UvzsWU
code:
row child component:
<template>
<tr>
<td class="name-logo-col">
<!-- dynamic routes for data -->
<router-link
:to="{ name: 'Companies', params: {id : row.Name} }">
<b>{{row.Name}}</b><br> ...
Sheet component:
<template>
<div class="container-fluid">
<div class="row">
<main role="main" class="col-md-12 ml-sm-auto col-lg-12 pt-3 px-4">
<div class=" pb-2 ma-3 ">
<h2 class="center">Funding</h2>
<v-divider></v-divider>
</div>
<div class="mb-5 table-responsive">
<!-- <v-data-table
:headers="headers"
:items="rows"
class="elevation-1">
</v-data-table> -->
<table class="table table-striped ">
<thead>
<tr>
<th>Name</th>
<th>Funding Date</th>
<th>Amount Raised</th>
<th>Round</th>
<th>Total Raised</th>
<th>Founder</th>
<th>Est.</th>
<th>Location</th>
<th>Lead Investor</th>
</tr>
</thead>
<tbody >
<Row v-bind:key="row.id" v-for="row in rows" v-bind:row="row" />
</tbody>...
//data
rows: [],
loading: true,
}
},
methods:{
async accessSpreadSheet() {
const doc = new GoogleSpreadsheet(process.env.VUE_APP_API_KEY);
await doc.useServiceAccountAuth(creds);
await doc.loadInfo();
const sheet = doc.sheetsByIndex[0];
const rows = await sheet.getRows({
offset: 1
})
this.rows = rows;
this.loading = false;
}
},
created() {
this.accessSpreadSheet();
// testing console.log(process.env.VUE_APP_API_KEY)
}
This where my view company dynamic router child component:
<template>
<v-container class="fill-height d-flex justify-center align-start" >
<h3> Company component: {{ $route.params.id }} </h3>
<p> {{$route.params.Location}} </p>
</v-container>
</template>
<script>
export default {
name: "Company",
data() {
return {
}
}
}
</script>
This is the parent component companies of company child :
<template>
<v-container class="fill-height d-flex justify-center align-start" >
<div>
<h1> Companies man view </h1>
<Company/>
<router-link to="/funds">
Back
</router-link>
</div>
</v-container>
</template>
<script>
import Company from '#/components/Company.vue'
export default {
name: "Companies",
components: {
Company
},
}
</script>
This is my router link index page related code:
{
path: '/companies/:id',
name: 'Companies',
props: true,
component: () => import(/* webpackChunkName: "add" */ '../views/Companies.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes,
store
})
I'm not pretty sure if I understand the whole question, but I will try to answer the part that I understand.
If you want to pass the other elements inside the row rather than just the Name or Location to route with the name "Companies", your first code actually just needs a little change.
You just need to pass the whole row and don't forget to parse it into a string, so the data will remain after reload.
<router-link
:to="{ name: 'Companies', params: {id : JSON.stringify(row)} }">
and then parse it back into JSON inside the Companies component using JSON.parse($route.params.id).
Or
you can use multiple params, so instead of naming your params inside router index as path: '/companies/:id, you should name it as path: '/companies/:Name/:Location' and then you pass the data of the other params.
<router-link
:to="{ name: 'Companies', params: { Name: row.Name, Location: row.Location } }">
But all params are mandatory, so you have to pass value to all params every time.
OR
Rather than using params, you can use query, it was more flexible. You can pass any additional query without modifying the path inside router index.
<router-link
:to="{ name: 'Companies', query: { Name: row.Name,Location: row.Location } }">
and delete /:id from your router index, so it would be just path: '/companies',
and after that you can directly access the query value inside the Companies component with $route.query.Name
So I watched the video and I think you're approaching this from the wrong angle.
It seems that you're unfamiliar with Vuex and you might assume you pass all that data through $route, which is not what it's supposed to be used for.
So this would ideally be the flow:
You get data from API
Data gets stored in the Vuex store
User navigates to page
Param.id get retrieved from $route
Id gets send to Vuex store as a parameter in a getter
Getter returns data to component filtered by the id
Data gets used in component
Doing it this way keeps all your data centralized and re-usable in any component. You can even see in real-time what is in the Vuex store by going to Chrome tools and clicking the Vuex tab.
Let me know if you have further questions!

Where are functions places when working with slots in Vue?

If component A is a parent to component B, where B has a slot. If A uses the slot with a button, where do you place the clickHandler, in A or in B?
Is both possible as long as you don't have both the same time? And if so, are there a best practice or does it depend on the situation?
You place it in A.
Slots are just cues for Vue to 'put the template here'. B doesn't know that you're rendering a button in that template. A determines that it's a button being rendered, and thus determines what happens when that button is pressed.
Technically, you could check what is being rendered in the slot from Component B, and apply a clickHandler to the slot if it's a button, as it's all just a render function under the hood. But for the sake of 'where do I put my function', that's generally too complex and rarely useful.
The child component could expose data to the parent one using scoped slot, in the parent we put some element which could have some events that have handlers defined in the parent component.
Example :
List
<template>
<div>
<ul>
<li v-for="(item, i) in items" :key="i">
<slot name="item" :item="item"></slot>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "list",
props: ["items"],
};
</script>
parent :
<template>
<list :items="items">
<template #item="{ item }">
<h3>{{ item }}</h3>
<span #click="deleteItem(item)">delete</span>
</template>
</list>
</template>
<script>
import List from "./List.vue";
export default {
data() {
return {
items: ["item 1", "item 2", "item 3"],
};
},
methods: {
deleteItem(item) {
console.log(item);
this.items = this.items.filter((_item) => _item !== item);
},
},
components: {
List,
},
};
</script>
LIVE DEMO

How can I set custom template for item in list and than use it inside `v-for` loop?

What I want to achieve is something like:
<li v-for="(item, index) in items" :key="index>
<div v-if="item.Component">
<item.Component :value="item.value" />
</div>
<div v-else>{{ item.value }}</div>
</li>
But anyway I don't like at all this solution. The idea of defining Component key for an item in items list is hard to maintain since at least it is hard to write it in template-style way (usually we are talking about too long HTML inside). Also I don't like to wrap item.Component inside div.
data() {
return {
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `123 {{ value }} 312`
}
}]
};
}
Does anyone know the best-practice solution for this and where Vue describes such case in their docs?
You can use Vue's <component/> tag to dynamically set your component in your list.
<li v-for="(item, index) in items" :key="index>
<component v-if="item.Component" :is="item.Component" :value="item.value"></component>
<div v-else>{{ item.value }}</div>
</li>
<script>
...,
data: () => ({
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `<div>123 {{ value }} 312</div>` // must be enclosed in a element.
}
}]
})
</script>
You can also import a component too so you can create a new file and put your templates and scripts there.
Parent.vue
<script>
import SomeComponent from "#/components/SomeComponent.vue"; //import your component here.
export default {
data() {
return {
list: [
{
value: "abc",
Component: SomeComponent // define your imported component here.
},
]
};
}
};
</script>
SomeComponent.vue
<template>
<div>123 {{ value }} 312</div>
</template>
<script>
export default {
name: "SomeComponent",
props: ["value"]
};
</script>
Here's a demo.

When using conditional rendering, how do I prevent repeating the child components on each condition?

Scenario
I have a custom button component in Vue:
<custom-button type="link">Save</custom-button>
This is its template:
// custom-button.vue
<template>
<a v-if="type === 'link'" :href="href">
<span class="btn-label"><slot></slot></span>
</a>
<button v-else :type="type">
<span class="btn-label"><slot></slot></span>
</button>
</template>
You can see from the template that it has a type prop. If the type is link, instead of the <button> element, I am using <a>.
Question
You'll notice from the template that I repeated the child component, i.e. <span class="btn-label"><slot></slot></span> on both root components. How do I make it so that I won't have to repeat the child components?
In JSX, it's pretty straightforward. I just have to assign the child component to a variable:
const label = <span class="btn-label">{text}</span>
return (type === 'link')
? <a href={href}>{label}</a>
: <button type={type}>{label}</button>
In this situation, I would probably opt to write the render function directly since the template is small (with or without JSX), but if you want to use a template then you can use the <component> component to dynamically choose what you want to render as that element, like this:
Vue.component('custom-button', {
template: '#custom-button',
props: [
'type',
'href',
],
computed: {
props() {
return this.type === 'link'
? { is: 'a', href: this.href }
: { is: 'button', type: this.type };
},
},
});
new Vue({
el: '#app',
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<custom-button type="button">Button</custom-button>
<custom-button type="submit">Submit</custom-button>
<custom-button type="link" href="http://www.google.com">Link</custom-button>
</div>
<template id="custom-button">
<component v-bind="props">
<span class="btn-label"><slot></slot></span>
</component>
</template>
Well you could always create a locally registered component...
// in custom-button.vue
components : {
'label' : {template : '<span class="btn-label"><slot></slot></span>'}
}

Vue custom filtering input component

I'am trying to create a component that have 'just' an text input. String typed in this input will be used to filter a list. My problem is that I cannot handle how to share this filter string between my component and the main app that contains the list to filter.
I tried several things and most of the time I get the error :
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value
So I looked Vuex but I thinks it cannot help in this case because I can have several filter component used in he same page for different list, and I don't want them to be synchronized ^^
Here is what I have:
The filter component
<script type="x/template" id="filterTpl">
<div>
<span class="filter-wrapper">
<input type="search" class="input input-filter" v-model.trim="filter" />
</span>
</div>
</script>
<script>
Vue.component('list-filter', {
props: {
filter: String
}
template: '#filterTpl'
});
</script>
And my main app:
<div id="contacts">
<list-filter :filter="filter"></list-filter>
<ul class="contacts-list managed-list flex">
<li class="contact" v-for="contactGroup in filteredData">
[...]
</li>
</ul>
</div>
<script>
var contactsV = new Vue({
el: '#contacts',
data: {
filter: "",
studyContactsGroups: []
},
computed: {
filteredData: function(){
// Using this.filter to filter the studyContactsGroups data
[...]
return filteredContacts;
}
}
});
</script>
Thanks for any help or tips :)
You can synchronize child value and parent prop either via explicit prop-event connection or more concise v-bind with sync modifier:
new Vue({
el: '#app',
data: {
rawData: ['John', 'Jane', 'Jim', 'Eddy', 'Maggy', 'Trump', 'Che'],
filter: ''
},
components: {
'my-input' : {
// bind prop 'query' to value and
// #input update parent prop 'filter' via event used with '.sync'
template: `<input :value="query" #input="updateFilter">`,
props: ['query'],
methods: {
updateFilter: function(e) {
this.$emit('update:query', e.target.value) // this is described in documentation
}
}
}
},
computed: {
filteredData: function() {
// simple filter function
return this.rawData.filter(el => el.toLowerCase()
.match(this.filter.toLowerCase()))
}
}
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<my-input :query.sync="filter"></my-input>
<hr>
<ul>
<li v-for="line in filteredData">{{ line }}</li>
</ul>
</div>