How to render components dynamically in Vue JS? - javascript

I am making a form generator, which uses components in it for input fields, buttons etc. I want to be able to generate the form depending on what options I pass to it.
But I can't get it to render the components.
I tried to return pure HTML but that won't render the components.
I call the form generator from my Home.vue template where I want the form with an options object like this:
options: {
name: {
type: 'input',
label: 'Name'
},
submit: {
type: 'button',
label: 'Send'
}
}
In template:
<template>
<form-generator :options="options"></form-generator>
</template>
In the form generator component I have tried multiple things like:
<template>
{{ generateForm(this.options) }}
// ... or ...
<div v-html="generateForm(this.options)"></div>
</template>
I include all the components like:
import {
FormButton,
FormInput
} from './FormComponents'
Now the final part is how do I make FormInput render?
This does not work since it outputs the HTML literally:
methods: {
generateForm(options) {
// .. do stuff with options ..
var form = '<form-input />'
return form
}
}

Vue has a very simple way of generating dynamic components:
<component :is="dynamicComponentName"></component>
So I suggest you define the options as an array and set the type to be the component name:
options: [
{
type: 'FormInput',
propsData: {label: 'Name'}
},
{
type: 'FormButton',
propsData: {label: 'Send'}
}
]
Then use it in the form generator like this:
<component :is="option.type" v-for="option in options"></component>
You can also pass properties as you'd pass to ant other component, but since it's dynamic and every component has a different set of properties i would pass it as an object and each component would access the data it needs:
<component :is="option.type" v-for="option in options" :data="option.propsData"></component>
UPDATE
Since you don't have control of the components it requires a bit more manipulation:
For each component that requires text, add a text attribute in the options:
options: [
{
type: 'FormInput',
propsData: {label: 'Name'}
},
{
type: 'FormButton',
text: 'Send',
propsData: {label: 'Send'}
}
]
And then just use it in the component:
<component :is="option.type" v-for="option in options">{{option.text}}</component>
For passing attributes, I think you can pass it using v-bind and then it will automatically destructure them, so if a button accepts 2 props: rounded, color
the options would look like:
{
type: 'FormButton',
text: 'Button',
propsData: {rounded: true, color: '#bada55'}
}
and then the component:
<component :is="option.type" v-for="option in options" v-bind="option.propsData">{{option.text}}</component>

you can create an Array like this:
components_data: [
{
name: 'checkbox',
data: false
},
{
name: 'text',
data: 'Hello world!'
}
]
and then loop through this array inside of the <component>:
<component
v-for="(component,i) in components_data"
:key="i"
:is="component.name"
:data="component.data"
/>
this will create 2 component [<text>, <checkbox>] dynamically and give them data via props.
when you push new data like this components_data.push({name:'image',data: {url:'cat.jpg'}}) it will render a new component as <image :data="{url:'cat.jpg'}"/>

Related

Passing dynamic functions from parent component to grandchild component Angular

I am creating a common table component for my angular application so that the component takes input for rows, columns, along with some action button handler functions and render table.
The table will be something like this
I
In this way, a single component can be used to render table for the whole application.
//parent-component.ts
parentFunction1(){
//edit User
}
parentFunction2(){
//delete User
}
parentFunction3(){
//view user
}
I am passing data from the parent component as
//inside some-parent.component.html
<common-table
[columns]="columnConfig"
[dataSource]="rowConfig">
</common-table>
In my common-table.component.html, based on conditions I need to render different components as:
//inside common-table.component.html
<table-cell [row]="row" [column]="column"></table-cell>
from table-cell.component.html I need to call functions of parent-component.ts. For different components, my function name may vary, is there any way in angular so that if json
[
{
name: 'Edit',
type: 'button',
outputHandler:parentFunction1
},
{
name: 'Delete',
type: 'button',
outputHandler:parentFunction2
},
{
name: 'View',
type: 'button',
outputHandler:parentFunction3
}
]
like this can be passed from parent component and use the functions of the parent component from grandchild table-cell.component.html
I can use output and eventemitter, but as number of functions passed and name of functions may vary, so It cannot be hard corded. How to achieve this. Please help as I searched a lot but could not get the solution.
This is how your root component looks like.
export class AppComponent {
title = "CodeSandbox";
myConfig: ConfigModel[] = [
{
name: "Edit",
type: "button",
outputHandler: this.parentFunction1
},
{
name: "Delete",
type: "button",
outputHandler: this.parentFunction2
},
{
name: "View",
type: "button",
outputHandler: this.parentFunction3
}
];
parentFunction1() {
console.log("parent func 1");
}
parentFunction2() {
console.log("parent func 2");
}
parentFunction3() {
console.log("parent func 3");
}
}
As you are passing this configuration to your grand child component. you can invoke the function directly from your configuration object.
<div *ngFor="let item of config">
<button (click)="action(item)">{{item.name}}</button>
</div>
export class ActionComponent {
#Input() config: ConfigModel[];
action(item: ConfigModel) {
console.log(item);
item.outputHandler();
}
}
Working Demo

