Pass one string piece of data from parent component to child. Vue.js 2 - vue.js

Hoping someone can see a simple mistake I'm making and help me correct it.
I'm trying to pass one string variable as a prop from a parent to a child. This string reveals itself on a checkbox select. There are three and depending which is selected, the name associated will be passed.
Basically, showing some components if different checkboxes are checked. I can clearly see the variable I'm passing shows up in the parent component just fine. I log it out in the method provided. However, it doesn't pass to the child component. I log out there and get undefined as the result.
I've tried multiple solutions, and read about passing props, but nothing is working.
Parent component looks like this.
<template>
<v-col>
<v-checkbox
label="LS"
color="primary"
value="ls"
v-model="checkedSensor"
#change="check($event)"
hide-details
/></v-checkbox>
</v-col>
<v-col cols="2" class="mx-auto">
<v-checkbox
label="DG"
color="primary"
value="dg"
v-model="checkedSensor"
#change="check($event)"
hide-details
/>
</v-col>
<v-col cols="2" class="mx-auto">
<v-checkbox
label="CR"
color="primary"
value="cr"
v-model="checkedSensor"
#change="check($event)"
hide-details
/>
<ls-sensor v-if="lsSensor" :sensorType="checkedSensor" />
</template>
<script>
export default{
data() {
return {
checkedSensor: " ",
lsSensor: false,
dgSensor: false,
crSensor: false,
};
},
methods: {
check: function (e) {
this.showDeviceComponent(e);
},
showDeviceComponent(e) {
if (e == "ls") {
this.lsSensor = true;
this.crSensor = false;
this.dgSensor = true;
console.log("file input component", this.checkedSensor)
} else if (e == "dg") {
this.dgSensor = true;
this.lsSensor = false;
this.crSensor = false;
} else {
this.crSensor = true;
this.dgSensor = false;
this.lsSensor = false;
}
},
},
}
</script>
Child component (just putting the script here as I don't have it's place in the template yet. In the mounted method, I should be seeing the value from the prop. It's showing up as undefined. Gahh.. what silliness I have made a mistake on? Your help is greatly appreciated.
export default {
props:['sensorType'],
data() {
return {
coating: false,
};
},
mounted() {
console.log("required input component, sensor type", this.sensorType)
},
components: {
coatings: require("#/components/shared/FormData/Coatings.vue").default,
"ls-sensor-panel": require("#/components/shared/FormData/LS_SensorPanel.vue").default,
},
};

The only thing that stands out to me is when you create camel cased property names, I am pretty sure that those have to be written as dash (-) separated attributes on a component?
<ls-sensor v-if="lsSensor" :sensor-type="checkedSensor" />
So prop: ['sensorType'] is passed as :sensor-type=""

Related

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 = {};
}
}

Vuetify async search is only submitting one character at a time and doesn't concat characters

I have the following vue component:
<template>
<v-form ref="form" #submit.prevent="search">
<v-row class="pa-0">
<v-col cols="12" md="2" class="d-flex">
<v-autocomplete
:items="projects"
item-value="id"
:item-text="item => `${item.number.number} ${item.name}`"
outlined
v-model="form.project_id"
label="Project number"
:search-input.sync="project"
dense
hide-details="auto"
class="align-self-center"
clearable
/>
</v-col>
</v-row>
</v-form>
</template>
<script>
export default {
watch: {
project( value )
{
this.queryProjects( { search: '', number: value } );
}
},
data()
{
return {
project: '',
projects: [],
}
},
methods: {
async queryProjects( search )
{
console.log(search);
if(!search)
{
return;
}
let response = await fetch(route('projects.search', search));
this.projects = await response.json();
},
}
}
</script>
This component should filter projects based on project number. The queryProject function is triggered but the problem is in the value from the project watcher. After each number entered the autocomplete field is set back to null and so it doesn't concat the complete project number. So if you would like to search for 19320 each number is parsed one by one and not as a whole number.
When a character is typed the output in console is the following:
1 for watcher value
{number: "1"} for queryProject search value
null for watcher value so looks like each character resets the input or rerenders the component.
The problem was in the line
:item-text="item => `${item.number.number} ${item.name}`"
When changed to just item-text="name" everything was working again.
A related bug report: https://github.com/vuetifyjs/vuetify/issues/11370

Dynamically update class based on individual form elements validation in Vuetify

