"this is null" when change in v-select [NuxtJs] - vue.js

this is my first post! Hope you can help.
So I'm on some new project to migrate to nuxtJs.
I have some trouble with my v-select and v-model, here's the code:
_id.vue:
<template>
<v-container fluid white>
<v-card>
<v-card-title class="headline">
TEST:
</v-card-title>
<v-card-text>
<p>info :</p>
<p>{{this.host.typeS}}</p>
<v-select
v-model="this.host.typeS"
:items="this.fields.typeS"
item-value="value"
item-text="value"
dense outlined>
</v-select>
</v-card-text>
</v-card>
</v-container>
</template>
script
export default {
name: 'EditPage',
data() {
return {
tab: null,
fields: [],
host: {},
tabHeaders: ["tab1", "tab2", "tab3", "tab4", "tab5", "tab6"]
}
},
async fetch() {
this.fields = await fetch("APIfields")
.then(res => res.json());
if (this.$nuxt._route.params.id) {
this.host = await fetch("APIhost" + this.$nuxt._route.params.id).then(res => res.json());
} else {
this.host = {};
}
}
}
responses
APIfields:
{"typeS": [{"value": "p","order": 100}, {"value":"v","order": 100}],
"otherThings": [{"value":"se", "order": 100},{"value":"sa", "order": 100}]}
APIhost/id:
{"typeS": "p",
"otherThings": "se"}
When everything run, My select is initiated with the value "p" and when clicked I see all the values this field can have.
When I try selecting "v" in the v-select, I'm redirected to my error layout, and the console say "Type error: this is null".
Without the v-model, I don't have any error, but the v-select have no selected value.
The goal is to initialise a form based on an existing host, change the data inside the form and then submit it to the database. The form should also be used to create new hosts (empty form)
The same error appear in my textfields and checkboxes.
Do you have any idea or track I can follow?

you don't need this when working on vue templates
<template>
<v-container fluid white>
<v-card>
<v-card-title class="headline">
TEST:
</v-card-title>
<v-card-text>
<p>info :</p>
<p>{{host.typeS}}</p>
<v-select
v-model="host.typeS"
:items="fields.typeS"
item-value="value"
item-text="value"
dense outlined>
</v-select>
</v-card-text>
</v-card>
</v-container>
</template>
note that :items takes an array but you are using fields.typeS so it's better to set field in data as
fields: {}
since it's an object

Related

How to use v-form inside a v-for and perform validation for a specific form?

