Vue 3: Edit modal data flow - javascript

I'm new to Vue and I created a list of items and each item has an edit-button. When I click the button, I open a modal window and pass the item's reactive data object to the modal (:data="itemData") to fill a form for editing the data.
When editing the data in the form, I don't want the original items in the list to change. Therefor I made a copy of the data prop (not sure if I did this correctly). It seems to work. When I save the changes, the list updates accordingly.
The problem is, this only works once. After editing and saving e.g. the 1st item in the list and then try to edit the 2nd item, nothing happens. I expected the edit form to update and show the 2nd item's data. I'm obviously doing something wrong, but I can't figure out what it is and how to do this properly. Please help.
Here's a very basic dummy for demonstration:
App.vue:
<script setup>
import { reactive } from 'vue';
import EditModal from './components/EditModal.vue'
import ListItem from './components/ListItem.vue'
const state = reactive({
items: [
{ id: 1, title: 'Item 1' },
{ id: 2, title: 'Item 2' }
],
currentItem: null,
showModal: false
});
function editItem(itemData) {
state.showModal = true;
state.currentItem = itemData;
}
function updateItem(itemData) {
state.items = state.items.map(item => item.id === itemData.id ? itemData : item);
}
</script>
<template>
<div>
<EditModal
v-if="state.currentItem"
:show="state.showModal"
:data="state.currentItem"
#save="itemData => updateItem(itemData)"
/>
<ListItem
v-for="itemData in state.items"
:data="itemData"
#edit="itemData => editItem(itemData)"
/>
</div>
</template>
ListItem.vue:
<script setup>
defineProps(['data']);
</script>
<template>
<div>
{{ data.title }}
<button #click="$emit('edit', data)">Edit</button>
</div>
</template>
EditModal.vue:
<script setup>
import { reactive, toRaw } from 'vue';
const props = defineProps(['show', 'data']);
const data = Object.assign({}, toRaw(props.data));
</script>
<template>
<div v-show="show">
<div class="modal">
<div class="modal-inner">
<input v-model="data.title">
<button #click="$emit('save', data)">Save</button>
</div>
</div>
</div>
</template>

For better solution (my opinion)
There is a getter and setter in 'computed' you could try to use it instead of using 'watch'
https://vuejs.org/guide/essentials/computed.html#writable-computed
<script setup>
import { reactive, computed } from 'vue';
const props = defineProps(['show', 'data']);
const state = reactive({ data: null });
const inputData = computed({
get() { return props.data; },
set(newVal) { state.data = newVal },
});
</script>
<template>
<div v-show="show">
<div class="modal">
<div class="modal-inner">
<input v-model="inputData">
<button #click="$emit('save', state.data)">Save</button>
</div>
</div>
</div>
</template>

Okay, I think I solved it. If someone has a better solution, please let me know.
EditModal:
<script setup>
import { toRaw, reactive, watch } from 'vue';
const props = defineProps(['show', 'data']);
let data = Object.assign({}, toRaw(props.data));
let state = reactive({ data: data });
watch(() => props.data, (selection, prevSelection) => {
data = Object.assign({}, toRaw(selection));
state.data = data;
});
</script>
<template>
<div v-show="show">
<div class="modal">
<div class="modal-inner">
<input v-model="state.data.title">
<button #click="$emit('save', state.data)">Save</button>
</div>
</div>
</div>
</template>

Related

Why cant i bind data to my v-model when passing data from parent to child component?