Update the inital array of a recursive treeview in VueJS

I was folling this tutorial for my own tree view with a recursive component in vuejs.
So the input array looks like this:
let tree = {
label: 'root',
nodes: [
{
label: 'item1',
nodes: [
{
label: 'item1.1'
},
{
label: 'item1.2',
nodes: [
{
label: 'item1.2.1'
}
]
}
]
},
{
label: 'item2'
}
]
}
<template>
<div>
...
<tree-menu
v-for="node in nodes"
:nodes="node.nodes"
:label="node.label" />
...
</div>
<template
<script>
export default {
props: [ 'label', 'nodes' ],
name: 'tree-menu'
}
</script>
So basically a label and a subarray of nodes is passed to a child node. Now I want to update or delete a node (e.g. item1.1), but reflect this change in the outmost array (here tree), because I want to send this updated structure to the server. How can I achive this? If I change the label of a node, this will be rendered in the DOM, but the tree array is not updated.
Here's how you can use the .sync modifier to update recursively:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('tree-node', {
template: `
<div style="margin-left: 5px;">
<input :value="label"
type="text"
#input="$emit('update:label', $event.target.value)" />
<tree-node v-for="(node, key) in nodes"
:key="key"
v-bind.sync="node" />
</div>
`,
props: ['label', 'nodes']
});
let tree = {
label: 'root',
nodes: [{
label: 'item 1',
nodes: [
{ label: 'item 1.1' },
{ label: 'item 1.2',
nodes: [
{ label: 'item 1.2.1' }
]
}
]
},
{ label: 'item 2' }
]
};
new Vue({
el: '#app',
data: () => ({
tree
})
})
#app {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<div id="app">
<div>
<tree-node v-bind.sync="tree" />
</div>
<pre v-html="tree" />
</div>
v-bind.sync="node" is shorthand for :label.sync="node.label" :nodes.sync="node.nodes". v-bind unwraps all object members as attributes of the tag, resulting in props for the component.
The other half of the solution is replacing v-model on the input with :value + an $emit('update:propName', $event.target.value) call on #input which updates the .sync-ed property in the parent. To conceptualize it, it's a DIY v-model exposed by Vue so it could be customized (you decide when to call the update and what to update with). You can replace the <input> with any other type of input, depending on what you're binding/modifying (checkboxes, textarea, select, or any fancier input wrapper your framework might feature). Depending on type of input you'll want to customize the listener: #change, #someCustomEvent, etc...
.sync makes everything reactive at each individual level. Since everything is :key-ed, no re-rendering actually happens (Vue only re-renders DOM elements which actually changed). If that wasn't the case, the input would lose focus upon re-rendering.
The update principle is: instead of making the change at child level you update the parent property which, through v-bind, sends it back to the child.
It's the same exact principle used by Vuex. Rather than changing some local prop you call a store mutation which comes back through getters and modifies the local value but it happens for any component using that store data, not just for current one.

Error: "Error in v-on handler: "TypeError: this.filter is undefined"" in a list rendering in vue?