I have an array of objects which I should loop through and show a form for each object's properties. The Save button is out of the for loop. In the attached sandbox, the 2nd object doesn't contain lastname. So, how do I perform validation on click of Save button only for the 2nd form? And is there any way to validate all the forms at once? Please refer to the sandbox for a better understanding.
https://codesandbox.io/s/jolly-kepler-m260fh?file=/src/components/Playground.vue
Check this codesandbox I made: https://codesandbox.io/s/stack-72356987-form-validation-example-4yv87x?file=/src/components/Playground.vue
You can validate all v-text-field at once if you move the v-form outside the for loop. All you need to do is give the form a ref value and a v-model to make use of the built in vuetify validation methods.
<template>
<v-container class="pl-10">
<v-form ref="formNames" v-model="validForm" lazy-validation>
<v-row v-for="(name, index) in names" :key="index">
<v-col cols="12">
<v-text-field
v-model="name.firstName"
outlined
dense
solo
:rules="rulesRequired"
/>
</v-col>
...
</v-row>
</v-form>
<v-btn type="submit" #click="submitForm" :disabled="!validForm">
Submit
</v-btn>
</v-container>
</template>
Then in the submit button all you need to do is call the validate() method of the form through the $refs object. You can also disable the submit button if any of the elements in the form don't pass the validation rules using the v-model of the form to disable the submit button.
<script>
export default {
name: "playground",
data: () => ({
validForm: true,
names: [
{
firstName: "john",
lastName: "doe",
age: 40,
},
{
firstName: "jack",
lastName: "",
age: 30,
},
],
rulesRequired: [(v) => !!v || "Required"],
}),
methods: {
submitForm() {
if (this.$refs.formNames.validate()) {
// Form pass validation
}
},
},
};
</script>
As submit button is outside of the forms. We can perform that validation on submit event by iterating the names array and check if any value is empty and then assign a valid flag value (true/false) against each object.
Demo :
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: () => ({
names: [
{
firstName: "john",
lastName: "doe",
age: 40,
valid: false
},
{
firstName: "jack",
lastName: "",
age: 30,
valid: false
},
],
requiredRule: [v => !!v || 'Value is required']
}),
methods: {
submitForm() {
this.names.forEach((obj, index) => {
if (!obj.lastName) {
obj.valid = false;
console.log(
`${index + 1}nd form is not valid as LastName is not available`
);
} else {
obj.valid = true;
}
});
// Now you can filter out the valid form objects based on the `valid=true`
}
}
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#2.6.6/dist/vuetify.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vuetify#2.6.6/dist/vuetify.min.css"/>
<div id="app">
<v-app id="inspire">
<v-container class="pl-10">
<v-row v-for="(name, index) in names" :key="index">
<v-form v-model="name.valid">
<v-col cols="12">
<v-text-field v-model="name.firstName" outlined dense solo required :rules="requiredRule" />
</v-col>
<v-col cols="12">
<v-text-field v-model="name.lastName" outlined dense solo required :rules="requiredRule" />
</v-col>
<v-col cols="12">
<v-text-field v-model="name.age" outlined dense solo required :rules="requiredRule" />
</v-col>
</v-form>
</v-row>
<v-btn type="submit" #click="submitForm"> Submit </v-btn>
</v-container>
</v-app>
</div>

Vuetify reset form after submitting

I am using a form inside dialog using vuetify.
Imported the component in page like this -
<template>
<div>
<topicForm :dataRow="dataRow" v-model="dialog" />
</div>
</template>
methods: {
openDialog(item = {}) {
this.dataRow = item;
this.dialog = true;
},
}
Dialog form code -->
<template>
<div>
<v-dialog v-model="value" max-width="500px" #click:outside="close">
<v-card outlined class="pt-5">
<v-form ref="form" class="px-3">
<v-card-text class="pt-5">
<v-row no-gutters>
<v-text-field
required
outlined
label=" Name"
v-model="data.name"
:rules="[rules.required]"
></v-text-field>
</v-row>
<v-row no-gutters>
<v-textarea
required
outlined
label=" Description"
v-model="data.description"
></v-textarea>
</v-row>
</v-card-text>
</v-form>
<v-divider> </v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
large
dark
outlined
color="success"
#click="save"
class="ma-3"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
props: [
"dataRow",
"value",
// ----------------------
],
methods: {
save() {
if (this.$refs.form.validate()) {
this.$root
.$confirm("Are you sure you want to save?")
.then((confirm) => {
if (confirm) {
this.ADD_TOPIC_DATA(this.data)
.then((data) => {
this.FETCH_TOPIC_DATA();
this.$refs.form.reset();
this.$refs.form.resetValidation();
this.close();
})
.catch((err) => {
console.log(err)
});
}
});
}
},
close() {
this.$emit("input", false);
},
}
watch: {
dataRow(val) {
this.data = { ...val };
},
},
Problem I am having is after adding a data, then if I try to add again by opening the dialog, the required field shows validation error, which is name here!
Image of that -->
Searched in stackoverflow. Found that should use this.$refs.form.reset(). Used that in save method without success. Also used this.$refs.form.resetValidation(), but don't work.
Any suggestion?
Thanks in advance.
The problem here is you're assigning new value to dataRow when opening the dialog which triggers validation inside the dialog. You could also use lazy-validation prop which allows you to only manually trigger the validation.

Imported component and console message

