Vue.js 2.7, "script setup"-approach, defineEmits with composable which needs a emit-function - vuejs2

I am using Vue.js 2.7, I have a (probably TS-only) issue with defineEmits and a self-written composable which needs an emit-function to pass in. The self-written composable should replace a Mixin and is actually too complex, but that's not the problem a like to ask about.
The following does not work.
vue
<script setup lang="ts">
import { defineEmits } from 'vue'; // <- removing this import has no effect
import { useIcomActCompatibility } from '#/composables/compatibility/useIcomActCompatibility';
import { ApiCallParameterType } from '#/services/ApiService';
import { ApiContacts_ListType } from '#/store/types/api/events';
const emit = defineEmits({
'i-do-nothing'(): boolean {
return true;
},
});
const { apiId /* TODO add other necessary stuff */ } = useIcomActCompatibility<
'contacts',
ApiContacts_ListType,
'List'
>({
usedApiId: 'contacts',
usedApiUrl: '/configuration/events/contacts',
usedDataByIdStorePath: 'apiEvents/getApiDataById',
emit, // <- TS2322
sendApiRequest: (_p: ApiCallParameterType): Promise<boolean> => (
console.log('calling dummy sendApiRequest'), Promise.resolve(true)
),
});
</script>
TS2322 is:
Type '(event: 'i-do-nothing') => void' is not assignable to type '(event: string, ...args: any[]) => void'.
Types of parameters 'event' and 'event' are incompatible.
Type 'string' is not assignable to type ''i-do-nothing''.
The code (Vue component) above, beside the TS-Error, could not be created.
Well so far. If I change the code like this?
Vue
<script lang="ts">
import { defineComponent, Ref } from 'vue';
import { useIcomActCompatibility } from '#/composables/compatibility/useIcomActCompatibility';
import { ApiCallParameterType, ApiDataType } from '#/services/ApiService';
import { ApiContacts_ListType } from '#/store/types/api/events';
export default defineComponent({
name: 'HttpServerAct',
setup(props, { emit }): { apiId: Ref<keyof ApiDataType> } {
const { apiId /* TODO add other necessary stuff */ } = useIcomActCompatibility<
'contacts',
ApiContacts_ListType,
'List'
>({
usedApiId: 'contacts',
usedApiUrl: '/configuration/events/contacts',
usedDataByIdStorePath: 'apiEvents/getApiDataById',
emit,
sendApiRequest: (_p: ApiCallParameterType): Promise<boolean> => (
console.log('calling dummy sendApiRequest'), Promise.resolve(true)
),
});
return { apiId };
},
});
</script>
Everything works. My expectation was/is that the 1. version should work also.
What am I doing wrong? I can't imagine that the TS error is solely responsible for this.

Your defineEmits should look like this:
const emit = defineEmits<{
(e: 'i-do-nothing'): boolean;
}>();
https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits

Related

Vue test utils: extend VueWrapper to add plugin method

I am using Vue test utils and Typescript. I have added Data Test ID Plugin.
How can I extend VueWrapper interface to avoid this error:
Property 'findByTestId' does not exist on type 'VueWrapper<{ $: ComponentInternalInstance; $data: { showUserMenu: boolean ...
One solution is to export a type that adds findByTestId:
// my-vue-test-utils-plugin.ts
import { config, DOMWrapper, createWrapperError, type VueWrapper } from '#vue/test-utils'
👇
export type TestWrapper = VueWrapper<any> & {
findByTestId: (selector: string) => DOMWrapper<HTMLElement>
}
const DataTestIdPlugin = (wrapper: VueWrapper<any>) => {
function findByTestId(selector: string) {
const dataSelector = `[data-testid='${selector}']`
const element = wrapper.element.querySelector(dataSelector)
if (element) {
return new DOMWrapper(element)
}
return createWrapperError('DOMWrapper')
}
return {
findByTestId
}
}
config.plugins.VueWrapper.install(DataTestIdPlugin as any)
Then, use type assertion (as keyword followed by the exported type above) on the mount() result:
// MyComponent.spec.ts
import type { TestWrapper } from './my-vue-test-utils-plugin.ts'
describe('MyComponent', () => {
it('renders properly', () => { 👇
const wrapper = mount(MyComponent) as TestWrapper
expect(wrapper.findByTestId('my-component').text()).toBe('Hello World')
})
})
Another option is to create a .d.ts file e.g. vue-test-utils.d.ts with the following content:
import { DOMWrapper } from '#vue/test-utils';
declare module '#vue/test-utils' {
export class VueWrapper {
findByTestId(selector: string): DOMWrapper[];
}
}
So you're able to extend the existing definition of the VueWrapper class.