I would like to pass data from my parent to child component and bind data to a input field through v-model to display data from my api call in parent component. But it seems to be problem when binding to input field i get this error message:
Unexpected mutation of "data" prop.eslintvue/no-mutating-props
Partent Component
<script lang="ts">
import { defineComponent,ref } from 'vue';
import axios from 'axios'
import ChildComponent from '../components/ChildComponent.vue';
export default defineComponent({
Component: { ChildComponent },
name: 'IndexPage',
setup() {
return {
fixed: ref(false),
data: []
};
},
mounted() {
this.getData();
},
methods: {
getData() {
axios.get('/api/Hotel/' + 2).then((response) => {
this.data = response.data;
this.fixed = true,
console.log(this.data);
});
}
},
components: { ChildComponent }
});
</script>
Child Component
<template>
<main>
<q-card class="height: 500px; width: 500px">
<q-card-section>
<div class="text-h6">Terms of Agreement</div>
<div class="q-pa-md">
<div class="q-gutter-md" style="max-width: 300px">
<div>
<q-input filled v-model="data.message" label="Filled" />
</div>
</div>
</div>
</q-card-section>
<q-separator />
<q-separator />
<q-card-actions align="right">
<q-btn flat label="Decline" color="primary" v-close-popup />
<q-btn flat label="Accept" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</main>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
props:['data'],
name: 'ChildComponent',
setup() {
return {
text: ref(''),
};
},
});
</script>
I have tried this to make mounted methods in my child components like this:
<div>
<q-input filled v-model="dataIn.message" label="Filled" />
</div>
export default defineComponent({
props:['data'],
name: 'ChildComponent',
setup() {
return {
text: ref(''),
dataIn:{}
};
},
mounted(){
this.dataIn = this.data
},
});
It seems to work but not optimal, i lost my data when i refresh my page. Anyone have a soulution ?
Props should be read readonly.
Your dataIn approach needs a watcher that will update your dataIn whenever your data props change
optionsApi:
export default defineComponent({
props:['data'],
name: 'ChildComponent',
data() {
text: '',
data: this.dataIn,
}
watcher: {
dataIn: (newValue,oldValue){
this.data = newValue
}
}
});
It seems that you want to make change to your data on your child component, you have to make it two-way binding. You should change your child code like this ( you are using custom q-input in your component and the attributes may differ a little but it is the same concept) :
<q-input
:value="value"
v-bind="$attrs"
v-on="$listeners"
#input="(v) => $emit('input', v)"
/>
and instead of using data prop you should change it to value :
props: {
value: {
type: [String], // multiple type also defenition accepted
default: "",
},
}
then in your parent simply use child component like this :
<your-child-component v-model="someData" />
If I understood you correctly, you need to emit event from child or to use computed property with getter/setter:
const { ref, onMounted, watch } = Vue
const app = Vue.createApp({
setup() {
const items = ref({})
const getData = () => {
items.value = ({id: 1, message: 'aaa'})
/*axios.get('/api/Hotel/' + 2).then((response) => {
this.data = response.data;
this.fixed = true,
console.log(this.data);
});*/
}
onMounted(async() => await getData())
// πŸ‘‡ react to chenges from child
const changed = (val) => {
items.value.message = val.message
}
return {
//fixed: ref(false),
items, changed
};
},
})
app.component('ChildComponent', {
template: `
<main>
<q-card class="height: 500px; width: 500px">
<q-card-section>
<div class="text-h6">Terms of Agreement</div>
<div class="q-pa-md">
<div class="q-gutter-md" style="max-width: 300px">
<div>
<!-- πŸ‘‡ listen to updating -->
<q-input filled v-model="text.message" #update:model-value="change" label="Filled" />
</div>
</div>
</div>
</q-card-section>
<q-separator />
<q-separator />
<q-card-actions align="right">
<q-btn flat label="Decline" color="primary" v-close-popup />
<q-btn flat label="Accept" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</main>
`,
props:['items'],
setup(props, {emit}) {
const text = ref(props.items)
// πŸ‘‡ emit event with changed value
const change = () => { emit('changed', text.value) }
// πŸ‘‡ watch for the props changes
watch(
() => props.items,
(newValue, oldValue) => {
text.value = newValue;
}
);
return {
text, change
};
},
})
app.use(Quasar)
app.mount('#q-app')
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/quasar#2.5.5/dist/quasar.prod.css" rel="stylesheet" type="text/css">
<div id="q-app">
{{items}}
<!-- πŸ‘‡ listen to child event -->
<child-component :items="items" #changed="changed"></child-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quasar#2.5.5/dist/quasar.umd.prod.js"></script>

How to toggle all el-collapse-items of ElementPlus Vue3 Library with a single button?

Using this code, how to expand and collapse.. toggle all el-collapse-items of ElementPlus Vue3 Library with a single button ?
<template>
<div class="demo-collapse">
<el-collapse v-model="activeName" accordion>
<el-collapse-item title="Consistency" name="1">
<script lang="ts" setup>
import { ref } from 'vue'
const activeName = ref('1')
</script>
https://element-plus.org/en-US/component/collapse.html#accordion
Take a look at following snippet pls :
const { ref } = Vue
const app = Vue.createApp({
setup() {
const items = ref([{id: 1, title: "first", text: "aaaaaaaaaaaa"}, {id: 2, title: "second", text: "bbbbbbbbbbbb"}, {id: 3, title: "third", text: "ccccccccccc"}])
const activeName = ref([1]);
const toggleAll = () => {
activeName.value = activeName.value.length === items.value.length
? []
: items.value.map(i => i.id)
}
return { items, activeName, toggleAll };
},
})
app.use(ElementPlus);
app.mount('#demo')
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
<script src="//unpkg.com/vue#3"></script>
<script src="//unpkg.com/element-plus"></script>
<div id="demo">
<div class="flex justify-space-between mb-4 flex-wrap gap-4">
<el-button type="primary" text bg #click="toggleAll">toggle all</el-button>
</div>
<div class="demo-collapse">
<el-collapse v-model="activeName" accordion>
<el-collapse-item v-for="item in items" :key="item.id" :title="item.title" :name="item.id">
<div>
{{ item.text }}
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
You can not do this in the accordion mode. As documentations says:
In accordion mode, only one panel can be expanded at once
To do this, you have to remove the accordion prop and change the activeName value to an array, just like in the documentation:
const activeNames = ref(['1'])
To expand/collapse all items you can create a function that will change the value of activeNames to contain all the names of el-collapse-item components or to be empty, e.g
toggleElements() {
if(activeName.value.length) {
activeName.value = [];
} else {
activeName.value = ['1', '2', '3', ...];
}
}

