I've tried a few different things and now must ask for some help (thanks in advance).
I'm creating a simple Vue js app where a component form emits data as a object (this works) to its parent.
the parent then runs a v-for loop and passes the Object data to a component which displays the data (this does not work).
Vue Version
($ vue --version
#vue/cli 4.4.1)
Parent code:
<template>
<div class="MEDS">
<goalForm #goal_data="goal_create($event)"/>
<section>
<div v-for="(goalItem, index) in this.goals_list_container" v-bind:key="index">
<goalItem :goal="goalItem"/>
</div>
</section>
</div>
</template>
// # is an alias to /src
import goalForm from '#/components/Goal_form.vue'
import goalItem from '#/components/Goal_item.vue'
export default {
name: 'MEDS',
components: {
goalForm,
goalItem
},
data(){
return{
// Recievs goals from goalForm on submit, this is an array of objects
goals_list_container: [],
}
},
methods: {
goal_create(payload){
this.goals_list_container.push(payload);
console.log(this.goals_list_container)
}
}
}
GOAL ITEM (that should display the goal)
<template>
<div>
<h3>{{goal.title}}</h3>
</div>
</template>
export default {
prop: ['goal'],
data(){
return {
goal: {}
}
},
watch: {
}
}
Goal_form: (EDITED)
<div>
<form action="" class="goalForm" #submit.prevent="emit_Goal">
<input type="text" name="title" id="title" v-model="title">
<input type="text" name="description" id="description" v-model="des">
<input type="date" name="dueDate" id="dueDate" v-model="dueDate">
<input type="number" name="priority" id="priority" v-model="priority">
<input type="submit" class="submit">
</form>
</div>
</template>
<script>
export default {
data() {
return {
title: null,
des: null,
dueDate: null,
priority: null,
goal_data: {}
}
},
methods: {
push_goal(){
this.goal_data.title = this.title
this.goal_data.des = this.des
this.goal_data.dueDate = this.dueDate
this.goal_data.priority = this.priority
},
emit_Goal(){
// Move goal details into Object to be Emited
this.push_goal()
// Emit the form to the parent on submit
this.$emit('goal_data', this.goal_data)
}
}
}
</script>
<style lang="scss" scoped>
form {
display: flex;
flex-direction: column;
input {
&[name="title"]{
}
}
}
</style>
The for loop seems to work as each submit of the Goal_form creates a new instance of the Goal_item component.... But it does not populate with any data.
I Am either getting this totally wrong or have missed something small but any help would be greatly appreciated -
W
Related
how can I pass class attribute from parent to child component element?
Look here:
https://codesandbox.io/s/pedantic-shamir-1wuuv
I'm adding class to the Component "Input Field"
And my goal is that the 'custom-class' will be implemented on the 'input' element in the child component
But just still using the class attribute, and not setting a new prop like "customClass" and accept it in the props of the component
Thanks!
This depends on the template structure of your ChildComponent
Your Parent Component can look like this:
<div id="app">
<InputField class="anyClass" />
</div>
If your Child looks like this:
<template>
<input ... />
</template
Because if you have only 1 root Element in your template this will inherit the classes given automatically.
if your Template e.g. looks like this: (only available in vue3)
<template>
<input v-bind="$attrs" />
<span> hi </span>
</template
You will need the v-bind="$attrs" so Vue knows where to put the attributes to. Another Solution would be giving classes as props and assigning it to the element with :class="classes"
The pattern for the customs form component in Vue 2 where the props go to a child element looks something like this.
<template>
<div class="input-field">
<label v-if="label">{{ label }}</label>
<input
:value="value"
:class="inputClass"
#input="updateValue"
v-bind="$attrs"
v-on="listeners"
/>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
label: {
type: String,
default: "",
},
value: [String, Number],
inputClass: {
type: String,
default: "",
},
},
computed: {
listeners() {
return {
...this.$listeners,
input: this.updateValue,
};
},
},
methods: {
updateValue(event) {
this.$emit("input", event.target.value);
},
},
};
</script>
The usage of those components could look something like this.
```html
<template>
<div id="app">
<InputField
v-model="name"
label="What is your name?"
type="text"
class="custom"
inputClass="input-custom"
/>
<p>{{ name }}</p>
</div>
</template>
<script>
import InputField from "./components/InputField";
export default {
name: "App",
components: {
InputField,
},
data() {
return {
name: "",
};
},
};
</script>
A demo is available here
https://codesandbox.io/s/vue-2-custom-input-field-4vldv?file=/src/components/InputField.vue
You need to use vue props . Like this:
child component:
<template>
<div>
<input :class="className" type="text" value="Test" v-bind="$attrs" />
</div>
</template>
<script>
export default {
props:{
className:{
type:String,
default:'',
}
}
};
</script>
Parent component:
<div id="app">
<InputField :class-name="'custom-class'" />
</div>
Since you're using vue2 the v-bind=$attrs is being hooked to the root element of your component the <div> tag. Check the docs. You can put this wrapper on the parent element if you need it or just get rid of it.
Here is a working example
Another approach
There is also the idea of taking the classes from the parent and passing it to the child component with a ref after the component is mounted
Parent Element:
<template>
<div id="app">
<InputField class="custom-class" ref="inputField" />
</div>
</template>
<script>
import InputField from "./components/InputField";
export default {
name: "App",
components: {
InputField,
},
mounted() {
const inputField = this.$refs.inputField;
const classes = inputField.$el.getAttribute("class");
inputField.setClasses(classes);
},
};
</script>
Child Element:
<template>
<div>
<input type="text" value="Test" :class="classes" />
</div>
</template>
<script>
export default {
data() {
return {
classes: "",
};
},
methods: {
setClasses: function (classes) {
this.classes = classes;
},
},
};
</script>
Here a working example
In Vue2, the child's root element receives the classes.
If you need to pass them to a specific element of your child component, you can read those classes from the root and set them to a specific element.
Example of a vue child component:
<template>
<div ref="root">
<img ref="img" v-bind="$attrs" v-on="$listeners" :class="classes"/>
</div>
</template>
export default {
data() {
return {
classes: null,
}
},
mounted() {
this.classes = this.$refs.root.getAttribute("class")
this.$refs.root.removeAttribute("class")
},
}
Pretty much it.
In App.vue I have a button to open a component . Inside the component BigForm, I also have a button to open a component named, . But as I open the component. Vue re-render the page with the warning:
The "data" option should be a function that returns a per-instance value in component definitions.
App.Vue
<template>
<div id="app">
<button #click="showModal()">Show</button>
<BigForm v-if="modal" />
</div>
</template>
<script>
import BigForm from "./components/BigForm";
export default {
name: "App",
components: {
BigForm,
},
data() {
return {
modal: false,
};
},
methods: {
showModal() {
this.modal = !this.modal;
},
},
};
</script>
BigForm.vue:
<template>
<div class="hello">
<form style="height: 300px; width: 300px; background-color: green">
<button #click="showSmallForm()">Show small form</button>
<SmallForm
v-if="toggleSmallForm"
style="width: 100px; height: 100px; background-color: yellow"
/>
</form>
</div>
</template>
<script>
import SmallForm from "./SmallForm";
export default {
name: "HelloWorld",
components: {
SmallForm,
},
data() {
return {
toggleSmallForm: false,
};
},
methods: {
showSmallForm() {
this.toggleSmallForm = !this.toggleSmallForm;
},
},
};
</script>
And SmallForm.vue:
<template>
<form>
<input placeholder="Small form input" />
<button>This is small form</button>
</form>
</template>
This is the code to my example in codesandbox:
https://codesandbox.io/s/late-cdn-ssu7f?file=/src/App.vue
The problem is not related to Vue itself but the HTML.
When you use <button> inside <form>, the default type of the button is submit (check this) - when you click the button, the form is submitted to the server causing the page to reload.
You can prevent that by explicitly setting type <button type="button"> (HTML way) or by preventing default action (Vue way) <button #click.prevent="showSmallForm()">Show small form</button> (see event modifiers)
Also note that using <form> in another <form> is not allowed in HTML
I'm new to Vue and Vuex.
I made a component with radio button, and this component is two-way bind with the store.
Now that the component is working, i would like to use it for multiple item by calling it several time, and indicates to it which item of the vuex store it should be bound to. In this way, i'll be able to loop of the array containing all item on which apply the radio button modification.
In my store, I have this array :
state {
[...other data...]
arrayX {
item1: { name : 'a', value : 'nok'}
item2: { name : 'b', value : 'ok}},
[...other data...]
This array is bind with v-model inside the component :
<template>
<div class="radio">
<input type="radio" name="a" value="nok" v-model="arrayX.item1.value" id="nok"/>
<label for="nok"> Nok </label>
</div>
<div class="radio">
<input type="radio" name="a" value="" v-model="arrayX.item1.value" id="empty"/>
<label for="empty">None</label>
</div>
<div class="radio">
<input type="radio" name="a" value="ok_but" v-model="arrayX.item1.value" id="ok_but"/>
<label for="ok_but">Ok but</label>
</div>
<div class="radio">
<input type="radio" name="a" value="ok" v-model="arrayX.item1.value" id="ok"/>
<label for="ok">Ok</label>
</div>
</div>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
name: 'SwitchColors',
computed:{
...mapFields(['arrayX' ])
}
}
</script>
I created an example scenario in my Vue 2 CLI sandbox app. Each radio button component is bound to the Vuex store array element through a computed property. I pass the store array index value as a prop from the parent to the radio component.
RadioButtonVuex.vue
<template>
<div class="radio-button-vuex">
<hr>
<div class="row">
<div class="col-md-6">
<input type="radio" value="One" v-model="picked">
<label>One</label>
<br>
<input type="radio" value="Two" v-model="picked">
<label>Two</label>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
pickedIndex: {
type: Number,
required: true
}
},
computed: {
picked: {
get() {
return this.$store.state.radioSelections[this.pickedIndex].picked;
},
set(newValue) {
this.$store.commit('updateSelection', { index: this.pickedIndex, newValue: newValue });
}
}
},
}
</script>
<style scoped>
label {
margin-left: 0.5rem;
}
</style>
Parent.vue
<template>
<div class="parent">
<h3>Parent.vue</h3>
<radio-button-vuex :pickedIndex="0" />
<radio-button-vuex :pickedIndex="1" />
</div>
</template>
<script>
import RadioButtonVuex from './RadioButtonVuex.vue'
export default {
components: {
RadioButtonVuex
}
}
</script>
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
radioSelections: [
{
picked: 'One'
},
{
picked: 'Two'
}
]
},
mutations: {
updateSelection(state, payload) {
state.radioSelections[payload.index].picked = payload.newValue;
}
}
})
A component that has a hidden input field should have a the value of the current page title. In Nuxt you can accomplish it using asyncData for all pages except for pages using Nuxt Content Module. The Nuxt Content Module does not allow asyncData but fetch is allowed but. Example:
components/Form.vue
<template>
<div>
<input
id="page"
type="hidden"
:value="article.title"
required
/>
</div>
</template>
<script>
export default {
async fetch() {
this.article = await this.$content('articles', this.params).fetch()
},
data() {
return { article: {} }
},
}
</script>
pages/articles/_slug.vue
<template>
<div>
<h1>{{ article.title }}</h1>
<Form />
</div>
</template>
<script>
export default {
async asyncData({ $content, params }) {
const article = await $content('articles', params.slug).fetch()
return { article }
},
}
</script>
This code gets no errors but the component never shows page title in the hidden input only in the h1 tag. (edit: fixed typo)
How I solved this was with props.
components/Form.vue
<template>
<form>
<label for="title"> Title </label>
<input name="title" type="hidden" :value="title" required />
<button type="submit">Send</button>
</form>
</template>
<script>
export default {
props: {
title: {
type: String,
required: false,
default: ''
},
}
}
</script>
and then in the pages/_slug.vue
<template>
<div>
<nuxt-content :document="doc" />
<Form :title="doc.title" />
</div>
</template>
This should work for you.
Hi I'm trying to send data from one component to another but not sure how to approach it.
I've got one component that loops through an array of items and displays them. Then I have another component that contains a form/input and this should submit the data to the array in the other component.
I'm not sure on what I should be doing to send the date to the other component any help would be great.
Component to loop through items
<template>
<div class="container-flex">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Name</p>
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in entries">
{{ entry.name }}
</li>
</ul>
</div>
<add-entry />
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
export default {
name: 'entry-list',
components: {
addEntry
},
data: function() {
return {
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
}
}
</script>
Component for adding / sending data
<template>
<div
class="entry-add"
v-bind:class="{ 'entry-add--open': addEntryIsOpen }">
<input
type="text"
name="addEntry"
#keyup.enter="addEntries"
v-model="newEntries">
</input>
<button #click="addEntries">Add Entries</button>
<div
class="entry-add__btn"
v-on:click="openAddEntry">
<span>+</span>
</div>
</div>
</template>
<script>
export default {
name: 'add-entry',
data: function() {
return {
addEntryIsOpen: false,
newEntries: ''
}
},
methods: {
addEntries: function() {
this.entries.push(this.newEntries);
this.newEntries = '';
},
openAddEntry() {
this.addEntryIsOpen = !this.addEntryIsOpen;
}
}
}
</script>
Sync the property between the 2:
<add-entry :entries.sync="entries"/>
Add it as a prop to the add-entry component:
props: ['entries']
Then do a shallow merge of the 2 and emit it back to the parent:
this.$emit('entries:update', [].concat(this.entries, this.newEntries))
(This was a comment but became to big :D)
Is there a way to pass in the key of name? The entry gets added but doesn't display because im looping and outputting {{ entry.name }}
That's happening probably because when you pass "complex objects" through parameters, the embed objects/collections are being seen as observable objects, even if you sync the properties, when the component is mounted, only loads first level data, in your case, the objects inside the array, this is performance friendly but sometimes a bit annoying, you have two options, the first one is to declare a computed property which returns the property passed from the parent controller, or secondly (dirty and ugly but works) is to JSON.stringify the collection passed and then JSON.parse to convert it back to an object without the observable properties.
Hope this helps you in any way.
Cheers.
So with help from #Ohgodwhy I managed to get it working. I'm not sure if it's the right way but it does seem to work without errors. Please add a better solution if there is one and I'll mark that as the answer.
I follow what Ohmygod said but the this.$emit('entries:update', [].concat(this.entries, this.newEntries)) didn't work. Well I never even need to add it.
This is my add-entry.vue component
<template>
<div
class="add-entry"
v-bind:class="{ 'add-entry--open': addEntryIsOpen }">
<input
class="add-entry__input"
type="text"
name="addEntry"
placeholder="Add Entry"
#keyup.enter="addEntries"
v-model="newEntries"
/>
<button
class="add-entry__btn"
#click="addEntries">Add</button>
</div>
</template>
<script>
export default {
name: 'add-entry',
props: ['entries'],
data: function() {
return {
addEntryIsOpen: false,
newEntries: ''
}
},
methods: {
addEntries: function() {
this.entries.push({name:this.newEntries});
this.newEntries = '';
}
}
}
</script>
And my list-entries.vue component
<template>
<div class="container-flex">
<div class="wrapper">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Competition Entries</p>
</div>
<div class="entries__header__search">
<input
type="text"
name="Search"
class="input input--search"
placeholder="Search..."
v-model="search">
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in filteredEntries">
{{ entry.name }}
</li>
</ul>
</div>
<add-entry :entries.sync="entries"/>
</div>
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
import pickWinner from '#/components/pick-winner.vue'
export default {
name: 'entry-list',
components: {
addEntry,
pickWinner
},
data: function() {
return {
search: '',
entries: [
{
name: 'Geoff'
},
{
name: 'Stu'
},
{
name: 'Craig'
},
{
name: 'Mark'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredEntries() {
if(this.search === '') return this.entries
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
</script>