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.
Related
I have a Vue app that can either randomize a title and subtitle OR manually edit these two values through a custom input component. When a user decides to edit, their input should then display on save those results on the parent component.
I have the randomizer and child component emitting the updated headings working, but having a troubled time updating the parents and state to display the custom input title and subtitle on save and getting a "undefined" error for both title and subtitle when I placed console logs in updateTitleAndSubtitle() in the actions section of the store.
The objective of this code challenging is to return the new values to the store and be able to display the custom inputs while having the randomizer handy whenever a user decides to use that instead.
Any direction on what I'm doing wrong or missing would be much appreciated. I've been reading article after article around Vuex and Vue2 for 3 days now with 2 months of experience using Vue.
Custom Input Child Component:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
:updateTitle="updateTitle"
v-model="inputTitle"
/>
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle" :updateSubtitle="updateSubtitle"
v-model="inputSubtitle"
/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
title: String,
subtitle: String,
},
computed: {
updateTitle() {
console.log('updateTitle: ', this.title);
return this.title;
},
updateSubtitle() {
console.log('updateSubtitle: ', this.subtitle);
return this.subtitle;
},
inputTitle: {
get() {
console.log('set title: ', this.title);
return this.title;
},
set(title) {
console.log('set title: ', title);
this.$emit('input', title);
},
},
inputSubtitle: {
get() {
return this.subtitle;
},
set(subtitle) {
console.log('set subtitle: ', subtitle);
this.$emit('input', subtitle);
},
},
},
};
</script>
Parent component:
<template>
<main class="home-page page">
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:title="title"
:subtitle="subtitle"
#update="v => onSave(v)"
/>
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '#/components/CustomInput.vue';
import { mapState, mapActions } from 'vuex';
export default {
name: 'Home',
components: {
CustomInput,
},
data() {
return {
editMode: false,
};
},
computed: {
...mapState(['title', 'subtitle']),
},
methods: {
...mapActions(['randomizeHeadings', 'updateHeadings']),
onEdit() {
this.editMode = true;
},
onCancel() {
this.editMode = false;
},
onSave(v) {
this.editMode = false;
this.title = v.title;
this.subtitle = v.subtitle;
this.updateTitleAndSubtitle(v);
},
},
mounted() {
this.randomizeHeadings();
},
};
Vuex Store:
import randomWords from 'random-words';
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }) {
const payload = {
title: this.title || null,
subtitle: this.subtitle || null,
};
commit('UPDATE_TITLE', payload);
commit('UPDATE_SUBTITLE', payload);
},
},
modules: {
},
});
I tested your code in my local development environment and find out that you need a lot of changes in your codes to work better. Here is the new vuex store code:
vuex store:
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }, inputUser) {
/* I changed the structure of this action to work correctly */
console.log(inputUser);
commit('UPDATE_TITLE', inputUser.title);
commit('UPDATE_SUBTITLE', inputUser.subtitle);
},
},
modules: {
},
});
Also here is the new Parent component code:
Parent component:
<template>
<main class="home-page page">
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:title="title"
:subtitle="subtitle"
#titleEvent = "myFuncTitle"
#subTitleEvent = "myFuncSubTitle"
/>
<!--
I removed this part from your component.
#update="v => onSave(v)"
and also added custom events (titleEvent and subTitleEvent) to the component
-->
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '../components/CustomInput.vue';
import { mapActions } from 'vuex';
export default {
name: 'Parent',
components: {
CustomInput,
},
data() {
return {
editMode: false,
/* defining new data for handling "cancel" button functionality */
temporaryTitle: "",
temporarySubTitle: ""
};
},
computed: {
/* defining setter and getter for each computed value separately */
title: {
// getter
get: function () {
return this.$store.state.title;
},
// setter
set: function (newValue) {
this.$store.commit('UPDATE_TITLE', newValue);
}
},
subtitle: {
// getter
get: function () {
return this.$store.state.subtitle;
},
// setter
set: function (newValue) {
this.$store.commit('UPDATE_SUBTITLE', newValue);
}
},
},
methods: {
/* changing the name of actions according to the names defined in "store" */
...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
onEdit() {
this.editMode = true;
this.temporaryTitle = this.$store.state.title;
this.temporarySubTitle = this.$store.state.subtitle;
},
onCancel() {
this.editMode = false;
this.$store.commit('UPDATE_TITLE', this.temporaryTitle);
this.$store.commit('UPDATE_SUBTITLE', this.temporarySubTitle);
},
myFuncTitle(event) {
console.log(event);
/* we could not set values to "computed" properties, if we had not defined "set: function ..." for them above. */
this.title = event;
},
myFuncSubTitle(event) {
this.subtitle = event;
},
onSave(v) {
this.editMode = false;
console.log(v); /* "v" is not related to your data. notice the console */
// this.title = v.title;
// this.subtitle = v.subtitle;
const payload = {
title: this.title,
subtitle: this.subtitle,
};
this.updateTitleAndSubtitle(payload);
},
},
created() {
this.randomizeTitleAndSubtitle();
},
};
</script>
And finally here is the code of new Custom Input component:
Custom Input:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
v-model="inputTitle"
#input="$emit('titleEvent', $event.target.value)"
/>
<!-- emitting event like above code for each input -->
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle"
v-model="inputSubtitle"
#input="$emit('subTitleEvent', $event.target.value)"
/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
title: String,
subtitle: String,
},
computed: {
inputTitle: {
get() {
console.log('set title: ', this.title);
return this.title;
},
set(title) {
console.log('set title: ', title);
},
},
inputSubtitle: {
get() {
return this.subtitle;
},
set(subtitle) {
console.log('set subtitle: ', subtitle);
},
},
},
};
</script>
<style scoped>
</style>
I tried to comment some changes to the codes, but the main changes are related to changing the name of mapActions actions according to the names defined in "store" and also provide a setter for computed properties.
I suggest that you read more in vue and vuex documentations, especially the page that is related to custom events and computed setters and vuex actions, if you have problems with my codes.
i try to build a little clothing web shop with nuxtjs. You can choose the color on the details page. The details page represents a pice of clothing. The ColorMenu is a component. If you choose something a color, it will emit it back to the details page and will send a new details request to my backend.
However, changing the color only works if you don't choose another piece of clothing. If you choose another piece of clothing (so the route parameters will change) and choose another color in the menu, there is a always an error that it cannot load anything. it seems that it sends repeated requests until the request is blocked.
The details routes are built according to this scheme: localhost/details/{sellableId}/{ideaId}/{appearanceId}
Details Page:
<template>
<section class="section">
<div v-if="details">
<div class="columns">
<div class="column">
<ImageCaroussel :images="details.images"></ImageCaroussel>
</div>
<div class="column">
<h3>Farben</h3>
<ColorMenu
:appearances="productType.appearances"
:appearanceIds="details.appearanceIds"
></ColorMenu>
</div>
</div>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex'
import Dropdown from '~/components/details/Dropdown.vue'
import ColorMenu from '~/components/details/ColorMenu.vue'
import ImageCaroussel from '~/components/details/ImageCaroussel.vue'
export default {
created() {
this.$nuxt.$on('selected', ($event) => (this.selected = $event))
this.$nuxt.$on('selectedColor', ($event) => this.setSelectedColor($event))
},
data() {
return {
modal: false,
selected: '',
selectedColor: '',
}
},
async asyncData({ store, params }) {
console.log('asyncfirst')
if (params.sellableId && params.appearanceId && params.ideaId) {
await store.dispatch('details/get_details', {
sellableId: params.sellableId,
appearanceId: params.appearanceId,
ideaId: params.ideaId,
})
let sellableId = params.sellableId
let appearanceId = params.appearanceId
let ideaId = params.ideaId
console.log('asyncsecond!')
return { sellableId, appearanceId, ideaId }
}
},
mounted() {
this.sellableId = this.$route.params.sellableId
this.appearanceId = this.$route.params.appearanceId
this.ideaId = this.$route.params.ideaId
console.log('Mounted!')
},
components: {
Dropdown,
ColorMenu,
ImageCaroussel,
},
computed: {
...mapState({
details: (state) => {
return state.details.details
},
currency: (state) => {
return state.sellable.currency
},
productType: (state) => {
return state.details.productType
},
}),
},
methods: {
checkout: async function (sellableId, size, appearanceId) {
let link = await this.$backendrepositories.basket.checkout(
sellableId,
size,
appearanceId
)
if (link.status === 200 && link.data) {
this.modal = true
setTimeout(() => {
window.location.href = link.data.link
}, 3000)
}
},
setSelectedColor: async function (event) {
this.selectedColor = event
await this.$store.dispatch('details/get_details', {
sellableId: this.sellableId,
appearanceId: this.selectedColor,
ideaId: this.ideaId,
})
},
},
}
</script>
ColorMenu Component:
<template>
<div>
<div
v-for="(cell, index) in appearances"
:key="index"
style="display: inline-block"
>
<label v-if="appearanceIds.includes(cell.id)" class="self-container">
<input type="radio" checked="checked" name="color" />
<span
class="checkmark"
:style="`background-color: ${cell.colors[0].value}`"
#click="select(cell.id)"
></span>
</label>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
selected: '',
}
},
props: ['appearances', 'appearanceIds'],
methods: {
select(select) {
this.selected = select
this.$nuxt.$emit('selectedColor', this.selected)
},
},
}
</script>
There is a live demo at https://akano-frontend.vercel.app/
I have a vue app where a user can randomize a title and subtitle OR edit the fields using a custom input component.
When a user chooses to edit, I'd like to send the updated title and subtitle from the input component to the store to mutate the title and subtitle state when clicking the save button after filling out the values desired in the input component.
Currently able to pass values from parent to child and had an emit present for the parent to listen to, however, I'm not sure how to update the original values to the custom ones and get "undefined" as a result from the $emit.
I can't seem to find a solution to this problem, all the forums I have been on haven't helped so I really hope someone on here can help me with my problem; would really appreciate it.
Parent.vue
<template>
<main class="home-page page">
<div v-if="!editMode">
<div>
<span>Title: </span>{{title}}
</div>
<div>
<span>Subtitle: </span>{{subtitle}}
</div>
<div>
<button #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button #click="onEdit">Edit</button>
</div>
</div>
<div v-else>
<DoubleInput
:value="{ title, subtitle }"
/>
<div>
<button #click="onCancel">Cancel</button>
<button #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import DoubleInput from '#/components/DoubleInput.vue';
import { mapState, mapActions } from 'vuex';
export default {
name: 'Parent',
components: {
DoubleInput,
},
data() {
return {
editMode: false,
};
},
computed: {
...mapState(['title', 'subtitle']),
},
methods: {
...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
onEdit() {
this.editMode = true;
},
onCancel() {
this.editMode = false;
},
onSave() {
this.editMode = false;
const newTitle = this.title;
const newSubtitle = this.subtitle;
this.updateTitleAndSubtitle({ newTitle, newSubtitle });
},
},
mounted() {
this.randomizeTitleAndSubtitle();
},
};
</script>
Child.vue
<template>
<div>
<label>Edit Title: </label>
<input type="text" ref="title" :value="value.title" #input="updateValue()" />
<label>Edit Subtitle: </label>
<input type="text" ref="subtitle" :value="value.subtitle" #input="updateValue()" />
</div>
</template>
<script>
export default {
name: 'Child',
props: ['value'],
methods: {
updateValue() {
this.$emit('input', {
title: this.$refs.title.value,
subtitle: this.$refs.subtitle.value,
});
},
},
};
</script>
Store
import Vue from 'vue';
import Vuex from 'vuex';
import randomWords from 'random-words';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }, value) {
const payload = {
title: value.title || null,
subtitle: value.subtitle || null,
};
commit('UPDATE_TITLE', payload);
commit('UPDATE_SUBTITLE', payload]);
},
},
modules: {
},
});
Where I was having the biggest issue was most in the Vuex store, not the parent to child lifecycle like I thought. The emit was working just fine and needed to add in some computed properties to the custom input component. How I was approaching the store was completely backwards and gutted the updateTitleAndSubtitle() action to what's shown below. And lastly, added an #input that would send the updated object of values to onEdit() to set the values to an empty object in the data. Then, use that object with the new values to dispatch/commit to the store! Vualá ~ the desired behavior, no errors, and ended up figuring it out with some time.
What I was missing was passing the new emitted data object to a store action to then mutate the state. The whole concept behind this code challenge was to take in data from the store, modify it through a component, send back the modified data to the store to then change the state. A bit overkill for this, BUT it's the practice and concept I needed to approach a much larger problem in an existing application at work.
Here's the code breakdown!
Custom Input:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
:setTitle="setTitle"
ref="title"
:value="value.title"
#input="updateValue()"
/>
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle"
:setSubtitle="setSubtitle"
ref="subtitle"
:value="value.subtitle"
#input="updateValue()"
/>
</div>
</template>
<script>
export default {
name: 'DoubleInput',
props: {
value: {
type: Object,
required: true,
},
},
computed: {
setTitle() {
// console.log('set title: ', this.value.title);
return this.value.title;
},
setSubtitle() {
// console.log('set subtitle: ', this.value.subtitle);
return this.value.subtitle;
},
},
methods: {
updateValue() {
this.$emit('input', {
title: this.$refs.title.value,
subtitle: this.$refs.subtitle.value,
});
},
},
};
</script>
Parent:
<template>
<main class="home-page page">
<!-- <span class="bold">Title:</span> {{ title }} <br>
<span class="bold">Subtitle:</span> {{ subtitle }}
<hr> -->
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:value="{ title, subtitle }"
#input="v => onEdit(v)"
/>
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave(v)">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '#/components/CustomInput.vue';
import { mapState, mapActions } from 'vuex';
export default {
name: 'Home',
components: {
CustomInput,
},
data() {
return {
editMode: false,
v: {},
};
},
computed: {
...mapState(['title', 'subtitle']),
},
methods: {
...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
onEdit(v) {
this.editMode = true;
this.v = v;
// console.log('returned value object: ', v);
},
onCancel() {
this.editMode = false;
},
onSave() {
this.editMode = false;
this.$store.dispatch('updateTitleAndSubtitle', this.v);
},
},
mounted() {
this.randomizeTitleAndSubtitle();
},
};
</script>
<style lang="stylus" scoped>
.bold
font-weight bold
.controls
width 100%
display flex
justify-content space-around
max-width 20rem
margin-top 2rem
margin-left auto
margin-right auto
.control-button
height 2.5rem
border-radius 1.25rem
background-color white
border 0.125rem solid black
padding-left 1.25rem
padding-right 1.25rem
&:hover
cursor pointer
background-color rgba(0, 0, 0, 0.1)
</style>
Store:
import Vue from 'vue';
import Vuex from 'vuex';
import randomWords from 'random-words';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
setTitle({ commit }, value) {
commit('UPDATE_TITLE', value);
},
setSubtitle({ commit }, value) {
commit('UPDATE_SUBTITLE', value);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ dispatch }, value) {
dispatch('setTitle', value.title);
dispatch('setSubtitle', value.subtitle);
},
},
modules: {
},
});
Your call to updateTitleAndSubtitle in the onSave() method isn't passing the new title and subtitle to the action.
onSave() {
this.editMode = false;
this.updateTitleAndSubtitle({ title: this.title, subtitle: this.subtitle });
},
Also, I would hesitate using state.title and state.subtitle as your edit mode vars as conventionally you should only change those values via mutations. Instead, pass a local var as the prop to your child component and call the method using it instead.
<DoubleInput v-model="title_subtitle" />
<script>
// ...
data() {
return {
// ...
title_subtitle: {},
};
},
// ...
methods: {
onEdit() {
this.editMode = true;
this.title_subtitle = {
title: this.title,
subtitle: this.subtitle,
};
},
// ...
onSave() {
this.editMode = false;
this.updateTitleAndSubtitle(this.title_subtitle);
},
},
// ...
}
Given your comments it makes more sense why the code is unnecessarily complex. It's perfectly fine to pass your state vars as props as long as your #input handler doesn't try to write back to it. You don't need the local var. Try this:
<CustomInput :value="{ title: this.title, subtitle: this.subtitle }" #input="onSave" />
with
onSave(value) {
this.editMode = false;
this.updateTitleAndSubtitle(value);
}
I have this simple registration page:
<template>
<div class="login">
<div class="login-content">
<h1 #click="redirect('/')">Logo</h1>
</div>
<div class="login-header">
<p class="paragraph-small right">Already have account?
<span class="paragraph-small pointer link" #click="redirect('/login')">Log in!</span>
</p>
</div>
<div class="login-inputs">
<div class="login-inputs-container">
<h1>Sign up</h1>
<Input :error="error" :title="'Email'" :type="'email'" :value="email" />
<Input :error="error" :title="'Password'" :type="'password'" :value="password" />
<Input :error="error" :title="'Repeat password'" :type="'password'" :styles="'padding-bottom: 10px'" :value="passwordRepeat" />
<Checkbox :value="tac" :label="`I have read and accepted <a href='/'>terms and conditions.</a>`" />
<Button :label="'Sign up'" :clickon="register" />
<p v-if="error">Passwords have to match!</p>
</div>
</div>
</div>
</template>
<script>
import { register } from "~/api";
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
import Input from "~/components/Input";
import Button from "~/components/Button";
import Checkbox from "~/components/Checkbox";
export default {
name: "register",
components: {
Input,
Button,
Checkbox
},
watch: {
password() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) },
passwordRepeat() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) }
},
computed: {
...mapGetters({
email: 'register/getEmail',
password: 'register/getPassword',
passwordRepeat: 'register/getPasswordRepeat',
status: 'register/getStatus',
error: 'register/getError',
tac: 'register/getTac'
})
},
methods: {
redirect(path) {
this.$router.push({ path })
},
async register() {
console.log(this.tac, this.password, this.passwordRepeat, this.email)
}
}
}
</script>
<style lang="scss">
#import "../assets/css/login";
</style>
As you can see, there are 4 fields where I want to change value - 3 Input and 1 Checkbox. When I provide data and click button in console I get the default values, I was trying to do something with mutations and actions, but it doesn't work.
Can it be because I use my components, not default?
Also, here is my store store/register.js
export const state = () => ({
email: null,
password: null,
passwordRepeat: null,
status: null,
error: false,
tac: false
})
export const mutations = {
setEmail(state, value) { state.email = value },
setPassword(state, value) { state.password = value },
setPasswordRepeat(state, value) { state.passwordRepeat = value },
setStatus(state, value) { state.status = value },
setError(state, value) { state.error = value },
setTac(state, value) { state.tac = value }
}
export const actions = {
fetchEmail(ctx, value) { ctx.commit('setEmail', value) },
fetchPassword(ctx, value) { ctx.commit('setPassword', value) },
fetchPasswordRepeat(ctx, value) { ctx.commit('setPasswordRepeat', value) },
fetchStatus(ctx, value) { ctx.commit('setStatus', value) },
fetchError(ctx, value) { ctx.commit('setError', value) },
fetchTac(ctx, value) { ctx.commit('setTac', value) },
}
export const getters = {
getEmail(state) { return state.email },
getPassword(state) { return state.password },
getPasswordRepeat(state) { return state.passwordRepeat },
getStatus(state) { return state.status },
getError(state) { return state.error },
getTac(state) { return state.tac },
}
If problem is that I use not default tags, but my components with props, here is Checkbox component:
<template>
<div class="checkbox-container">
<label class="container">
<input type="checkbox" :value="innerValue" #input="onInput">
<span class="checkmark"></span>
</label>
<p class="checkbox-paragraph" v-html="label" />
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: ''
},
value: {
type: Boolean,
default: false
}
},
name: "Checkbox",
watch: {
value(value) {
this.innerValue = value
},
innerValue(value) {
this.$emit('input', value)
}
},
data() {
return {
innerValue: this.value
}
},
methods: {
onInput() {
this.$nextTick(() => {
this.innerValue = !this.innerValue
})
}
}
}
</script>
One way that can help you change the value of your checkbox is like this.
Checkbox Component:
<template>
<div class="checkbox-container">
<label class="container">
<input type="checkbox" #change="$emit('checkbox', value)" />
<span class="checkmark"></span>
</label>
</div>
</template>
<script>
export default {
name: 'Checkbox',
data() {
return {
value: false,
}
},
}
</script>
Now inside your register page you can use the checkbox component in template like this:
<Checkbox #checkbox="checkboxChanged" />
Now in the same page and in method section add this method:
checkboxChanged(event) {
this.$store.dispatch('register/fetchTac', event)
},
},
This way, when the value of checkbox changes you can have the changed value in your store too and get it with mapGetter. You can do the same to your inputs.
Okay, here is my working answer, I don't really know if it's correct, but it doesn't contain any errors or warnings:
<template>
<div class="login">
<div class="login-content">
<h1 #click="redirect('/')">Logo</h1>
</div>
<div class="login-header">
<p class="paragraph-small right">Already have account?
<span class="paragraph-small pointer link" #click="redirect('/login')">Log in!</span>
</p>
</div>
<div class="login-inputs">
<div class="login-inputs-container">
<h1>Sign up</h1>
<Input :error="error" :title="'Email'" :type="'email'" v-model="email" />
<Input :error="error" :title="'Password'" :type="'password'" v-model="password" />
<Input :error="error" :title="'Repeat password'" :type="'password'" :styles="'padding-bottom: 10px'" v-model="passwordRepeat" />
<Checkbox v-model="tac" :label="`I have read and accepted <a href='/'>terms and conditions.</a>`" />
<Button :label="'Sign up'" :clickon="register" />
<p v-if="error">Passwords have to match!</p>
</div>
</div>
</div>
</template>
<script>
import { register } from "~/api";
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
import Input from "~/components/Input";
import Button from "~/components/Button";
import Checkbox from "~/components/Checkbox";
export default {
name: "register",
components: {
Input,
Button,
Checkbox
},
watch: {
...mapActions(['fetchTac', 'fetchError', 'fetchStatus', 'fetchPasswordRepeat', 'fetchPassword', 'fetchEmail']),
password() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) },
passwordRepeat() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) }
},
computed: mapGetters(['getError', 'getEmail', 'getPassword', 'getPasswordRepeat', 'getStatus', 'getTac']),
data() {
return {
email: null,
password: null,
passwordRepeat: null,
status: null,
error: false,
tac: false
}
},
methods: {
redirect(path) {
this.$router.push({ path })
},
async register() {
console.log(this.passwordRepeat, this.password, this.email, this.tac)
}
}
}
</script>
But I still have one problem, as you can see, I have getters and data at the same time, I can actually remove data, but it will cause such warning:
Property or method "propname" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property
It will work, but I will have this warning.
EDIT
I solved this problem that way:
computed: {
error: {
get() { return this.$store.getters.getError },
set(value) { this.$store.commit('setError', value) }
},
email: {
get() { return this.$store.getters.getEmail },
set(value) { this.$store.commit('setEmail', value) }
},
password: {
get() { return this.$store.getters.getPassword },
set(value) { this.$store.commit('setPassword', value) }
},
passwordRepeat: {
get() { return this.$store.getters.getPasswordRepeat },
set(value) { this.$store.commit('setPasswordRepeat', value) }
},
status: {
get() { return this.$store.getters.getStatus },
set(value) { this.$store.commit('setStatus', value) }
},
tac: {
get() { return this.$store.getters.getError },
set(value) { this.$store.commit('setTac', value) }
}
},
// data() {
// return {
// email: null,
// password: null,
// passwordRepeat: null,
// status: null,
// error: false,
// tac: false
// }
// },
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)),
}