how to pass data from api as prop in vuejs

I am getting data from api which I want to pass as prop to a child component
Parent component
<template>
<div class="dashboard">
<items name="Admins" value={{data.items.hubs}} bgColor="#a80c0c" />
</div>
</template>
import Items from './Items.vue'
import { Admin } from '#/services/AdminService';
export default {
name: "AdminDashboard",
components: {
Items
},
setup(){
onMounted(() => {
showLoader(true);
Admin.getDashboardItems()
.then((response) => {
data.items = response.data.data
})
.catch((error) => {
})
.finally(() => {
showLoader(false);
});
});
return {
data
}
}
}
I have gotten the value I need from the api and passed it to data.items
If i display it on the parent component.
It works fine but on the child component
it does not work
Child Component
<template>
<div class="col-md-3">
<div class="items" :style="{ backgroundColor: bgColor }">
<div class="d-flex space-between">
<div></div>
<div>
<h5>{{ value }}</h5>
<span>{{ name }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Items",
props: ["bgColor", "value", "name"]
}
</script>
The child components display {{data.items.hubs}} instead of the value of hub
The data from the api
{"data":{"users":1,"incubatees":1,"hubs":2,"investors":1,"events":0,"admins":3,"programs":0}}
After some research
I found out I was not supposed to use {{}}
instead I should do it this way on the parent component
<items name="Hubs" :value="data.items.hubs" bgColor="#d79a2b" />

Passing Props Child -> Parent -> Other Child

I'm trying to create a basic form that, once input is submitted, sends data to the parent and is then rendered in a list as a card. Documentation has been pointing me towards using the Event Bus, but it all seems a little too over engineered for such a simple task. Not to mention its not working xD. Am I on the right track here? or missing the whole idea?
The Data seems to be updating on submit, but I'm not seeing a card render. I'm also seeing the follow error,
Property or method "initiativeList" is not defined on the instance but referenced during render.
I do, however, notice a particularly odd change in the render. Instead of a child being rendered in EncounterList.js the child's attributes are merging into the parent .
Any help is greatly appreciated.
EncounterDashboard.js
<template>
<div>
<NewCharacterForm #add-char="addChar" />
<EncounterList v-bind="encounterList" #add-char="addChar" />
</div>
</template>
<script>
import Character from "../classes/Encounter";
import NewCharacterForm from "./NewCharacterForm/NewCharacterForm.vue";
import EncounterList from "./EncounterList/EncounterList";
import EventBus from "./EventBus.js";
export default {
name: "EncounterDashboard",
components: { NewCharacterForm, EncounterList },
data() {
return {
newChar: {},
encounterList: []
};
},
methods: {
addChar(newChar) {
this.newChar = newChar;
this.encounterList.push(newChar);
EventBus.$emit("add-to-list", this.encounterList);
}
}
};
</script>
NewCharacterForm.js
<template>
<div class="new-char-wrapper">
<form class="char-form" ref="form" v-on:submit.prevent="handleSubmit">
<NewCharInput class="name-input" label="NAME" name="name" v-model="name" />
<div class="stat-wrapper">
<NewCharInput
class="init-input"
label="INITIATIVE"
name="initiative"
v-model="initiative"
type="number"
/>
<NewCharInput class="hp-input" label="HP" name="hp" v-model="hp" type="number" />
</div>
<div class="submit-row">
<button class="submit">SUBMIT</button>
</div>
</form>
</div>
</template>
<script>
import NewCharInput from "./NewCharInput";
import Character from "../../classes/Character";
import { uuid } from "vue-uuid";
export default {
name: "NewCharacterForm",
components: { NewCharInput },
data() {
return {
name: "",
initiative: "",
hp: 0
};
},
props: ["addChar"],
methods: {
handleSubmit() {
const charName = this.$refs.form.name.value;
const charInitiative = this.$refs.form.initiative.value;
const charHp = this.$refs.form.hp.value;
const charId = this.$uuid.v4();
const newChar = new Character(charName, charInitiative, charId, charHp);
this.$emit("add-char", newChar);
}
}
};
</script>
EncounterList.js
<template>
<div class="encounter-list">
<div class="header-row">
<h2 class="header col-init">INIT</h2>
<h2 class="header col-name">NAME</h2>
<h2 class="header col-hp">HP</h2>
</div>
<EncounterCard
v-for="character in initiativeList"
v-bind:key="character.id"
v-bind:hp="character.hp"
v-bind:name="character.name"
v-bind:initiative="character.initiative"
/>
</div>
</template>
<script>
import EncounterCard from "../EncounterCard/EncounterCard";
import EventBus from "../EventBus";
export default {
name: "EncounterList",
components: { EncounterCard },
data() {
return {
data: {
initiativeList: []
}
};
},
methods: {
populateList(charList) {
this.initiativeList = charList;
}
},
mounted() {
EventBus.$on("add-to-list", charList => {
this.populateList(charList);
});
}
};
</script>
EncounterCard.js
<template>
<div class="encounter-card-wrapper">
<h1 class="char-init">{{character.initiative}}</h1>
<h1 class="char-name">{{character.name}}</h1>
<h1 class="char-hp">{{character.hp}}</h1>
</div>
</template>
<script>
export default {
name: "EncounterCard",
props: ["character"]
};
</script>
data() {
return {
data: { //Is this what you're trying to do?
initiativeList: []
}
};
},
If the data attribute is intended, "initiativeList" should be changed to "data.initiativeList".
<EncounterCard
v-for="character in data.initiativeList"
v-bind:key="character.id"
v-bind:hp="character.hp"
v-bind:name="character.name"
v-bind:initiative="character.initiative"
/>
and
populateList(charList) {
this.data.initiativeList = charList;
}

