vue 3 composition api, chaining filters uppon reactive data - vue.js

I have an orbject array as data and I would like to have several select menus in order to filter the data and show it on screen in a reactive way.
I have managed to get one filter to work, but the idea here is to be able to chain filters so that the result can be filtered by any combination of select vlaues.
I have broken down my code to a minimalistic example below :
<template>
<div class="mp" style="width: 100vw;">
<div class="col-2">
<span class="lbl">FILTER1</span>
<select v-model="selectedMA" class="form-select" >
<option value="">all</option>
<option value="Germany">Germany</option>
</select>
</div>
<div class="col-2">
<span class="lbl">FILTER2</span>
<select v-model="selectedMA2" class="form-select" >
<option value="">all</option>
<option value="Tier1">Tier1</option>
</select>
</div>
<p>DATA :</p>
<p> {{actorListTest2}} </p>
</div>
</template>
<script >
import {onMounted, onBeforeMount, ref, computed, watch} from 'vue'
import getActorDocs from '../composables/getActorDocs'
export default {
setup(){
const {actorDocs, loadActors} = getActorDocs()
const selectedMA = ref("all")
const selectedMA2 = ref("all")
loadActors(); // load data
var filteredActors = actorDocs
const actorListTest2 = computed(() => {
if(selectedMA.value == "all" && selectedMA2.value == "all"){return filteredActors}
else{
return filteredActors.filter(obj => {
return obj.country == selectedMA.value
}).filter(obj => {
return obj.type == selectedMA2.value
})}
});
return {filteredActors, actorListTest2, selectedMA, selectedMA2, actorDocs}
}//setup
}
</script>
<style scoped>
</style>

Inspired by this post (Array filtering with multiple conditions javascript), I figured out a way to get it to work. Might not be the best way to do it , but it works
const actorListTest2 = computed(() => {
if(selectedMA.value == "" && selectedMA2.value == ""){return filteredActors}
else {
return filteredActors.filter(obj => {
return (!selectedMA2.value.length || obj.type == selectedMA2.value)
&& (!selectedMA.value.length || obj.country == selectedMA.value)
})}
});

Related

Unable to filter data in props according to search criteria in Vue 3

I'm designing a client management system linked to a mySQL database and is trying to filter this displayed data from the database that is called through an API using laravel.
I have now tried it every which way and I'm struggling. Any help with this will be appreciate it.
My code is as follows.
My Template:
<template>
<div class="team">
<div class="team-members table">
<div class="table-header table-row">
<div class="table-col name">Name</div>
<div class="table-col phone">Phone</div>
<div class="table-col phone">Address</div>
<div class="table-col search">
<input type="text" class="search-bar" v-model="searchQuery" placeholder="Search"/>
</div>
<div class="table-col table-actions">Actions</div>
</div>
<div class="member table-row" v-for="user in clientSearch" :key="user.id">
<div class="table-col name">{{ user.name }}</div>
<div class="table-col phone">{{ user.phone }}</div>
<div class="table-col phone">{{ user.address }}</div>
<div class="table-col actions">
<div class="button-group group-end">
<button class="button button-small" #click="() => toggleForm(user.id)">Update</button>
<button class="button button-small button-alert" #click="() => deleteUser(user.id)">Delete</button>
</div>
</div>
</div>
</div>
</div>
</template>
My script:
<script>
import APIController from '#/controllers/api'
import { ref, reactive, computed } from 'vue'
export default {
props: ["users", "fetchUsers", "toggleForm"],
data(){
return {
}
},
setup (props) {
const clients = reactive(props.users);
const searchQuery = ref("");
const deleteUser = async id => {
const success = await APIController.DeleteUser(id);
if(success) {
props.fetchUsers();
}
}
const clientSearch = computed(() => {
return clients.filter((item) => {
return item.value.name.toLowerCase().indexOf(searchQuery.value.toLowerCase()) != -1;
});
});
return {
deleteUser,
clientSearch,
clients,
searchQuery
}
},
}
</script>
I've tried everything I could think of. And my internet research has turned up nothing. Please help.
I manage to figure out the issue. Through a lot of trial and error and console.logging almost everything I could think of I came to the conclusion that I wasn't accessing the data in the props properly when filtering the array. The API endpoint wrote the data as an array inside an Object and I wasn't targeting the array part of the Object properly with the filter function. So my advise is to study the anatomy of the variable you wish to filter and make sure you target the right data as the filter() function only works on arrays and not key-value pair Objects.
I eventually did away with the reactive variable and wrote the fuction as follows.
const clientList = computed(() => {
let client = ref([]);
client.value = props.users;
if(client.value.user){
return client.value.user.filter(item => { // client.value.user is the actual array...
return item.name.toLowerCase().match(searchQuery.value.toLowerCase());
});
} else {
return client.value.user;
}
});