I am trying to build a component that creates filter buttons and then sends the type attribute in the filters object through the event bus to be handled elsewhere in my app. However, when I added the array of objects (filters) in the data section, I am getting an error of this.filter is not defined when I click on a button.
I would like to keep the filters array in this component because I am also trying to dynamically change the active class to whichever button has been clicked.
Am I missing something that has to do with props? Why am I unable to display the buttons when the data and v-for was on another component? These were the questions I have been asking myself in order of solving this issue.
<template>
<div>
<button
v-for="(filter, index) in filters"
:key="index"
:class="{ active: index === activeItem }"
#click="emitFilter(), selectItem(index)"
:filter="filter"
>
{{ filter.name }}
</button>
</div>
</template>
<script>
import EventBus from '#/components/EventBus'
export default {
props: {
filter: { type: String }
},
data() {
return {
activeItem: 0,
filterResult: '',
filters: [
{ name: 'All', type: 'all' },
{ name: 'Holidays', type: 'holiday' },
{ name: 'Seasons', type: 'season' },
{ name: 'Events', type: 'custom' }
]
}
},
methods: {
emitFilter() {
this.filterResult = this.filter
EventBus.$emit('filter-catagories', this.filterResult)
},
selectItem(index) {
this.activeItem = index
}
}
}
</script>
My button component is used in a filters component
<template>
<div>
<span>filters</span>
<FilterBtn />
</div>
</div>
</template>
<script>
import FilterBtn from '#/components/FilterBtn'
export default {
components: {
FilterBtn
}
// data() {
// return {
// filters: [
// { name: 'All', type: 'all' },
// { name: 'Holidays', type: 'holiday' },
// { name: 'Seasons', type: 'season' },
// { name: 'Events', type: 'custom' }
// ]
// }
// }
}
</script>
As you can see, the commented section is where I had my filters originally, but I had to move them to the button component in order to add the active class.
I'm assuming you were actually trying to access the filter iterator of v-for="(filter, index) in filters" from within emitFilter(). For this to work, you'd need to pass the filter itself in your #click binding:
<button v-for="(filter, index) in filters"
#click="emitFilter(filter)">
Then, your handler could use the argument directly:
export default {
methods: {
emitFilter(filter) {
this.filterResult = filter
//...
}
}
}
You are passing a prop called filter typed string to your component. When you output {{ filter.name }} you are actually referring to this property instead of the variable filter you create within the v-for loop.
Unless you passed a property called "filter" to your component, this property will be undefined. Therefore outputting filter.name will result in this error message.
Yea you dont pass an prop to your component thats why its undefined.
<FilterBtn filter="test" />
Here i pass an prop named filter with the value of test.
Sure you could pass dynamic props. Just bind it.
<FilterBtn :filter="yourData" />
I need to ask: Are you passing an object or an string?
Because you defined your prop to be a string, but you actually use it as an object
{{ filter.name }}
Maybe you should also set the type to Object.
props: {
filter: { type: Object }
},

Building a form with Vuejs. Pass data from children to parent

