Problems with $ref between parent and child component - vue.js

I'm using dynamic forms to break up a long form. Beneath each dynamic form are two action buttons which, when clicked, send the user back one form set or onto the next one (unless the end of the form has been reached - the 'Next' button is replaced by a 'Submit' button).
As I have three different forms that all use the same action buttons, I decided to create a child component ('ActionButtons') for action buttons - and in doing so I've gone from code that worked to code that doesn't.
Specifically, I'm having problems with ref information involved in controlling the 'Next' button. The error message is Error in nextTick: "TypeError: Cannot read property '$v' of undefined"
The code that worked fine (i.e. before I created 'ActionButtons') is:
<template>
...
<keep-alive>
<component
ref="currentStep"
:is="currentStep"
#update="processStep"
:wizard-data="formvars"
></component>
</keep-alive>
...
<button
#click="nextButtonAction"
:disabled="!canGoNext"
class="btn"
>{{isLastStep ? 'Submit' : 'Next'}}</button>
...
</template>
<script>
import Gap2InputInfo from './Gap2InputInfo'
import Gap2Materials from './Gap2Materials'
export default {
name: 'GAP2Form',
components: {
Gap2InputInfo,
Gap2Materials
},
data () {
return {
currentStepNumber: 1,
canGoNext: false,
wizardData: {},
steps: [
'Gap2InputInfo',
'Gap2Materials',
],
formvars: {
id: 0,
purchase: null,
name: null,
quantity: null,
supplier: null,
nsteps: 0
},
updatedData: null
}
},
computed: {
isLastStep () {
return this.currentStepNumber === this.length
},
length () {
this.formvars.nsteps = this.steps.length;
return this.steps.length
},
currentStep () {
return this.steps[this.currentStepNumber - 1];
},
},
methods: {
nextButtonAction () {
if (this.isLastStep) {
this.submitForm()
} else {
this.goNext()
}
},
processStep (step) {
Object.assign(this.formvars, step.data);
this.canGoNext = step.valid
},
goBack () {
this.currentStepNumber--;
this.canGoNext = true;
},
goNext () {
this.currentStepNumber++;
this.$nextTick(() => {
this.canGoNext = !this.$refs.currentStep.$v.$invalid
})
}
}
}
</script>
In creating the child component, I send the following props to the child
<action-buttons :currentStepNumber="currentStepNumber" :canGoNext="canGoNext" :isLastStep="isLastStep" :currentStep="currentStep"></action-buttons>
and have moved the methods associated with the button actions from the parent to the child component.
My child component is:
<template>
<div>
<div id="actions" class="buttons">
<button
#click="goBack"
v-if="mcurrentStepNumber > 1"
class="btn-outlined"
>{{$t('back')}}
</button>
<button
#click="nextButtonAction"
:disabled="!canGoNext"
class="btn"
>{{isLastStep ? 'Submit' : 'Next'}}</button>
</div>
</div>
</template>
<script>
import {required} from 'vuelidate/lib/validators'
export default {
name: "ActionButtons",
props: ['currentStepNumber','canGoNext','isLastStep','currentStep'],
data: function () {
return {
mcurrentStepNumber: this.currentStepNumber,
mcurrentStep: this.currentStep,
mcanGoNext: this.canGoNext,
misLastStep: this.isLastStep
}
},
methods: {
nextButtonAction () {
if (this.misLastStep) {
this.submitForm()
} else {
this.goNext()
}
},
goBack () {
this.mcurrentStepNumber--;
this.mcanGoNext = true;
},
goNext () {
this.mcurrentStepNumber++;
this.$nextTick(() => {
this.mcanGoNext = !this.$refs.currentStep.$v.$invalid **** Error triggered at this line
})
}
}
}
</script>
Now when I click the 'Next' button, I get the Cannot read property '$v' of undefined error message. If I've interpreted it correctly, it cannot read the $refs data. I tried altering the code in the child component to
this.mcanGoNext = !this.$parent.$refs.currentStep.$v.$invalid
but the outcome remained the same. Where am I going wrong?
Thanks, Tom.

Related

How to set dynamic value in variable or connect two variable Vue