I have a problem with props in imported component.
I imported component with dialog template and I want display text from props and it works, but I have errors in console.
In console I see this error:
[Vue warn]: Error in render: "TypeError: Cannot read property
'test' of null"
But after open dialog value is correctly.
How can I fix this problem so that the error in console doesn't show up?
My code:
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<v-card class="">
<TipItem v-for="tip in tips" :key="tip.id" v-on:open-dialog="openTipDetailsDialog(tip)"></TipItem>
</v-card>
</v-col>
</v-row>
<TipDetailsDialog :dialog="tipDetailsDialog" :tip="tipDetails" v-on:close-dialog="tipDetailsDialog = false"></TipDetailsDialog>
</v-container>
</template>
<script>
import TipItem from "../components/TipItem";
import TipDetailsDialog from "../components/TipDetailsDialog";
export default {
name: "Tips",
components: {
TipItem,
TipDetailsDialog
},
data: () => ({
tips: [],
tipDetailsDialog: false,
tipDetails: null
}),
methods: {
openTipDetailsDialog(tip) {
this.tipDetailsDialog = true;
this.tipDetails = tip;
}
}
};
</script>
TipDetailsDialog
<template>
<v-dialog
:value="dialog"
fullscreen
hide-overlay
transition="dialog-bottom-transition"
#input="$emit('close-dialog')"
>
<v-card class="rounded-0">
<v-toolbar color="primary">
<v-toolbar-title>Szczegóły</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon dark #click="$emit('close-dialog')">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-container>
<v-row>
<v-col cols="6">
<v-card>
{{ tip.test }}
</v-card>
</v-col>
<v-col cols="6">
<v-card>
{{ tip.test2 }}
</v-card>
</v-col>
</v-row>
</v-container>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: "TipDetailsDialog",
props: ["dialog", "tip"]
};
</script>
tipDetails is initialized as null, and then bound to <TipDetailsDialog>.tip, which causes it to try to render tip.test (where tip is null).
Since TipDetailsDialog doesn't really render anything meaningful without a populated tip, that component should probably be conditionally rendered only when tipDetails is truthy:
<TipDetailsDialog v-if="tipDetails" :tip="tipDetails" />
tipDetails is initialized as null, and then bound to
<TipDetailsDialog>.tip, which causes it to try to render tip.test
(where tip is null).
Since TipDetailsDialog doesn't really render anything meaningful
without a populated tip, that component should probably be
conditionally rendered only when tipDetails is truthy:
html <TipDetailsDialog v-if="tipDetails" :tip="tipDetails" />
Or you can set a default value to the prop:
props: {
tip: {
type: String,
required: false,
default: () => ""
},
},

Value of v-text-field returns null when text area has a value