vue 3 composition api hooks and data rendering

I would like to show data in template when the page loads after i fetch the data from a database.
The data is returned with async/await.
The select menus I use for filtering are now reactive only once I select a value from the menu. But I would like the full(unfiltered) data array to show when the page first loads and before any filters are applied.
How can I wait untill the data is fully fetched besore passing it to my template ?
<template>
<div class="mp" style="width: 100vw;">
<div class="col-2">
<span class="lbl">FILTER1</span>
<select v-model="selectedMA" class="form-select" >
<option value="">all</option>
<option value="Germany">Germany</option>
</select>
</div>
<div class="col-2">
<span class="lbl">FILTER2</span>
<select v-model="selectedMA2" class="form-select" >
<option value="">all</option>
<option value="Tier1">Tier1</option>
</select>
</div>
<p>DATA :</p>
<p> {{actorListTest2}} </p>
</div>
</template>
<script >
import {watchEffect, ref, computed} from 'vue'
import getActorDocs from '../composables/getActorDocs'
export default {
setup(){
const {actorDocs, loadActors} = getActorDocs()
const selectedMA = ref("")
const selectedMA2 = ref("")
loadActors();
var filteredActors = actorDocs
const actorListTest2 = computed(() => {
if(selectedMA == null && selectedMA2 == null){return filteredActors}
if(selectedMA.value == "" && selectedMA2.value == ""){return filteredActors}
else {
return filteredActors.filter(obj => {
return (!selectedMA2.value.length || obj.type == selectedMA2.value)
&& (!selectedMA.value.length || obj.country == selectedMA.value)
})}
});
watchEffect(() => console.log(actorListTest2.value))
return {filteredActors, actorListTest2, selectedMA, selectedMA2, actorDocs}
}//setup
}
</script>
As requested, here is getActorDocs():
import {ref} from 'vue'
import { projectFirestore } from '../firebase/config'
import { collection, getDocs } from "firebase/firestore";
const getActorDocs = () => {
const actorDocs = []
const error = ref(null)
const loadActors = async () => {
try {
const querySnapshot = await getDocs(collection(projectFirestore, "actors"));
querySnapshot.docs.map(doc => {
actorDocs.push(doc.data())
})
} catch (err) {
error.value = err.message
console.log(error.value)
}
}
return { actorDocs, error, loadActors}
}
export default getActorDocs

Get each HTML element in a Vue slot from JavaScript

