Saving the select filters values of items in list component after going back from viewing an item component - vue.js

After the user goes back from viewing the item (question) component, I am trying to keep the user's filtered list of questions based on two selects (category and sub category) and selected page from pagination in the questions list component.
I am new to Vue environment and not sure if I am following the best practice but here what I have tried to do:
1- I tried using the event bus, but I reached a point where I am able to get an object that contains the question category object to the main component and then it would be used as an attribute inside of the Onchange method which normally be triggered after the category select event happens. I was able to show the user the same list of filtered items, however, the problem with this approach that:
a- I can not update the select values to show the selected options which gave the questions list.
I have tried to get the selected element using the ref attributes, however the select element is undefined even though I put it in the mounte stage of the life cycle. It works with an ugly hack by using a setTimeout method and it shows the element after one second.
b- The filtered list of items has pagination and this approach does not show the user the same page that they picked the item from.
c- Calling the server again
2- I tried to store a global value in mixins file, however after saving the value in the mixins file and even though the object value is received in the mixins file but after updating the mixins data and then calling it from the questions list, it returns an empty object.
3- I tried using keep-alive
Code for approach 1:
The category object is eager loaded with the question using an event emit. After the user goes back from viewing the question I pass the category object with the event using beforeDestroy method:
beforeDestroy () {
EventBus.$emit('backToQuestions', this.question.category);
}
This is how the category object look like:
{…}=>
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
created_at:
id:
parent_id:
title:
updated_at:
This is how I populate the filtered questions list
created () {
EventBus.$on('backToQuestions', category => {
this.onChange(category)
});
this.fetch(`/categories`);
}
My select:
<div class="col-md-4">
<select ref="main" class="form-control" v-model="mainSelected" #change="onChange(mainSelected)">
<option disabled value="">Questions Category (All)</option>
<option v-for="option in parentCategories" :value="option">{{ option.title }}</option>
</select>
</div>
<div v-if="subCategory" class="btn-group">
<select class="form-control" v-model="subSelected" #change="onChange(subSelected)">
<option disabled value="" selected >{{ categoryTitle }} Qs Categories</option>
<option v-for="option in childCategories" :value="option">{{ option.title }}</option>
</select>
</div>
The following is my onChange method just for reference:
onChange(option) {
this.categoryOption = option;
this.dataReady = false;
this.subCategory = true;
this.questions= {};
this.questionsArray =[];
this.categoryTitle = option.title;
this.categoryId = option.id;
if(option.children){
this.childCategories = option.children;
}
axios.get(`/categories/${this.categoryId}`)//getting the category questions
.then(({data}) => {
this.questions = data;
this.questionsArray.push(...data.data);
this.nextUrl = data.next_page_url;
this.dataReady = true;
this.emptyCheck(this.questionsArray);
})
.catch((err) => {
swal("Failed!", err.response.data.message, "info");
this.$router.push('dashboard') ;
})
}
$refs of the select divs always return undefined unless I used setTimeout.
Code for approach 2:
After including the mixins file in both components I put the following code in mixins:
setCategory (questionCategory) {
console.log("TCL: setCategory -> questionCategory", questionCategory)
this.category = questionCategory;
console.log("TCL: setCategory -> this.category", this.category)
}
,
getCategory () {
return this.category ;
}
The value of the object received by the set method is correct but after updating the mixins data method this.category is returning the following only:
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
i.e. without the category object details. I tried to stringify the object and then call it, this.category shows an empty variable.
3- Using keep-alive, however it does not work, I tried to wrap both the router-view and router-link.
<keep-alive include="questions-list">
<router-link to="/questions-list" class="nav-link">
<i class="nav-icon fas fa-question-circle orange"></i>
<p>
Questions
</p>
</router-link>
</keep-alive>
I even tried using include the name of the questions list component with no result.
Sorry for the long question but I have been stuck for a while with this and it is the core of my application.