I'm trying to get the value from vuetify's v-text-field. The problem I am having is that I thought by typing something in, the field will automatically assign whatever is typed to vue-text-field's value. However this doesn't seem to be the case. Below is the code directly from vuetify with some additional code attempting to extract the value:
<template>
<v-card
class="overflow-hidden"
color="purple lighten-1"
dark
>
<v-toolbar
flat
color="purple"
>
<v-icon>mdi-account</v-icon>
<v-toolbar-title class="font-weight-light">
{{setterTitle}}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn
color="purple darken-3"
fab
small
#click="isEditing = !isEditing"
>
<v-icon v-if="isEditing">
mdi-close
</v-icon>
<v-icon v-else>
mdi-pencil
</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-text-field ref="rowCount"
:disabled="!isEditing"
:value="2"
autofocus
color="white"
label="Number of Rows"
></v-text-field>
<v-autocomplete
:disabled="!isEditing"
:items="states"
:filter="customFilter"
color="white"
item-text="name"
label="Number of Columns"
></v-autocomplete>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="!isEditing"
color="success"
#click="save"
>
Save
</v-btn>
</v-card-actions>
<v-snackbar
v-model="hasSaved"
:timeout="2000"
absolute
bottom
left
>
Your profile has been updated
</v-snackbar>
</v-card>
</template>
<script>
export default {
props: {
setterTitle: String
},
data () {
return {
hasSaved: false,
isEditing: null,
model: null,
states: [
{ name: 'Single', abbr: 'S', id: 1 },
{ name: 'Double', abbr: 'D', id: 2 },
{ name: 'Triple', abbr: 'T', id: 3 },
{ name: 'Custom', abbr: 'C', id: 0 },
// { name: 'New York', abbr: 'NY', id: 5 },
],
}
},
methods: {
customFilter (item, queryText, itemText) {
const textOne = item.name.toLowerCase()
const textTwo = item.abbr.toLowerCase()
const searchText = queryText.toLowerCase()
console.log(textOne, textTwo,itemText)
return textOne.indexOf(searchText) > -1 ||
textTwo.indexOf(searchText) > -1
},
save () {
this.isEditing = !this.isEditing
this.hasSaved = true
console.log(this.$refs.rowCount.value)
},
},
}
</script>
As you can see, in the v-text-field tag I added a ref. I then tried to use that reference to extract the value from the save() method. Provided that I also have :value="2" assigned, this.$refs.rowCount.value always return 2. However if I don't have the value property set as 2 then this.$refs.rowCount.value returns null even though I've typed something in the v-text-field. I'm guessing I am misunderstanding something here. Maybe when I type to the field, it doesn't automatically assign what is typed to the value? Thank you for your help.
I've attached the api for v-text-field https://vuetifyjs.com/en/api/v-text-field/#component-pages
v-text-field is designed as common custom input control in Vue - it has a value prop and a input event which means it can by used with v-model - see the docs how that works
As a user, you do this:
<template>
<v-text-field v-model="text" />
</template>
<script>
export default {
data: function() {
return {
text: '' // initial value
}
}
}
</script>
To your question - value is a prop. All props in Vue are one way only - parent can send data to the component (and change that data in the future) but child component cannot change value of the prop it receives. That's why when you type something into the textbox the v-text-field must use an input event to tell the parent component that something has changed and that parent should update "it's own source" of value prop...
v-model is Vue's "syntactic sugar" how to work with these two more easily...

Update data from local copy of Vuex store

