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

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.

Related

Click Button in Parent, get data from Child to Parent and use it in a method (Vue 3)

I'm working with Vue3 and Bootstrap 5.
MY PROBLEM: I want to click a button in my parent.vue. And after clicking this I want to have the data from my child.vue inside of the method in my parent.vue - method .
But my data is always empty, except I need another ```setTimeout"-function. But actually I don't want to use it.
I think there is a better solution for the props Boolean as well..
If there are any question left regarding my problem, please ask me!
Thanks for trying helping me out!
PARENT:
<template>
<Child :triggerFunc="triggerFunc" #childData="childData"/>
<button type="button" class="btn btn-success" #click="get_data()">Get Data</button>
</template>
<script>
export default {
data() {
return {
triggerFunc: false,
c_data: [],
}
},
methods: {
childData(data) {
this.c_data = data;
},
get_data() {
this.triggerFunc = true;
setTimeout(() => {
this.triggerFunc = false;
}, 50);
console.log(this.c_data);
//HERE I WANT TO USE "C_DATA" BUT OF COURSE IT's EMPTY. WITH ANOTHER SET_TIMEOUT IT WOULD WORK
//BUT I DON'T WANT TO USE IT. BUT WITHOUT IT'S EMPTY.
//LIKE THIS IT WOULD WORK BUT I DON'T WANT IT LIKE THAT
setTimeout(() => {
console.log(this.c_data);
}, 50);
}
},
}
</script>
CHILD:
<template>
<!-- SOME BUTTONS, INPUTS, ETC. IN HERE -->
</template>
<script>
export default {
data() {
return {
input1: "",
input2: "",
}
},
props: {
triggerFunc: Boolean,
},
triggerFunc(triggerFunc) {
if(triggerFunc == true) {
this.save_data()
}
}
methods: {
save_data() {
var data = [
{
Input1: this.input1,
Input2: this.input2
},
]
this.$emit("childData", data);
},
},
}
</script>
Parent can very well hold/own the data of it's children. In that case, the children only render/display the data. Children need to send events up to the parent to update that data. (Here parent is the Key component and child is a Helper for the parent.)
So, here parent always has the master copy of the child's data in its own data variables.
Also, you are using # for binding properties, which is wrong. # is for event binding. For data binding use ':' which is a shorthand for v-bind:
You can just say :childData=c_data
PS: You seem to be getting few of the basics wrong. Vue is reactive and automatically binds the data to the variables. So, you don't have to do this much work. Please look at some basic Vue examples.
Refer: https://sky790312.medium.com/about-vue-2-parent-to-child-props-af3b5bb59829
Edited code:
PARENT:
<template>
<Child #saveClick="saveChildData" :childData="c_data" />
</template>
<script>
export default {
data() {
return {
c_data: [{Input1:"", Input2:""}]
}
},
methods: {
saveChildData(incomingData) {
//Either set the new value, or copy all elements.
this.c_data = incomingData;
}
},
}
</script>
CHILD:
<template>
<!-- SOME BUTTONS, INPUTS, ETC. IN HERE -->
<!-- Vue will sync data to input1, input2. On button click we can send data to parent. -->
<button #click.prevent="sendData" />
</template>
<script>
export default {
props:['childData'],
data() {
return {
input1: "",
input2: "",
}
},
methods: {
sendData() {
var data = [
{
Input1: this.input1,
Input2: this.input2
},
]
this.$emit("saveClick", data); //Event is "saveClick" event.
},
},
beforeMount(){
//Make a local copy
this.input1 = this.childData[0].Input1;
this.input2 = this.childData[0].Input2;
}
}
</script>

How to pass object's attribute as prop to vue.js component

Given the following component:
<script>
export default {
name: 'MyComponent',
props: {
blueprint: {
type: Object,
default () {
return {
attribute: 0,
otherAttribute: 5
}
}
}
},
data () {
return {
attribute: this.blueprint.attribute,
otherAttribute: this.blueprint.otherAttribute
}
}
}
</script>
I want to use the blueprint prop to populate data fields with some default values which could also be defined when using the component.
But how can I pass only one field of prop blueprint?
When I do this:
<my-component :blueprint="{attribute: someVar}" />
the otherAttribute of the default blueprint will be gone, of course.
Can I only set the one field of the prop and merge it with the default of the other, something like this:
<my-component :blueprint.attribute="someVar" />
<!-- It doesn't work like this, but maybe you get the idea what I want -->
Sadly the blueprint prop has too many fields to pass each field on it's own.
yeah, your Answer is fine. Here is my solution
<script>
export default {
name: 'MyComponent',
props: {
blueprint: {
type: Object
}
},
data () {
return {
blueprintDefault: {
attribute: 0,
otherAttribute: 5
}
}
},
mounted () {
this.blueprint = {...this.blueprintDefault, ...this.blueprint}
}
}
</script>
I found a solution that seems to work for me. My component now looks like this:
<script>
export default {
name: 'MyComponent',
props: {
blueprint: {
type: Object
}
},
data () {
return {
attribute: this.blueprint.attribute ?? 0,
otherAttribute: this.blueprint.otherAttribute ?? 5
}
}
}
</script>
I removed the default part of the prop and now set the default values directly in the data instead. That way if my blueprint prop does not include all attributes, the other default values will still be there.

Is it possible to add content to a global Vue component from a single file comp?

I have made a global component that will render the content we want.
This component is very simple
<template>
<section
id="help"
class="collapse"
>
<div class="container-fluid">
<slot />
</div>
</section>
</template>
<script>
export default {
name: 'VHelp',
};
</script>
I use it inside my base template with
<v-help />
I'm trying to add content to this component slot from another single file component using.
<v-help>
<p>esgssthsrthsrt</p>
</v-help>
But this logically create another instance of my comp, with the p tag inside. Not the correct thing I want to do.
So I tried with virtual DOM and rendering function, replacing slot by <v-elements-generator :elements="$store.state.help.helpElements" /> inside my VHelp comp.
The store helpElements is a simple array with objects inside.
{
type: 'a',
config: {
class: 'btn btn-default',
},
nestedElements: [
{
type: 'span',
value: 'example',
},
{
type: 'i',
},
],
},
Then inside my VElementsGenerator comp I have a render function that with render element inside virtual DOM from an object like
<script>
import {
cloneDeep,
isEmpty,
} from 'lodash';
export default {
name: 'VElementsGenerator',
props: {
elements: {
type: Array,
required: true,
},
},
methods: {
iterateThroughObject(object, createElement, isNestedElement = false) {
const generatedElement = [];
for (const entry of object) {
const nestedElements = [];
let elementConfig = {};
if (typeof entry.config !== 'undefined') {
elementConfig = cloneDeep(entry.config);
}
if (entry.nestedElements) {
nestedElements.push(this.iterateThroughObject(entry.nestedElements, createElement, true));
}
generatedElement.push(createElement(
entry.type,
isEmpty(elementConfig) ? entry.value : elementConfig,
nestedElements
));
if (typeof entry.parentValue !== 'undefined') {
generatedElement.push(entry.parentValue);
}
}
if (isNestedElement) {
return generatedElement.length === 1 ? generatedElement[0] : generatedElement;
}
return createElement('div', generatedElement);
},
},
render(createElement) {
if (this.elements) {
return this.iterateThroughObject(this.elements, createElement);
}
return false;
},
};
</script>
This second method is working well but if I want to render complex data, the object used inside the rendering function is very very long and complex to read.
So I'm trying to find another way to add content to a global component used inside a base layout only when I want it on a child component.
I can't use this VHelp component directly inside children comps because the HTML page architecture will be totally wrong.
I'm wondering if this is possible to add content (preferably HTML) to a component slot from a single file comp without re-creating a new instance of the component?
Furthermore I think this is very ugly to save HTML as string inside a Vuex store. So I don't even know if this is possible and if I need to completely change the way I'm trying to do this.
Any ideas ?
In the store, you should only store data and not an HTML structure. The way to go with this problem would be to store the current state of the content of the v-help component in the store. Then, you would have a single v-help component with a slot (like you already proposed). You should pass different contents according to the state in the store. Here is an abstract example:
<v-help>
<content-one v-if="$store.state.content === 'CONTENT_ONE' />
<content-two v-else-if="$store.state.content === 'CONTENT_TWO' />
<content-fallback v-else />
</v-help>
Child element somewhere else:
<div>
<button #click="$store.commit('setContentToOne')">Content 1</button>
</div>
Vuex Store:
state: {
content: null
},
mutations: {
setContentToOne(state) {
state.content = 'CONTENT_ONE';
}
}
Of course it depends on your requirements and especially on how many different scenarios are used if this is the best way to achieve this. If I understood you correctly, you are saving help elements to the store. You could also save an array of currently selected help elements in there and just display them directly in the v-help component.
EDIT:
Of course you can also just save the static component (or its name) in the store. Then, you could dynamically decide in the child components, which content is shown in v-help. Here is an example:
<v-help>
<component :is="$store.state.helpComponent" v-if="$store.state.helpComponent !== null" />
</v-help>
Test Component:
<template>
test component
</template>
<script>
export default {
name: 'test-component'
};
</script>
Child element somewhere else (variant 1, storing the name in Vuex):
<div>
<button #click="$store.commit('setHelpComponent', 'test-component')">Set v-help component to 'test-component'</button>
</div>
Child element somewhere else (variant 2, storing the whole component in Vuex):
<template>
<button #click="$store.commit('setHelpComponent', testComponent)">Set v-help component to testComponent (imported)</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponent() {
return TestComponent;
}
}
};
</script>
Child element somewhere else (variant 3, storing the name, derived from the imported component, in Vuex; I would go with this variant):
<template>
<button #click="$store.commit('setHelpComponent', testComponentName)">Set v-help component to 'test-component'</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponentName() {
return TestComponent.name;
}
}
};
</script>
Vuex Store:
state: {
helpComponent: null
},
mutations: {
setHelpComponent(state, value) {
state.helpComponent = value;
}
}
See also the documentation for dynamic components (<component :is=""> syntax): https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components

vuejs passing array of checkboxes to parent template is only passing one value

I looked at potential dupes of this, such as this one and it doesn't necessarily solve my issue.
My scenario is that I have a component of orgs with label and checkbox attached to a v-model. That component will be used in combination of other form components. Currently, it works - but it only passes back one value to the parent even when both checkboxes are click.
Form page:
<template>
<section>
<h1>Hello</h1>
<list-orgs v-model="selectedOrgs"></list-orgs>
<button type="submit" v-on:click="submit">Submit</button>
</section>
</template>
<script>
// eslint-disable-next-line
import Database from '#/database.js'
import ListOrgs from '#/components/controls/list-orgs'
export default {
name: 'CreateDb',
data: function () {
return {
selectedOrgs: []
}
},
components: {
'list-orgs': ListOrgs,
},
methods: {
submit: function () {
console.log(this.$data)
}
}
}
</script>
Select Orgs Component
<template>
<ul>
<li v-for="org in orgs" :key="org.id">
<input type="checkbox" :value="org.id" name="selectedOrgs[]" v-on:input="$emit('input', $event.target.value)" />
{{org.name}}
</li>
</ul>
</template>
<script>
import {db} from '#/database'
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
}
},
mounted () {
this.populateOrgs(this)
}
}
</script>
Currently there are two fake orgs in the database with an ID of 1 and 2. Upon clicking both checkboxes and clicking the submit button, the selectedOrgs array only contains 2 as though the second click actually over-wrote the first. I have verified this by only checking one box and hitting submit and the value of 1 or 2 is passed. It seems that the array method works at the component level but not on the component to parent level.
Any help is appreciated.
UPDATE
Thanks to the comment from puelo I switched my orgListing component to emit the array that is attached to the v-model like so:
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: [],
selectedOrgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
},
updateOrgs: function () {
this.$emit('updateOrgs', this.$data.selectedOrgs)
}
},
mounted () {
this.populateOrgs(this)
}
}
Then on the other end I am merely console.log() the return. This "works" but has one downside, it seems that the $emit is being fired before the value of selectedOrgs has been updated so it's always one "check" behind. Effectively,I want the emit to wait until the $data object has been updated, is this possible?
Thank you so much to #puelo for the help, it helped clarify some things but didn't necessarily solve my problem. As what I wanted was the simplicity of v-model on the checkboxes populating an array and then to pass that up to the parent all while keeping encapsulation.
So, I made a small change:
Select Orgs Component
<template>
<ul>
<li v-for="org in orgs" :key="org.id">
<input type="checkbox" :value="org.id" v-model="selectedOrgs" name="selectedOrgs[]" v-on:change="updateOrgs" />
{{org.name}}
</li>
</ul>
</template>
<script>
import {db} from '#/database'
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
},
updateOrgs: function () {
this.$emit('updateOrgs', this.$data.selectedOrgs)
}
},
mounted () {
this.populateOrgs(this)
}
}
</script>
Form Page
<template>
<section>
<h1>Hello</h1>
<list-orgs v-model="selectedOrgs" v-on:updateOrgs="updateSelectedOrgs"></list-orgs>
<button type="submit" v-on:click="submit">Submit</button>
</section>
</template>
<script>
// eslint-disable-next-line
import Database from '#/database.js'
import ListOrgs from '#/components/controls/list-orgs'
export default {
name: 'CreateDb',
data: function () {
return {
selectedOrgs: []
}
},
components: {
'list-orgs': ListOrgs
},
methods: {
updateSelectedOrgs: function (org) {
console.log(org)
},
submit: function () {
console.log(this.$data)
}
}
}
</script>
What the primary change here is I now fire a method of updateOrgs when the checkbox is clicked and I pass the entire selectedOrgs array via the this.$emit('updateOrgs', this.$data.selectedOrgs)`
This takes advantage of v-model maintaining the array of whether they're checked or not. Then on the forms page I simply listen for this event on the component using v-on:updateOrgs="updateSelectedOrgs" which contains the populated array and maintains encapsulation.
The documentation for v-model in form binding still applies to custom components, as in:
v-model is essentially syntax sugar for updating data on user input
events...
https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage and
https://v2.vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model
So in your code
<list-orgs v-model="selectedOrgs"></list-orgs>
gets translated to:
<list-orgs :value="selectedOrgs" #input="selectedOrgs = $event.target.value"></list-orgs>
This means that each emit inside v-on:input="$emit('input', $event.target.value) is actually overwriting the array with only a single value: the state of the checkbox.
EDIT to address the comment:
Maybe don't use v-model at all and only listen to one event like #orgSelectionChange="onOrgSelectionChanged".
Then you can emit an object with the state of the checkbox and the id of the org (to prevent duplicates):
v-on:input="$emit('orgSelectionChanged', {id: org.id, state: $event.target.value})"
And finally the method on the other end check for duplicates:
onOrgSelectionChanged: function (orgState) {
const index = selectedOrgs.findIndex((org) => { return org.id === orgState.id })
if (index >= 0) selectedOrgs.splice(index, 1, orgState)
else selectedOrgs.push(orgState)
}
This is very basic and not tested, but should give you an idea of how to maybe solve this.