I am creating a custom select component in VueJS 2. The component is to be used as below by the end-user.
<custom-select>
<option value="value 1">Option 1</option>
<option value="value 2">Option 2</option>
<option value="value 3">Option 3</option>
...
<custom-select>
I know the Vue <slot> tag and usage. But how do I get the user provided <option> tags as an array/list so I can get its value and text separately for custom rendering inside the component?
Those <option>s would be found in the default slot array (this.$slots.default), and you could get to the inner text and value of the <option>s like this:
export default {
mounted() {
const options = this.$slots.default.filter(node => node.tag === 'option')
for (const opt of options) {
const innerText = opt.children.map(c => c.text).join()
const value = opt.data.attrs.value
console.log({ innerText, value })
}
}
}
demo
You can achieve it, using v-bind and computed property
new Vue({
el: '#vue',
data: {
selected: '',
values: [
{
code: '1',
name: 'one'
},
{
code: '2',
name: 'two'
}
]
},
computed: {
selectedValue() {
var self = this;
var name = "";
this.values.filter(function(value) {
if(value.code == self.selected) {
name = value.name
return;
}
})
return name;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="vue">
<div>
<select v-model="selected">
<option v-for="value in values" v-bind:value="value.code">
{{ value.name }}
</option>
</select>
</div>
<strong>{{ selected }} {{ selectedValue }}</strong>
</div>

Select checkbox and shows its related objects in Vue.js

In my Vue.js project, I have two separate components are Country and States. I have merged them in one page. So now if I select one country it will display related states. How to do this?
<template>
<div>
<div style=margin-left:355px><country-index></country-index></div>
<div style=margin-left:710px><state-index></state-index></div>
</div>
</template>
<script>
import { ROAST_CONFIG } from '../../../config/config.js';
import CountryIndex from './components/country/Index';
import StateIndex from './components/state/Index';
import { listen } from '../../../util/history.js';
import axios from 'axios'
let baseUrl = ROAST_CONFIG.API_URL;
export default {
name: 'LocationsView',
layout: 'admin/layouts/default/defaultLayout',
middleware: 'auth',
components: {
'country-index' : CountryIndex,
'state-index' : StateIndex,
},
data() {
return { currentComponent:'','countryId':''}
},
methods: {
updateCurrentComponent(){
console.log(this.$route.name);
let vm = this;
let route = vm.$route;
if(this.$route.name == "Locations"){
this.currentComponent = "country-index";
}
}
},
mounted() {
let vm = this;
let route = this.$route;
window.addEventListener('popstate',this.updateCurrentComponent);
},
created() {
this.updateCurrentComponent();
}
}
Country Component
<template>
<div style="display:flex;height:100%">
<d-dotloader v-if="componentLoading" />
<div id="parent" class="list-manager" v-if="!componentLoading">
<div class="list-header">
<div class="bulk-action" :class="{'hide': showTop}" >
<div class="pull-left">
Countries
</div>
<!-- /pull-left -->
<div class="pull-right">
<d-button #click.native = "addCountry();"><i class="icon icon-sm"></i><span>New</span></i></d-button>
</div>
</div>
<!-- /bulk-action -->
<div class="bulk-action" :style ="{display:(showTop)?'block!important':'none!important'}" >
<div class="btn-toolbar">
<d-check field-class="check" v-model="selectAll" wrapper-class="field-check field-check-inline" label-position="right" label="" value="sel" #click.native = "toggleSelectAll();"/>
<d-button :is-loading="isLoading" #click.native = "deleteCountry();">Delete<i class="icon icon-sm" name="trash-2"></i></d-button>
<!-- <div class="pull-right mt5"><div class="green-bubble"></div>{{SelectedItems}}</div> -->
<d-button #click.native = "closeBulkToolBar();">close<i class="icon icon-sm" name="x"></i></d-button>
</div>
</div>
<!-- /bulk-action -->
</div>
<d-dotloader v-if="subListComponentLoading" />
<d-list-items :data="fetchData" #rowClick="changeCountryView" ref="itemsTable">
<d-list-cell column-class="list-item-check" :column-styles="{width: '40px'}" type="selectAll">
<template scope="row">
<div class="field-check field-check-inline" #click.stop="toggleSelect(row.rowIndex)" >
<input type="checkbox" class="check" :id="row.id" :value="row.id" :checked="row.selectAll">
<label></label>
</div>
<d-button #click.native = "editCountry(row.id);">Edit</d-button>
</template>
</d-list-cell>
<d-list-cell column-class="list-item-content">
<template scope="row">
<div class="list-item-content">
<div class="list-item-title">
<div class="pull-right">{{row.ISO_Code}}</div>
<div title="" class="pull-left">{{row.country_name}}</div>
</div>
<div class="list-item-meta">
<div class="pull-right">{{row.Default_Currency}} | {{row.Call_prefix}} </div>
<div class="pull-left">{{row.Zone}}</div>
</div>
<span class="list-item-status enabled"></span>
</div>
</template>
</d-list-cell >
</d-list-items>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { ROAST_CONFIG } from '../../../../../config/config.js';
var baseUrl = ROAST_CONFIG.API_URL;
export default {
data () {
return {
SelectedItems:"",
isLoading:false,
show:true,
searchBy: '',
activeSearch: '',
showTop: false,
selectAll : false,
componentLoading:true,
subListComponentLoading:false,
showModal: false,
form :{
country_name: '',
isCountryEnabled: true,
}
}
},
methods: {
async fetchData ({search, page, filter, sort,rows}) {
let resData;
let vm = this;
axios.defaults.headers.common['Authorization'] = "Bearer "+localStorage.getItem('token');
const res = await axios.post(baseUrl+'/country/fetch',{search, page, filter, sort,rows})
.then((response) => {
if( (typeof(response) != 'undefined') && (typeof(response.data) != 'undefined') && (typeof(response.data.fetch) != 'undefined')){
return response.data.fetch;
}
});
return res;
},
toggleSelect(rowId){
if(typeof(this.$refs.itemsTable.rows[rowId]) != 'undefined'){
this.$refs.itemsTable.rows[rowId].selectAll = !this.$refs.itemsTable.rows[rowId].selectAll;
let data = this.$refs.itemsTable.rows;
let status = false;
let selectAllStatus = true;
let items = 0;
for(var i=0;i <= data.length;i++){
if((typeof(data[i])!= 'undefined')&&(data[i].selectAll)){
items++;
this.SelectedItems = items +" Selected Items";
status = true;
}
if((typeof(data[i])!= 'undefined')&&(!data[i].selectAll)){
selectAllStatus = false;
}
this.showTop = status
}
}
},
toggleSelectAll(){
this.selectAll = !this.selectAll;
let items = 0;
let data = this.$refs.itemsTable.rows;
let status = false;
let rowId = '1'
for(var i=0;i <= data.length;i++){
if((typeof(data[i])!= 'undefined')){
items++;
this.SelectedItems = items +" Selected Items";
status = this.selectAll;
data[i].selectAll = status;
}
}
this.showTop = status
},
closeBulkToolBar(){
this.SelectedItems = "";
this.showTop = false;
},
}
}
State Component
<template>
<div style="display:flex;height:100%">
<d-dotloader v-if="componentLoading" />
<div id="parent" class="list-manager" v-if="!componentLoading">
<div class="list-header">
<div class="bulk-action" :class="{'hide': showTop}" >
<div class="pull-left">
States
</div>
<!-- /pull-left -->
<div class="pull-right">
<d-button #click.native = "addState();"><i class="icon icon-sm"></i><span>New</span></i></d-button>
</div>
</div>
<!-- /bulk-action -->
<div class="bulk-action" :style ="{display:(showTop)?'block!important':'none!important'}" >
<div class="btn-toolbar">
<d-check field-class="check" v-model="selectAll" wrapper-class="field-check field-check-inline" label-position="right" label="" value="sel" #click.native = "toggleSelectAll();"/>
<d-button :is-loading="isLoading" #click.native = "deleteState();">Delete<i class="icon icon-sm" name="trash-2"></i></d-button>
<!-- <div class="pull-right mt5"><div class="green-bubble"></div>{{SelectedItems}}</div> -->
<d-button #click.native = "closeBulkToolBar();">close<i class="icon icon-sm" name="x"></i></d-button>
</div>
</div>
<!-- /bulk-action -->
</div>
<d-dotloader v-if="subListComponentLoading" />
<d-list-items :data="fetchData" #rowClick="changeStateView" ref="itemsTable">
<d-list-cell column-class="list-item-check" :column-styles="{width: '40px'}" type="selectAll">
<template scope="row">
<div class="field-check field-check-inline" #click.stop="toggleSelect(row.rowIndex)" >
<input type="checkbox" class="check" :id="row.id" :value="row.id" :checked="row.selectAll">
<label></label>
</div>
<d-button #click.native = "editState(row.id);">Edit</d-button>
</template>
</d-list-cell>
<d-list-cell column-class="list-item-content">
<template scope="row">
<div class="list-item-content">
<div class="list-item-title">
<div class="pull-right">{{row.ISO_Code}}</div>
<div title="" class="pull-left">{{row.state_name}}</div>
</div>
<div class="list-item-meta">
<div class="pull-left">{{row.country_name}} </div>
<div class="pull-right">{{row.Zone}}</div>
</div>
<span class="list-item-status enabled"></span>
</div>
</template>
</d-list-cell >
</d-list-items>
</div>
<state-add></state-add>
<state-edit></state-edit>
</div>
</template>
<script>
import axios from 'axios'
import { ROAST_CONFIG } from '../../../../../config/config.js';
var baseUrl = ROAST_CONFIG.API_URL;
export default {
data () {
return {
SelectedItems:"",
isLoading:false,
show:true,
searchBy: '',
activeSearch: '',
showTop: false,
selectAll : false,
componentLoading:true,
subListComponentLoading:false,
showModal: false,
form :{
country_name: '',
isCountryEnabled: true,
}
}
},
methods: {
async fetchData ({search, page, filter, sort,rows}) {
let resData;
let vm = this;
axios.defaults.headers.common['Authorization'] = "Bearer "+localStorage.getItem('token');
const res = await axios.post(baseUrl+'/state/fetch',{search, page, filter, sort,rows})
.then((response) => {
if( (typeof(response) != 'undefined') && (typeof(response.data) != 'undefined') && (typeof(response.data.fetch) != 'undefined')){
return response.data.fetch;
}
});
return res;
},
changeStateView(row){
if(typeof(this.$children[7]) != 'undefined'){
this.$parent.stateId = row.id;
this.viewComponent = "state-main";
this.$children[7].readState(this.$parent.stateId);
this.$router.push({name:"StatesView", params: {id:row.id}});
}
},
toggleSelect(rowId){
if(typeof(this.$refs.itemsTable.rows[rowId]) != 'undefined'){
this.$refs.itemsTable.rows[rowId].selectAll = !this.$refs.itemsTable.rows[rowId].selectAll;
let data = this.$refs.itemsTable.rows;
let status = false;
let selectAllStatus = true;
let items = 0;
for(var i=0;i <= data.length;i++){
if((typeof(data[i])!= 'undefined')&&(data[i].selectAll)){
items++;
this.SelectedItems = items +" Selected Items";
status = true;
}
if((typeof(data[i])!= 'undefined')&&(!data[i].selectAll)){
selectAllStatus = false;
}
this.showTop = status
}
}
},
toggleSelectAll(){
this.selectAll = !this.selectAll;
let items = 0;
let data = this.$refs.itemsTable.rows;
let status = false;
let rowId = '1'
for(var i=0;i <= data.length;i++){
if((typeof(data[i])!= 'undefined')){
items++;
this.SelectedItems = items +" Selected Items";
status = this.selectAll;
data[i].selectAll = status;
}
}
this.showTop = status
},
closeBulkToolBar(){
this.SelectedItems = "";
this.showTop = false;
},
}
}
Without your component codes it will be difficult to accuratly answer but I can give a try. To communicate between your two components that don't have parent/child relationship you can use an EventBus. You have several choices on how to set up your EventBus; you can pass your event through your Vue root instance using $root, or you can create a dedicated Vue component like in this example.
Considering that you already have binded the event countrySelected($event) on each of your country checkbox, you could achieve to display the related states using something like this:
./components/country/Index
The CountryIndex trigger an event while a country is selected
methods: {
countrySelected(event) {
let currentTarget = event.currentTarget
this.$root.$emit("display-states",currentTarget.countryId);
}
}
./components/state/Index
The stateIndex component listen to the event and display the related state
mounted() {
/**
* Event listener
*/
this.$root.$on("display-states", countryId => {
this.diplayStates(countryId);
});
},
methods: {
displayStates(countryId) {
//your method selecting the states to be diplayed
}

VueJS Computed Data Search Not Functioning

I am attempting to create a search function in my Vue.js 2 application. However, even though my algorithm is giving me the right results, I am not getting the proper filter. Right now, whenever I run a search, I get nothing on the page. Here is my code:
computed:{
filteredSmoothies: function(){
return this.smoothies.filter(smoothie => {
var stuff = smoothie.title.split(" ").concat(smoothie.description.split(" ")).concat(smoothie.category)
var sorted = [];
for (var i = 0; i < stuff.length; i++) {
sorted.push(stuff[i].toLowerCase());
}
console.log(sorted)
if(this.search != null){
var indivthings = this.search.split(" ")
indivthings.forEach(item => {
if(sorted.includes(item.toLowerCase())){console.log("true")} else {console.log("false")}
if(sorted.includes(item.toLowerCase())){return true}
})
} else {
return true
}
})
}
}
Here is my relevant HTML:
<div class="container">
<label for="search">Search: </label>
<input type="text" name="search" v-model="search">
</div>
<div class="index container">
<div class="card" v-for="smoothie in filteredSmoothies" :key="smoothie.id">
<div class="card-content">
<i class="material-icons delete" #click="deleteSmoothie(smoothie.id)">delete</i>
<h2 class="indigo-text">{{ smoothie.title }}</h2>
<p class="indigo-text">{{ smoothie.description }}</p>
<ul class="ingredients">
<li v-for="(ing, index) in smoothie.category" :key="index">
<span class="chip">{{ ing }}</span>
</li>
</ul>
</div>
<span class="btn-floating btn-large halfway-fab pink">
<router-link :to="{ name: 'EditSmoothie', params: {smoothie_slug: smoothie.slug}}">
<i class="material-icons edit">edit</i>
</router-link>
</span>
</div>
</div>
As the discussions in the comments, the root cause should be:
you didn't return true/false apparently inside if(this.search != null){}, it causes return undefined defaultly.
So my opinion is use Javascript Array.every or Array.some. Also you can use for loop + break to implement the goal.
Below is one simple demo for Array.every.
computed:{
filteredSmoothies: function(){
return this.smoothies.filter(smoothie => {
var stuff = smoothie.title.split(" ").concat(smoothie.description.split(" ")).concat(smoothie.category)
var sorted = [];
for (var i = 0; i < stuff.length; i++) {
sorted.push(stuff[i].toLowerCase());
}
console.log(sorted)
if(this.search != null){
var indivthings = this.search.split(" ")
return !indivthings.every(item => { // if false, means item exists in sorted
if(sorted.includes(item.toLowerCase())){return false} else {return true}
})
} else {
return true
}
})
}
or you can still use forEach, the codes will be like:
let result = false
var indivthings = this.search.split(" ")
indivthings.forEach(item => {
if(sorted.includes(item.toLowerCase())){result = true}
})
return result
filteredSmoothies is not returning a list. You iterate over to see if indivthings is contained within sorted, but you don't do anything with that information. I think you need to modify your sorted list based on the results of indivthings.