I am implementing a user profile edit page that initially consists of the data loaded from the vuex store. Then the user can freely edit his data and finally store them in the store.
Since the user can also click the cancel button to revert back to his original state, I decided to create a 'local' view copy of the user data fetched from the store. This data will be held in the view and once the user presses save, they will be saved in the store.
The view looks as following:
<template class="user-profile">
<v-form>
<template v-if="profile.avatar">
<div class="text-center">
<v-avatar width="120" height="120">
<img
:src="profile.avatar"
:alt="profile.firstname"
>
</v-avatar>
</div>
</template>
<div class="text-center mt-4">
<v-btn
color="primary"
dark
#click.stop="showImageDialog=true"
>
Change Image
</v-btn>
</div>
<v-row>
<v-col>
<v-text-field
label="First name"
single-line
disabled
v-model="profile.firstname"
></v-text-field>
</v-col>
<v-col>
<v-text-field
label="Last name"
single-line
disabled
v-model="profile.lastname"
></v-text-field>
</v-col>
</v-row>
<v-text-field
label="Email"
single-line
v-model="profile.email"
></v-text-field>
<v-text-field
id="title"
label="Title"
single-line
v-model="profile.title"
></v-text-field>
<v-textarea
no-resize
clearable
label="Biography"
v-model="profile.bio"
></v-textarea>
<v-dialog
max-width="500"
v-model="showImageDialog"
>
<v-card>
<v-card-title>
Update your profile picture
</v-card-title>
<v-card-text>
<v-file-input #change="setImage" accept="image/*"></v-file-input>
<template v-if="userAvatarExists">
<vue-cropper
ref="cropper"
:aspect-ratio="16 / 9"
:src="profile.avatar"
/>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green darken-1"
text
#click="showImageDialog=false"
>
Cancel
</v-btn>
<v-btn
color="green darken-1"
text
#click="uploadImage"
>
Upload
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="mt-8">
<v-btn #click="onUpdateUser">Update</v-btn>
</div>
</v-form>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
export default {
components: { VueCropper},
mounted() {
this.profile = this.getUserProfile ? this.getUserProfile : {}
},
data() {
return {
profile: {},
avatar: null,
userAvatarExists: false,
showImageDialog: false,
}
},
watch: {
getUserProfile(newData){
this.profile = newData;
},
deep: true
},
computed: {
...mapGetters({
getUserProfile: 'user/me',
})
},
methods: {
...mapActions({
storeAvatar: 'user/storeAvatar',
updateUser: 'user/update'
}),
onUpdateUser() {
const data = {
id: this.profile.id,
email: this.profile.email,
title: this.profile.title,
bio: this.profile.bio,
avatar: this.profile.avatar,
}
this.updateUser(data)
},
uploadImage() {
this.$refs.cropper.getCroppedCanvas().toBlob((blob => {
this.storeAvatar(blob).then((filename => {
this.profile.avatar = filename.data
this.$refs.cropper.reset()
}));
this.showImageDialog = false
}));
},
setImage(file) {
this.userAvatarExists = true;
if (file.type.indexOf('image/') === -1) {
alert('Please select an image file');
return;
}
if (typeof FileReader === 'function') {
const reader = new FileReader();
reader.onload = (event) => {
this.$refs.cropper.replace(event.target.result);
};
reader.readAsDataURL(file);
} else {
alert('Sorry, FileReader API not supported');
}
}
}
}
</script>
Issues/Questions:
As you can see from the code, after the user changes his profile
picture, the image should be rendered based on the
v-if="profile.avatar". The issue is that after the
profile.avatar is set in the uploadImage function, the
template does not see this change and no image is rendered.
However if I change the code so that the profile.avatar becomes
just avatar (it is no longer within the profile object), the
template starts to see the changes and renders the image
correctly. Why so? Does it have something to do with making a
copy from the store in the watch function?
Is it in general a good approach to keep the profile just as a local
view state or should it rather be stored in the vuex store even if
it is just a temporary data?
As you can see in the mounted function, I am setting the profile
value based on the getUserProfile getter. This is because the
watch function does not seem to be called again when switching
routes. Is there any other way how to do this?
The issue is due to the reactivity of data properties
You have used profile as an object, default it doesn't have any properties like avatar or firstname, its just empty
In vue js, If you are declaring an object, whatever the key mention in the declaration is only the part of reactivity. Once the keys inside profile changes, it rerenders the template
But still you can add new properties to a data property object by using $set
lets say in data you have declared
profile: {}
if you want to set avatar as new reactive property in runtime use
this.$set(this.profile, key, value)
which is
this.$set(this.profile, avatar, imageData)
In your above code, the setIuploadImage function
uploadImage() {
var self = this;
self.$refs.cropper.getCroppedCanvas().toBlob((blob => {
self.storeAvatar(blob).then((filename => {
self.$set(self.profile, "avatar", filename.data)
self.$refs.cropper.reset()
}));
self.showImageDialog = false
}));
},
this won't work inside arrow function in vuejs, so just preserved the this inside another variable "self" and used inside arrow function
Also in mounted function, if this.getUserProfile returns empty object, then as per javascript empty object is always truthy and directly assigning object to profile doesn't make the object reactive
mounted() {
this.profile = this.getUserProfile ? this.getUserProfile : {}
},
above code can be written as
mounted() {
if (this.getUserProfile && Object.keys(this.getUserProfile).length) {
var self = this;
Object.keys(this.getUserProfile).map(key => {
self.$set(self.profile, key, self.getUserProfile[key])
});
} else {
this.profile = {};
}
}