Take a look at the official Vuetify form validation example.
The very first example, if you click in a field and then outside it, it is automatically validated. The entire field becomes red and you get a hint in red text.
What I would like is based on that built-in/native validation to add or remove a class (that turns the text red) on a completely separate HTML element.
It would be ideal if something like hint-for="" exists. Some way to connect a separate HTML element with the form field validation.
I have tried to condition the class with the "valid" property of the form element, something like this: this.$refs.form.$children[1].valid but this doesn't exist on page load and throws errors.
Right now I have some results by basically having double validation, the normal one that validates automatically based on the "rules" property on the form field, and a custom one that I call my self on #input and on #blur of the form field, but this is largely inefficient so I'm hoping there's a better way.
You can use the value of the v-form to track the validity of your form. In order to listen to changes you can use the input event like this
<template>
<div>
<v-form lazy-validation v-model="valid" #input="updateOtherElement">
<v-text-field
v-model="email"
:rules="emailRules"
label="Email"
required
></v-text-field>
</v-form>
</div>
</template>
<script>
export default {
data () {
return {
valid: true,
email: "",
emailRules: [
v => /.+#.+/.test(v) || 'E-mail must be valid',
],
}
},
methods: {
updateOtherElement(valid) {
// update other elements css
}
}
}
</script>
An alternative would be to track the changes with a watcher
This is what I came up with.
I had some trouble with validation being active immediately and nonexistent text fields on page load but with this setup, it works.
So once validation kicks in the fields will turn red by the native Vuetify validation if they are not valid, and I toggle the "invalid" class on a completely separate piece of HTML with custom functions. What is important here that each text field has it's own "subheader" which will turn red only if that single connected text-filed is invalid, not the entire form.
<template>
<v-form
ref="form"
lazy-validation
>
<v-subheader v-bind:class="passwordValid()">
Password *
</v-subheader>
<v-text-field
:rules="rules.password"
ref="password"
></v-text-field>
<v-subheader v-bind:class="passwordAgainValid()">
Password Again *
</v-subheader>
<v-text-field
:rules="rules.passwordAgain"
ref="passwordAgain"
></v-text-field>
</v-form>
<v-btn
v-on:click="save"
>
Save
</v-btn>
</template>
<script>
export default {
methods: {
save() {
let self = this
self.activateRules()
self.$nextTick(function () {
if (self.$refs.form.validate()) {
self.rules = {}
// submit...
}
})
},
activateRules () {
this.rules = {
password: [
v => v.length > 0 || ''
],
passwordAgain: [
v => v.length > 0 || ''
]
}
},
passwordValid: function () {
let passwordValid = true
if (this.$refs.password) {
passwordValid = this.$refs.password.valid
}
return {
'error--text': !passwordValid
}
},
passwordAgainValid: function () {
let passwordAgainValid = true
if (this.$refs.passwordAgain) {
passwordAgainValid = this.$refs.passwordAgain.valid
}
return {
'error--text': !passwordAgainValid
}
}
}
}
</script>

vue: Dynamically inject property data into <component />

