This is probably just a basic vue question, but I am using Vue Apollo.
I have this component that is setup like this:
export default defineComponent({
name: "Products",
components: { Product },
props: {
categorySlug: {
type: String,
required: true,
},
},
setup(props) {
const { categorySlug } = toRefs(props);
const page = ref(1);
const skip = ref(0);
const result = reactive({ search: null, laoding: null, error: null });
Object.assign(result, useListProducts(categorySlug.value, page.value));
const more = () => {
skip.value += 12;
page.value += 1;
Object.assign(result, useListProducts(categorySlug.value, page.value));
};
return { ...toRefs(result), ...{ skip, more } };
},
});
As you can see I have a reactive object setup which is assigned to when the component setup method is invoked:
const result = reactive({ search: null, laoding: null, error: null });
Object.assign(result, useListProducts(categorySlug.value, page.value));
In my template, I iterate over the results like this:
<base-fade-up class="row" :duration="0.1" tag="div">
<v-col
cols="12"
xl="2"
v-for="(product, index) in products"
:key="product.id"
:data-index="index"
:data-skip="skip"
>
<product class="product" :product="product"></product>
</v-col>
</base-fade-up>
The problem I have, is that I want to add a more button:
<v-btn color="primary" #click="more()" v-if="search.hasMoreResults"
>Show more</v-btn
>
When pressed, it executes the more() method:
const more = () => {
skip.value += 12;
page.value += 1;
Object.assign(result, useListProducts(categorySlug.value, page.value));
};
As you can see, I am now getting page 2, so page one products are no longer in the search object.
What I would like to do is get a copy of the products from the first query and then add to them every time the more button is pressed.
I tried this:
setup(props) {
const { categorySlug } = toRefs(props);
const page = ref(1);
const skip = ref(0);
const products = ref([]);
const result = reactive({ search: null, laoding: null, error: null });
Object.assign(result, useListProducts(categorySlug.value, page.value));
const more = () => {
skip.value += 12;
page.value += 1;
Object.assign(result, useListProducts(categorySlug.value, page.value));
};
products.value.push(result.search.value.items);
return { ...toRefs(result), ...{ products, skip, more } };
},
but when I run this code, I get an error:
[Vue warn]: Error in data(): "TypeError: Cannot read property 'value' of null"
which is complaining about result.search.value in this line:
products.value.push(result.search.value.items);
I believe it's because the promise has not been resolved.
I am using Apollo's useResult method inside my useListProducts which looks like this:
import { useQuery, useResult } from "#vue/apollo-composable";
import * as listProducts from "#/graphql/api/query.products.gql";
export function useListProducts(slug: string, page = 1, itemsToShow = 12) {
const request: {
identifier?: string;
searchTerm: string;
itemsToShow: number;
page: number;
filters: any;
facets: string[];
} = {
searchTerm: "*",
itemsToShow,
page,
filters: [
{
key: "CategorySlug",
value: `'${slug}'`,
},
],
facets: ["Criteria/Attribute,count:100"],
};
const { result, loading, error } = useQuery(listProducts, {
search: request,
});
const search = useResult(result, null, (data) => data.search);
return { search, loading, error };
}
Can anyone tell me what I am doing wrong?
After some digging around, I found something in the documentation about pagination (which is essentially what I am doing), so because of that I managed to adjust my code to this:
export default defineComponent({
name: "Products",
components: { Product },
props: {
categorySlug: {
type: String,
required: true,
},
},
setup(props) {
const { categorySlug } = toRefs(props);
const page = ref(1);
const skip = ref(0);
const { search, loading, error, fetchMore } = useListProducts(
categorySlug.value,
page.value
);
const more = () => {
skip.value += 12;
page.value += 1;
const request = {
...params,
...{
page: page.value,
filters: [
{
key: "CategorySlug",
value: `'${categorySlug.value}'`,
},
],
},
};
fetchMore({
variables: { search: request },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
//return fetchMoreResult;
console.log(fetchMoreResult);
return Object.assign({}, prev, {
search: {
hasMoreResults: fetchMoreResult.search.hasMoreResults,
total: fetchMoreResult.search.total,
facets: [...prev.search.facets, ...fetchMoreResult.search.facets],
items: [...prev.search.items, ...fetchMoreResult.search.items],
__typename: prev["__typename"],
},
});
},
});
};
return { search, loading, error, skip, more };
},
});
Which satisfies everything Estus mentioned in the comments, but it does throw an error, which I will create a new post about :D
Related
Here is my ThreadsStore
import { defineStore } from "pinia";
import sourceData from "#/data.json";
import { useUsersStore } from "../stores/UsersStore";
import { usePostsStore } from "../stores/PostsStore";
import { useForumsStore } from "../stores/ForumsStore";
import { findById, upsert } from "#/helpers";
export const useThreadsStore = defineStore("ThreadsStore", {
state: () => {
return {
threads: sourceData.threads,
};
},
getters: {
thread: (state) => {
return (id) => {
const thread = findById(state.threads, id);
return {
...thread,
get author() {
return findById(useUsersStore().users, thread.userId);
},
get repliesCount() {
return thread.posts.length - 1;
},
get contributorsCount() {
return thread.contributors.length;
},
};
};
},
},
actions: {
async createThread({ text, title, forumId }) {
const id = "ggqq" + Math.random();
const userId = useUsersStore().authId;
const publishedAt = Math.floor(Date.now() / 1000);
const thread = { forumId, title, publishedAt, userId, id };
this.threads.push(thread);
this.appendThreadToUser({ userId, threadId: id });
this.appendThreadToForum({ forumId, threadId: id });
usePostsStore().createPost({ text, threadId: id });
return findById(this.threads, id);
},
async updateThread({ title, text, id }) {
const thread = findById(this.threads, id);
const post = findById(usePostsStore().posts, thread.posts[0]);
const newThread = { ...thread, title };
const newPost = { ...post, text };
this.setThread({ thread: newThread });
this.setPost({ post: newPost });
return newThread;
},
appendThreadToForum({ forumId, threadId }) {
const forum = findById(useForumsStore().forums, forumId);
forum.threads = forum.threads || [];
forum.threads.push(threadId);
},
appendThreadToUser({ userId, threadId }) {
const user = findById(useUsersStore().users, userId);
user.threads = user.threads || [];
user.threads.push(threadId);
},
setPost({ post }) {
upsert(usePostsStore().posts, post);
},
setThread({ thread }) {
upsert(this.threads, thread);
},
},
});
Here is my page
<template>
<div class="col-large push-top">
<h1>
{{ thread.title }}
<router-link
:to="{ name: 'ThreadEdit', id: this.id }"
class="btn-green btn-small"
>
Edit Thread
</router-link>
</h1>
<p>
By <a href="#" class="link-unstyled">{{ thread.author.name }}</a
>, <AppDate :timestamp="thread.publishedAt" />.
<span
style="float: right; margin-top: 2px"
class="hide-mobile text-faded text-small"
>{{ thread.repliesCount }} replies by
{{ thread.contributorsCount }} contributors</span
>
</p>
<post-list :posts="threadPosts" />
<post-editor #save="addPost" />
</div>
</template>
<script>
import { mapState, mapActions } from "pinia";
import { useThreadsStore } from "../stores/ThreadsStore";
import { usePostsStore } from "../stores/PostsStore";
import PostList from "#/components/PostList";
import PostEditor from "#/components/PostEditor";
export default {
name: "ThreadShow",
components: {
PostList,
PostEditor,
},
props: {
id: {
required: true,
type: String,
},
},
computed: {
...mapState(useThreadsStore, ["threads", "thread"]),
...mapState(usePostsStore, ["posts"]),
threadPosts() {
return this.posts.filter((post) => post.threadId === this.id);
},
},
methods: {
...mapActions(usePostsStore, ["createPost"]),
addPost(eventData) {
const post = {
...eventData.post,
threadId: this.id,
};
this.createPost(post);
},
},
};
</script>
In my computed I would like to map thread from my store for use in the template. I have not been able to figure out how to pass the id parameter to pinia properly in order to get it to return the thread properly.
The tutorial I am following along with uses vuex but I wanted to figure out how to use pinia so the conversion has been somewhat confusing
For anyone who might have something similar in the future. You can pass in a value to a getter like so
...mapState(useThreadsStore, {
thread(store) {
return store.thread(this.id);
},
}),
I am trying to add custom emoji set to vue-emoji-picker
based https://codepen.io/DCzajkowski/pen/gObWjEQ
I have implemented this with partial success. I get everything loaded, except "RECENT" not added to the emoji list. Any help is greatly appreciated.
//my store/index.js
import recentEmojis from "./modules/recentEmojis";
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
recentEmojis,
},
// store/modules/recentEmojis/index.js
export const defaultEmojis = {
"Frequently used": {
thumbs_up: "+1",
red_hreat: "heart",
},
People: {
},
Nature: {
},
Objects: {
},
Places: {
},
Symbols: {
},};
const getName = (emoji) => {
console.log("getName");
const emojiMap = Object.values(defaultEmojis).reduce(
(a, b) => ({ ...a, ...b }),
{});
const object = Object.entries(emojiMap).find(
// eslint-disable-next-line no-unused-vars
([_name, value]) => value === emoji
);
return object ? object[0] : null;
};
export default {
namespaced: true,
defaultEmojis,
state: {
recentlyUsedEmojis: [],
},
mutations: {
recentlyUsedEmojis: (state, recentlyUsedEmojis) =>
(state.recentlyUsedEmojis = recentlyUsedEmojis),
},
actions: {
addEmojiToRecent: ({ state, commit }, emoji) => {
const name = getName(emoji);
const rest = state.recentlyUsedEmojis
.map(
// eslint-disable-next-line no-unused-vars
([_name, value]) => value
)
.includes(emoji)
? state.recentlyUsedEmojis.filter(
// eslint-disable-next-line no-unused-vars
([_name, value]) => value !== emoji
)
: state.recentlyUsedEmojis.length > 5
? state.recentlyUsedEmojis.slice(0, -1)
: state.recentlyUsedEmojis;
commit("recentlyUsedEmojis", [[name, emoji], ...rest])},},
getters: {
recentlyUsedEmojis: (state) =>
state.recentlyUsedEmojis.length
? { Recent: Object.fromEntries(state.recentlyUsedEmojis) }
: {},
},
}
//in my vue instance # src/pages/edtor.default.vue
...
<emoji-picker :emoji-table="emojis" #emoji="append" :search="search">
....
<script> import axios from "axios";
import EmojiPicker from "vue-emoji-picker"
import { defaultEmojis } from "../../store/modules/recentEmojis/index.js" // <<<<
export default { name: "ABCD", components: { EmojiPicker, },
...
computed: { emojis()
{ return { ...this.$store.getters.recentlyUsedEmojis, ...defaultEmojis } }, },
......
methods: {
append(emoji)
{ this.input += emoji this.$store.dispatch("recentEmojis/addEmojiToRecent", emoji) },
}
replaced
...this.$store.getters.recentlyUsedEmojis,
with
...this.$store.getters['recentEmojis/recentlyUsedEmojis']
I use FlatList with useState.
const [state, setState] = useState(route);
<FlatList
keyboardDismissMode={true}
showsVerticalScrollIndicator={false}
data={state}
keyExtractor={(comment) => "" + comment.id}
renderItem={renderComment}
/>
When I change the datㅁ which is contained in state, I want to re-run Flatlist with new data.
So after I mutate my data, I try to rerun useQuery first in order to change state. I put refetch module here.
1)
const { data: updatePhoto, refetch } = useQuery(SEE_PHOTO_QUERY, {
variables: {
id: route?.params?.photoId,
},
});
If I put button, this onValid function will executed.
<ConfirmButton onPress={handleSubmit(onValid)}>
onValid function changes data and after all finished, as you can see I put refetch().
=> all this process is for that if I add comment and press confirm button, UI (flatlist) should be changed.
const onValid = async ({ comments }) => {
await createCommentMutation({
variables: {
photoId: route?.params?.photoId,
payload: comments,
},
});
await refetch();
console.log(updatePhoto);
};
But when I console.log data after all, it doesnt' contain added data..
what is the problem here?
If you need more explanation, I can answer in real time.
please help me.
add full code
export default function Comments({ route }) {
const { data: userData } = useMe();
const { register, handleSubmit, setValue, getValues } = useForm();
const [state, setState] = useState(route);
const [update, setUpdate] = useState(false);
const navigation = useNavigation();
useEffect(() => {
setState(route?.params?.comments);
}, [state, route]);
const renderComment = ({ item: comments }) => {
return <CommentRow comments={comments} photoId={route?.params?.photoId} />;
};
const { data: updatePhoto, refetch } = useQuery(SEE_PHOTO_QUERY, {
variables: {
id: route?.params?.photoId,
},
});
const createCommentUpdate = (cache, result) => {
const { comments } = getValues();
const {
data: {
createComment: { ok, id, error },
},
} = result;
if (ok) {
const newComment = {
__typename: "Comment",
createdAt: Date.now() + "",
id,
isMine: true,
payload: comments,
user: {
__typename: "User",
avatar: userData?.me?.avatar,
username: userData?.me?.username,
},
};
const newCacheComment = cache.writeFragment({
data: newComment,
fragment: gql`
fragment BSName on Comment {
id
createdAt
isMine
payload
user {
username
avatar
}
}
`,
});
cache.modify({
id: `Photo:${route?.params?.photoId}`,
fields: {
comments(prev) {
return [...prev, newCacheComment];
},
commentNumber(prev) {
return prev + 1;
},
},
});
}
};
const [createCommentMutation] = useMutation(CREATE_COMMENT_MUTATION, {
update: createCommentUpdate,
});
const onValid = async ({ comments }) => {
await createCommentMutation({
variables: {
photoId: route?.params?.photoId,
payload: comments,
},
});
await refetch();
console.log(updatePhoto);
};
I'm new to Vue.js and I have created one simple form for the user and storing data using API.
On submit I'm calling this function:
setup(props, { emit }) {
const blankData = {
customer: '',
template: '',
rate: '',
property_from: '',
property_to: '',
move_date: '',
num_days: '',
token: '',
details: '',
customer_options: [],
template_options: [],
rate_options: [],
property_from_options: [],
property_to_options: [],
}
const userData = ref(JSON.parse(JSON.stringify(blankData)))
const resetuserData = () => {
userData.value = JSON.parse(JSON.stringify(blankData))
}
const toast = useToast()
const onSubmit = () => {
store.dispatch('app-user/addUser', userData.value)
.then(
response => {
if (response.status === 1) {
this.$router.push({ name: 'edit-user', params: { id: 10 } })
}
toast({
component: ToastificationContent,
props: {
title: response.message,
icon: response.toastIcon,
variant: response.toastVariant,
},
})
},
error => {
console.log(error)
},
)
}
const {
refFormObserver,
getValidationState,
resetForm,
} = formValidation(resetuserData)
return {
userData,
onSubmit,
refFormObserver,
getValidationState,
resetForm,
}
},
And trying to redirect the user to the edit page after user creation but I'm getting this error and not redirecting:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'router')
I have tried with this stackoverflow answer but getting same error:
const onSubmit = () => {
const self = this
store.dispatch('app-user/addUser', userData.value)
.then(
response => {
if (response.status === 1) {
self.$router.push({ name: 'edit-user', params: { id: 10 } })
}
},
error => {
console.log(error)
},
)
}
Any idea what I'm doing wrong in my code?
You're using Vue 3 and a setup function; there is no this in a setup function.
See Accessing the Router and current Route inside setup.
Untested, but probably something like this will work:
setup() {
const router = useRouter()
const onSubmit = () => {
// ... code omitted ...
router.push({ name: 'edit-user', params: { id: 10 } })
}
return {
onSetup,
// other stuff...
}
}
I think this might be help
router with composition api
https://next.router.vuejs.org/guide/advanced/composition-api.html
I'm sending 2 api requests before render the page:
const Profile = {
template: '#profile',
attributes: null,
photos: [],
data: function () {
return {attributes: Profile.attributes, photos: Profile.photos};
},
beforeRouteEnter: function (to, from, next) {
function getProfile() {
return axios.get('user/get-profile?access-token=1', {responseType: 'json'});
}
function getPhotos() {
return axios.get('photos?access-token=1', {responseType: 'json'});
}
axios.all([getProfile(), getPhotos()])
.then(axios.spread(function (profile, photos ) {
console.log(profile, photos );
next(vm => {
vm.setProfile(profile);
vm.setPhotos(photos);
})
}));
},
methods: {
setProfile: function (response) {
Profile.attributes = response.data;
console.log(Profile.attributes);
},
setPhotos: function (response) {
Profile.photos = response.data;
console.log(response);
},
}
};
The problem is rendering occures before setProfile and setPhotos methods. How to correct render my component?
As mentioned in the comments, the first async/await solution is a little bit misleading, because all lifecycle methods are synchronous. This works, because the code is transpiled to an synchronous function with an IIFE inside.
Below I have added a few more recent snippets.
Old answer
Try it with async/await. I've removed beforeRouteEnter, axios.spread and added create.
const Profile = {
template: '#profile',
attributes: null,
photos: [],
data() {
return {
attributes: null,
photos: null,
};
},
async created() {
const getProfile = await axios.get('user/get-profile?access-token=1');
const getPhotos = await axios.get('photos?access-token=1');
this.setProfile(profile);
this.setPhotos(photos);
},
methods: {
setProfile(response) {
this.attributes = response.data;
console.log(this.attributes);
},
setPhotos(response) {
this.photos = response.data;
console.log(response);
},
},
};
Shorter
const Profile = {
template: '#profile',
attributes: null,
photos: [],
data() {
return {
attributes: null,
photos: null,
};
},
async created() {
this.attributes = await axios.get('user/get-profile?access-token=1');
this.photo = await axios.get('photos?access-token=1');
},
};
Updated answer
You can use an async function inside your lifecycle method.
const Profile = {
template: '#profile',
attributes: null,
photos: [],
data() {
return {
attributes: null,
photos: null,
};
},
created() {
const fetchData = async () => {
const { data: attributes } = await axios.get(
'user/get-profile?access-token=1'
);
const { data: photos } = await axios.get('photos?access-token=1');
this.attributes = attributes;
this.photos = photos;
};
fetchData();
},
};
Vue 3 and setup()
In Vue 3 you can use async setup(). If you use this, you must wrap your component with Suspense. Caution! This API is currently experimental https://vuejs.org/guide/built-ins/suspense.html#suspense=.
<Suspense>
<template #default>
<YourComponent />
</template>
<template #fallback>
<div>Loading ...</div>
</template>
</Suspense>
export default {
name: 'YourComponent',
async setup() {
const { data: attributes } = await axios.get('user/get-profile?access-token=1');
const { data: photos } = await axios.get('photos?access-token=1');
return {
attributes,
photos
}
}
}
You should just be able to return the Promise that is returned from calling axios.all like:
return axios.all([getProfile(), getPhotos()])
// .then() => ...
Or you could add a property to the data object and use this to show a loader until all Promises have resolved
const Profile = {
template: '#profile',
attributes: null,
photos: [],
data: function () {
return {attributes: Profile.attributes, photos: Profile.photos, isLoading: true};
},
beforeRouteEnter: function (to, from, next) {
function getProfile() {
return axios.get('user/get-profile?access-token=1', {responseType: 'json'});
}
function getPhotos() {
return axios.get('photos?access-token=1', {responseType: 'json'});
}
axios.all([getProfile(), getPhotos()])
.then(axios.spread(function (profile, memes) {
console.log(profile, memes);
this.isLoading = false
next(vm => {
vm.setProfile(profile);
vm.setPhotos(photos);
})
}));
},
methods: {
setProfile: function (response) {
Profile.attributes = response.data;
console.log(Profile.attributes);
},
setPhotos: function (response) {
Profile.photos = response.data;
console.log(response);
},
}
};
Template code omitted, but you can just switch the content you display based on the isLoading. If you go down this route then probably best to create an abstraction for the loader.
I would also suggest you might want to look at vuex rather than coupling all of your data to any specific component state.
Maybe This can help someone:
Try Promise:
let fetchData = new Promise((resolve, reject) => {
axios.get(YOUR_API)
.then(function (response) {
resolve();
})
.catch(function () {
reject('Fail To Load');
});
});
fetchData.then(
function(success) {
YOUR_SUCCESS_LOGIC
},
function(error) {
console.log(error);
}
);