How do I edit vue data passed as a property? - vue.js

I have a Vuex data store with an array of contacts. I display that array in ContactList.vue, where I have an "Edit" button that navigates to CreateEditContact.vue (I am passing in the contact as a prop named contactProp). All of this works, but as we all know, you can't edit a prop directly. I tried checking to see if the prop is populated when the template is created, and copying it to contact in data but it isn't working. What's the right way to do this?
ContactList.vue
<template>
<!-- ...redacted for brevity -->
<!-- I'm reusing the same .vue for creating new and editing contacts -->
<!-- edit contact button -->
<v-btn text
:to="{name: 'createEditContact',
params: {
action: 'edit',
id: contact.id,
contactProp: contact
}}">Edit</v-btn>
<!-- new contact button -->
<v-btn
:to="{ name: 'createEditContact', params: { action: 'create' } }"
outlined
rounded>New</v-btn>
</template>
CreateEditContact.vue
<template>
<div>
<h3>Create or Edit a Contact</h3>
<v-form #submit.prevent="saveContact">
<v-container>
<v-row>
<v-col>
<v-text-field label="Name" v-model="contact.name" />
</v-col>
<v-col>
<v-text-field label="Phone Number" v-model="contact.phone" />
</v-col>
<v-col>
<v-text-field label="Extension" v-model="contact.extension" />
</v-col>
<v-col>
<v-btn type="submit">Save</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</div>
</template>
<script>
import axios from "axios";
const options = {
headers: { "Content-Type": "application/json" }
};
export default {
name: "CreateEditContact",
props: {
action: {
type: String,
required: true,
validator: value => ["create", "edit"].indexOf(value) !== -1
},
id: {
required: false
},
// contactProp gets set correctly when editing...
contactProp: {}
},
data() {
return {
// the empty contact (when creating a new contact)
contact: {
id: 0,
name: "",
phone: "",
extension: "",
fullString: "",
telephoneUrl: ""
}
};
},
methods: {
saveContact() {
// redacted for brevity
},
created() {
// when contactProp is populated, I need to use that...
// but contact is always empty when I try to edit one.
// this next line isn't doing what I think it should.
if (this.contactProp) this.contact = this.contactProp;
}
}
};
</script>

Related

Vuetify v-dialog not showing the second time

I'm trying to to conditionally show a dialog. The dialog shows up the first time just fine.
But when second time I try to remount the component I can't see the dialog.
The dialog also fetches some data when it is mounted. I can see the logs that the dialog is being mounted and unmounted and also the network request on chrome devtools network tab but I can't see the dialog.
index.vue
<v-list-item-title #click="changeDialogState">
<TestDialog
v-if="showEditDialog"
:id="item.id"
#editDone="changeDialogState"/>
Edit
</v-list-item-title>
------------------------
//This is defined inside methods
changeDialogState() {
this.showEditDialog = !this.showEditDialog // this.showEditDialog is just a boolean value define inside data()
},
Testdialog.vue
<template>
<v-dialog v-model="dialog" width="700">
<v-progress-circular
v-if="fetching"
:size="70"
:width="7"
color="purple"
indeterminate
></v-progress-circular>
<v-card v-else>
<v-card-title> New Customer </v-card-title>
<v-divider></v-divider>
<v-card-text>
<customer-form
v-model="customer"
:errors="errors"
:initial-customer="customer"
#submit="editCustomer"
>
<template v-slot:footer>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text #click="$emit('editDone')"> Cancel </v-btn>
<v-btn color="primary" type="submit" class="ma-1">
Save Customer
</v-btn>
</v-card-actions>
</template>
</customer-form>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { formHandling, paginatedResponse } from '~/mixins'
export default {
mixins: [formHandling, paginatedResponse],
props: {
value: {
type: Object,
default: () => ({
//some stuff here
}),
},
error: {
type: Array,
required: true,
default: () => {
return []
},
},
id: {
type: String,
required: true,
},
},
async fetch() {
this.fetching = true
this.customer = await this.$axios.$get(`/customer/${this.id}/`)
this.fetching = false
},
data() {
return {
dialog: true,
customer: {
...
},
errors: {
...
},
fetching: true,
}
},
mounted() {
console.log('TestDialog - mounted')
},
beforeDestroy() {
console.log('TestDialog - unmounted')
},
methods: {
async editCustomer(customer) {
try {
await this.$axios.$put(`/customer/${this.id}/`, customer)
this.dialog = false
} catch (err) {
this.setErrors(err)
}
},
},
}
</script>

