Conditional List Rendering with Vuex state - vue.js

Initially I have a list of animations stored in Cards. Each card has a few tags. From a top filter menu I want to be able to display only Cards, that match with the filter that has been set. A vuex state holds the information of all currently applied filters.
My markup looks like this:
<div class="Feed__cards">
<Card
v-for="(card, index) in filteredCards"
:key="card.id"
:id="card.id"
:name="card.name"
:tags="card.tags"
:description="card.description"
:video="card.video"
:valueset="getValueSet(index)"
class="Feed__card"
>
</Card>
In my methods I wanted to do something like this (activeTagsElements is a computed property, mapState from Vuex):
compare(tags) {
tags.forEach(tag => {
if(this.activeTagsElements.includes(tag)){
return true
}
})
},
getAllAnimations() {
this.$axios.get('/api/animations/all')
.then(response => {
this.allCards = response.data;
this.allMaxIndex = response.data.length - 1;
response.data.forEach((animation, index) => {
this.getTags(animation.id, index, this.allCards, this.allMaxIndex, 'all');
});
}).catch((error) => {
console.log(error)
})
},
getTags(id, index, slot, max, type) {
this.$axios.get('/api/animationtags/' + id)
.then(response => {
slot[index].tags = response.data.map(tag => {
return tag.name;
});
if(index == max && type == 'initial'){
this.initialLoad = true;
} else if(index == max && type == 'all') {
this.allLoad = true;
}
}).catch((error) => {
console.log(error);
})
}
I also tried watching the change of the vuex state but couldn't get to the point of how to get the actual tags from each element to compare it to the vuex state.
Any hints are very appreciated.

The vue way is to create a computed property of the filtered tasks. Then you just v-for throught them.
<Card
v-for="card in filteredAnimations"
:key="card.id"
:id="card.id"
:name="card.name"
:tags="card.tags"
>
</Card>
This should work, and is efficient as it will only rerun filteredTags if either Vuex store changes or the activeElements from your filter changes.
<script>
export default {
data() {
return {
activeTagsElements: []
}
},
computed: {
animations() {
return this.$store.animations
},
filteredAnimations() {
return this.animations.filter(animation => {
return this.activeTagsElements.includes(animation)
})
}
}
}
</script>

Related

How to run a specific function if the component has been called by a specific component and not by other components in Vue?

I have a component called select-diagnosis which is used by many different components.
When select-diagnosis is called by a specific component called PtdTreatment, it needs to run a specific function inside the fetchDiagnosis function, while when called by other components it will not run that specific function.
The fetchDiagnosis needs to understand that select-diagnosis component has been called by the PtdTreatment component.
How to do something like that?
This is the code from PtdTreatment component:
<el-form-item
label="diagnosis"
prop="dimission_diagnosis"
v-if="form.data_dimission">
<select-diagnosis
v-model="form.diagnosis_dimission"
:defaultValue="_.get(record, 'clean_diagnosis_dimission')"
/>
</el-form-item>
And this is the select-diagnosis component:
<template>
<el-select
v-bind="$attrs"
:value="value"
#change="onChange"
#clear="onClear"
clearable
filterable
remote
:remote-method="fetchDiagnosis"
:loading="loadingSelect"
>
<el-option
v-for="item in items"
:key="`diagnosis-${item.id}`"
:label="item.code + ' - ' + item.description"
:value="item.code"
>
</el-option>
</el-select>
</template>
<script>
export default {
name: "SelectDiagnosis",
inheritAttrs: false,
props: ["value", "defaultValue"],
data() {
return {
loadingSelect: false,
items: []
};
},
methods: {
fetchDiagnosis(query) {
const valid = query !== "" && query.length > 2;
if (!valid) return;
this.loadingSelect = true;
let params = { string: query };
axios
.get("/config/diagnosi", { params })
.then(({ data }) => {
//pseudo code
// if this component is called by **select-diagnosis** then do this
this.items = data.filter(diagnosi => {
const code = diagnosi.codice.replace(/\b0+/g, "");
if (code.length >= 4) {
return diagnosi;
}
});
// else do this
this.items = data;
})
.finally(() => (this.loadingSelect = false));
},
onChange(x) {
this.$emit("input", x);
},
onClear() {
this.$emit("input", null);
this.items = [];
}
},
watch: {
defaultValue: {
immediate: true,
handler(newVal, oldVal) {
if (newVal && oldVal === undefined) {
this.items = [newVal];
this.$emit("input", newVal.codice);
}
}
}
}
};
</script>
There are a number of ways to accomplish this, the two that come to mind immediately use props.
You could pass a filterDiagnoses boolean prop to select-diagnosis. If it's true, run the filter logic.
<select-diagnosis v-model="form.diagnosis_dimission" :defaultValue="_.get(record, 'clean_diagnosis_dimission')" :filterDiagnoses="true" />
You could invert control to the parent function and expose a filterFn callback prop - the parent function passes a function prop to the child that the child will call upon fetching the diagnoses (this feels cleaner and more extensible):
/* in the PtdTreatment component */
/* ... */
methods: {
filterDiagnosis(data) {
// filter data
},
}
/* in the PtdTreatment template */
<select-diagnosis v-model="form.diagnosis_dimission" :defaultValue="_.get(record, 'clean_diagnosis_dimission')" :filterFn="filterDiagnosis" />
/* in the select-diagnosis component */
fetchDiagnosis(query) {
const valid = query !== "" && query.length > 2;
if (!valid) return;
this.loadingSelect = true;
let params = { string: query };
axios
.get("/config/diagnosis", { params })
.then(({ data }) => {
if (this.filterFn) {
this.items = this.filterFn(data);
} else {
this.items = data;
}
})
.finally(() => (this.loadingSelect = false));
},
}
You can set a prop on the child component which specifies the 'identity' of the parent component, then test for that in the child:
<select-diagnosis
v-model="form.diagnosis_dimission"
:defaultValue="_.get(record, 'clean_diagnosis_dimission')"
parent="PtdTreatment"
/>
Then in the child (simplified example):
export default {
props: ["value", "defaultValue", "parent"],
methods: {
fetchDiagnosis(query) {
if (this.parent === "PtdTreatment") {
// Parent-specific code here
}
}
}
}

