I've been trying to pass props to data like this, as I saw on another post:
Child Component:
props: {
idInput: { type: String, required: false },
nameInput: { type: String, required: false },
},
data() {
return {
id: this.idInput,
name: this.nameInput,
}
}
So I can use it here:
<t-input v-model="name" type="text" />
Parent Component:
data() { return { game: {} } },
beforeCreated() { this.game = { name: "myName", id: "myID" }
<ChildComponent :name-input="game.name" :id-input="game.id" />
The problem is that "name" appears to be undefined, while if I do the same but changing "name" to "nameInput" it works, but I get the Vue error telling me not to use props like that. Any ideas?
Here is a functional example I created to demonstrate this case:
const comp = Vue.component('comp', {
template: '#myComp',
props: {
idInput: { type: String, required: false },
nameInput: { type: String, required: false },
},
data() {
return {
id: this.idInput,
name: this.nameInput,
}
}
});
new Vue({
el: "#myApp",
data () {
return {
game: {}
}
},
created() {
this.game = { name: 'myName', id: 'myID' };
},
components: { comp }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="myApp">
<comp :name-input="game.name" :id-input="game.id" />
{{game}}
</div>
<template id="myComp">
<div>
{{idInput}}
<br>
<input v-model="name" type="text" />
</div>
</template>
EDIT:
After checking the code, I think the problem is that you're setting the game atrribute in the data of the parent component on beforeCreated. Set it on created instead.
EDIT
OP found another way to do it. Instead of passing props one by one, pass just 1 prop with the object and use its attributes on the v-model.
Related
I have the following vue component
<template>
<div class="box"
:data-target="dropAreaClass"
:class="{ 'js-draggable': isDraggable }"
:id="id"
:draggable="isDraggable"
#dragstart="dragStart"
#dragend="dragEnd">
{{ id }}
</div>
</template>
<script>
export default {
name: 'ActionBox',
props: {
dropAreaClass: {
default: 'js-droppable--any',
type: String,
},
id: {
default: null,
type: String,
required: true,
},
isDraggable: {
default: true,
type: Boolean,
},
},
data: () => ({
dropAreas: null,
}),
mounted() {
this.dropAreas = document.querySelectorAll(`.${this.dropAreaClass}`);
},
methods: {
dragEnd(event) {
this.dropAreas.forEach(dropArea => {
dropArea.classList.remove('drop');
});
event.currentTarget.classList.remove('dragging');
},
dragStart(event) {
this.dropAreas.forEach(dropArea => {
dropArea.classList.add('drop');
});
event.currentTarget.classList.add('dragging');
event.dataTransfer.setData('text', event.currentTarget.id);
},
},
};
</script>
This is a simple div which I can drag a drop into multiple columns in the parent component - once it is dropped in one of the target columns, the following function is fired to move the component to the column it is dropped in:
drop(event) {
const droppedElement = document.getElementById(event.dataTransfer.getData('text'));
if (event.currentTarget.classList.contains(droppedElement.dataset.target)) {
event.currentTarget.prepend(droppedElement);
event.currentTarget.classList.remove('drop');
}
},
This all works fine, however, once it is dropped, I can no longer drag the component to another column as it seems to have lost all it's event bindings. Is there a way to keep the events after dropping?
Might be a simple solution, but I'm currently not seeing it. I have an object that describes several configurations.
Object looks like this:
export const fieldSelectionDefault = {
cohort: {
currency_key: null,
salary_key: null,
timeframe_key: null
},
school: {
currency_key: null,
salary_key: null,
timeframe_key: null,
response_count_key: null,
},
}
export const cohortListFieldDefault = {
field_student: { ...fieldSelectionDefault },
field_alum_1: { ...fieldSelectionDefault },
field_alum_2: { ...fieldSelectionDefault },
field_alum_3: { ...fieldSelectionDefault },
}
Now, I have a parent component where I have a form. This form will list each field_* to have a <CohortFieldConfig /> component where we can input the values of the fieldSelectionDefault.
In the parent form, I add them like this:
<h5>Student</h5>
<CohortFieldConfig
:key="'settings.field_student'"
:disabled="settings.active_entities.student"
:selection-fields="settings.field_student"
#update-fields="(val) => test(val, 'stu')"
/>
<h5>Alumnus 1</h5>
<CohortFieldConfig
:key="'settings.field_alum_1'"
:disabled="settings.active_entities.alum_1"
:selection-fields="settings.field_alum_1"
#update-fields="(val) => test(val, 'alum')"
/>
CohortFieldConfig looks like this (example of one inputs, removed js imports):
<template>
<div>
<a-form-item label="Currency input">
<a-input
:disabled="!disabled"
placeholder="Select a currency form key"
v-model="objSelectionFields.cohort.currency_key"
/>
</a-form-item>
<FieldSelector
#select="val => (objSelectionFields.cohort.currency_key = val)"
:user="user"
:disabled="!disabled"
/>
</div>
</template>
<script>
export default {
name: 'CohortFieldConfig',
components: { FieldSelector },
props: {
selectionFields: {
type: [Object, null],
default: () => {
return { ...fieldSelectionDefault }
},
},
disabled: {
type: Boolean,
default: () => false,
},
},
data: function() {
return {
fieldSelectionDefault,
objSelectionFields: { ...this.selectionFields },
}
},
watch: {
objSelectionFields: {
handler(){
this.$emit('update-fields', this.objSelectionFields)
},
deep: true
}
},
methods: {
update() {
// not really used atm
this.$emit('update-fields', this.objSelectionFields)
},
},
}
</script>
When you type in the input, BOTH are updated at the same time. For student & alum_1.
The update-fields event is fired for both (same) components
Whats the reason? I've tried setting different key, doesn't work.
UPDATE
As pointed out in the comments, the issue was I was giving the same object. To correct this, I make a (deep) copy of the object as so:
export const cohortListFieldDefault = {
field_student: JSON.parse(JSON.stringify(fieldSelectionDefault)),
field_alum_1: JSON.parse(JSON.stringify(fieldSelectionDefault)),
field_alum_2: JSON.parse(JSON.stringify(fieldSelectionDefault)),
field_alum_3: JSON.parse(JSON.stringify(fieldSelectionDefault)),
}
So I want to show the user a preview of what an email will look like before it's sent out. To avoid styles from leaking from the parent page into the preview, I've decided to use an iframe. I want the preview to update in real time as the user enters form details.
How would I render a component within an iframe so that its props update automatically when the parent form is updated? This is the code I have so far:
this is the html:
<template>
<div id="confirmation">
<h2>Give a gift</h2>
<form #submit.prevent="checkout()">
<div class="date-section">
<label class="wide">Send</label>
<input type="radio" name="sendLater" v-model="sendLater" required :value="false">
<span>Now</span>
<input type="radio" name="sendLater" v-model="sendLater" required :value="true">
<span style="margin-right: 5px;">Later: </span>
<date-picker :disabled="!sendLater" v-model="date" lang="en" />
</div>
<div>
<label>Recipient Email</label>
<input type="email" class="custom-text" v-model="form.email" required>
</div>
<div>
<label>Recipient Name</label>
<input type="text" class="custom-text" v-model="form.name" required>
</div>
<div>
<label>Add a personal message</label>
<textarea v-model="form.message" />
</div>
<p class="error" v-if="error">Please enter a valid date.</p>
<div class="button-row">
<button class="trumpet-button" type="submit">Next</button>
<button class="trumpet-button gray ml10" type="button" #click="cancel()">Cancel</button>
</div>
</form>
<iframe id="preview-frame">
<preview-component :form="form" :sender-email="senderEmail" :term="term" />
</iframe>
</div>
</template>
here is the js (note: PreviewComponent is the actual preview that will be rendered in the iframe):
export default {
name: 'ConfirmationComponent',
components: {
DatePicker,
PreviewComponent
},
props: {
term: {
required: true,
type: Object
}
},
data() {
return {
form: {
name: null,
email: null,
message: null,
date: null
},
date: null,
sendLater: false,
error: false
}
},
computed: {
senderEmail() {
// utils comes from a separate file called utils.js
return utils.user.email || ''
}
},
watch: {
'form.name'(val) {
this.renderIframe()
},
'form.email'(val) {
this.renderIframe()
}
},
methods: {
renderIframe() {
if (this.form.name != null && this.form.email != null) {
console.log('rendering iframe')
// not sure what to do here......
}
}
}
}
I've tried all sorts of things but what seems to be the hardest is setting the props of the preview-component properly. Any help you all can give would be appreciated.
So as posted in one of the comments, Vuex works perfectly for this.
I also ended up creating a custom "IFrame" component that renders whatever you have inside its slot in an iframe.
Here is my Vuex store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
form: {
name: null,
email: null,
message: null
},
senderEmail: null,
term: null,
styles: null
},
mutations: {
updateForm(state, form) {
state.form = form
},
updateEmail(state, email) {
state.senderEmail = email
},
updateTerm(state, term) {
state.term = term
},
stylesChange(state, styles) {
state.styles = styles
}
}
})
my IFrame component:
import Vue from 'vue'
import { store } from '../../store'
export default {
name: 'IFrame',
data() {
return {
iApp: null,
}
},
computed: {
styles() {
return this.$store.state.styles
}
},
render(h) {
return h('iframe', {
on: {
load: this.renderChildren
}
})
},
watch: {
styles(val) {
const head = this.$el.contentDocument.head
$(head).html(val)
}
},
beforeUpdate() {
this.iApp.children = Object.freeze(this.$slots.default)
},
methods: {
renderChildren() {
const children = this.$slots.default
const body = this.$el.contentDocument.body
const el = document.createElement('div') // we will mount or nested app to this element
body.appendChild(el)
const iApp = new Vue({
name: 'iApp',
store,
data() {
return {
children: Object.freeze(children)
}
},
render(h) {
return h('div', this.children)
}
})
iApp.$mount(el)
this.iApp = iApp
}
}
}
finally here is how data is passed to the PreviewComponent from the ConfirmationComponent:
export default {
name: 'ConfirmationComponent',
mounted() {
this.$store.commit('updateEmail', this.senderEmail)
this.$store.commit('updateTerm', this.term)
},
watch: {
'form.name'(val) {
this.updateIframe()
},
'form.email'(val) {
this.updateIframe()
}
},
methods: {
updateIframe() {
this.$store.commit('updateForm', this.form)
}
}
}
then lastly the actual PreviewComponent:
import styles from '../../../templates/styles'
export default {
name: 'PreviewComponent',
mounted() {
this.$store.commit('stylesChange', styles)
},
computed: {
redemption_url() {
return `${window.config.stitcher_website}/gift?code=`
},
custom_message() {
if (this.form.message) {
let div = document.createElement('div')
div.innerHTML = this.form.message
let text = div.textContent || div.innerText || ''
return text.replace(/(?:\r\n|\r|\n)/g, '<br>')
}
return null
},
form() {
return this.$store.state.form
},
term() {
return this.$store.state.term
},
senderEmail() {
return this.$store.state.senderEmail
}
}
}
hopefully this will help somebody.
I'm a newbie of Vue, and I'm trying to simply clear the data of input component once I've submitted, but it seems I'm missing something, because since it's parent data is cleared, I still see the filled value of the input component.
Here is a living example.
I've set to the input child component v-model="title" from it's parent wrapper. Once I submit the data to the parent, I call addItem and in the end, I supposed to clear the data model just by clear it this.title = '', but I probably do something wrong on how to bind data from parent to child.
And above the code, starting from the parent component:
<template>
<form #submit="addItem" class="todo-insert">
<input-item
icon="create"
name="title"
placeholder="Add a ToVue item..."
v-model="title"
/>
<button-item tone="confirm" class="todo-insert__action">
Aggiungi
</button-item>
</form>
</template>
<script>
import ButtonItem from '#vue/Form/ButtonItem/ButtonItem.vue'
import InputItem from '#vue/Form/InputItem/InputItem.vue'
import uuid from 'uuid'
export default {
name: 'TodoInsert',
components: {
ButtonItem,
InputItem
},
data () {
return {
title: ''
}
},
methods: {
addItem (e) {
e.preventDefault()
const todo = {
id: uuid.v4(),
isComplete: false,
title: this.title
}
this.$emit('add-todo', todo)
this.title = ''
}
}
}
</script>
<style lang="scss" scoped src="./TodoList.scss"></style>
This is the child input component:
<template lang="html">
<label class="input">
<div v-if="label" class="input__label text-sans text-sans--label">
{{ label }}
</div>
<div class="input__row">
<input
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
<div v-if="placeholderLabel" class="input__placeholder text-sans text-sans--placeholder">
{{ placeholderLabel }}
</div>
<div v-if="icon" class="input__icon-area">
<icon-item
:name="icon"
/>
</div>
</div>
</label>
</template>
<script>
import IconItem from '../../IconItem/IconItem.vue'
export default {
name: 'InputItem',
props: {
autocomplete: {
type: String,
default: 'off'
},
icon: String,
label: String,
name: {
type: String,
default: 'input-text'
},
placeholder: String,
placeholderLabel: String
},
computed: {
hasPlaceholderLabel () {
return this.placeholderLabel ? 'input__field--placeholder-label' : ''
},
isDirty () {
// return this.$attrs.value ? 'input__field--dirty' : ''
return 'input__field--dirty'
}
},
methods: {
updateValue: function (value) {
this.$emit('input', value)
}
},
components: {
IconItem
}
}
</script>
<style lang="scss" src="./InputItem.scss"></style>
What am I missing?
Your child component is bound unidirectionally. It means that it can change the value, but does not receive any update from the parent component. To receive updates, you need to receive the property value in your child:
props: {
value: String
}
Then, you need to pass the value received to the input :
<input
:value="value"
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
Now the input should update when the parent component changes the value
Is there a way to simplify this code?
The button should also change the localValue of the child.
Vue.component('my-input', {
template: `
<div>
<b>My Input:</b> <br>
localValue: {{ localValue }} <br>
<input v-model="localValue">
</div>
`,
props: ['value'],
data() {
return { localValue: this.value }
},
watch: {
value () {
this.localValue = this.value
},
localValue () {
this.$emit('input', this.localValue)
}
}
})
new Vue({
el: '#app',
data: () => ({
parentValue: 'Inital value'
}),
methods: {
change () {
this.parentValue = 'Changed value'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
<div id="app">
<my-input v-model="parentValue"></my-input>
<button #click="change">Change</button><br>
parentValue: {{ parentValue }}
</div>
I have always faced difficulties when I need to do so.
I will be very grateful for the help!
If you avoid using v-model inside your custom form component, you really only need
<b>My Input:</b> <br>
localValue: {{ value }} <br>
<input :value="value" #input="$emit('input', $event.target.value)">
No data, no watch, that's it.
See https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
If you really want something representing a value local to your component, the Vue docs favour using computed values over watchers (ref: https://v2.vuejs.org/v2/guide/computed.html#Watchers).
The idea here is to create a computed value with getter and setter to facilitate a simplified one-way data flow.
Vue.component('my-input', {
template: `<div><b>My Input:</b> <br>localValue: {{ localValue }} <br><input v-model="localValue"></div>`,
props: ['value'],
computed: {
localValue: {
get () {
return this.value
},
set (value) {
this.$emit('input', value)
}
}
}
})
new Vue({
el: '#app',
data: () => ({
parentValue: 'Inital value'
}),
methods: {
change () {
this.parentValue = 'Changed value'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
<div id="app">
<my-input v-model="parentValue"></my-input>
<button #click="change">Change</button><br>
parentValue: {{ parentValue }}
</div>
How to pass complex objects to child component (potentially down a few layers):
Parent component:
<child v-model='parentEntity' />
Child component:
model: {
prop: 'modelValue',
event: 'update:modelValue',
},
props: {
modelValue: {
type: Object,
required: true,
},
},
...
entity: {
// getter
get() {
return Object.assign({}, this.modelValue);
},
// setter
set(newValue) {
this.$emit('update:modelValue', newValue);
},
},
...