I'm trying to create a layout component which dynamically creates given components from a parent in either one or the other column. In the layout component I'm using the directive to create the children components. That works, however I'm having a hard time to get the data into the components themselves (they have different sets of properties).
Therefore:
How to inject property data into the < component :is="componentFromParent" / > directive?
I've got something to work, but it's pretty messy. I could do it via a "genericProps" property which contains all the data. Then forcing the children to update and they, via updated(), use Object.assign(this, 'props', this.genericProps) to unpack the genericProps and overwrite it's $props.
It feels pretty much as if I'm working against some princiles of vue.js here. When debugging the this.$options.components of the layout component are filled, however all these components don't have any propsData assigned. But directly using Object.assign() on the this.$options.components doesn't work, as it seems the layout component as the actually instances of these components as it's this.$childrend.
Also I've tried looping through the this.$childrend and assigning the propsData of the this.$options.components but there would have to be a key to match the components with it's correct child and at that point the childrend don't have any properties filled.
Code snippets to illustrate the (ugly) example which kinda works:
Parent Template
<template>
<two-column-layout :firstColumn="$vuetify.breakpoint.mdAndUp ? firstColumn : singleColumn"
:secondColumn="$vuetify.breakpoint.mdAndUp ? secondColumn : []"
/>
</template>
Parent Code
This code is called via mounted() or via watch when the async call from the API is finished.
createMetadataContent() {
let currentContent = this.currentMetadataContent;
const components = this.$options.components;
currentContent = this.mixinMethods_enhanceMetadataEntry(currentContent, this.cardBGImages);
if (currentContent && currentContent.title !== undefined) {
this.body = metaDataFactory.createBody(currentContent);
this.$set(components.MetadataBody, 'genericProps', this.body);
this.citation = metaDataFactory.createCitation(currentContent);
this.$set(components.MetadataCitation, 'genericProps', this.citation);
// a few more components and data here
this.firstColumn = [
components.MetadataBody,
// a few more components here
];
this.secondColumn = [
components.MetadataCitation,
// a few more components here
];
this.singleColumn = [
components.MetadataBody,
components.MetadataCitation,
// a few more components here
];
this.$forceUpdate();
}
}
TwoColumnLayout Template
<template>
<v-layout row wrap>
<v-flex>
<v-layout column>
<v-flex mb-2
v-for="(entry, index) in firstColumn"
:key="`left_${index}`"
>
<component :is="entry"
:genericProps="entry.genericProps"
/>
</v-flex>
</v-layout>
</v-flex>
<v-flex v-if="secondColumn" >
<v-layout column>
<v-flex mb-2
v-for="(entry, index) in secondColumn"
:key="`right_${index}`"
>
<component :is="entry"
:genericProps="entry.genericProps"
/>
</v-flex>
</v-layout>
</v-flex>
</v-layout>
</template>
TwoColumnLayout Code
updated() {
this.$children.forEach((child) => {
child.$forceUpdate();
});
},
Child Code
props: {
genericProps: Object,
id: String,
citationText: String,
citationXmlLink: String,
ciationIsoXmlLink: String,
ciationGCMDXmlLink: String,
fixedHeight: Boolean,
showPlaceholder: Boolean,
},
updated: function updated() {
if (this.genericProps) {
Object.assign(this, 'props', this.genericProps);
}
},
I once had a similar situation where I had a dynamic <compnonent :is="currentComponent"> and solved it by storing a 'childPayload'-attribute to my data and passed it as prop to the children. Whenever my payload changed, it got refreshed in the children.
<template>
<div>
<div class="btn btn-primary" #click="changePayload">Change Payload</div>
<component :is="isWhat" :payload="payload"></component>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
isWhat: 'div',
payload: { any: 'data', you: 'want', to: 'pass'}
}
},
methods: {
changePayload() { // any method or api call can change the data
this.payload = { some: 'changed', data: 'here' }
}
}
}
</script>
I could solve it via slot but I've still needed to use forceUpdate() in the LayoutComponent...
However with the genericProps it's good enough for my case.
Parent Template
<template v-slot:leftColumn >
<div v-for="(entry, index) in firstColumn" :key="`left_${index}`" >
<component :is="entry" :genericProps="entry.genericProps" />
</div>
</template>
<template v-slot:rightColumn>
<div v-for="(entry, index) in secondColumn" :key="`right_${index}`" >
<component :is="entry" :genericProps="entry.genericProps" />
</div>
</template>
Parent Code
This code is called via mounted() or via watch when the async call from the API is finished.
createMetadataContent() {
let currentContent = this.currentMetadataContent;
const components = this.$options.components;
currentContent = this.mixinMethods_enhanceMetadataEntry(currentContent, this.cardBGImages);
if (currentContent && currentContent.title !== undefined) {
this.body = metaDataFactory.createBody(currentContent);
this.$set(components.MetadataBody, 'genericProps', this.body);
this.citation = metaDataFactory.createCitation(currentContent);
this.$set(components.MetadataCitation, 'genericProps', this.citation);
// a few more components and data here
this.firstColumn = [
components.MetadataBody,
// a few more components here
];
this.secondColumn = [
components.MetadataCitation,
// a few more components here
];
this.singleColumn = [
components.MetadataBody,
components.MetadataCitation,
// a few more components here
];
this.$forceUpdate();
}
}
TwoColumnLayout Template
<v-flex v-bind="firstColWidth" >
<v-layout column>
<slot name="leftColumn" />
</v-layout>
</v-flex>
<v-flex v-if="secondColumn" v-bind="secondColWidth" >
<v-layout column>
<slot name="rightColumn" />
</v-layout>
</v-flex>
TwoColumnLayout Code
updated() {
this.$children.forEach((child) => {
child.$forceUpdate();
});
},
Child Code
All child components have the genericProps object which is accessed via mixin method
props: {
genericProps: Object,
},
computed: {
title() {
return this.mixinMethods_getGenericProp('title');
},
id() {
return this.mixinMethods_getGenericProp('id');
},
description() {
return this.mixinMethods_getGenericProp('description');
},
}
Mixin method
mixinMethods_getGenericProp(propName) {
return this.genericProps[propName] ? this.genericProps[propName] : null;
},

How to Create Component on the fly?

First of all here is my structure
CHILD COMPONENT
// HTML
<v-select
v-bind:items="selectItems"
v-model="selectedItemModel"
label="Category"
item-value="text"
></v-select>
<v-text-field
label="Enter Value"
type="number"
v-model="compValModel"
></v-text-field>
// REMOVE BUTTON for deleting this component in render.
<v-btn icon>
<v-icon>cancel</v-icon>
</v-btn>
// JS
props: {
selectItems: {
type: Array,
required: true
},
selectedItem: {
type: String
},
compVal: {
type: Number
},
}
data () {
return {
selectedItemModel: this.selectedItem,
compValModel: this.compVal
}
},
watch: {
selectedItemModel(value) {
this.$emit('selectedItemInput', value);
},
compValModel(value) {
this.$emit('compValInput', value);
}
}
PARENT COMPONENT
// HTML
<component
:selecItems="selectItems"
:selectedItem="selectOneItem"
:compVal="compOneVal"
#selectedItemInput="selectOneItem = $event"
#compValInput="compOneVal = $event"
></component>
// ADD BUTTON for adding the above component more.
<v-btn icon>
<v-icon>cancel</v-icon>
</v-btn>
My Case
When i click on that plus button, it should create new component. and when i click on that REMOVE button from that component, it should delete that component.
Right now i have followed this. The problem in this approach is, whenever a new component is created. the value of the exsisting dynamically created values got refereshed.
My Question
Whats the good way to duplicate the component dynamically?
Since we are going to create components dynamically, how to create data values also dynamically?