Vue2 set a variable from an api callback

I have this function return a call back as:
function fetchShifts(ctx, callback) {
const accountId = selectedAccount.value.value.id
store.dispatch('app-action-center/fetchShifts', {
accountId,
})
.then(shifts => {
const data = []
shifts.forEach(async (shift, index) => {
const user = await store.dispatch('app-action-center/fetchUserDetails',
{
assignedTo: shift.assignedTo,
})
.then(res => res)
data.push({
...shift,
user: user.fullName,
})
if (index === (shifts.length - 1)) { callback(data) }
})
})
}
In the vue file I try to set it as:
data() {
return {
shifts: this.fetchShifts,
}
},
or
data() {
return {
shifts: null,
}
},
created() {
this.shifts = this.fetchShifts()
}
None of them work, I want to make this shifts variable ready when the component mounted so I can put it in the <app v-for="shift in shifts" />
At this moment, this code work fine with <b-table :items="fetchShifts /> but I don't know how to convert to <ul v-for="shift in shifts></ul>
Try like this:
<ul v-for="shift in shifts" :key="shift.id">
</ul>
export default
{
data()
{
return {
shifts: [],
};
},
created()
{
this.fetchShifts(undefined, (shiftsArray) =>
{
this.shifts = shiftsArray;
});
}
}
Explanation - initially you start with an empty array. Then you asynchronously fetch the shifts. The callback is called as soon as all the shifts and the corresponding users have been fetched - and in this callback you update the array with the shifts, which in turn triggers component re-rendering.
Vue is truly amazing!

Clone / Copy state is returning empty state

