How to watch only after the initial load from API in VueJS? - javascript

I am getting data from API with which I am populating the form in my component. I need to trigger watchers only after the initial populating of data. Like in async way. But the watcher is getting triggered immediately. I need to disable the Update button only if any value is changed after the initial populating of data.
<template>
<div id="app">
<input type="text" v-model="user.userId" /> <br />
<br />
<input type="text" v-model="user.title" /> <br />
<br />
<button :disabled="isDisabled">Update</button>
</div>
</template>
<script>
export default {
name: "App",
watch: {
user: {
handler(oldVal, newVal) {
if (oldVal != newVal) {
this.isLoaded = false;
}
},
deep: true,
},
},
computed: {
isDisabled() {
return this.isLoaded;
},
},
async created() {
await fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
this.user = json;
this.isLoaded = true;
});
},
data() {
return {
user: {
userId: 0,
id: 0,
title: "",
completed: false,
},
isLoaded: true,
};
},
};
</script>
I have referred Vue, await for Watch and Are watches asynchronous? and Vue.js How to watcher before mounted() , can't get data from watch but I am unable to follow.
Here's a preview : https://codesandbox.io/embed/great-euler-skd3v?fontsize=14&hidenavigation=1&theme=dark

This needs to be determined with some condition.
isLoaded already serves the purpose of determining the state of initial loading, but the name is confusing, because it determines that data is not loaded.
It can be:
watch: {
user: {
if (this.isLoading && oldVal != newVal) {
this.isLoading = false;
}
...
The watcher doesn't need to be deep and could be unwatched when it's not needed:
async created() {
let unwatchUser = this.$watch('user', (oldVal, newVal) => {
if (this.isLoading && oldVal != newVal) {
this.isLoading = false;
unwatchUser();
}
})
...
A common way to designate that data hasn't been loaded yet is to set it to null, i.e. no value. This doesn't need isLoading flag or a watcher. If null is undesirable because of referred object properties, this can be overcome with optional chaining and conditional rendering:
<div v-if="user">
<input type="text" v-model="user.userId" />
...
<div v-else class="spinner"/>

The simplest answer for the question:
Q: How to watch only after the initial load from API in VueJS?
A: Add flag inside your watch (e.g. isLoaded).
Also there is couple things wrong with your code:
async/await in created does nothing,
isDisabled is not needed cause is basing only on 1 value from data. You can just use this value instead (isLoading).
If you api calls fail, isLoading flag will not change, better approach is to move it to finally.
Solution of your problem (codesandbox) :
<template>
<div id="app">
<div v-if="!isFetching">
<input type="text" v-model="user.userId" /> <br />
<br />
<input type="text" v-model="user.title" /> <br />
<br />
<button :disabled="!isLoaded">Update</button>
</div>
<div v-else>Loading...</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
user: {
userId: 0,
id: 0,
title: "",
completed: false,
},
isFetching: false,
isLoaded: false
};
},
watch: {
user: {
handler(oldVal, newVal) {
if (!this.isFetching) {
// this comparision doesn't work (cause oldVal/newVal is an object)
if (oldVal != newVal) {
this.isLoaded = false;
}
}
},
deep: true
},
},
created() {
this.isFetching = true;
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
this.user = json;
this.isLoaded = true;
})
.finally(() => this.isFetching = false)
},
};
</script>

Related

Vuex - updating data in store from child $emit

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.

Vuex returns default values, not provided [Nuxt.js/Vuex]

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

VueJS Duplicate components all updating at the same time

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

How do I render a child component within an iframe in Vue?

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.

Vue.js2 - Object.assign({}, this.var) preventing watch method

returning this.user (a global computed property) works as expected. Of course, I'm making a copy because I do not want to overwrite the actual user data. So, I'm using Object.assign. However, once I include return Object.assign({}, this.user) (opposed to this.user), the watch method no longer functions.
Here is my template (I am using bootstrap-vue):
<template>
<form role="form">
<b-form-group
label="First Name"
label-for="basicName"
:label-cols="3"
:horizontal="true">
<b-form-input id="user-name-first" type="text" v-model="userFormData.fname"></b-form-input>
</b-form-group>
<b-form-group
label="Last Name"
label-for="basicName"
:label-cols="3"
:horizontal="true">
<b-form-input id="user-name-lirst" type="text" v-model="userFormData.lname"></b-form-input>
</b-form-group>
<b-form-group
label="Email"
label-for="user-email"
:label-cols="3"
:horizontal="true">
<b-form-input id="user-email" type="text" v-model="userFormData.email"></b-form-input>
</b-form-group>
<b-form-group
:label-cols="3"
:horizontal="true">
<b-button type="submit" variant="primary">Save changes</b-button>
<b-button type="button" variant="secondary" #click="userFormCancel">Cancel</b-button>
</b-form-group>
</form>
</template>
So, this works and sets editsPending to true whenever changes are applied to userProfile (via v-model on an input)
<script>
export default {
name: 'userProfile',
data () {
return {
editsPending: false
}
},
computed: {
userFormData: function () {
return this.user
}
},
watch: {
userFormData: {
deep: true,
handler (val) {
this.editsPending = true
}
}
},
methods: {
userFormCancel () {
this.editsPending = false
}
}
}
</script>
...but this does not; userFormData becomes a clone of user but editsPending is not affected by updates to userFormData.
<script>
export default {
name: 'userProfile',
data () {
return {
editsPending: false
}
},
computed: {
userFormData: function () {
return Object.assign({}, this.user)
}
},
watch: {
userFormData: {
deep: true,
handler (val) {
this.editsPending = true
}
}
},
methods: {
userFormCancel () {
this.editsPending = false
}
}
}
</script>
Can anyone explain why this may be happening and suggest a viable solution?
A computed property will only re-evaluate when some of its
dependencies have changed. (source)
That's why it works with return this.user and not with Object.assign because it's not a reactive dependency.
If you want reactive data you should initialize userFormData as an empty object data and assign your user when your Vue instance is created:
data () {
return {
editsPending: false,
userFormData: {}
}
},
created() {
this.userFormData = Object.assign({}, this.user)
},
Tested different things to reproduce the behaviour you see.
I suspect that in your template your are binding your inputs to UserFormdata (incorrect)
<input v-model="userFormData.name">
Instead of (correct)
<input v-model="user.name">
If you could share your template that would help ;)
Edit: After template addition.
new Vue({
el: '#app',
data () {
return {
editsPending: false,
user: { name: 'John Doe' },
userCachedData: {},
}
},
created() {
this.userCachedData = Object.assign({}, this.user);
},
watch: {
user: {
deep: true,
handler (val) {
this.editsPending = true
}
}
},
methods: {
userFormCancel () {
this.editsPending = false
}
}
})
<div id="app">
{{ user }}
{{ userCachedData }}
<br>
<input v-model="user.name" />
{{ this.editsPending }}
</div>
Codepen: https://codepen.io/aurelien-bottazini/pen/BVNJaG?editors=1010
You can use the $emit to assign value to object:
mounted() {
this.$emit("userFormData", this.user);
}

Categories