Upgraded to Vue 2.7 and now getting a bunch of warnings: [Vue warn]: Vue 2 does not support readonly arrays

Background
I recently upgraded from Vue v2.6.14 to Vue 2.7 by following this guide: https://blog.vuejs.org/posts/vue-2-7-naruto.html.
I made some changes (e.g., removing #vue/composition-api and vue-template-compiler, upgrading to vuex-composition-helpers#next, etc.).
Problem
The application loads for the most part, but now I get a ton of console errors:
[Vue warn]: Vue 2 does not support readonly arrays.
It looks like even just console.log(workspaces.value); (see code below) raises the warning.
Question
How do I resolve this issue?
Thank you!
Code
<script lang="ts">
import {
defineComponent,
onMounted,
computed,
} from 'vue';
import { createNamespacedHelpers } from 'vuex-composition-helpers';
import {
modules,
actionTypes,
getterTypes,
} from '#/store/types';
import _ from 'lodash';
const workspaceModule = createNamespacedHelpers(modules.WORKSPACE_MODULE);
export default defineComponent({
setup() {
const { newWorkspace, listWorkspaces } = workspaceModule.useActions([
actionTypes.WorkspaceModule.NEW_WORKSPACE,
actionTypes.WorkspaceModule.LIST_WORKSPACES,
]);
const { workspaces } = workspaceModule.useGetters([
getterTypes.WorkspaceModule.GET_WORKSPACES,
]);
onMounted(async () => {
await listWorkspaces({
Archived: false,
Removed: false,
});
console.log(workspaces.value);
});
return {
/*
workspacesSorted: computed(() => {
return _.orderBy(workspaces.value, ['LastUpdated'], ['desc']);
}),
*/
}
}
});
</script>
src/store/modules/workspace/getters.ts
import { GetterTree } from 'vuex';
import { WorkspaceState } from './types';
import { RootState } from '../../types';
import { getterTypes } from '../../types';
export const getters: GetterTree<WorkspaceState, RootState> = {
[getterTypes.WorkspaceModule.GET_WORKSPACES](context: WorkspaceState) {
return context.Workspaces;
},
[getterTypes.WorkspaceModule.GET_ALL_WORKSPACES](context: WorkspaceState) {
return context.AllWorkspaces;
}
}
src/store/modules/workspace/actions.ts
export const actions: ActionTree<WorkspaceState, RootState> = {
async [actionTypes.WorkspaceModule.LIST_WORKSPACES]({ commit }, payload: ListWorkspace) {
const wss = await list(payload.Archived, payload.Removed);
wss.forEach((ws) => {
ws.Archived = payload.Archived;
ws.Removed = payload.Removed;
});
commit(mutationTypes.WorkspaceModule.SET_WORKSPACES, wss);
},
};
src/store/modules/workspace/actions.ts
export const mutations: MutationTree<WorkspaceState> = {
[mutationTypes.WorkspaceModule.SET_WORKSPACES](ctx: WorkspaceState, wss: Workspace[]) {
ctx.Workspaces = wss;
},
};
src/service/useWorkspace.ts
const list = async(archived: boolean, removed: boolean) => {
const res = await get<Workspace[], AxiosResponse<Workspace[]>>('/workspace/list', {
params: {
archived,
removed,
}
});
return success(res);
};
When I call store.state.WorkspaceModule.Workspaces directly (either in the console or in computed), I get no errors:
import { useStore } from '#/store';
export default defineComponent({
setup() {
const store = useStore();
onMounted(async () => {
await listWorkspaces({
Archived: false,
Removed: false,
});
console.log(store.state.WorkspaceModule.Workspaces);
});
return {
workspacesSorted: computed(() =>
store.state.WorkspaceModule.Workspaces
),
}
}
});
This might be because workspaces is based on a getter, which are read-only. As mentioned in the blog you were referring to, readonly is not supported for arrays in Vue 2.7:
readonly() does create a separate object, but it won't track newly added properties and does not work on arrays.
It was (partially) supported for arrays in the Vue 2.6 Composition Api Plugin though:
readonly() provides only type-level readonly check.
So that might be causing the error. If it is mandatory for you, you might need to upgrade to vue3, or stick with 2.6 for a while. The composition Api plugin is maintained until the end of this year...
A workaround may be to skip the getter and access the state directly, since it is a quite simple getter which only returns the current state of Workspaces.
Hope this helps.

v-model and Composition API with provide and inject

I would like to know how can I show the value from composition API with v-model and Composition API.
Currently I have my store.js :
import { reactive, toRefs, computed } from "vue";
export default function users() {
// State
const state = reactive({
userForm: null,
});
// Mutations
const UPDATE_USER_FORM = (user) => {
state.userForm = user;
};
// Actions
const updateUserForm = (payload) => {
UPDATE_USER_FORM(payload);
};
// Getters
let getUserForm = computed(() => state.userForm);
return {
...toRefs(state),
updateUserForm,
getUserForm
}
}
I provide my store in createApp :
import users from '#/Stores/users';
...
let myApp = createApp({ render: () => h(app, props) });
myApp.provide('userStore', users());
I inject my store in my component :
setup(props, context) {
const userStore = inject('userStore');
return { userStore }
}
In the template I use it, but I don't see the value :
I try this :
<div>userForm : {{userStore.userForm}}</div> // see the user object
<div>userForm with value : {{userStore.userForm.value.firstname}}</div> // see the firstname value
<div>userForm no value : {{userStore.userForm.firstname}}</div> // don't see the firstname
<input v-model="userStore.userForm.firstname"> // don't see the firstname
I would like to use the value in the input...
First thing that you should do is to put the state outside the composable function in order to be available for all components as one instance :
import { reactive, toRefs, computed } from "vue";
// State
const state = reactive({
userForm: null,
});
export default function users() {
// Mutations
...
return {
state,
updateUserForm,
getUserForm
}
}
second thing is to import the composable function in any component you want since the inject/provide could have some reactivity issues :
<input v-model="state.userForm.firstname">
...
import users from './store/users'
....
setup(props, context) {
const {state,updateUserForm,getUserForm} = users();
return { state }
}

Vue Composition API reactivity doesn't work properly

I am using Vue2, Vuetify, Vue Composition API(#vue/composition-api)
The problem I faced is that composition api reactivity doesn't work properly.
Let me show you some code
---- companies.vue ----
<template>
...
<v-data-table
:headers="companiesHeaders"
:items="companies"
:loading="loadingCompanies"
/>
...
</template>
<script>
...
import { useCompanies } from '#/use/companies'
export default {
setup: (_, props) => {
...
const {
companies,
loadingCompanies,
getCompanies
} = useCompanies(context)
onMounted(getCompanies)
return {
...,
companies,
loadingCompanies
}
}
}
</script>
---- #/use/companies.ts ----
import { ref } from '#vue/composition-api'
export const useCompanies = (context: any) => {
const { emit, root } = context
const companies = ref([])
const loadingCompanies = ref(false)
const getCompanies = async () => {
if (loadingCompanies.value) { return }
try {
loadingCompanies.value = true
companies.value = (await root.$repositories
.companies.getCompanies()).data
console.log(companies.value)
// This log works properly. It logs company list once received
// But even after this async function is finished, companies and loadingCompanies are not updated automatically
} catch (err) {} finally {
loadingCompanies.value = false
}
}
return {
companies,
loadingCompanies
}
}
I tried with both ref and reactive.
But reactivity for whatever inside companies.vue doesn't work.
I resolved the issue.
The issue was that company variable instance was created in 2 places(one for create company dialog and one for table), so changes in one place(create company dialog) didn't affect to the other(table).
Thanks.

Vuex: createNamespacedHelpers with dynamic namespace

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']
}
},