I am having an issue using lodash's cloneDeep to clone the user object passed in from the store. When I attempt to render the data in the template {{ user }} shows the data retrieved from the store and {{ userCopy }} shows the empty store. I am not sure why this is happening, I am new to Vue.
store/staff.js
import StaffService from '~/services/StaffService.js'
export const state = () => ({
user: {
offers: '',
legal: ''
}
})
export const mutations = {
SET_USER(state, user) {
state.user = user
},
}
export const actions = {
fetchUser({ commit, getters }, id) {
const user = getters.getUserById(id)
if (user) {
commit('SET_USER', user)
} else {
StaffService.getUser(id)
.then((response) => {
commit('SET_USER', response.data)
})
.catch((error) => {
console.log('There was an error:', error.response)
})
}
},
}
export const getters = {
getUserById: (state) => (id) => {
return state.staff.find((user) => user.id === id)
}
}
pages/settings/_id.vue
<template>
<div>
{{ user }} // will display the whole object
{{ userCopy }} // will only display empty store object
</div>
</template>
<script>
import _ from 'lodash'
data() {
return {
userCopy: _.cloneDeep(this.$store.state.staff.user)
}
},
computed: {
...mapState({ user: (state) => state.staff.user })
},
created() {
this.$store.dispatch('staff/fetchUser', this.$route.params.id)
},
</script>
My guess would be that a Vue instance's data is initialized before state becomes available. While computed props are populated/updated as their data source change.
If the component doesn't need to change the value of user during runtime, I'd suggest turning it into a computed property.
If your component does change the value during runtime (such as when it's v-model'd to an input), there are two approaches you can do.
Method 1: Using mounted hook
This is done by placing user in data property and then assigning a value when the instance is mounted, like so:
mounted () {
this.$data.userCopy = _.cloneDeep(this.$store.state.staff.user)
}
Method 2: Using computed with getter and setter functions.
Normally, you shouldn't change a computed value. But it can be done using a setter function. With this, when Vue detects an attempt to change a computed prop it will execute set() with the old and new values as arguments. This function would change the value at its source, allowing get()'s returned value to reflect this. For example:
computed: {
userCopy: {
get () {
return _.cloneDeep(this.$store.state.staff.user)
},
set (newValue) {
this.$store.commit('updateStaff', newValue) // Replace this line with your equivalent state mutator.
}
}
}

Extract data from [__ob__: Observer]

I'm trying to get data from a API that i created, this data is listed just as i wanted, but i can't manipulate this data because it returns a [ob: Observer]. How can i extract data from this ?
Store.js:
items: state => {
item.getAll().then(response => {
state.items = response.data
})
return state.items
},
Component.vue:
...mapGetters(["items"]),
<v-card flat v-for="item in items" :key="item.title">
Just as #skirtle commented i needed to move my function to actions because i'm using a async task.
My final code ended up that way:
getters: {
items: state => state.items,
...
}
mutations: {
'LOAD_ITEMS'(state, data) {
const items = []
for (let key in data) {
const item = data[key];
item.id = key;
items.push(item);
}
state.items = items;
...
}
actions: {
loadItems({ commit, state }) {
...
return axios.get("item.json")
.then(response => {
commit("LOAD_ITEMS", response.data);
}).catch(error => console.log(error));
}
}
I'm using firebase, so i did a loop to store the key.
If you has your own API maybe you can just store response.data directly in a JS object without using a loop.

Relay Moder - Pagination

I am already working on Pagination.
I used PaginationContainer for that. It work’s but no way what I am looking for.
I got button next which call props.relay.loadMore(2) function. So when I click on this button it will call query and add me 2 more items to list. It works like load more. But I would like instead of add these two new items to list, replace the old item with new.
I try to use this getFragmentVariables for modifying variables for reading from the store but it’s not working.
Have somebody Idea or implemented something similar before?
class QueuesBookingsList extends Component {
props: Props;
handleLoadMore = () => {
const { hasMore, isLoading, loadMore } = this.props.relay;
console.log('hasMore', hasMore());
if (!hasMore() || isLoading()) {
return;
}
this.setState({ isLoading });
loadMore(1, () => {
this.setState({ isLoading: false });
});
};
getItems = () => {
const edges = idx(this.props, _ => _.data.queuesBookings.edges) || [];
return edges.map(edge => edge && edge.node);
};
getItemUrl = ({ bid }: { bid: number }) => getDetailUrlWithId(BOOKING, bid);
render() {
return (
<div>
<button onClick={this.handleLoadMore}>TEST</button>
<GenericList
displayValue={'bid'}
items={this.getItems()}
itemUrl={this.getItemUrl}
emptyText="No matching booking found"
/>
</div>
);
}
}
export default createPaginationContainer(
QueuesBookingsList,
{
data: graphql`
fragment QueuesBookingsList_data on RootQuery {
queuesBookings(first: $count, after: $after, queueId: $queueId)
#connection(
key: "QueuesBookingsList_queuesBookings"
filters: ["queueId"]
) {
edges {
cursor
node {
id
bid
url
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`,
},
{
direction: 'forward',
query: graphql`
query QueuesBookingsListQuery(
$count: Int!
$after: String
$queueId: ID
) {
...QueuesBookingsList_data
}
`,
getConnectionFromProps(props) {
return props.data && props.data.queuesBookings;
},
getFragmentVariables(prevVars, totalCount) {
console.log({ prevVars });
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, variables, fragmentVariables) {
return {
count: variables.count,
after: variables.cursor,
queueId: fragmentVariables.queueId,
};
},
},
);
As I figure out, there are two solutions, use refechConnection method for Pagination Container or use Refech Container.