The problem of data transfer between two child components

Good afternoon, I have two child components Header and Pagination. In Header, I have an input search engine and two inputs (title and body) in order to be able to add a post to Pagination. I managed to transfer the search value to the Pagination component, but I don’t know how to transfer the value from two inputs (title, body). I use to transfer the event bus. Help me please pass the value of the two inputs (title, body) into the Pagination component when you click the AddPost button.
My code on GitHub
Screenshot of app
My code of component Header:
<template>
<div class="header">
<input type="text" v-model="search" class="header_input_search" placeholder="Search" #input="saveMessage" />
<img src="src/assets/milk.png">
<div class="header_div_inputs">
<input type="text" v-model="createTitle" class="created"/>
<p><input type="text" v-model="createBody" class="createBody"/></p>
</div>
<button #click="addPost()" class="addPost">AddPost</button>
</div>
</template>
<script>
import axios from 'axios';
import {eventEmitter} from './main'
export default {
name: 'Header',
data () {
return {
search: '',
createTitle: '',
createBody: '',
}
},
methods:{
saveMessage(){
eventEmitter.$emit('messageSave', this.search)
},
}
}
</script>
My code of component Pagination:
<template>
<div class = "app">
<ul>
<li v-for="(post, index) in paginatedData" class="post" :key="index">
<router-link :to="{ name: 'detail', params: {id: post.id, title: post.title, body: post.body} }">
<img src="src/assets/nature.jpg">
<p class="boldText"> {{ post.title }}</p>
</router-link>
<p> {{ post.body }}</p>
</li>
</ul>
<div class="allpagination">
<button type="button" #click="page -=1" v-if="page > 0" class="prev"><<</button>
<div class="pagin">
<button class="item"
v-for="n in evenPosts"
:key="n.id"
v-bind:class="{'selected': current === n.id}"
#click="page=n-1">{{ n }} </button>
</div>
<button type="button" #click="page +=1" class="next" v-if="page < evenPosts-1">>></button>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import {eventEmitter} from './main'
export default {
name: 'app',
data () {
return {
current: null,
page: 0,
visiblePostID: '',
pSearch: ''
}
},
mounted(){
this.$store.dispatch('loadPosts')
},
computed: {
...mapState([
'posts'
]),
evenPosts: function(posts){
return Math.ceil(this.posts.length/6);
},
paginatedData() {
const start = this.page * 6;
const end = start + 6;
return this.filteredPosts.slice(start, end);
},
filteredPosts() {
return this.posts.filter((post) => {
return post.title.match(this.pSearch);
});
},
},
created(){
eventEmitter.$on('messageSave', (string) => {
this.pSearch = string
})
}
}
</script>
You can wrap title and body in an object
addPost() {
const post = {
title: this.createTitle,
body: this.createBody
}
eventEmitter.$emit('postAdd', post)
}
and then listen as normal
created(){
eventEmitter.$on('postAdd', (post) => {
console.log(post)
// do whatever you want
})
}
I have not worked on vue js but agreed with #ittus answer. You can make an object consisting of your required data which you want to share across the component and pass it as an event data.

Categories