Vue 3 reuse composition API in separate file - vuejs3

I'm new to Vue.js and I'm trying to do the following.
I have a working Vue 3 application where I'm testing the composition Api feature.
My App.vue file is simple and has the following:
<template>
<div>
<p>Counter: {{ counter }}</p>
<button #click="increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const counter = ref(3);
function increaseCounter() {
counter.value++;
}
return {
counter,
increaseCounter
};
}
};
</script>
Now everything is working with this code.
What I want to do is to separate the script code into a new separate file and import in the App.vue to reuse it. I tried the following but with no luck:
<template>
<div>
<p> Msg: {{ msg }} </p>
<p>Counter: {{ counterApi.counter }}</p>
<button #click="counterApi.increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { counterApi } from "./counter-api.js";
export default {
created: () => {
counterApi.increaseCounter;
}
};
</script>
counter-api.js
import { ref } from "vue";
export default {
setup() {
const counter = ref(3);
function increaseCounter() {
counter.value++;
}
return {
counter,
increaseCounter
};
}
};
I'm getting the following error
Cannot read property 'increaseCounter' of undefined.
Thanks in advance

You did not showed what is inside counter-api.js. Based on import you have, it should be something like this:
export function CounterApi {
const counter = ref(3);
function increaseCounter() {
counter.value++;
}
return {
counter,
increaseCounter
};
}
Or if we use more ES6:
export const CounterApi = () {
const counter = ref(3);
const increaseCounter = () {
counter.value++;
}
return {
counter,
increaseCounter
};
}
You imported file, but it's not enough because you did nothing with it. Instead:
<template>
<div>
<p>Counter: {{ counter }}</p>
<button #click="increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { CounterApi } from "./counter-api";
export default {
setup() {
const { counter, increaseCounter } = CounterApi();
return {
counter,
increaseCounter
};
}
};
</script>
Or
<template>
<div>
<p>Counter: {{ someName.counter }}</p>
<button #click="someName.increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { CounterApi } from "./counter-api";
export default {
setup() {
const someName = CounterApi(); // use all under single const
return { someName };
}
};
</script>
There is plenty articles about composition API, for example: https://vueschool.io/articles/vuejs-tutorials/state-management-with-composition-api/
Also it is good idea and common convention to name this composable as useCounterApi and then const counterApi = useCounterApi() or with dectruction const { something, something } = useCounterApi()
It is all in official docs:
https://v3.vuejs.org/api/composition-api.html

Related

How to access instance in vue3 composition API lifecycle hooks