I am now using keep-alive approach. The silly mistake was that I only named the component when I declared my routes.
{ path: '/questions-list', name:"questions-list", component: require('./components/qa/Questions.vue')}
For those who are facing the same problem, you should declare the name of the component inside of the component export object like the following:
export default {
name: 'questions-list',
data () {
return {
}
}

Related

Vue.js passing a variable through a modal

I am trying to pass a variable from a Parent (page) component to a Child (modal) component. After reading a few examples, this works fine. The variable in question is brought in from another component as a route param. If i refresh the page, the variable is lost and can no longer be passed to the child. My question is, is the best way to persist this using the store, or is it possible to persist another way if the user refreshed? Any help would be appreciated
Parent
<b-container>
<Modal v-show="displayModal" #close="closeModal">
<template v-slot:body>
<ExpressionCreate v-show="displayModal" :lender-id="lenderId" #close="closeModal"/>
</template>
</Modal>
<div class="card" style="width: 100%;">
<div class="card-header">
<h5>{{this.lenderName}}</h5>
<b-alert :show="this.loading" variant="info">Loading...</b-alert>
</div>
<b-btn block variant="success" #click="showCreateLenderModal">Add Expression</b-btn>
....
created () {
this.lenderId = this.$route.params.lenderId
...
navigate () {
router.go(-1)
},
showCreateLenderModal () {
this.displayModal = true
},
toggleDisplayModal (isShow) {
this.displayModal = isShow
},
async closeModal () {
this.displayModal = false
}
Child
<label>lender id:</label>{{this.lenderId}}
...
props: {
lenderId: {
type: Number
}
},
You can use VueSession to persist.
Simply persist your lender_id with
this.$session.set('lender_id', this.lender_id)
and you can get it later as
saved_lender = this.$session.get('lender_id')
if (saved_lender) {
// handle saved case here
}
You can use query params in URI by doing:
$route.push('/', { query: { param: 3123 } })
This will generate the URL /?param=3123 and this will work after refresh. Or, you can use vuex and vuex-localstorage plugin, that let you persist data in the browser, and you can close the browser and open again and data will be restored.
In order for the state of application to persist, I recommend that you use vuex to manage the state and use a library that abstracts the persisting to vue in a clean way. One popular library is vuex-persist. https://www.npmjs.com/package/vuex-persist
If you dont want to use a store, VueSession, a package linked here should do the trick

v-model not always updating in Vue

Short question
The v-model which binds a string to an input field won't update in some cases.
Example
I am using Vue within a Laravel application. This is the main component which contains two other components:
<template>
<div>
<select-component
:items="items"
#selectedItem="updateSelectedItems"
/>
<basket-component
:selectedItems="selectedItems"
#clickedConfirm="confirm"
#clickedStopAll="stopAll"
/>
<form ref="chosenItemsForm" method="post">
<!-- Slot for CSRF token-->
<slot name="csrf-token"></slot>
<input type="text" name="chosenItems" v-model="selectedItemsPipedList" />
</form>
</div>
</template>
<script>
export default {
props: ["items"],
data: function() {
return {
selectedItems: [],
selectedItemsPipedList: ""
};
},
methods: {
updateSelectedItems: function(data) {
this.selectedItems = data;
this.selectedItemsPipedList = this.selectedItems
.map(item => item.id)
.join("|");
},
confirm() {
this.$refs.chosenItemsForm.submit();
},
stopAll() {
this.updateSelectedItems([]);
this.confirm();
}
}
};
</script>
The method updateSelectedItems is called from the select-component and it works fine. In the end, the selectedItemsPipedList contains the selected items from the select-component, which looks like "1|2|3" and this value is bound to the input field in the chosenItemsForm. When the method confirm is called from the basket-component, this form is posted to the Laravel backend and the post request contains the chosen items as piped list. So far, so good.
The method stopAll is called from the basket-component and it will remove all the selected items from the array. Therefore it will call the method updateSelectedItems with an empty array, which will clear the selectedItems array and then clear the selectedItemsPipedList. After that, confirm is called which will post the form again. But, the post value still contains the selected items (e.g. '1|2|3'), instead of "". It looks like the v-model in my form is not updated, which is strange because it does work when selecting items. Why is it working when adding items, and doesn't when removing all items?
I believe you have a timing issue here. The value of the properties haven't been propagated to the DOM yet, so the form submission is incorrect. Try this instead:
stopAll() {
this.updateSelectedItems([]);
//NextTick waits until after the next round of UI updates to execute the callback.
this.$nextTick(function() {this.confirm()});
}

How to add different components to the page on-the-fly with Vue.js

My goal is to allow the user to dynamically build a form from different components, based on what they choose in a select element. For example, they can choose to add a Heading, then maybe a Paragraph, then another Heading, etc. Each "part" is a separate Component.
I know this sort of thing has been asked before, but I'm only day 2 into Vue and I think I'm 90% of the way there - I'm just missing something. What I believe I'm stuck on is how to add a component to my app's data to allow Vue to render it out.
Here is the relevant markup:
<div id="creator">
<template v-for="part in existingParts">
<component :is="part"></component>
</template>
<select class = "custom-select" id = "new-part-chooser" v-model="newPart" v-on:change="addPart">
<option disabled value = "">Add a new part</option>
<option
v-for="part in possibleParts"
v-bind:value="part.toLowerCase()"
>{{ part }}</option>
</select>
<?php
// These simply bring in the templates for the components
// I know this isn't standard practice but... one thing at a time
include 'component-heading.html';
include 'component-paragraph.html';
?>
</div>
and my javascript file:
Vue.component("part-heading",{
data:function(){
return {
text: ""
}
},
template:"#component-heading-template"
});
Vue.component("part-paragraph",{
data:function(){
return {
text: ""
}
},
template:"#component-paragraph-template"
});
const Creator = new Vue({
el:"#creator",
data:{
newPart:"",
possibleParts:[
"Heading",
"Paragraph"
],
existingParts:[]
},
methods:{
addPart:function(e){
/*** This is where I'm stuck - what do I push here? ***/
this.existingParts.push();
}
}
});
I've read through the docs and Google'd the hell out of the topic, but every setup seems to be just different enough that I can't figure out how to apply it.
I was missing the fact that in the markup, the :is directive causes Vue to create a component that matches the name of the element in existingParts. So push()-ing "part-heading" into existingParts causes Vue to render an instance of the "part-heading" component.
The updated, working code is:
this.existingParts.push('part-'+this.newPart);

Vue: initial triggering of a computed property

In my Vue component, I have a computed property which watches the state and the component, and returns an object.
currentOrganization () {
if (this.$store.state.organizations && this.selectedOrganization) {
return this.$store.state.organizations.filter(org => org.id === this.selectedOrganization)[0];
}
}
Unfortunately, this property does not update automatically when the component loads, though I can see that I have both this.$store.state.organizations and this.selectedOrganization. It does, however, update when I dispatch an action to my Vuex store via the select component below:
<b-form-select id="orgSelect"
v-model="selectedOrganization"
:options="this.$store.state.organizations.map(org => {return {value: org.id, text:org.name}})"
size="sm"
v-on:input="currentOrganizationChange()"
class="w-75 mb-3">
<template slot="first">
<option :value="null" disabled>Change Organization</option>
</template>
</b-form-select>
The component dispatches a Vuex action:
currentOrganizationChange () {
this.$store.dispatch('setCurrentOrganization', this.selectedOrganization);
},
How can I make currentOrganization() compute initially?
The computed will not calculate until you use it.
console.log it, or just write current organization; somewhere in code which executes and it will calculate :)
You should ensure that your computed always returns a value. A linter would have warned you of that error. Chances are your condition is failing -- perhaps this.selectedOrganization is falsy.

Default select option is not being updated in DOM, even though the v-model attribute is updating

I have a component that builds a select/dropdown menu.
There is an API call that results in both collection and selectedOption getting updated and successfully building the dropdown.
<!-- src/components/systems/SystemEdit.vue -->
<custom-dropdown v-model="system.sensor_type" id="sensor_type" name="sensor_type" :collection="sensorTypeDropdown.collection" :firstOption="sensorTypeDropdown.firstOption" :selectedOption="sensorTypeDropdown.selected"></custom-dropdown>
<!-- src/components/CustomDropdown.vue -->
<template>
<select v-model="selection" required="">
<option v-show="firstOption" value="null">{{firstOption}}</option>
<option v-for="item in collection" :value="item[1]">{{ item[0] }}</option>
</select>
</template>
<script>
export default {
props: ['value', 'firstOption', 'collection', 'selectedOption'],
name: 'customDropdown',
data () {
return {
selection: null
}
},
watch: {
selection: function(sel) {
this.selection = sel
this.$emit('input',sel)
}
},
updated: function() {
if(this.selectedOption) {
this.selection = this.selectedOption
}
}
}
</script>
<style>
</style>
The dropdown output looks like this:
<select required="required" id="sensor_type" name="sensor_type">
<option value="null">Select sensor type...</option>
<option value="sensor1">sensor1</option>
<option value="sensor2">sensor2</option>
<option value="sensor3">sensor3</option>
</select>
If I set the selection: "sensor1" data attribute, then sensor1 is selected, as expected.
Instead, if I update this.selection = this.selectedOption or this.selection = "sensor1", then it does not become the selected option… even though console.log(this.selection) confirms the change. I have tried setting it in the created hook and elsewhere. If I save a change, the dropdown default successfully sets on the hot reload. But that first load where it initializes will not work.
How can I get this.selection = this.selectedOption to properly reflect in the DOM to show that option as selected?
I'm using Vue 2.1.10
Edit 1:
Still no luck, now with this approach:
data () {
return {
selection: this.setSelected()
}
},
methods: {
setSelected: function() {
var sel = null
if(this.selectedOption) {
sel = this.selectedOption
}
console.log(sel)
return sel
}
}
Even though sel outputs "sensor1" and the Vue inspector confirms selection:"sensor1"
Edit 2:
I've narrowed down the problem but cannot yet find a solution.
methods: {
setSelected: function() {
var sel = null
if(this.selectedOption) {
sel = this.selectedOption
}
return sel
}
}
It fails on return sel.
sel is not being treated as a string; I suspect this is a Vue bug.
Works:
return 'sensor1'
Fails:
return sel
I have tried all these workarounds (none of them work):
sel = String(this.selectedOption)
sel = (this.selectedOption).toString()
sel = new String(this.selectedOption)
sel = this.selectedOption+''
sel = 'sensor1'
The only way this will "work" is if I use the string literal, which is useless to me.
I am stumped.
SOLVED
In order for this to work, the selectedOption prop that I am passing to the component must be rendered somewhere in the component's template, even if you have no use for it in the template.
Fails:
<select v-model="selection" required="">
Works:
<select v-model="selection" required="" :data-selected="selectedOption">
That data attribute can be :data-whatever… as long as it is rendered somewhere in the template.
This works, too:
<option v-show="!selectedOption" value="null">{{selectedOption}}</option>
or
<div :id="selectedOption"></div>
I hope this helps someone. I'm going to let Evan You know about it, as it seems to be something that should be fixed or at least noted in the official documentation.
Summary:
Props used in a component's script block must also be rendered somewhere in the component's template, otherwise some unexpected behaviour may occur.
The prop I am using in the script block, selectedOption, needs to be rendered in the template even though it's not being used there.
So this:
<select v-model="selection" required="">
becomes:
<select v-model="selection" required="" :data-selected="selectedOption">
Now the selected item successfully updates in the DOM.
VueJS has a specific configuration for v-model on custom components. In particular the component should have a value prop, which will pick up the v-model from the parent. You then this.$emit('input', newVal) on change like you have configured.
I'm not sure exactly where the breakdown is with your component but here is a working example. You may have just over complicated changing the selected data. If configured correctly you can just emit the change and let the parent handle actually updating what is assigned to v-model. In the example I included some buttons to change the selected var from both the parent and component to illustrate changing the var (parent) or emitting a change (component).