I have an array with a variable ecoveneer_magma_oak that I want to change by pressing a button.
When I change the variable in console, everything is good but I don't see my value on the page.
PS: I am a beginner in Vue.
Here is my code
<template>
<div>
<swiper-slide
v-for="door in doors"
:key="door.id"
class="card"
:door="doors"
></swiper-slide>
<div class="modelName">{{ door.model }}</div>
</div>
</template>
<script>
export default {
data() {
return {
ecoveneer_magma_oak: 'Дуб Магма',
doors: [
{
image: '1.jpg',
model: '018 BG',
ecoveneer_magma_oak: 'Дуб Бордо',
decor: this.ecoveneer_magma_oak,
id: '1',
},
],
}
},
methods: {
changeLange(lang) {
this.ecoveneer_magma_oak = 'Ecoveneer Magma Oak'
},
},
}
</script>
Ok I have spend 2 work days for find answer but nothing Now I declare variable not in data then I change variable and refresh DOM but variable "decor" not changing
var ecoveneer_magma_oak= 'Дуб Бордо'
export default {
data() {
doors: [
{
image:"1.jpg",
model:"018 BG",
decor: ecoveneer_magma_oak,
id:"1"
}],
methods: {
changeLange(lang){
if(lang==='RU'){
this.renderComponent = false;
this.$nextTick(() => {
// Add the component back in
this.renderComponent = true;
});
ecoveneer_magma_oak="Ecoveneer Magma Oak"
}
}

Passing data from child component to parent and then to another child not working on page load but works after minor potentially unrelated change

I am new to Vuejs and come across this bug which I have no idea what I have done wrong. I am not receiving any console errors. It doesn't work on initial page load but it seems to work after I comment something out (or make a minor change). It will still then continue to work if I reverse the changes I just made and put it back to the original code. But once again on a fresh page load it won't work.
The issue: I am making a to do list and on page load when I add new tasks through the input field, the list does not appear on the page like it should be. I also console log the data array for this and it shows it is getting added to the array but is not getting rendered to the page. No console errors. In my code I will comment out some other data property (there are 2 additional ones below todosList in the TodoList.vue file that are currently not being used yet) and save and then the tasks will automatically appear on the page. So I think oh ok that might be the issue so with this new minor change I decide to refresh the page to see if it works as expected. Nope it doesn't so I then uncomment out what I previously commented out and save and the list appears again. But once again if I refresh the page it doesn't work. It only seems to be if I make a change inside the data function in the TodoList.vue file.
Additional info: The data is stored in the parent todos[] (App.vue), updated/pushed to array in a child (TodoCreate.vue) and sent back to the parent using $emit. This data is then sent through to another child (TodoList.vue) using props so that it can be rendered on the page.
Wondering if there is something that is not quite right in my code which is causing this to bug out like that. I will include everything in case it is something that looks unrelated to me but could be causing it.
Here is also a link to a code sandbox where the issue can be replicated by following the instructions on the page https://codesandbox.io/s/adding-new-todo-not-working-properly-jwwex?file=/src/components/TodoList.vue
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue
<template>
<div :class="currentMode">
<the-header #modeToggled="updateMode($event)"></the-header>
<main>
<todo-create #addedTodos="updateTodos"></todo-create>
<todo-list :todos="todos"></todo-list>
</main>
</div>
</template>
<script>
import TheHeader from './components/TheHeader.vue';
import TodoCreate from './components/TodoCreate.vue';
import TodoList from './components/TodoList.vue';
export default {
name: 'App',
components: {
TheHeader,
TodoCreate,
TodoList,
},
data() {
return {
currentMode: {
dark_mode: true,
light_mode: false
},
todos: [],
}
},
methods: {
updateMode(mode) {
this.currentMode = mode;
},
updateTodos(data) {
this.todos = data;
console.log(this.todos);
},
toggleCompleted() {
}
},
// provide() {
// return {
// todos: this.todos,
// };
// }
}
</script>
TheHeader.vue
<template>
<h1>To-do App</h1>
<div>
<label for="toggle-mode" aria-label="Toggle light and dark mode"></label>
<input type="checkbox" id="toggle-mode" #change="toggleMode">
</div>
</template>
<script>
export default {
emits: ['modeToggled'],
data() {
return {
toggleState: false,
}
},
methods: {
toggleMode() {
this.toggleState = !this.toggleState;
this.$emit('modeToggled', this.modeClasses);
}
},
computed: {
modeClasses() {
return {
dark_mode: !this.toggleState,
light_mode: this.toggleState
}
}
}
}
</script>
TodoCreate.vue
<template>
<div>
<label for="newtodo" class="sr-only">Create new to do</label>
<input type="text" id="newtodo" placeholder="Create a new todo..." v-model="todoval" v-on:keyup.enter="addTodo" >
</div>
</template>
<script>
export default {
emits: ['addedTodos'],
data() {
return {
todoval: '',
taskNumber: 0,
todos: [],
};
},
methods: {
addTodo() {
const val = this.todoval;
const taskNumber = this.taskNumber;
this.todos.push({ taskID: taskNumber, value: val, complete : 'not-completed'});
this.todoval = '';
this.taskNumber++;
console.log(this.todos);
this.$emit('addedTodos', this.todos);
},
}
}
</script>
TodoList.vue
<template>
<ul class="todo-items" :class="filterClass">
<li class="drop-zone" v-for="(listItem, index) in todosList" :class="listItem.complete" :key="listItem.taskID"
#drop='onDrop($event, index)'
#dragover.prevent
#dragenter.prevent>
<div class="drag-el" draggable="true"
#dragstart='startDrag($event, index)'>
<label :for="'checkbox-'+index" :aria-label="'Mark task ' + listItem.value + ' as completed'"></label>
<input type="checkbox" :id="'checkbox-'+index" #change="toggleCompleted(index, listItem.value, listItem.complete, listItem.taskID)">
<input type="text" disabled :value="listItem.value">
<img src="../assets/icon-cross.svg" #click="removeTask(index)">
</div>
</li>
</ul>
</template>
<script>
export default {
props: {
todos: Object,
filterClass: String
},
// inject: ['todos'],
data() {
return {
todosList: this.todos,
// completedTodos: [],
// activeTodos: [],
};
},
// watch: {
// todosList(data) {
// data.filter(function(todo) {
// if(todo.completed == 'completed') {
// completedTodos.push(todos);
// }
// });
// }
// },
methods: {
startDrag: (evt, item) => {
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('itemID', item)
},
onDrop (evt, list) {
const itemID = evt.dataTransfer.getData('itemID');
const movedData = this.todosList[itemID];
this.todosList.splice(itemID,1);
this.todosList.splice(list,0, movedData);
},
toggleCompleted() {
// still need to write this method
},
removeTask() {
// still need to write this method
}
}
}
</script>

VUE Js child is not updating when parent updates

I am using VUE JS and I want to have group of checkboxes as below. When someone clicked on main checkbox all the checkboxes under that check box should be selected. Please find the attached image for your reference
To accomplish this scenario I am using 2 main components. When someone clicked on a component I am adding that component to selectedStreams array. Selected stream array structure is similar to below structure
checked: {
g1: ['val1'],
g2: []
}
When I click on heading checkbox I am triggering function
clickAll and try to change the selectedStreams[keyv].
But this action doesn't trigger the child component and automatically checked the checkbox.
Can I know the reason why when I changed the parent v-model value it is not visible in child UI.
Parent Component
<template>
<div>
<b-form-group>
<StreamCheckBox
v-for="(opts, keyv) in loggedInUsernamesf"
:key="keyv"
:name="opts"
:main="keyv"
v-model="selectedStreams[keyv]"
#clickAll="clickAll($event, keyv)"
></StreamCheckBox>
</b-form-group>
</div>
</template>
<script>import StreamCheckBox from "./StreamCheckBox";
export default {
name: "GroupCheckBox",
components: {StreamCheckBox},
data(){
return {
selectedStreams:{}
}
},
computed: {
loggedInUsernamesf() {
var username_arr = JSON.parse(this.$sessionStorage.access_info_arr);
var usernames = {};
if (!username_arr) return;
for (let i = 0; i < username_arr.length; i++) {
usernames[username_arr[i].key] = [];
var payload = {};
payload["main"] = username_arr[i].val.name;
payload["type"] = username_arr[i].val.type;
if (username_arr[i].val.permissions) {
for (let j = 0; j < username_arr[i].val.permissions.length; j++) {
payload["value"] = username_arr[i].key + username_arr[i].val.permissions[j].streamId;
payload["text"] = username_arr[i].val.permissions[j].streamId;
}
}
usernames[username_arr[i].key].push(payload);
}
return usernames;
},
},
methods: {
clickAll(e, keyv) {
if (e && e.includes(keyv)) {
this.selectedStreams[keyv] = this.loggedInUsernamesf[keyv].map(
opt => {
return opt.value
}
);
}
console.log(this.selectedStreams[keyv]);
}
}
}
</script>
<style scoped>
</style>
Child Component
<template>
<div style="text-align: left;">
<b-form-checkbox-group style="padding-left: 0;"
id="flavors"
class="ml-4"
stacked
v-model="role"
:main="main"
>
<b-form-checkbox
class="font-weight-bold main"
:main="main"
:value="main"
#input="checkAll(main)"
>{{ name[0].main }}</b-form-checkbox>
<b-form-checkbox
v-for="opt in displayStreams"
:key="opt.value"
:value="opt.value"
>{{ opt.text }}</b-form-checkbox>
</b-form-checkbox-group>
</div>
</template>
<script>
export default {
name:"StreamCheckBox",
props: {
value: {
type: Array,
},
name: {
type: Array,
},
main:{
type:String
}
},
computed:{
role: {
get: function(){
return this.value;
},
set: function(val) {
this.$emit('input', val);
}
},
displayStreams: function () {
return this.name.filter(i => i.value)
},
},
methods:{
checkAll(val)
{
this.$emit('clickAll', val);
}
}
}
</script>
First of all, I am not sure what is the purpose of having props in your parent component. I think you could just remove props from your parent and leave in the child component only.
Second of all, the reason for not triggering the changes could be that you are dealing with arrays, for that you could set a deep watcher in your child component:
export default {
props: {
value: {
type: Array,
required: true,
},
},
watch: {
value: {
deep: true,
//handle the change
handler() {
console.log('array updated');
}
}
}
}
You can find more info here and here.

VueJS: how to trigger 'change' on <input> changed programmatically

I'm going to build a customized virtual keyboard, so that's the first problem I've encountered.
I have an input element, whose value is changed from outside, in my case by pressing a button. The problem is that there seems to be no way to trigger the normal 'change' event.
Neither clicking outside the input, nor pressing Enter gives any result. What might be the correct way of solving this problem?
<template>
<div class="app-input">
<input #change="onChange" type="text" v-model="value" ref="input">
<button #click="onClick">A</button>
</div>
</template>
<script>
export default {
name: "AppInput",
data() {
return {
inputDiv: null,
value: ""
};
},
props: {
msg: String
},
methods: {
onClick() {
this.value = this.value + "A";
this.inputDiv.focus();
},
onChange() {
alert("changed");
}
},
mounted() {
this.$nextTick(() => {
this.inputDiv = this.$refs.input;
});
}
};
</script>
The whole pen can be found here.
v-on:change would only trigger on a direct change on the input element from a user action.
What you are looking for is a wathcer for your data property, whenever your value changes, watcher will execute your desired function or task.
watch: {
value: function() {
this.onChange();
}
}
The watch syntax is elaborated on the provided official vuejs docs link. use your data property as the key and provide a function as a value.
Check the snippet.
export default {
name: "AppInput",
data() {
return {
inputDiv: null,
value: ""
};
},
props: {
msg: String
},
methods: {
onClick() {
this.value = this.value + "A";
this.inputDiv.focus();
},
onChange() {
alert("changed");
}
},
// this one:
watch: {
value: function() {
this.onChange();
}
},
// --- rest of your code;
mounted() {
this.$nextTick(() => {
this.inputDiv = this.$refs.input;
});
}
};
When I build any new vue application, I like to use these events for a search input or for other inputs where I don't want to fire any functions on #change
<div class="class">
<input v-model="searchText" #keyup.esc="clearAll()" #keyup.enter="getData()" autofocus type="text" placeholder="Start Typing ..."/>
<button #click="getData()"><i class="fas fa-search fa-lg"></i></button>
</div>
These will provide a better user experience in my opinion.

Fetch data in component on initiation using parameters from Vuex store

I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component