How to dynamically render vue.js components? - javascript

does anyone know of a way to dynamically render vue.js components? For example, I have a table component that should render different buttons based on the prop definition:
Vue.component('my_table', {
template: '#my_table',
props: {
data: Array,
columns: Array,
actions: Array
}
});
Ideally, my template would be defined in the tmpl value of my action object:
<tbody>
<tr v-for="entry in data">
<td v-for="key in columns">
{{entry[key.field]}}
</td>
<td v-for="action in actions">
{{ action.tmpl }}
</td>
</tr>
</tbody>
Here's my fiddle:
https://jsfiddle.net/zfya92dh/43/
Anyone have any ideas?
Thanks in advance!

<component :is="action.tmpl"></component>
Here is your fiddle revised.
const button1 = Vue.extend({
template:'<button class="btn btn-primary">View</buttton>'
})
const button2 = Vue.extend({
template:'<button class="btn btn-primary" #click="some_method">Delete</buttton>',
methods:{
some_method(){
alert('clicked')
}
}
})
And the relevant data.
data: {
actions: [
{
name: 'view',
label: 'View Company',
method: 'click',
tmpl: button1
},
{
name: 'delete',
label: 'Delete Company',
method: 'click2',
tmpl: button2
},
],

You just want to insert HTML instead of plain text? Change
<td v-for="action in actions">
{{ action.tmpl }}
</td>
to
<td v-for="action in actions" v-html="action.tmpl"></td>
However, if your HTML includes Vue markup, the markup will not be respected. You will need to create actual components to represent each of your configurations and then use :is to tell Vue which one to use at a given time. Vue doesn't use string-based templating, so there is no way to make passing a template string work.

Related

Elegant way to use dynamic components with props in Vue

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>

Vue - Dynamic component event listener

Problem : I am trying to create a table component for my app which would be use by other components to render a table. It could have three possible cell values :
Text
HTML
Component
I am able to render all the above values but I am stuck at binding an event listener. What I am trying to achieve is something like this :
Pass a method and event which is to be binded to the component and the table should bind that with respective cell.
So for example :
TABLE JSON
{
"cell-1":{
"type":"html",
"data":"<h4>text-1</h4>",
"method": someMethod
}
}
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit(someMethod)"
v-on:click.native="someMethod"
></span>
</td>
</tr>
</template>
</tbody>
Above is just a snippet of what I am trying, the table loops through the object passed and renders accordingly.
I have already tried
SO Solution 1
SO Solution 2
Please let me know if any more info is required.
The best way is to have the method/handler inside the parent component and then trigger is using the emit functionality such that in
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit('trigger-handler', {method: 'method1', data: {}})"
></span>
</td>
</tr>
</template>
</tbody>
and in
Parent.vue
<table-component #trigger-handler="triggerHandler" />
inside script
export default {
data() {
},
methods: {
triggerHandler(payload) {
// payload is actually the object passed from the child
this[payload.method](payload.data); // call a specific method
},
method1(data) {
},
method2(data) {
},
method3(data) {
}
}
}

Vue2 modal in v-for list

I'm trying to implement a vue2 modal as described in the vue docs at https://v2.vuejs.org/v2/examples/modal.html.
It looks something like this:
<tbody v-if="fields.length">
<tr v-for="(item, index) in fields">
<td>#{{ item.name }}</td>
<td><input type="checkbox" id="checkbox" v-model="item.active"></td>
<td>
<button id="show-modal" #click="showModal = true">Show Modal</button>
</td>
</tr>
<modal :item="item" v-if="showModal" #close="showModal = false">
<h3 slot="header">Hello World</h3>
</modal>
</tbody>
Vue.component('modal', {
template: '#modal-template',
data: function() {
return {
item: ''
}
}
});
While the button shows up and does pop up the modal, I get a warning that Property or method "item" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option., and when I inspect the modal component in the Vue dev tools, the item is still '', it's not populated by the :item binding.
What's the right way to pass the item object into the modal so we can use it in the window?
If you're passing item as a value from the parent to a child component, you cannot use data--you must use props instead!
Vue.component('modal', {
template: '#modal-template',
props: ['item'],
data: function() {
return {
}
}
});
There are 2 things:
You don't reference item in the data object instead you should reference it as a prop for the component props: ['item'].
The modal is not inside the loop's scope, which ends at the closing </tr> tag. You could change this so that the click action performs a method that takes the item and assigns it to a variable that is always passed in like a prop. This would be similar to how you are doing it now, you would just change showModal = true to openModal(item) then have a method that would set the appropriate 2 values. Something like this:
openModal(item) {
this.showModal = true;
this.modalItem = item;
}
...
data: () => {
modalItem: null
...
}

How to use new Vue in a single file component?

I'm still learning Vue.js and I have a minor issue:
I have a single file component with an array of checkboxes, and I have looked at the documentation for using multiple checkboxes, but the example there requires me to declare:
new Vue({
el: '#example-3',
data: {
checkedNames: []
}
})
Edit However I have set this up in my single file component within the <script> tag within data(), but it just checks/unchecks all boxes at once (but feeds back true/false correctly):
<script>
export default {
name: 'PhrasesDetail',
data () {
return {
game: '',
language: '',
checkedPhrasesArr: {
el: '#selection',
data: {
checkedPhrasesArr: []
}
}
}
},
...
</script>
The Question is where and how do I declare the checkbox array so that it reacts/recognises the individual elements?
These are my checkboxes:
<tr v-for="(keyTransPair, index) in this.language.data">
<td id="selection"><input type="checkbox" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
...
</tr>
I have assembled a complete example. (I don´t know anything about your language.data object so I´m just using fake data).
<template>
<div>
<!-- Your Checkboxes -->
<table>
<tr v-for="(keyTransPair, index) in language.data">
<td id="selection"><input type="checkbox" :value="keyTransPair" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
</tr>
</table>
<!-- Show the selected boxes -->
{{ checkedPhrasesArr }}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
// Your language data
language: {
data: {
keyOne: "one",
keyTwo: "two",
keyThree: "three"
}
},
// The Checkbox data
checkedPhrasesArr: []
}
}
}
</script>
Note that the checkbox values are bound with :value="keyTransPair". You can change the value to anything you want. This value will be added to the array if the checkbox is checked.
By the way: You use <td id="selection"> within the v-for loop. So the id "selection" will not be unique. You should better use a class instead of an id here.

