Passing dynamic functions from parent component to grandchild component Angular - javascript

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

Related

Pass dynamic and responsive props to dynamic components with v-bind! NuxtJS

Long story short(maybe it is not so short after all): on the same page I want to import and load dynamic components based on the selected module.
I have an object defined in the assets which contains the informations about the components that should be loaded for each module, it looks like this:
export const modules = {
module1: {
calls: {...},
components: [
{
url: 'shared/PreviewItem',
properties: [
{
name: 'width',
value: 'leftComponentWidth'
}
]
},
{
url: 'shared/ResizingDivider',
properties: []
},
{
url: 'forms/FormItem',
properties: [
{
name: 'width',
value: 'rightComponentWidth'
},
{
name: 'item',
value: 'item'
}
]
}
]
},
module2: {...}
}
Then I have my index page:
<template>
<div class="item-content">
<component
:is="component"
v-for="(component, i) in dataComponents"
:key="i"
v-bind="component.propertiesToPass"
#emit-action="emitAction($event)"
/>
</div>
</template>
<script>
export default {
data() {
return {
item: null,
rightComponentWidth: 50,
leftComponentWidth: 50,
dataComponents: []
}
},
created() {
this.importComponents()
},
methods: {
importComponents() {
this.dataComponents = []
modules[this.$route.params.module].components.forEach(
(component) => {
import(`~/components/${component.url}`).then((res) => {
res.default.propertiesToPass = []
component.properties.forEach((prop) => {
res.default.propertiesToPass.push({
[prop.name]: this[prop.value]
})
})
this.dataComponents.push(res.default)
})
}
)
},
emitAction(event) {
this[event.function](event.props)
},
changeComponentsWidth(event) {
this.leftComponentWidth -= event
this.rightComponentWidth = 100 - this.leftComponentWidth
}
}
}
}
</script>
As it is probably easy to understand I have to components and one divider between them that can be dragged to the right or to the left for resize the width of the other two components.
The components are getting loaded and imported correctly, and the props are passed right, so the width of both of the components in the start are 50 50.
The issue is that by doing [prop.name]: this[prop.value] I am setting the props to the value of this[prop.value] variable, and not to the variable itself, so, when I try to resize the components by using the divider, the variables get updated but the props get not.
Then the props are not responsive or reactive, are fixed.
The only way to update the props of the components is to add the following lines to the changeComponentsWidth() method:
this.dataComponents[0].propertiesToPass[0].width = this.leftComponentWidth
this.dataComponents[2].propertiesToPass[0].width = this.rightComponentWidth
But this is not a very dynamic way.
So My question is:
Is it possible to bind the props to the variable itself instead of just passing its value?
Or are there other "dynamic" ways to keep my props "responsive and reactive"?

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 }
},

passing function as prop in vuejs

So i am creating a base "TableComponent", with selectable rows etc. This TableComponent takes a prop called "buttons".
The TableComponent expects the buttons to be an array of objects like this:
buttons: [{
label: 'Manage Themes',
click: () => {
if(this.selectedRows.length) {
this.$router.push({ name: 'ManagePageOptions', query: { concernable_type: this.type, concernable_ids: this.selectedRows.map(row => row.id).join(',') }});
} else {
alert('Please select 1 or more company or user');
}
}
}]
And so in the table component, a row of buttons is created based on what is passed like so:
<b-button v-for="button in this.buttons" #click="button.click" :key="button.label"> {{ button.label }} </b-button>
Problem is that in the buttons click function, "this" refers to the parent component, and so i don't have access to the selected rows etc.
EDIT:
I have found that just passing "this" to the click function solves it. Not sure if it is wise though.
buttons = [{
label: 'Manage Pages',
click: (vm) => {
if(vm.selectedRows.length) {
vm.$router.push({ name: 'ManagePageOptions', query: { concernable_type: vm.type, concernable_ids: vm.selectedRows.map(row => row.id).join(',') }});
} else {
alert(Lang('Please select 1 or more company or user'))
}
}
}];
TableComponent data:
data() {
return {
selectedRows: [],
m_this: this,
};
},
_
<b-button v-for="button in this.buttons" #click="button.click(m_this)" :key="button.label"> {{ button.label }} </b-button>
I agree with comments that there are better ways to do this, scoped slots is probably the best....
But, if you really want to pass functions like that you need:
Change handler definition from click: () => {} to click: function() {} - reason is that this works completely different in arrow functions and as a result this is bound to the parent Vue component
When passing handler into button component (inside TableComponent template), bind the handler to this of your TableComponent as described here

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.

How to render components dynamically in Vue JS?

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'}"/>

Categories

Resources