I stumbled into a totally unexpected problem while refactoring my code to composition API: there doesn't seem to be any (documented) way of accessing current instance from the lifecycle hooks.
sample code:
import { defineComponent, onMounted } from 'vue';
export default defineComponent({
setup() {
onMounted(() => {
console.log(this); // <-- will be undefined
});
},
mounted() {
console.log(this); // <-- will be the component
},
}
I've spent hours trying to find a solution to this and ultimately just used the old options API to get what I want. None of examples, tutorials or documentation - that I read - use this in the hooks.
But I find it unbelievable that only undocumented getCurrentInstance would be the way to get the current instance from the hook.
So, which doc did I miss?
UPDATE
Here is the same example with a component
const { createApp, ref, onMounted } = Vue;
const MyComponent = {
setup() {
const id = ref(Math.round(Math.random() * 100000));
const count = ref(0);
const plus = () => { count.value++; }
const minus = function() { count.value--; }
onMounted(() => {
count.value = Math.round(Math.random() * 10)
});
return {id, count, plus, minus }
},
template: `id: {{id}} <button type="button" #click="minus()">-1</button>
{{count}}
<button type="button" #click="plus()">+1</button><hr/>`
}
const App = {
components: {
MyComponent
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<my-component v-for="i in 5" />
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
What for do you need this in the component?
If you create your component with Composition API, then you can access all the properties directly, without using this.
Here is a very basic example:
const { createApp, ref, onMounted } = Vue;
const App = {
setup() {
const count = ref(0);
const up = () => { count.value++; }
const down = function() { count.value--; }
onMounted(() => {
count.value = 10
});
return {count, up, down }
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<button type="button" #click="down()">-1</button>
{{count}}
<button type="button" #click="up()">+1</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

Vue3 composition API and pinia | submit data then reload the data

I'm learning Vue 3 composition API and Pinia. I'm making a todo.
When I submit a todo data through Pinia, I can submit to the DB, but it won't re-render until reload the page.
Do I need to use 'watch' to watch the state todos:[] and execute fetchTodos()?
any good solution?
here both codes, hope someone can help me. Thank you in advance.
----- VUE -----
<script setup>
import { ref, onMounted } from 'vue'
import { storeToRefs } = from 'pinia'
import { useTodoStore } from '../store/todo'
const store = useTodoStore()
const { getTodos } = storeToRefs(store)
onMounted(() => {
store.fetchTodos()
})
const todo = ref('')
const initForm = () => {
todo.value = ''
}
// submit via Pinia
const onSubmitToPinia = () => {
const payload = {
todo: todo.value,
}
store.addTodoFromPinia(payload)
initForm()
store.fetchTodo()
}
</script>
<template>
<h4>TODO</h4>
<!-- form addTodo -->
<form class="row g-4">
<div class="col-auto">
<input
class="form-control"
v-model="newName"
type="text"
placeholder="todo">
</div>
<div>
<button
class="btn btn-primary"
type="button"
#click="onSubmitToPinia(payload)">
submit through pinia</button>
</div>
</form>
<!-- render data from pinia -->
<div class="todo"
v-for="getTodo in getTodoss.todo"
:key="getTodo.id">
<b class="ms-2">{{ getTodo.todo }}</b>
</div>
</template>
---- PINIA ----
import { defineStore } from 'pinia'
import axios from "axios"
export const useAboutStore = defineStore('todo',{
state: () => {
return {
todos: []
}
},
getters: {
getTodos(state) {
return state.todos
}
},
actions: {
async fetchTodos() {
try {
const data = await axios.get('http://localhost:5000/todo')
this.todos = data.data
}
catch (error) {
alert(error)
console.log(error)
}
},
addTodoFromPinia(payload) {
const path = 'http://localhost:5000/todo'
axios.post(path, payload)
}
},
})
You don't need to use storeToRefs to accomplish what you want nor do you need to watch the state of the store.
<template>
<div class="todo"
v-for="getTodo in store.todos"
:key="getTodo.id">
<b class="ms-2">{{ getTodo.todo }}</b>
</div>
</template>
If for any reason the vue complains that the array is empty put a v-if checking if the store.todos.length is != 0.
And also fix your typos.
If the problem persists show me your new code and I help you again.

How to make a pinia work with nested objects in vue3

How can I get a reactive component that updates nested properties:
I have a pinia store defined as follows
import { defineStore } from "pinia"
export const useStore = defineStore({
id: "poc",
state: () => ({ str: "", nested: { obj: "" } }),
persist: {
enabled: true,
strategies: [{ storage: localStorage }],
},
})
and the following vue3 component
<script lang="ts">
import { ref } from "vue"
import { storeToRefs } from "pinia"
import { useStore } from "./store"
export default {
setup() {
const store = useStore()
const example = storeToRefs(store)
const mStr = ref(example.str)
const mObj = ref(example.nested.value.obj) // <--- this is where I believe the problem is
store.str = mStr.value
store.nested.obj = mObj.value
return { mObj, mStr, store }
},
}
</script>
<template>
<h1>PoC</h1>
<input v-model="mObj" placeholder="obj" />
<input v-model="mStr" placeholder="str" />
</template>
when I update the str field it works as expected, but for nested object it doesn't. My suspicion is that I lose reactivity when calling nested.value, that said - I don't know how to make it reactive.
a little bit more digging and https://github.com/vuejs/pinia/discussions/854 finally gave me enough to come up with a (much more elegant) solution on my own.
<script lang="ts">
import { useStore } from "./store"
export default {
setup() {
const store = useStore()
return { store }
},
}
</script>
<template>
<h1>test</h1>
<input v-model="store.str" placeholder="obj" />
<input v-model="store.nested.obj" placeholder="str" />
</template>
FOR PINIA: destructuring the state checkout :storeToRefs()
In order to extract properties from the store while keeping its reactivity, you need to use storeToRefs(). It will create refs for every reactive property. This is useful when you are only using state from the store but not calling any action. Note you can destructure actions directly from the store as they are bound to the store itself too
<script>
import { useStore } from "./store"
import { storeToRefs } from 'pinia' // NOTE this
export default {
setup() {
const store = useStore()
const {str, nested } = storeToRefs(store)
return { str, nested }
},
}
</script>
<template>
<h1>test</h1>
<input v-model="str" placeholder="obj" />
<input v-model="nested.obj" placeholder="str" />
</template>

#vue/test-utils how to test v-if in vue3 when the parameter is imported

here are all code. parameter isInApp is imported from tools.ts. I had mount the vue component and add options ,In this case, how to mock isInApp value to finish the test
// a.vue
<template>
<div class="test" v-if="isInApp">test</div>
</template>
<script lang="ts">
import { isInApp } from './tools'
export default {
setup() {
return {
isInApp,
}
},
}
</script>
//tools.ts
export const isInApp = navigator.userAgent.indexOf('baidu') > -1
// a.spec.ts
import { mount } from '#vue/test-utils'
import a from './a.vue'
test('test',async ()=>{
const wrapper = mount(a,{
data(){
return{
isInApp: true, // I had set the data here, but it doesn't work, how to fixed it ?
}
}
})
expect(wrapper.find('.test').exists()).toBeTruthy() // Received: false
})

How to make an object passed as a prop in Vue 3 reactive?

I need to reactively change my component when a field in passed object changes.
<template>
<my-component :prop="prop" />
</template>
<script>
export default {
data() {
return {
prop: {
key: 'value',
flag: true
}
}
}
}
</script>
mycomponent.vue
<template>
<div v-if="flag">Yay, it's a flag!</div>
<div v-else>I am very sad rn</div>
</template>
<script>
export default {
props: {
prop: Object
},
setup(props) {
const prop = ref(props, 'prop')
const flag = // *
return { flag }
}
}
</script>
Don't know what to do here, prop.flag, prop.value.flag doesn't work.
I also tried something like const flag = ref(prop, 'flag') and then flag.value, or even const flag = req(prop.value, 'flag'), but no luck.
Props are accessible and reactive in components should you declare them. Since you haven't, they won't be available.
For example, this is all you need:
<template>
<div v-if="prop.flag">Yay, it's a flag!</div>
<div v-else>I am very sad rn</div>
</template>
<script>
export default {
props: {
prop: Object
}
}
</script>
Just use toRef or toRefs
<template>
<div v-if="flag">Yay, it's a flag!</div>
<div v-else>I am very sad rn</div>
</template>
<script>
import { toRefs, toRef } from 'vue';
export default {
props: {
prop: Object
},
setup(props) {
const { prop } = toRefs(props);
//alternative
const prop = toRef(props, 'prop');
const flag = // *
return { flag }
}
}
</script>

Resources