Toggle row independently of others in an {{#each}} - Ember

I've got this chunk of code in my controller to output everything I'd like to in the first row. The second row is going to be available upon a click action. However, I'm trying to make it so that when I click any row shown, it'll only display the 2nd row that given row. Not the 2nd row of all the others.
This is in my template.
{{#each chosenSomething as |something|}}
<tr class='main' {{action 'toggleHelp'}}>
<td>{{something.this}}</td>
<td>{{something.that}}</td>
<td>{{something.hey}}</td>
<td>{{something.you}}</td>
</tr>
{{#if isDisplay}}
<tr class='help'>
<td class='instruction'></td>
<td class='video'></td>
</tr>
{{/if}}
{{/each}}
This is my JS for the action.
toggleHelp() {
this.toggleProperty('isDisplay');
},
I know how I would approach this using jQuery, but I'd like to know the Ember way of doing such action. I thought about including index, but unsure how it should be written.
I would suggest making a component and handing the toggle within.
https://ember-twiddle.com/e6ccad3d2ffdc4a9d627db12a1317fa5?openFiles=templates.components.toggle-thing.hbs%2Ctemplates.components.toggle-thing.hbs
// toggle-thing.hbs
<header {{action 'toggleAnswer'}}>
{{data.question}}
</header>
{{#if open}}
<footer>
{{data.answer}}
</footer>
{{/if}}
// toggle-thing.js
import Ember from 'ember';
export default Ember.Component.extend({
// tagName: 'tr', // or whatever if you like
// classNames: ['thing', 'row', 'etc'], // smooth out the template a bit later...
open: false, // default
actions: {
toggleAnswer() {
this.toggleProperty('open');
console.log( this.get('open') );
},
},
});
// application.hbs or wherever you want to list the things
<ul class='thing-list'>
{{#each model as |thing|}}
<li class='thing'>
{{toggle-thing data=thing}}
</li>
{{/each}}
</ul>
has a 'thing' model and is pulling in some question answers to the model hook for example.
import Ember from 'ember';
var data = [
{
question: "How to be funny",
answer: "Stand on your head",
},
{
question: "How to be smart",
answer: "Just listen more often.",
},
];
export default Ember.Route.extend({
model() {
return data;
},
});
The Ember guide has a toggle example: https://guides.emberjs.com/v2.13.0/tutorial/simple-component/
Augment each element of chosenSomething to include the open/close state.
You'll then be able to have:
{{#each chosenSomething as |something|}}
<tr class='main' {{action 'toggleHelp' something}}>
<td>{{something.this}}</td>
<td>{{something.that}}</td>
<td>{{something.hey}}</td>
<td>{{something.you}}</td>
</tr>
{{#if something.isDisplay}}
<tr class='help'>
<td class='instruction'></td>
<td class='video'></td>
</tr>
{{/if}}
{{/each}}
and the JS:
toggleHelp(something) {
Ember.set(something, 'isDisplay', !something.isDisplay);
}
If you don't want to modify the original data, you can have a computed property generate the appropriate data such as:
chosenSomethingWithState() {
return (this.get('chosenSomething') || []).map((item)=>{isDisplay:false, item})
}.property('chosenSomething')
and adjust the template accordingly

Categories