Related
I have 2 buttons. One adds a movie to local storage, the other removes it from there. I made a function that basically switches the button. If the movie is added it shows "remove", if the movie's not been added it shows the button "add".
The function works but it doesn't know when the boolean changes so the button doesn't change. Someone explained that i should use watch property, but how am I supposed to watch an output of a function?
here is the code
<template>
<div>
<div class="card" v-for="movie in movies"
:key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button v-show="!showButton(movie.id)" type="submit" #click="storeMovie(movie.id)" >
Aggiungi
</button>
<button v-show="showButton(movie.id)" type="submit" #click="removeMovie(movie.id)">
Rimuovi
</button>
</div>
<div class="card" v-for="favourite in watchlist"
:key="favourite.id">
{{favourite.title}}
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'HomeComp',
data () {
return {
movies: [],
watchlist: [],
movie: null,
}
},
mounted () {
axios
.get('https://api.themoviedb.org/3/movie/popular?api_key=###&language=it-IT&page=1&include_adult=false®ion=IT')
.then(response => {
this.movies = response.data.results
// console.log(response.data.results)
})
.catch(error => {
console.log(error)
this.errored = true
})
.finally(() => this.loading = false)
},
watch: {
switchButton(oldValue, newValue) {
if (oldValue != newValue) {
this.showButton(id) = true;
} //made an attempt here
}
},
methods: {
storeMovie(id) {
const favouriteMovie = this.movies.find(movie => movie.id === id )
this.watchlist.push(favouriteMovie);
localStorage.setItem("watchlist", JSON.stringify(this.watchlist));
},
removeMovie(id) {
const removedMovie = this.watchlist.find(movie => movie.id === id )
const indexMovie = this.watchlist.indexOf(removedMovie);
if (indexMovie > -1) {
this.watchlist.splice(indexMovie, 1);
}
localStorage.setItem("watchlist", JSON.stringify(this.watchlist));
},
showButton(id) {
const favouriteMovie = this.watchlist.find(movie => movie.id === id )
if (favouriteMovie && favouriteMovie.length > 0) {
return true
} else{
return false
}
}
},
}
</script>
<style scoped lang="scss">
</style>
A better approach would be to store the state of a movie being stored or not in the watchlist directly on the movie object.
Then use a computed to get the watchlist from the movie list instead of using two different arrays.
<template>
<div>
<div class="card" v-for="movie in movies" :key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button v-show="!movie.toWatch" type="submit" #click="storeMovie(movie.id)">
{{ movie.toWatch ? 'Rimuovi' : 'Aggiungi' }}
</button>
</div>
<div class="card" v-for="favourite in watchList" :key="favourite.id">
{{favourite.title}}
</div>
</div>
</template>
<script>
export default {
name: 'HomeComp',
data() {
return {
movies: [],
}
},
computed: {
// Get the watchList from the movies list
watchList() {
return this.movies.filter(movie => movie.toWatch)
}
},
watch: {
watchList(newWatchList) {
// Update the localStorage whenever the list changes
localStorage.setItem("watchlist", JSON.stringify(newWatchList));
}
},
mounted() {
// your axios call
},
methods: {
storeMovie(id) {
const favouriteMovie = this.movies.find(movie => movie.id === id)
if (favouriteMovie) {
// just reverse the boolean
favouriteMovie.toWatch = !favouriteMovie.toWatch
}
},
},
}
</script>
I'm having trouble creating pagination with vue. My task is to make sure that when you click on the numbers of the buttons, new tasks from jsonplaceholder are loaded.
I have successfully loaded the first and second page. I assume this is related directly to my this.fetchTodos() action. I'm just learning vue and I need help figuring out how to update the data when moving to a new page without loading.
In this case, it is necessary that the url of the page changes (get request). My page state is changing, but posts are not loading when clicking on the third page.
Below is the code of four files that I think will help you understand the situation.
Maybe you will easier with GitHub, please check pagination branch
Thanks in advance for your help! If you have questions or need more information, write in the comments
TodoListView.vue - is starting page, where is todos fetching and rendered on page.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="todolist">
<ContainerBootstrap>
<div class="row">
<div class="col-12 text-center">
<TitlePage v-if="todos" text="Список задач"/>
<TitlePage v-else text="Список задач пуст, создайте новую!"/>
<button-bootstrap data-bs-toggle="modal" data-bs-target="#createTodo" css-class="btn-lg btn-primary mt-2 mb-4">Создать задачу</button-bootstrap>
<ModalBootstrap #create="createTodo" :todos="todos" css-id="createTodo"/>
<SearchBootstrap v-if="todos" #search="searchTodo"/>
<div v-if="todos" class="d-flex justify-content-end mt-2">
<button-bootstrap #click.native="setCompletedToAllTodo()" css-class="btn-lg btn-success">Отменить всё как "Выполненные"</button-bootstrap>
</div>
</div>
</div>
<TodoList v-if="todos" :todos="searchedTodos"/>
<PaginationBootstrap :page="page" :total-pages="totalPages" class="mt-4"/>
</ContainerBootstrap>
</div>
</template>
<script>
import ContainerBootstrap from "#/components/UI/ContainerBootstrap";
import TitlePage from "#/components/TitlePage";
import TodoList from "#/components/TodoList";
import {mapState, mapActions, mapMutations, mapGetters} from 'vuex'
import ButtonBootstrap from "#/components/UI/ButtonBootstrap";
import ModalBootstrap from "#/components/UI/ModalBootstrap";
import SearchBootstrap from "#/components/UI/SearchBootstrap";
import PaginationBootstrap from "#/components/UI/PaginationBootstrap";
export default {
name: "TodoListView",
components: {
PaginationBootstrap,
SearchBootstrap, ModalBootstrap, TodoList , ButtonBootstrap, TitlePage, ContainerBootstrap},
data: function() {
return {
isShow: false,
}
},
methods: {
...mapActions({
fetchTodos: "todos/fetchTodos"
}),
...mapMutations({
setSearchQuery: 'todos/setSearchQuery'
}),
createTodo(todo) {
this.$store.commit('todos/addTodo', todo);
},
setCompletedToAllTodo() {
console.log('hello')
this.$store.commit('todos/setCompletedToAllTodo')
},
searchTodo(query) {
this.$store.state.todos.searchQuery = query;
}
},
mounted() {
this.fetchTodos()
},
computed: {
...mapState({
todos: state => state.todos.todos,
isTodosLoading: state => state.todos.isTodosLoading,
page: state => state.todos.page,
limit: state => state.todos.limit,
totalPages: state => state.todos.totalPages,
searchQuery: state => state.todos.searchQuery
}),
...mapGetters({
searchedTodos: 'todos/searchedTodos'
})
}
}
</script>
TodoListPaginationView - is second file, where is loading second page and another when click on pagination.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="todolist">
<ContainerBootstrap>
<div class="row">
<div class="col-12 text-center">
<TitlePage :text="'Страница №'+ page"/>
<router-link to="/todolist">
<button-bootstrap css-class="btn-lg btn-primary mt-2 mb-4">Вернуться к началу</button-bootstrap>
</router-link>
</div>
<TodoList v-if="todos" :todos="searchedTodos"/>
<PaginationBootstrap :page="page" :total-pages="totalPages" class="mt-4"/>
</div>
</ContainerBootstrap>
</div>
</template>
<script>
import ContainerBootstrap from "#/components/UI/ContainerBootstrap";
import TitlePage from "#/components/TitlePage";
import ButtonBootstrap from "#/components/UI/ButtonBootstrap";
import TodoList from "#/components/TodoList";
import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import PaginationBootstrap from "#/components/UI/PaginationBootstrap";
export default {
name: "TodoListPaginationView",
components: {PaginationBootstrap, TodoList, ButtonBootstrap, TitlePage, ContainerBootstrap},
methods: {
...mapActions({
fetchTodos: "todos/fetchTodos",
}),
...mapMutations({
setSearchQuery: 'todos/setSearchQuery'
})
},
computed: {
...mapState({
todos: state => state.todos.todos,
isTodosLoading: state => state.todos.isTodosLoading,
page: state => state.todos.page,
limit: state => state.todos.limit,
totalPages: state => state.todos.totalPages,
searchQuery: state => state.todos.searchQuery
}),
...mapGetters({
searchedTodos: 'todos/searchedTodos'
})
},
mounted() {
this.fetchTodos()
},
}
</script>
PaginationBootstrap.vue - third file, where is logic for pagination. Ui bootstrap 5 file.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Предыдущая</a></li>
<li v-for="pageNumber in totalPages" :key="pageNumber" :class="{'active' : page === pageNumber}" class="page-item">
<span #click="changePage(pageNumber)" class="page-link">{{pageNumber}}</span>
</li>
<li class="page-item"><a class="page-link" href="#">Далее</a></li>
</ul>
</nav>
</template>
<script>
export default {
name: "PaginationBootstrap",
props: {
page: Number,
totalPages: Number
},
methods: {
changePage(pageNumber) {
this.$store.commit('todos/setPage', pageNumber);
if (pageNumber === 1) {
this.$router.push('/todolist')
}
else {
this.$router.push({name: 'todolistPagination', params: {page: pageNumber}})
}
}
}
}
</script>
<style lang="scss" scoped>
.pagination {
.page-item {
.page-link {
cursor: pointer;
}
}
}
</style>
todosModule.js - last file, where is vuex logic for todos.
import axios from "axios";
export const todosModule = {
state: () => ({
todos: [],
page: 1,
limit: 10,
totalPages: 0,
isTodosLoading: false,
searchQuery: '',
}),
mutations: {
setTodos(state, todos) {
state.todos = todos
},
setPage(state, page) {
state.page = page
},
setTotalPages(state, totalPages) {
state.totalPages = totalPages
},
setLoadingTodos(state, bool) {
state.isTodosLoading = bool
},
setCompleted(state, completed) {
const index = state.todos.findIndex(todo => todo.id === completed.id);
state.todos[index].completed = completed.completed
},
setCompletedToAllTodo(state) {
state.todos.map(obj => {
obj.completed = true
})
},
removeTodo(state, id) {
const index = state.todos.findIndex(todo => todo.id === id)
state.todos.splice(index, 1)
},
addTodo(state, todo) {
state.todos.unshift(todo);
},
setTitle(state, tusk) {
const index = state.todos.findIndex(todo => todo.id === tusk.id);
state.todos[index].title = tusk.title
},
setSearchQuery(state, searchQuery) {
state.searchQuery = searchQuery;
}
},
actions: {
async fetchTodos({state, commit}) {
try {
commit('setLoadingTodos' , true)
const response = await axios.get('https://jsonplaceholder.typicode.com/todos', {
params: {
_page: state.page,
_limit: state.limit
}
})
commit('setTotalPages', Math.ceil(response.headers['x-total-count'] / state.limit))
commit('setTodos', response.data)
}
catch (e) {
console.log(e)
}
finally {
commit('setLoadingTodos', false)
}
},
async getCurrentPage({commit}, currentPage) {
try {
commit('setPage', currentPage)
}
catch (e) {
console.log(e);
}
}
},
getters: {
searchedTodos(state) {
return [...state.todos].filter(todo => todo.title.toLowerCase().includes(state.searchQuery.toLowerCase()))
},
},
namespaced: true
}
Okey, I found solution for myself.
Most importent thing is watcher. I added to TodoListPaginationView.vue next code:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="todolist">
<ContainerBootstrap>
<div class="row">
<div class="col-12 text-center">
<TitlePage :text="'Страница №'+ page"/>
<router-link to="/todolist">
<button-bootstrap css-class="btn-lg btn-primary mt-2 mb-4">Вернуться к началу</button-bootstrap>
</router-link>
</div>
<TodoList v-if="todos" :todos="searchedTodos"/>
<PaginationBootstrap :page="page" :total-pages="totalPages" class="mt-4"/>
</div>
</ContainerBootstrap>
</div>
</template>
<script>
import ContainerBootstrap from "#/components/UI/ContainerBootstrap";
import TitlePage from "#/components/TitlePage";
import ButtonBootstrap from "#/components/UI/ButtonBootstrap";
import TodoList from "#/components/TodoList";
import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import PaginationBootstrap from "#/components/UI/PaginationBootstrap";
export default {
name: "TodoListPaginationView",
components: {PaginationBootstrap, TodoList, ButtonBootstrap, TitlePage, ContainerBootstrap},
methods: {
...mapActions({
fetchTodos: "todos/fetchTodos",
}),
...mapMutations({
setSearchQuery: 'todos/setSearchQuery'
})
},
computed: {
...mapState({
todos: state => state.todos.todos,
isTodosLoading: state => state.todos.isTodosLoading,
page: state => state.todos.page,
limit: state => state.todos.limit,
totalPages: state => state.todos.totalPages,
searchQuery: state => state.todos.searchQuery
}),
...mapGetters({
searchedTodos: 'todos/searchedTodos'
})
},
watch: {
page: function (val) {
if (val) {
this.fetchTodos()
}
},
},
mounted() {
this.fetchTodos();
},
}
</script>
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 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);
}
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.