Vue.js pass $store data from different modules

Hi I need to understand how to "pass" some $store values from settings module to header module, being the $store updated by settingsmodule
I have this app.vue module:
<template>
<v-app>
<router-view name="sidebar" />
<router-view name="header" :handleSettingsDrawer="handleSettingsDrawer" />
<v-main class="vue-content">
<v-fade-transition>
<router-view name="settings" />
</v-fade-transition>
</v-main>
<router-view name="footer" />
<app-settings-drawer
:handleDrawer="handleSettingsDrawer"
:drawer="settingsDrawer"
/>
</v-app>
</template>
in the <router-view name="header" /> there are a couple of v-menu to select currency and language:
<v-menu offset-y close-on-click>
<template v-slot:activator="{ on }">
<v-btn v-on="on" small fab class="mr-3">
<v-avatar size="30">
<v-img :src="currentLocaleImg"></v-img>
</v-avatar>
</v-btn>
</template>
<v-list dense class="langSel">
<v-list-item
v-for="(item, index) in langs"
:key="index"
#click="handleInternationalization(item.value,item.rtl)"
>
<v-list-item-avatar tile class="with-radius" size="25">
<v-img :src="item.img"></v-img>
</v-list-item-avatar>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
in the script section I import the availableLanguages and availableCurrencies , I set them in data() where I also reference the user:
data() {
return {
items: [
{ icon: "settings", text: "Settings",link: "/settings" },
{ icon: "account_balance_wallet", text: "Account", link:"/tos" },
{ divider: true },
{ icon: "login", text: "Log In",link:"/auth/login" },
{ icon: "desktop_access_disabled", text: "Lock Screen",link:"/auth/lockscreen" },
{ icon: "exit_to_app", text: "Logout",link:"/auth/logout" },
],
langs: availableLocale,
currs: availableCurrency,
user: this.$store.state.userdata.user,
};
},
then, in computed: I have the 2 functions that should get the current value of curr and lang:
currentLocaleImg()
{
return this.langs.find((item) => item.value === this.$store.state.userdata.user.locale).img;
},
currentCurrencyImg() {
return this.currs.find((itemc) => itemc.value === this.$store.state.userdata.user.currency).img;
}
}
BUT the value of this.$store.state.userdata.user is updated firing the loadData() function mounted in the <router-view name="settings" /> module of App.vue
mounted(){
this.loadData();
},
methods:
{
async loadData(){
let jwt=sessionStorage.getItem('jwt');
try{
const res = await this.$http.post('/ajax/read_settings.php', {"jwt":jwt});
this.$store.dispatch("setUser", res.data.user);
this.$store.dispatch("setLocale", res.data.user.locale);
this.$store.dispatch("setCurrency", res.data.user.currency);
}
the problem is that header module does not have the value of this.$store.state.userdata.user.locale
(and .currency) and I get twice the following message:
vue.runtime.esm.js:1888 TypeError: Cannot read property 'img' of undefined
I do not know how to "pass" the data from one module to the other (or perhaps because the header is rendered before the settings module) and therefore the value of the currency and of the language are not known yet
What's wrong in this procedure?
How can I fix it?
There's a race condition in which you try to use data in the template that isn't loaded yet. But first, avoid setting store data directly into component data because the component data won't be updated when the store changes. Replace this:
user: this.$store.state.userdata.user
With a computed:
computed: {
user() {
return this.$store.state.userdata.user;
}
}
You can use v-if to conditionally render only when some data is available:
<v-img :src="currentLocaleImg" v-if="user"></v-img>
The currentLocaleImg computed will not even trigger until user has some data so you will avoid the error. Do the same for currentCurrencyImg if necessary.

How to create input fields dynamicaly in vuejs (vuetify)

I am new to vuejs. I'd wanted to create input fields dynamically by the click of a button using vuetify. My attempt to implement is shown below and commented line wise to show what I was trying to achieve.
I there a way this can be done? Any suggestion is welcome.
<template>
<v-container>
<v-row>
<v-col cols="12" sm="12">
<v-btn color="success">Add Input</v-btn>
</v-col>
<v-col cols="12" sm="12" ref="mount">
<!-- inputs fields are appended here -->
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
data() {
return {
items: {},
};
},
methods: {
addField() {
// create element
let textField = document.createElement("v-text-field");
// add it to the data property
this.$data.items["firstName"] = "";
// add the vmodel attribute to the element
textField.setAttribute("v-model", "firstName");
// finally mount it in the templates
this.$refs.mount.appendChild(textField);
},
},
};
</script>
You should use an array of objects like so basic: [{ name: "", time: "" }]. Then you can use a loop to bind inputfields to those properties and add items to that array. You can see a working example here
<div v-for="item in basic" :key="item.id">
<button #click="addRow">Add row</button>
<input type="text" v-model="item.name" />
<input type="text" v-model="item.time" />
{{ item.name }} - {{ item.time }}
</div>
data () {
return {
id: 1,
basic: [{ name: "", time: "" }]
};
}
addRow () {
console.log("added");
this.id += 1;
this.basic.push({
name: "",
time: ""
});
}

Why is my imported VueJS Component not displayed?

I have the following Vue view named "PasswordResetView":
<template>
<v-content>
<v-card>
<v-card-title primary-title>
Passwort ändern
</v-card-title>
<v-card-text>
<v-text-field
id="password"
label="Passwort"
name="password"
prepend-icon="mdi-lock"
type="password"
/>
<v-text-field
id="passwordRepeated"
label="Passwort wiederholen"
name="passwordRepeated"
prepend-icon="mdi-lock"
type="password"
/>
<v-text-field
id="mail"
label="E-Mail"
name="mail"
prepend-icon="mdi-lock"
type="text"
/>
</v-card-text>
</v-card>
</v-content>
</template>
<script>
import axios from "axios";
export default {
name: "passwordreset",
data() {
return {
password: "",
passwordRepeated: "",
mail: "",
errormessage: "",
};
},
methods: {
changePassword() {
let payload = {mail: this.mail, password:this.password, passwordRepeated: this.passwordRepeated};
axios({
method: "post",
url: "/api/anonymous/register/pwreset",
data: payload,
}).then(() => {
this.$props.passwortresetkey = "good"
})
},
}
};
</script>
<style scoped/>
The view is imported by another vue component "PasswordReset" as following:
<template>
<div>
<PasswordReset v-if="pwresetkey === 'good'"></PasswordReset>
<div v-else>
<v-card>
<v-card-title primary-title>
Passwort ändern
</v-card-title>
<v-card-text>
Leider ist der Link ungültig.
</v-card-text>
</v-card>
</div>
</div>
</template>
<script>
import PasswordReset from "../../../components/anon/PasswordReset";
export default {
name:"passwordreset",
components: PasswordReset
};
</script>
The corresponding router:
{
path: "/pwreset",
name: "pwreset",
meta: {
requiresDisponent: false,
requiresRecurring: false,
requiresOneTime: false,
requiresAnon: true
},
component: () => import("#/views/recurring/account/PasswordReset"),
props: true
},
However, when I start the application, only the content from "PasswordReset" is shown (the v-card), but not the input fields.
Also, in the component "PasswordReset" it says that export default is ununsed.
Why is this marked as unused and the view not imported?
From vuejs docs:
If you use kebab-case
Vue.component('my-component-name', { /* ... */ })
When defining a component with kebab-case, you must also use kebab-case when referencing its custom element, such as in .
if you use PascalCase
Vue.component('MyComponentName', { /* ... */ })
When defining a component with PascalCase, you can use either case when referencing its custom element. That means both and are acceptable. Note, however, that only kebab-case names are valid directly in the DOM (i.e. non-string templates).
Try changing your password reset component like so:
<script>
import PasswordReset from "../../../components/anon/PasswordReset";
export default {
name:"passwordreset",
components: {PasswordReset}
};
</script>
VueJS Component Registration

Grabbing data from multiple child components Vue js

I'm breaking my head for a few days now, trying to figure out how to grab the data from child components.
Situation is like this.
I have one parent component called Post where user can select date, title, description and which can contain multiple instances of Event compontents.
Event component contains fields like title, description, attendees.
User should be able to add multiple Eventcomponents which means I have multiple components Event within the Post component.
So, I can't figure out how can I compose my components to have an array of Event objects inside my Post component which I can later on send to my API.
the structure of the post object I need is:
// Post.vue
{
"date": '',
"name": '',
"description": '',
"events": {
{
"title": '',
"description": '',
"attendees": ''
},
{
"title": '',
"description": '',
"attendees": ''
}
}
}
So, I don't know should and how I would use vuex for it. I've tried using $emit to pass the data but I couldn't find it fit to get the data into Post model.
Can someone point me where should I look for it?
EDIT #1: Added sample code
The code for the components:
<template>
<v-form>
<v-container>
<v-row>
<v-col
cols="12"
md="4"
>
<v-date-picker v-model="post.date" scrollable>
<v-spacer />
<v-btn text color="primary" #click="modal = false">
Cancel
</v-btn>
<v-btn text color="primary" #click="$refs.dialog.save(date)">
OK
</v-btn>
</v-date-picker>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="post.name"
label="name"
required
/>
</v-col>
<v-col
cols="12"
md="4"
>
<v-textarea
v-model="post.description"
name="description"
label="Description"
dense
value
rows="4"
hint
/>
</v-col>
</v-row>
<v-row>
<v-btn primary rounded #click="addLine">
Add Event
</v-btn>
<v-expansion-panels accordion>
<UserEvent
v-for="(line, index) in lines"
:key="index"
#addLine="addLine"
#removeLine="removeLine(index)"
/>
</v-expansion-panels>
</v-row>
</v-container>
</v-form>
</template>
<script>
import UserEvent from './partials/event'
export default {
name: 'Post',
components: { UserEvent },
data () {
return {
post: [],
lines: [],
blockRemoval: true
}
},
watch: {
lines () {
this.blockRemoval = this.lines.length <= 1
}
},
mounted () {
},
methods: {
addLine () {
const checkEmptyLines = this.lines.filter(line => line.number === null)
if (checkEmptyLines.length >= 1 && this.lines.length > 0) { return }
this.lines.push({
title: null,
description: null,
attendees: null
})
},
removeLine (lineId) {
if (!this.blockRemoval) { this.lines.splice(lineId, 1) }
}
}
}
</script>
And the child component UserEvent
// UserEvent.vue
<template>
<v-expansion-panel>
<v-expansion-panel-header>Event details</v-expansion-panel-header>
<v-expansion-panel-content>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="event.title"
label="Title"
required
/>
</v-col>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="event.atttendees"
label="Atendees"
required
/>
</v-col>
<v-col
cols="12"
md="12"
>
<v-textarea
v-model="event.description"
name="description"
label="Description"
dense
value
rows="4"
hint
/>
</v-col>
<v-col
cols="12"
md="3"
>
<div class="block float-right">
<v-btn #click="removeLine(index)" />
<v-btn v-if="index + 1 === lines.length" #click="addLine" />
</div>
</v-col>
</v-row>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
export default {
name: 'UserEvent',
props: ['line', 'index'],
data () {
return {
event: []
}
},
methods: {
addLine () {
this.$emit('addLine')
},
removeLine (index) {
this.$emit('removeLine', index)
}
}
}
</script>
Here's an example with a similar structure what was posed in the question:
{
name: String,
events: [
title: String,
description: String,
],
}
This example allows the user to open a form to add a new event. When that form is submitted, the event data is added to the parent component's state.
Parent
<template>
<div>
<input v-model="name" />
<ul v-if="events.length">
<li v-for="(event, index) in events" :key="index">
<span>{{ event.title }}</span>
<span>{{ event.description }}</span>
</li>
</ul>
<Event v-if="isNewEventFormVisible" #submit="addEvent" />
<button v-else #click="showNewEventForm">add event</button>
</div>
</template>
import Event from '~/components/Event';
export default {
components: { Event },
data() {
return {
name: 'Example Post',
events: [],
isNewEventFormVisible: false,
};
},
methods: {
addEvent({ title, description }) {
this.isNewEventFormVisible = false;
this.events.push({ title, description });
// TODO: call you API here to update
},
showNewEventForm() {
this.isNewEventFormVisible = true;
},
},
};
Event
<template>
<form #submit.prevent="onSubmit">
<input v-model.trim="title" type="text" />
<br />
<textarea v-model.trim="description" />
<button type="submit">submit</button>
</form>
</template>
export default {
data() {
return {
title: '',
description: '',
};
},
methods: {
onSubmit() {
this.$emit('submit', {
title: this.title,
description: this.description,
});
},
},
};
You could imagine a more sophisticated version of this where events are editable. In that case, each Event could take props and bind them as values to its input instead of using v-models.