Updating VueJS component data attributes when prop updates

I'm building a VueJS component which needs to update the data attributes when a prop is updated however, it's not working as I am expecting.
Basically, the flow is that someone searches for a contact via an autocomplete component I have, and if there's a match an event is emitted to the parent component.
That contact will belong to an organisation and I pass the data down to the organisation component which updates the data attributes. However it's not updating them.
The prop being passed to the organisation component is updated (via the event) but the data attibute values is not showing this change.
This is an illustration of my component's structure...
Here is my code...
Parent component
<template>
<div>
<blink-contact
:contact="contact"
v-on:contactSelected="setContact">
</blink-contact>
<blink-organisation
:organisation="organisation"
v-on:organisationSelected="setOrganisation">
</blink-organisation>
</div>
</template>
<script>
import BlinkContact from './BlinkContact.vue'
import BlinkOrganisation from './BlinkOrganisation.vue'
export default {
components: {BlinkContact, BlinkOrganisation},
props: [
'contact_id', 'contact_name', 'contact_tel', 'contact_email',
'organisation_id', 'organisation_name'
],
data () {
return {
contact: {
id: this.contact_id,
name: this.contact_name,
tel: this.contact_tel,
email: this.contact_email
},
organisation: {
id: this.organisation_id,
name: this.organisation_name
}
}
},
methods: {
setContact (contact) {
this.contact = contact
this.setOrganisation(contact.organisation)
},
setOrganisation (organisation) {
this.organisation = organisation
}
}
}
</script>
Child component (blink-organisation)
<template>
<blink-org-search
field-name="organisation_id"
:values="values"
endpoint="/api/v1/blink/organisations"
:format="format"
:query="getQuery"
v-on:itemSelected="setItem">
</blink-org-search>
</template>
<script>
export default {
props: ['organisation'],
data() {
return {
values: {
id: this.organisation.id,
search: this.organisation.name
},
format: function (items) {
for (let item of items.results) {
item.display = item.name
item.resultsDisplay = item.name
}
return items.results
}
}
},
methods: {
setItem (item) {
this.$emit('organisationSelected', item)
}
}
}
</script>
How can I update the child component's data properties when the prop changes?
Thanks!
Use a watch.
watch: {
organisation(newValue){
this.values.id = newValue.id
this.values.search = newValue.name
}
}
In this case, however, it looks like you could just use a computed instead of a data property because all you are doing is passing values along to your search component.
computed:{
values(){
return {
id: this.organisation.id
search: this.organisation.name
}
}
}

Categories

Resources