I'm trying to build a form using "v-for" for input component and then generate a pdf file with PDFMake using data from inputs. But I didn't know how to pass the data from input component back to parent.
I read a lot of topics, but can't find a way to do this.
Here is short code without additional inputs, checkboxes etc. I plan to use around 15 inputs with different parameters to generate final PDF. Some of parameters also will be used to change final data depending of conditional statements.
Everything is work fine if code in one file, without loop and components. But not now.
Here is parent:
<template lang="pug">
.form
Input(v-for="data in form.client_info" v-bind:key="data.id" v-bind:data="data")
button(#click="pdfgen") Download PDF
</template>
<script>
export default {
components: {
Input: () => import('#/components/items/form/input')
},
data() {
return {
client_name: '',
client_email: '',
form: {
client_info: [
{id:'client_name', title:'Name'},
{id:'client_email', title: 'Email'},
{id:'foo', title: 'foo'}
],
}
}
},
methods: {
pdfgen: function () {
var pdfMake = require('pdfmake/build/pdfmake.js')
if (pdfMake.vfs == undefined) {
var pdfFonts = require('pdfmake/build/vfs_fonts.js')
pdfMake.vfs = pdfFonts.pdfMake.vfs;
}
if (this.foo) {
var foo = [
'Foo: ' + this.foo
];
} else {
foo = ''
]
}
var docDefinition = {
content: [
'Name: ' + this.client_name,
'Email: ' + this.client_email,
'\n',
foo
]
}
pdfMake.createPdf(docDefinition).download('Demo.pdf');
}
}
}
</script>
Here is a children (Input component):
<template lang="pug">
label.form_item
span.form_item_title {{ data.title }}
input.form_item_input(:v-model="data.id" type="text")
</template>
<script>
export default {
props: ['data']
}
</script>
Any ideas how to make it work?
You'll want to use a method that vue has build-in named $emit().
Before going into how to do that, a quick explanation. Because vue attempts to make data flow one-directional there is not a super quick way to just pass data back to a parent. What Vue proposes instead is to pass a method to the child component that, when called, will 'emit' the value it changed to it's parent and the parent can then do what it wants with that value.
So, in your parent component you'll want to add a method that will handle a change when the child emits. This could look something like:
onChildValueChanged(value){ this.someValue = value }
The value we passed to the function will be coming from our child component. We will need to define in our child component what this function should do. In your child component you could have a function that looks like so:
emitValueChange(event){ this.$emit('childFunctionCall', this.someChildValue) }
Next we need to tie those two functions together by adding an attribute on our child template. In this example that might look like:
<Child :parentData="someData" v-on:childFunctionCall="onChildValueChanged"></Child>
What that above template is doing is saying that when the function on:childFunctionCall is 'emited' then our function in the parent scope should fire.
Finally, in the child template we just need to add some event that calls out emiter. That could look like:
<button v-on:click="emitToParent">This is a button</button>
So when our button is clicked, the emiter is called. This triggers the function in our child component named 'emitToParent' which in turn calls the function we passed to our child component.
You'll have to tailor your use case to match the exam
I found a solution using Vuex.
So now my components look like this.
Here is parent:
<template lang="pug">
.form
Input(v-for="data in formClient" v-bind:key="data.id" v-bind:data="data")
button(#click="pdfgen") Download PDF
</template>
<script>
export default {
components: {
Input: () => import('#/components/items/form/input'),
store: () => import('#/store'),
},
computed: {
formClient() { return this.$store.getters.client }
}
}
</script>
Here is a children (Input component):
<template lang="pug">
label.form_item
span.form_item_title {{ data.title }}
input.form_item_input(v-model="data.value" :type="data.input_type")
</template>
<script>
export default {
props: ['data'],
computed: {
form: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
}
</script>
Here is a store module:
<script>
export default {
actions: {},
mutations: {},
state: {
form: {
client: [
{id:'client_name', title:'Name', value: ''},
{id:'client_email', title: 'Email', value: ''},
{id:'foo', title: 'foo', value: ''}
]
}
},
getters: {
client: state => {
return state.form.client;
}
}
}
</script>
Now I can read updated data from store directly from PDFMake function.

Get v-model value in child component Vue

I am using a Vue2Editor in my Vuetify App. I have made a component of text-editor as:
<vue-editor
:value="text"
#input="updateText"
></vue-editor>
And it's props are:
props: {
text: {
type: String,
required: true
}
},
For the validation, I am calling it in the parent component and giving it v-model (VeeValidate requires it):
<text-editor
:text="UnitData.Details"
v-model="UnitData.Details"
#updateText="UnitData.Details = $event"
data-vv-name="details"
v-validate="'required|min:100'"
/>
Now look, text and v-model have same values, I need to get v-model in my child component (used vModel prop but not worked), so that I don't end up with duplicate code, any suggestions?
text-editor component:
<vue-editor
:value="value"
#input="updateText"
></vue-editor>
props: {
value: {
type: String,
required: true
}
},
methods: {
updateText () {
this.$emit('input', this.value)
}
}
parent
<text-editor
v-model="UnitData.Details"
/>
See https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

Categories

Resources