I have a route that has its own model, which does not come from the Ember store (let's say it can come from "anywhere" for the sake of this question).
model() {
return RSVP.hash({
value: someCall()
});
}
this.owner.lookup('route:routeName').model() does not seem to work, neither does this.owner.lookup('controller:controllerName').get('model.X') or any of the other things I've tried.
Nor does it seem to be mentioned at https://guides.emberjs.com/v3.1.0/testing/testing-routes/
How would you retrieve a route's model in a test?
The Ember router doesn't appear to have any kind of public interface to get the model according to the official docs (https://emberjs.com/api/ember/3.1/classes/EmberRouter). It can access the model function internally though. This feels a bit hackish, but I was able to get it to work:
Router:
import Route from '#ember/routing/route';
import { hash } from 'rsvp';
export default Route.extend({
model() {
return hash({
value: 'wibble'
});
},
getMyModel: function() {
return this.get('model')();
}
});
Router test:
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | test', function(hooks) {
setupTest(hooks);
test('do something with the router model...', function(assert) {
let route = this.owner.lookup('route:test');
assert.ok(route);
route.getMyModel().then(function(model) {
console.log(model);
assert.equal(model.value, 'wibble');
});
});
});
You can go with:
route.get('model')()
Related
So I'm using vue-query to get data from my API. The current way I'm doing that looks a little like this. I have a folder in src called hooks, and it may contain a file such as usePosts.ts. That file contains code like this:
import { useQuery, useMutation, useQueryClient } from "vue-query"
import axios, { AxiosError } from "axios"
import {
performOptimisticAdd,
handleMutateSuccess,
handleMutateError,
} from "./handlers"
export interface Post {
id: number
title: string
body: string
user: number // user_id
}
export function usePostsListQuery() {
return useQuery<Post[], AxiosError>(
"posts",
() => axios.get("/v1/posts").then(resp => resp.data),
{ placeholderData: [] }
)
}
export function useAddPostMutation() {
const client = useQueryClient()
return useMutation<Post, AxiosError>(
post => axios.post("/v1/posts", post).then(resp => resp.data),
{
onMutate: performOptimisticAdd(client, "posts"),
onSuccess: handleMutateSuccess(),
onError: handleMutateError()
}
)
}
Of course I'm not showing all the code, for brevity.
Now in my Vue components, I'm often doing something like this:
<script setup>
import { usePostsListQuery, useAddPostMutation } from "#/hooks/usePosts";
import { useUsersListQuery } from "#/hooks/useUsers";
const { data: posts } = $(usePostsListQuery())
const { data: users } = $(useUsersListQuery())
const { mutate: addPost } = $(useAddPostMutation())
const postsWithUsers = $computed(() => posts.map(
post => ({ ...post, user: users.find(user => user.id === post.user) })
))
const addPostWithUserId = (newPost: Post) => addPost({ ...newPost, user: newPost.user.id })
</script>
Because I want to be able to directly access the user associated with a post. And of course, the way I'm doing it works. But it doesn't seem right to do that transformation inside a Vue-component. Because that means I need to repeat that same code inside every new Vue-component.
So I'm wondering what would be the best place to do this transformation. One obstacle is that useQuery() may only be called during / inside the setup() function. So I'm a bit limited in terms of where I'm allowed to call these queries.
Maybe I could just put it inside usePosts.ts? But is that really the best place? I can imagine that it might make all my hooks very messy, because then every hook suddenly has TWO responsibilities (talking to my API, and transforming the output and input). I feel like that breaks the single responsibility principle?
Anyhow, this is why I'd love to hear some of your opinions.
I want to use my vuex modules as classes to make my code more clean and readable. I used the section (Accessing modules with NuxtJS) at the bottom of this document: https://github.com/championswimmer/vuex-module-decorators/blob/master/README.md
I've searched for the solution for almost 3 days and tried out this link:
vuex not loading module decorated with vuex-module-decorators
but, it didn't work.
Also, I used getModule directly in the component like the solution in this issue page: https://github.com/championswimmer/vuex-module-decorators/issues/80
import CounterModule from '../store/modules/test_module';
import { getModule } from 'vuex-module-decorators';
let counterModule: CounterModule;
Then
created() {
counterModule = getModule(CounterModule, this.$store);
}
Then, accessing method elsewhere
computed: {
counter() {
return counterModule.getCount
}
}
it didn't work for me!
This is my Module in store folder in Nuxtjs project:
import { ICurrentUser } from '~/models/ICurrentUser'
import { Module, VuexModule, Mutation, MutationAction } from 'vuex-module-decorators'
#Module({ stateFactory: true, namespaced: true, name: 'CurrentUserStore' })
export default class CurrentUser extends VuexModule {
user: ICurrentUser = {
DisplayName: null,
UserId: null,
};
#Mutation
setUser(userInfo: ICurrentUser) {
this.user = userInfo;
}
get userInfo() {
return this.user;
}
}
In index.ts file in sore folder:
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import CurrentUser from './currentUser'
let currentUserStore: CurrentUser
const initializer = (store: Store<any>): void => {
debugger
currentUserStore = getModule(CurrentUser, store)
}
export const plugins = [initializer]
export {
currentUserStore,
}
I think the problem stems from this line:
currentUserStore = getModule(CurrentUser, store)
currentUserStore is created as object but properties and methods are not recognizable.
when I want to use getters or mutation I get error. For instance, "unknown mutation type" for using mutation
Probably several months late but I struggled with a similar issue, and eventually found the solution in https://github.com/championswimmer/vuex-module-decorators/issues/179
It talks about multiple requirements (which are summarised elsewhere)
The one that relates to this issue is that the file name of the module has to match the name you specify in the #Module definition.
In your case, if you rename your file from currentUser to CurrentUserStore' or change the name of the module toCurrentUser`, it should fix the issue.
In almost all guides, tutorial, posts, etc that I have seen on vuex module registration, if the module is registered by the component the createNamespacedHelpers are imported and defined prior to the export default component statement, e.g.:
import {createNamespacedHelpers} from 'vuex'
const {mapState} = createNamespacedHelpers('mymod')
import module from '#/store/modules/mymod'
export default {
beforeCreated() {
this.$store.registerModule('mymod', module)
}
}
this works as expected, but what if we want the module to have a unique or user defined namespace?
import {createNamespacedHelpers} from 'vuex'
import module from '#/store/modules/mymod'
export default {
props: { namespace: 'mymod' },
beforeCreated() {
const ns = this.$options.propData.namespace
this.$store.registerModule(ns, module)
const {mapState} = createNamespacedHelpers(ns)
this.$options.computed = {
...mapState(['testVar'])
}
}
}
I thought this would work, but it doesnt.
Why is something like this needed?
because
export default {
...
computed: {
...mapState(this.namespace, ['testVar']),
...
},
...
}
doesnt work
This style of work around by utilising beforeCreate to access the variables you want should work, I did this from the props passed into your component instance:
import { createNamespacedHelpers } from "vuex";
import module from '#/store/modules/mymod';
export default {
name: "someComponent",
props: ['namespace'],
beforeCreate() {
let namespace = this.$options.propsData.namespace;
const { mapActions, mapState } = createNamespacedHelpers(namespace);
// register your module first
this.$store.registerModule(namespace, module);
// now that createNamespacedHelpers can use props we can now use neater mapping
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
}),
// because we use spread operator above we can still add component specifics
aFunctionComputed(){ return this.name + "functions";},
anArrowComputed: () => `${this.name}arrows`,
};
// set up your method bindings via the $options variable
this.$options.methods = {
...mapActions(["initialiseModuleData"])
};
},
created() {
// call your actions passing your payloads in the first param if you need
this.initialiseModuleData({ id: 123, name: "Tom" });
}
}
I personally use a helper function in the module I'm importing to get a namespace, so if I hadmy module storing projects and passed a projectId of 123 to my component/page using router and/or props it would look like this:
import projectModule from '#/store/project.module';
export default{
props['projectId'], // eg. 123
...
beforeCreate() {
// dynamic namespace built using whatever module you want:
let namespace = projectModule.buildNamespace(this.$options.propsData.projectId); // 'project:123'
// ... everything else as above
}
}
Hope you find this useful.
All posted answers are just workarounds leading to a code that feels verbose and way away from standard code people are used to when dealing with stores.
So I just wanted to let everyone know that brophdawg11 (one of the commenters on the issue #863) created (and open sourced) set of mapInstanceXXX helpers aiming to solve this issue.
There is also series of 3 blog posts explaining reasons behind. Good read...
I found this from veux github issue, it seems to meet your needs
https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
{
props: ['namespace'],
computed: mapState({
state (state) {
return state[this.namespace]
},
someGetter (state, getters) {
return getters[this.namespace + '/someGetter']
}
}),
methods: {
...mapActions({
someAction (dispatch, payload) {
return dispatch(this.namespace + '/someAction', payload)
}
}),
...mapMutations({
someMutation (commit, payload) {
return commit(this.namespace + '/someMutation', payload)
})
})
}
}
... or maybe we don't need mapXXX helpers,
mentioned by this comment https://github.com/vuejs/vuex/issues/863#issuecomment-439039257
computed: {
state () {
return this.$store.state[this.namespace]
},
someGetter () {
return this.$store.getters[this.namespace + '/someGetter']
}
},
Using the vuex-module-decorator I have a authenticate action that should mutate the state.
#Action
public authenticate(email: string, password: string): Promise<Principal> {
this.principal = null;
return authenticator
.authenticate(email, password)
.then(auth => {
const principal = new Principal(auth.username);
this.context.commit('setPrincipal', principal);
return principal;
})
.catch(error => {
this.context.commit('setError', error);
return error;
});
}
// mutations for error and principal
But this fail with the following message:
Unhandled promise rejection Error: "ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an #Action?
That works only in dynamic modules.
If not dynamic use this.context.commit("mutationName", payload) and this.context.getters["getterName"]
What I don't understand is that it works well with #MutationAction and async. However I miss the return type Promise<Principal>.
#MutationAction
public async authenticate(email: string, password: string) {
this.principal = null;
try {
const auth = await authenticator.authenticate(email, password);
return { principal: new Principal(auth.username), error: null };
} catch (ex) {
const error = ex as Error;
return { principal: null, error };
}
}
--
At this time I feel blocked and would like to have some help to implement an #Action that can mutate the state and return a specific type in a Promise.
Just add rawError option to the annotation so it becomes
#Action({rawError: true})
And it display error normally. this is because the the library "vuex-module-decorators" wrap error so by doing this you will able to get a RawError that you can work with
You can vote down this answer if you would like because it isn't answering the specific question being posed. Instead, I am going to suggest that if you are using typescript, then don't use vuex. I have spent the past month trying to learn vue /vuex and typescript. The one thing I am committed to is using typescript because I am a firm believer in the benefits of using typescript. I will never use raw javascript again.
If somebody would have told me to not use vuex from the beginning, I would have saved myself 3 of the past 4 weeks. So I am here to try and share that insight with others.
The key is Vue 3's new ref implementation. It is what really changes the game for vuex and typescript. It allows us to not have to rely on vuex to automatically wrap state in a reactive. Instead, we can do that ourselves with the ref construct in vue 3. Here is a small example from my app that uses ref and a typescript class where I was expecting to use vuex in the past.
NOTE1: the one thing you lose when using this approach is vuex dev tools.
NOTE2: I might be biased as I am ported 25,000 lines of typescript (with 7000 unit tests) from Knockout.js to Vue. Knockout.js was all about providing Observables (Vue's ref) and binding. Looking back, it was kind of ahead of its time, but it didn't get the following and support.
Ok, lets create a vuex module class that doesn't use vuex. Put this in appStore.ts. To simplify it will just include the user info and the id of the club the user is logged into. A user can switch clubs so there is an action to do that.
export class AppClass {
public loaded: Ref<boolean>;
public userId: Ref<number>;
public userFirstName: Ref<string>;
public userLastName: Ref<string>;
// Getters are computed if you want to use them in components
public userName: Ref<string>;
constructor() {
this.loaded = ref(false);
initializeFromServer()
.then(info: SomeTypeWithSettingsFromServer) => {
this.userId = ref(info.userId);
this.userFirstName = ref(info.userFirstName);
this.userLastName = ref(info.userLastName);
this.userName = computed<string>(() =>
return this.userFirstName.value + ' ' + this.userLastName.value;
}
}
.catch(/* do some error handling here */);
}
private initializeFromServer(): Promise<SomeTypeWithSettingsFromServer> {
return axios.get('url').then((response) => response.data);
}
// This is a getter that you don't need to be reactive
public fullName(): string {
return this.userFirstName.value + ' ' + this.userLastName.value;
}
public switchToClub(clubId: number): Promise<any> {
return axios.post('switch url')
.then((data: clubInfo) => {
// do some processing here
}
.catch(// do some error handling here);
}
}
export appModule = new AppClass();
Then when you want to access appModule anywhere, you end up doing this:
import { appModule } from 'AppStore';
...
if (appModule.loaded.value) {
const userName = appModule.fullName();
}
or in a compositionApi based component. This is what would replace mapActions etc.
<script lang="ts">
import { defineComponent } from '#vue/composition-api';
import { appModule } from '#/store/appStore';
import footer from './footer/footer.vue';
export default defineComponent({
name: 'App',
components: { sfooter: footer },
props: {},
setup() {
return { ...appModule }
}
});
</script>
and now you can use userId, userFirstName, userName etc in your template.
Hope that helps.
I just added the computed getter. I need to test if that is really needed. It might not be needed because you might be able to just reference fullName() in your template and since fullName() references the .value variables of the other refs, fullName might become a reference itself. But I have to check that out first.
I sugest this simple solution, work fine for me 👌:
// In SomeClassComponent.vue
import { getModule } from "vuex-module-decorators";
import YourModule from "#/store/YourModule";
someMethod() {
const moduleStore = getModule(YourModule, this.$store);
moduleStore.someAction();
}
If the action has parameters, put them.
Taken from: https://github.com/championswimmer/vuex-module-decorators/issues/86#issuecomment-464027359
I have an apollo-wrapped component that's supposed to provide my component with response data from the github graphql v4 api. I intend to use a string(SEARCH_QUERY) from another part of the app to be used in my gql query but github keeps returning undefined. I am following offical apollo docs http://dev.apollodata.com/react/queries.html#graphql-options.
I dont see what I am doing wrong.
import React, { Component } from 'react';
import { Text, FlatList } from 'react-native';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { SEARCH_QUERY } from './Home' // this is a string like "react"
// The data prop, which is provided by the wrapper below contains,
// a `loading` key while the query is in flight and posts when ready
const ReposList = ({ data: { loading, search }}) => <Text>SearchResults</Text>
// this doesnt work because I cant properly inject 'SEARCH_QUERY' string
const searchRepos = gql`
query searchRepos($type: searchType!, $query: String!) {
search(type: REPOSITORY, query: $query, first: 100) {
edges {
node {
... on Repository {
nameWithOwner
owner {
login
}
}
}
}
}
}
`
// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (ReposList here)
export default graphql(searchRepos, {
options: { variables: { query: SEARCH_QUERY }, notifyOnNetworkStatusChange: true }
}
)(ReposList);
This query without variables works well and returns search results as expected. straight forward, right?
const searchRepos = gql`{
search(type: REPOSITORY, query: "react", first: 100) {
edges {
node {
... on Repository {
nameWithOwner
owner {
login
}
}
}
}
}
}
`
When this is used github returns undefined.
const searchRepos = gql`
query searchRepos($type: searchType!, $query: String!) {
search(type: REPOSITORY, query: $query, first: 100) {
edges {
node {
... on Repository {
nameWithOwner
owner {
login
}
}
}
}
}
}
`
Your query is erroring out because you've defined a variable $type -- but you don't actually use it inside your query. You don't have to actually send any variables with your query -- you could define one or more in your query and then never define any inside the graphql HOC. This would be a valid request and it would be up to the server to deal with the undefined variables. However, if you define any variable inside the query itself, it has to be used inside that query, otherwise the query will be rejected.
While in development, you may find it helpful to log data.error to the console to more easily identify issues with your queries. When a query is malformed, the errors thrown by GraphQL are generally pretty descriptive.
Side note: you probably don't want to use a static values for your variables. You can calculate your variables (and any other options) from the props passed down to the component the HOC is wrapping. See this section in the docs.
const options = ({someProp}) => ({
variables: { query: someProp, type: 'REPOSITORY' },
notifyOnNetworkStatusChange: true,
})
export default graphql(searchRepos, {options})(ReposList)