Vue Composition API reactivity doesn't work properly - vue.js

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.

Related

CASL - Vue 3 - Element not showing for role

I am having a bit of a challenge implementing CASL in my app.
I have created the following composable useAppAbility ("hook") that defines all the rules:
import { AbilityBuilder, createMongoAbility, subject } from "#casl/ability";
import { useAbility } from "#casl/vue";
const service = {};
const user = {};
const subscription = {};
const invoice = {};
const account = {};
const ability = createMongoAbility();
const ROLES = ["admin", "account_owner", "beneficiary", "super_admin"];
const defineAbilityFor = (role: Object) => {
const { can, rules } = new AbilityBuilder(createMongoAbility);
const is = (r: string) => {
return ROLES.indexOf(r) >= ROLES.indexOf(role);
};
if (is("admin")) {
can("add", subject("User", user));
can("remove", subject("User", user));
}
return ability.update(rules);
};
export { defineAbilityFor, ability, subject };
export const useAppAbility = () => useAbility();
Added the plugin to the main.ts:
import { ability } from "#/composables/useAppAbility";
import { abilitiesPlugin } from "#casl/vue";
createApp(App)
.use(abilitiesPlugin, ability, {
useGlobalProperties: true,
})
//stuff
.mount("#app");
And then, I found that using the beforeEach hook in the router and passing in the user before each route was the simplest way to deal with page load and SPA routing.
I have therefore added the following to my router/index.ts:
import { ability, defineAbilityFor } from "#/composables/useAppAbility";
import useAuth from "#/composables/useAuth";
const {
getUserByClaims,
} = useAuth();
// routes
router.beforeEach(async (to, _from, next) => {
defineAbilityFor(getUserByClaims.value.roles)
})
At this stage I can verify that the user is being passed properly to the defineAbilityFor function and when using the ability.on("update") hook to log the rules object, I have the following output:
Which seems to confirm that the rules for this user are built and updated correctly?
However, when trying to display a button for the said admin in a component, the button does not show.
MyComponent.vue:
<script setup lang="ts">
import { useAppAbility, subject } from "#/composables/useAppAbility";
const { can } = useAppAbility();
</script>
<template>
<div v-if="can('add', subject('User', {}))">TEST FOR CASL</div> <!-- DOES NOT SHOW-->
</template>
Not sure where to go from there, any help would be appreciated.
Thanks

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.

Returning Apollo useQuery result from inside a function in Vue 3 composition api

I'm having some issues finding a clean way of returning results from inside a method to my template using Apollo v4 and Vue 3 composition API.
Here's my component:
export default {
components: {
AssetCreationForm,
MainLayout,
HeaderLinks,
LoadingButton,
DialogModal
},
setup() {
const showNewAssetModal = ref(false);
const onSubmitAsset = (asset) => {
// how do I access result outside the handler function
const { result } = useQuery(gql`
query getAssets {
assets {
id
name
symbol
slug
logo
}
}
`)
};
}
return {
showNewAssetModal,
onSubmitAsset,
}
},
}
The onSubmitAsset is called when user clicks on a button on the page.
How do I return useQuery result from the setup function to be able to access it in the template? (I don't want to copy the value)
You can move the useQuery() outside of the submit method, as shown in the docs. And if you'd like to defer the query fetching until the submit method is called, you can disable the auto-start by passing enabled:false as an option (3rd argument of useQuery):
export default {
setup() {
const fetchEnabled = ref(false)
const { result } = useQuery(gql`...`, null, { enabled: fetchEnabled })
const onSubmitAsset = (asset) => {
fetchEnabled.value = true
}
return { result, onSubmitAsset }
}
}
demo

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 3 with Vuex 4

I'm using Vue 3 with the composition API and trying to understand how I can map my state from Vuex directly so the template can use it and update it on the fly with the v-model.
Does mapState works or something else to solve this issue? Right no I need to get my state by a getter, print it out in the template, and then do a manual commit for each field in my state... In Vue 2 with Vuex, I had this 100% dynamic
To make two-way binding between your input and store state you could use a writable computed property using set/get methods :
setup(){
const store=useStore()
const username=computed({
get:()=>store.getters.getUsername,
set:(newVal)=>store.dispatch('changeUsername',newVal)
})
return {username}
}
template :
<input v-model="username" />
I've solved it!
Helper function:
import { useStore } from 'vuex'
import { computed } from 'vue'
const useMapFields = (namespace, options) => {
const store = useStore()
const object = {}
if (!namespace) {
console.error('Please pass the namespace for your store.')
}
for (let x = 0; x < options.fields.length; x++) {
const field = [options.fields[x]]
object[field] = computed({
get() {
return store.state[namespace][options.base][field]
},
set(value) {
store.commit(options.mutation, { [field]: value })
}
})
}
return object
}
export default useMapFields
And in setup()
const {FIELD1, FIELD2} = useMapFields('MODULE_NAME', {
fields: [
'FIELD1',
etc…
],
base: 'form', // Deep as next level state.form
mutation: 'ModuleName/YOUR_COMMIT'
})
Vuex Mutation:
MUTATION(state, obj) {
const key = Object.keys(obj)[0]
state.form[key] = obj[key]
}